R5: CongressTrading als Strategie (Signal -> ExecutionService) + Positionen im Modul-Fenster
- CongressTradingStrategy: neuer Scrape-Trade -> TradeSignal (buy/sell-Mapping, sonst ignoriert)
-> Core-IExecutionService; CongressScrapeWorker ruft sie je neuem Trade (try/catch-isoliert)
- Modul-Fenster: offene Positionen des Moduls via IPortfolioService.GetPositionsAsync("CT")
- Tests: CongressTradingStrategy (Mapping/Ausfuehrung, gemockter ExecutionService) -> 51/51 gruen
- Handel bleibt sicher aus (TradingEnabled=false + NullBrokerClient), bis Broker+Freigabe
- Build + smoke-ui + App-Start ok
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
130 lines
4.7 KiB
C#
130 lines
4.7 KiB
C#
using IBKRTrader.Core.Logging;
|
|
using IBKRTrader.Core.Trading;
|
|
using IBKRTrader.Core.Workers;
|
|
using IBKRTrader.Modules.CongressTrading.Database;
|
|
|
|
namespace IBKRTrader.Modules.CongressTrading.UI;
|
|
|
|
/// <summary>
|
|
/// Eigenständiges Fenster des CongressTrading-Moduls: DB-Kennzahlen, manueller Scrape-Trigger
|
|
/// und die offenen Positionen des Moduls (aus dem Core-Portfolio).
|
|
/// </summary>
|
|
public sealed class CongressTradingForm : Form
|
|
{
|
|
private const string ScrapeWorkerName = "CT-ScrapeWorker";
|
|
|
|
private readonly CongressRepository _repo;
|
|
private readonly WorkerEngine _engine;
|
|
private readonly IPortfolioService _portfolio;
|
|
private readonly LoggingService _logger;
|
|
|
|
private readonly Label _lblTrades = new() { AutoSize = true, Location = new Point(20, 70) };
|
|
private readonly Label _lblMembers = new() { AutoSize = true, Location = new Point(20, 100) };
|
|
private readonly Button _btnRefresh = new() { Text = "Aktualisieren", Location = new Point(20, 140), Width = 140 };
|
|
private readonly Button _btnScrape = new() { Text = "Scrape jetzt", Location = new Point(170, 140), Width = 140 };
|
|
private readonly Label _lblStatus = new() { AutoSize = true, Location = new Point(20, 185), ForeColor = SystemColors.GrayText };
|
|
|
|
private readonly DataGridView _positions = new()
|
|
{
|
|
Location = new Point(20, 250),
|
|
Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right,
|
|
ReadOnly = true,
|
|
AllowUserToAddRows = false,
|
|
RowHeadersVisible = false,
|
|
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill
|
|
};
|
|
|
|
public CongressTradingForm(CongressRepository repo, WorkerEngine engine, IPortfolioService portfolio, LoggingService logger)
|
|
{
|
|
_repo = repo;
|
|
_engine = engine;
|
|
_portfolio = portfolio;
|
|
_logger = logger;
|
|
|
|
Text = "Congress Trading [CT]";
|
|
Width = 900;
|
|
Height = 600;
|
|
StartPosition = FormStartPosition.CenterScreen;
|
|
MinimumSize = new Size(600, 400);
|
|
BuildLayout();
|
|
}
|
|
|
|
private void BuildLayout()
|
|
{
|
|
var title = new Label
|
|
{
|
|
Text = "Congress Trading",
|
|
Font = new Font(Font.FontFamily, 14f, FontStyle.Bold),
|
|
Location = new Point(18, 20),
|
|
AutoSize = true
|
|
};
|
|
|
|
var posLabel = new Label
|
|
{
|
|
Text = "Offene Positionen (Modul CT):",
|
|
Location = new Point(20, 225),
|
|
AutoSize = true
|
|
};
|
|
|
|
_positions.Size = new Size(ClientSize.Width - 40, ClientSize.Height - 270);
|
|
|
|
_btnRefresh.Click += async (_, _) => await RefreshStatsAsync();
|
|
_btnScrape.Click += async (_, _) => await TriggerScrapeAsync();
|
|
|
|
Controls.Add(title);
|
|
Controls.Add(_lblTrades);
|
|
Controls.Add(_lblMembers);
|
|
Controls.Add(_btnRefresh);
|
|
Controls.Add(_btnScrape);
|
|
Controls.Add(_lblStatus);
|
|
Controls.Add(posLabel);
|
|
Controls.Add(_positions);
|
|
}
|
|
|
|
protected override async void OnShown(EventArgs e)
|
|
{
|
|
base.OnShown(e);
|
|
await RefreshStatsAsync();
|
|
}
|
|
|
|
private async Task RefreshStatsAsync()
|
|
{
|
|
try
|
|
{
|
|
var trades = await _repo.GetTradeCountAsync();
|
|
var members = await _repo.GetMemberCountAsync();
|
|
_lblTrades.Text = $"Trades in DB: {trades:N0}";
|
|
_lblMembers.Text = $"Mitglieder in DB: {members:N0}";
|
|
|
|
var positions = await _portfolio.GetPositionsAsync(CongressTradingModule.LogTag);
|
|
_positions.DataSource = positions
|
|
.Select(p => new { p.Symbol, Stück = p.Quantity, Ø_Kurs = p.AvgPrice, Wert = p.Notional })
|
|
.ToList();
|
|
|
|
_lblStatus.Text = $"Aktualisiert: {DateTime.Now:HH:mm:ss}";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_lblTrades.Text = "Trades in DB: n/v";
|
|
_lblMembers.Text = "Mitglieder in DB: n/v";
|
|
_lblStatus.Text = $"DB nicht erreichbar: {ex.Message}";
|
|
_logger.Warn(CongressTradingModule.LogTag, $"Kennzahlen konnten nicht geladen werden: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private async Task TriggerScrapeAsync()
|
|
{
|
|
try
|
|
{
|
|
_lblStatus.Text = "Scrape angestoßen...";
|
|
await _engine.TriggerWorkerAsync(ScrapeWorkerName);
|
|
_logger.Info(CongressTradingModule.LogTag, "Scrape-Worker manuell ausgelöst (aus Modul-Fenster).");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_lblStatus.Text = $"Scrape fehlgeschlagen: {ex.Message}";
|
|
_logger.Error(CongressTradingModule.LogTag, "Manueller Scrape-Trigger fehlgeschlagen.", ex);
|
|
}
|
|
}
|
|
}
|