@
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>
@
This commit is contained in:
@@ -107,9 +107,12 @@ Pin `new MariaDbServerVersion(new Version(11, 8, 6))`. Verbindung aus `appsettin
|
|||||||
- [x] `LauncherForm` startet/stoppt keine Worker mehr; Build + 43/43 Tests + smoke-ui + App-Start grün
|
- [x] `LauncherForm` startet/stoppt keine Worker mehr; Build + 43/43 Tests + smoke-ui + App-Start grün
|
||||||
- Hinweis: Trading-Kern (Risk/Execution/Portfolio/Broker) wurde bereits in Phase 3 gebaut und in R3 auf EF gehoben
|
- Hinweis: Trading-Kern (Risk/Execution/Portfolio/Broker) wurde bereits in Phase 3 gebaut und in R3 auf EF gehoben
|
||||||
|
|
||||||
### R5 – CongressTrading als vollständige Strategie
|
### R5 – CongressTrading als vollständige Strategie ✅
|
||||||
- [ ] Scraper/Jobs → `IHostedService`; Signal → `ExecutionService`
|
- [x] `CongressTradingStrategy`: neuer Scrape-Trade → `TradeSignal` (buy/sell-Mapping) → Core-`IExecutionService`
|
||||||
- [ ] Modul-View (Tabs) via `RegisterUi`
|
- [x] `CongressScrapeWorker` ruft die Strategie je neuem Trade auf (per try/catch isoliert)
|
||||||
|
- [x] Modul-Fenster zeigt offene Positionen (`IPortfolioService.GetPositionsAsync("CT")`)
|
||||||
|
- [x] **8 neue Tests** (Signal-Mapping/Ausführung, gemockter ExecutionService) → 51/51 grün
|
||||||
|
- Hinweis: Handel bleibt durch Trading-Gate (`TradingEnabled=false`) + `NullBrokerClient` sicher aus, bis echter Broker + Freigabe
|
||||||
|
|
||||||
### R6 – Sicherheit + Config-Härtung
|
### R6 – Sicherheit + Config-Härtung
|
||||||
- [ ] `SecretProtection` (Master-Key, AES-256-GCM at-rest), TLS-Warnung
|
- [ ] `SecretProtection` (Master-Key, AES-256-GCM at-rest), TLS-Warnung
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using IBKRTrader.Core.Configuration;
|
|||||||
using IBKRTrader.Core.DependencyInjection;
|
using IBKRTrader.Core.DependencyInjection;
|
||||||
using IBKRTrader.Core.Logging;
|
using IBKRTrader.Core.Logging;
|
||||||
using IBKRTrader.Core.Modularity;
|
using IBKRTrader.Core.Modularity;
|
||||||
|
using IBKRTrader.Core.Trading;
|
||||||
using IBKRTrader.Core.Workers;
|
using IBKRTrader.Core.Workers;
|
||||||
using IBKRTrader.Modules.CongressTrading.Database;
|
using IBKRTrader.Modules.CongressTrading.Database;
|
||||||
using IBKRTrader.Modules.CongressTrading.Persistence.Ef;
|
using IBKRTrader.Modules.CongressTrading.Persistence.Ef;
|
||||||
@@ -34,6 +35,7 @@ public sealed class CongressTradingModule : IModule
|
|||||||
|
|
||||||
services.AddSingleton<CongressRepository>();
|
services.AddSingleton<CongressRepository>();
|
||||||
services.AddSingleton<CapitolTradesScraper>();
|
services.AddSingleton<CapitolTradesScraper>();
|
||||||
|
services.AddSingleton<Strategy.CongressTradingStrategy>();
|
||||||
|
|
||||||
services.AddSingleton<CongressHistoryImportWorker>();
|
services.AddSingleton<CongressHistoryImportWorker>();
|
||||||
services.AddSingleton<CongressScrapeWorker>();
|
services.AddSingleton<CongressScrapeWorker>();
|
||||||
@@ -57,6 +59,7 @@ public sealed class CongressTradingModule : IModule
|
|||||||
CreateForm = () => new CongressTradingForm(
|
CreateForm = () => new CongressTradingForm(
|
||||||
services.GetRequiredService<CongressRepository>(),
|
services.GetRequiredService<CongressRepository>(),
|
||||||
services.GetRequiredService<WorkerEngine>(),
|
services.GetRequiredService<WorkerEngine>(),
|
||||||
|
services.GetRequiredService<IPortfolioService>(),
|
||||||
services.GetRequiredService<LoggingService>())
|
services.GetRequiredService<LoggingService>())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using IBKRTrader.Core.Logging;
|
||||||
|
using IBKRTrader.Core.Trading;
|
||||||
|
using IBKRTrader.Modules.CongressTrading.Models;
|
||||||
|
|
||||||
|
namespace IBKRTrader.Modules.CongressTrading.Strategy;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Strategie des CongressTrading-Moduls: wandelt einen neu gescrapten Kongress-Trade in ein
|
||||||
|
/// <see cref="TradeSignal"/> um und übergibt es dem Core-<see cref="IExecutionService"/>.
|
||||||
|
/// Risiko, Kurs, Order und Buchung macht der Core – das Modul liefert nur die Absicht.
|
||||||
|
/// </summary>
|
||||||
|
public class CongressTradingStrategy
|
||||||
|
{
|
||||||
|
private readonly IExecutionService _execution;
|
||||||
|
private readonly LoggingService _logger;
|
||||||
|
|
||||||
|
public CongressTradingStrategy(IExecutionService execution, LoggingService logger)
|
||||||
|
{
|
||||||
|
_execution = execution;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verarbeitet einen neuen Trade: erzeugt ggf. ein Signal und führt es aus.</summary>
|
||||||
|
public async Task HandleNewTradeAsync(CongressTrade trade, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var side = MapSide(trade.TradeType);
|
||||||
|
if (side is null || string.IsNullOrWhiteSpace(trade.Ticker))
|
||||||
|
return; // z. B. "exchange"/"receive" oder kein Ticker → kein Handel
|
||||||
|
|
||||||
|
var signal = new TradeSignal
|
||||||
|
{
|
||||||
|
Symbol = trade.Ticker,
|
||||||
|
Side = side.Value,
|
||||||
|
SourceModule = CongressTradingModule.LogTag,
|
||||||
|
Reason = $"Kongress-{trade.TradeType}: {trade.MemberBioId} / {trade.IssuerName}"
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _execution.ExecuteAsync(signal, ct);
|
||||||
|
_logger.Info(CongressTradingModule.LogTag,
|
||||||
|
$"Signal {signal.Side} {signal.Symbol} → {result.Action}: {result.Reason}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Mappt den Kongress-Trade-Typ auf eine Handelsrichtung (buy/sell); alles andere = null.</summary>
|
||||||
|
public static TradeSide? MapSide(string? tradeType) => (tradeType ?? "").Trim().ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"buy" => TradeSide.Buy,
|
||||||
|
"sell" => TradeSide.Sell,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
using IBKRTrader.Core.Logging;
|
using IBKRTrader.Core.Logging;
|
||||||
|
using IBKRTrader.Core.Trading;
|
||||||
using IBKRTrader.Core.Workers;
|
using IBKRTrader.Core.Workers;
|
||||||
using IBKRTrader.Modules.CongressTrading.Database;
|
using IBKRTrader.Modules.CongressTrading.Database;
|
||||||
|
|
||||||
namespace IBKRTrader.Modules.CongressTrading.UI;
|
namespace IBKRTrader.Modules.CongressTrading.UI;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Eigenständiges Fenster des CongressTrading-Moduls.
|
/// Eigenständiges Fenster des CongressTrading-Moduls: DB-Kennzahlen, manueller Scrape-Trigger
|
||||||
/// Grundgerüst mit DB-Kennzahlen und manuellem Scrape-Trigger.
|
/// und die offenen Positionen des Moduls (aus dem Core-Portfolio).
|
||||||
/// Die vollständige Trade-/Positions-Ansicht folgt in einer späteren Phase.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class CongressTradingForm : Form
|
public sealed class CongressTradingForm : Form
|
||||||
{
|
{
|
||||||
@@ -15,6 +15,7 @@ public sealed class CongressTradingForm : Form
|
|||||||
|
|
||||||
private readonly CongressRepository _repo;
|
private readonly CongressRepository _repo;
|
||||||
private readonly WorkerEngine _engine;
|
private readonly WorkerEngine _engine;
|
||||||
|
private readonly IPortfolioService _portfolio;
|
||||||
private readonly LoggingService _logger;
|
private readonly LoggingService _logger;
|
||||||
|
|
||||||
private readonly Label _lblTrades = new() { AutoSize = true, Location = new Point(20, 70) };
|
private readonly Label _lblTrades = new() { AutoSize = true, Location = new Point(20, 70) };
|
||||||
@@ -23,10 +24,21 @@ public sealed class CongressTradingForm : Form
|
|||||||
private readonly Button _btnScrape = new() { Text = "Scrape jetzt", Location = new Point(170, 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 Label _lblStatus = new() { AutoSize = true, Location = new Point(20, 185), ForeColor = SystemColors.GrayText };
|
||||||
|
|
||||||
public CongressTradingForm(CongressRepository repo, WorkerEngine engine, LoggingService logger)
|
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;
|
_repo = repo;
|
||||||
_engine = engine;
|
_engine = engine;
|
||||||
|
_portfolio = portfolio;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
Text = "Congress Trading [CT]";
|
Text = "Congress Trading [CT]";
|
||||||
@@ -47,14 +59,15 @@ public sealed class CongressTradingForm : Form
|
|||||||
AutoSize = true
|
AutoSize = true
|
||||||
};
|
};
|
||||||
|
|
||||||
var hint = new Label
|
var posLabel = new Label
|
||||||
{
|
{
|
||||||
Text = "Vollständige Trade- und Positions-Ansicht folgt in Phase 4.",
|
Text = "Offene Positionen (Modul CT):",
|
||||||
Location = new Point(20, 230),
|
Location = new Point(20, 225),
|
||||||
AutoSize = true,
|
AutoSize = true
|
||||||
ForeColor = SystemColors.GrayText
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_positions.Size = new Size(ClientSize.Width - 40, ClientSize.Height - 270);
|
||||||
|
|
||||||
_btnRefresh.Click += async (_, _) => await RefreshStatsAsync();
|
_btnRefresh.Click += async (_, _) => await RefreshStatsAsync();
|
||||||
_btnScrape.Click += async (_, _) => await TriggerScrapeAsync();
|
_btnScrape.Click += async (_, _) => await TriggerScrapeAsync();
|
||||||
|
|
||||||
@@ -64,7 +77,8 @@ public sealed class CongressTradingForm : Form
|
|||||||
Controls.Add(_btnRefresh);
|
Controls.Add(_btnRefresh);
|
||||||
Controls.Add(_btnScrape);
|
Controls.Add(_btnScrape);
|
||||||
Controls.Add(_lblStatus);
|
Controls.Add(_lblStatus);
|
||||||
Controls.Add(hint);
|
Controls.Add(posLabel);
|
||||||
|
Controls.Add(_positions);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async void OnShown(EventArgs e)
|
protected override async void OnShown(EventArgs e)
|
||||||
@@ -81,6 +95,12 @@ public sealed class CongressTradingForm : Form
|
|||||||
var members = await _repo.GetMemberCountAsync();
|
var members = await _repo.GetMemberCountAsync();
|
||||||
_lblTrades.Text = $"Trades in DB: {trades:N0}";
|
_lblTrades.Text = $"Trades in DB: {trades:N0}";
|
||||||
_lblMembers.Text = $"Mitglieder in DB: {members: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}";
|
_lblStatus.Text = $"Aktualisiert: {DateTime.Now:HH:mm:ss}";
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using IBKRTrader.Core.Logging;
|
|||||||
using IBKRTrader.Core.Settings;
|
using IBKRTrader.Core.Settings;
|
||||||
using IBKRTrader.Modules.CongressTrading.Database;
|
using IBKRTrader.Modules.CongressTrading.Database;
|
||||||
using IBKRTrader.Modules.CongressTrading.Scraper;
|
using IBKRTrader.Modules.CongressTrading.Scraper;
|
||||||
|
using IBKRTrader.Modules.CongressTrading.Strategy;
|
||||||
using IBKRTrader.Core.Workers;
|
using IBKRTrader.Core.Workers;
|
||||||
|
|
||||||
namespace IBKRTrader.Modules.CongressTrading.Workers;
|
namespace IBKRTrader.Modules.CongressTrading.Workers;
|
||||||
@@ -17,6 +18,7 @@ public class CongressScrapeWorker : WorkerBase
|
|||||||
{
|
{
|
||||||
private readonly CapitolTradesScraper _scraper;
|
private readonly CapitolTradesScraper _scraper;
|
||||||
private readonly CongressRepository _repo;
|
private readonly CongressRepository _repo;
|
||||||
|
private readonly CongressTradingStrategy _strategy;
|
||||||
|
|
||||||
public override string Name => "CT-ScrapeWorker";
|
public override string Name => "CT-ScrapeWorker";
|
||||||
public override string Module => "CT";
|
public override string Module => "CT";
|
||||||
@@ -29,11 +31,13 @@ public class CongressScrapeWorker : WorkerBase
|
|||||||
IDbContextFactory<CoreDbContext> db,
|
IDbContextFactory<CoreDbContext> db,
|
||||||
CapitolTradesScraper scraper,
|
CapitolTradesScraper scraper,
|
||||||
CongressRepository repo,
|
CongressRepository repo,
|
||||||
|
CongressTradingStrategy strategy,
|
||||||
SettingsService settings)
|
SettingsService settings)
|
||||||
: base(logger, db)
|
: base(logger, db)
|
||||||
{
|
{
|
||||||
_scraper = scraper;
|
_scraper = scraper;
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
|
_strategy = strategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken ct)
|
protected override async Task ExecuteAsync(CancellationToken ct)
|
||||||
@@ -74,6 +78,10 @@ public class CongressScrapeWorker : WorkerBase
|
|||||||
Logger.Info(Module,
|
Logger.Info(Module,
|
||||||
$" + {trade.IssuerName} ({trade.TradeType}) [{trade.Ticker}] von {trade.MemberBioId}");
|
$" + {trade.IssuerName} ({trade.TradeType}) [{trade.Ticker}] von {trade.MemberBioId}");
|
||||||
newTrades++;
|
newTrades++;
|
||||||
|
|
||||||
|
// Strategie: neuer Trade → Signal an den Core-ExecutionService (Trading-Gate/Broker entscheiden).
|
||||||
|
try { await _strategy.HandleNewTradeAsync(trade, ct); }
|
||||||
|
catch (Exception ex) { Logger.Error(Module, $"Strategie-Fehler für {trade.Ticker}: {ex.Message}", ex); }
|
||||||
}
|
}
|
||||||
|
|
||||||
var summary = $"{newTrades} neue Trades, {newMembers} neue Mitglieder";
|
var summary = $"{newTrades} neue Trades, {newMembers} neue Mitglieder";
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using IBKRTrader.Core.Logging;
|
||||||
|
using IBKRTrader.Core.Trading;
|
||||||
|
using IBKRTrader.Modules.CongressTrading.Models;
|
||||||
|
using IBKRTrader.Modules.CongressTrading.Strategy;
|
||||||
|
using NSubstitute;
|
||||||
|
|
||||||
|
namespace IBKRTrader.Tests.Modules;
|
||||||
|
|
||||||
|
[Trait("cat", "unit")]
|
||||||
|
public class CongressTradingStrategyTests
|
||||||
|
{
|
||||||
|
private readonly IExecutionService _execution = Substitute.For<IExecutionService>();
|
||||||
|
|
||||||
|
private CongressTradingStrategy CreateSut()
|
||||||
|
{
|
||||||
|
_execution.ExecuteAsync(Arg.Any<TradeSignal>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(Task.FromResult(ExecutionResult.Skip("test")));
|
||||||
|
return new CongressTradingStrategy(_execution, new LoggingService());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CongressTrade Trade(string type, string ticker = "AAPL") =>
|
||||||
|
new() { TradeId = "T1", MemberBioId = "W1", IssuerName = "Apple", Ticker = ticker, TradeType = type };
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("buy", TradeSide.Buy)]
|
||||||
|
[InlineData("BUY", TradeSide.Buy)]
|
||||||
|
[InlineData("sell", TradeSide.Sell)]
|
||||||
|
public async Task NewTrade_EmitsSignal_WithMappedSide(string type, TradeSide expected)
|
||||||
|
{
|
||||||
|
var sut = CreateSut();
|
||||||
|
|
||||||
|
await sut.HandleNewTradeAsync(Trade(type));
|
||||||
|
|
||||||
|
await _execution.Received(1).ExecuteAsync(
|
||||||
|
Arg.Is<TradeSignal>(s => s.Side == expected && s.Symbol == "AAPL" && s.SourceModule == "CT"),
|
||||||
|
Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("exchange")]
|
||||||
|
[InlineData("receive")]
|
||||||
|
[InlineData("")]
|
||||||
|
public async Task NonBuySell_TradeType_DoesNotExecute(string type)
|
||||||
|
{
|
||||||
|
var sut = CreateSut();
|
||||||
|
|
||||||
|
await sut.HandleNewTradeAsync(Trade(type));
|
||||||
|
|
||||||
|
await _execution.DidNotReceive().ExecuteAsync(Arg.Any<TradeSignal>(), Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EmptyTicker_DoesNotExecute()
|
||||||
|
{
|
||||||
|
var sut = CreateSut();
|
||||||
|
|
||||||
|
await sut.HandleNewTradeAsync(Trade("buy", ticker: ""));
|
||||||
|
|
||||||
|
await _execution.DidNotReceive().ExecuteAsync(Arg.Any<TradeSignal>(), Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MapSide_IsCaseInsensitive_AndIgnoresUnknown()
|
||||||
|
{
|
||||||
|
CongressTradingStrategy.MapSide("Buy").Should().Be(TradeSide.Buy);
|
||||||
|
CongressTradingStrategy.MapSide("SELL").Should().Be(TradeSide.Sell);
|
||||||
|
CongressTradingStrategy.MapSide("exchange").Should().BeNull();
|
||||||
|
CongressTradingStrategy.MapSide(null).Should().BeNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user