Eigentumsregeln 3 und 4: gebundener Ausfuehrungszugang und eine Grenze mit Zaehnen
Regel 3 verlangt, dass die Buchzuordnung serverseitig gesetzt wird und TradeSignal.SourceModule nicht vertrauenswuerdig ist. Bisher war nur der unbekannte Name abgefangen; ein Modul konnte weiterhin das Kuerzel eines ANDEREN registrierten Moduls eintragen und dessen Cap, dessen Konzentration und dessen GuV belasten. Jedes Modul bekommt jetzt einen an sein Buch gebundenen IModuleExecution-Zugang statt des gemeinsamen IExecutionService. Der Zugang ueberschreibt die Herkunft mit seinem eigenen Buch - was im Signal steht, spielt keine Rolle mehr. Der Zugang steht ausdruecklich NICHT im Container: was dort steht, kann jedes Modul aufloesen, auch den Zugang eines anderen. Der Host uebergibt stattdessen eine Fabrik beim Registrieren (neue Ueberladung von IModule.RegisterServices mit Vorgabe, damit ein nicht handelndes Modul nichts davon wissen muss), und das Modul reicht den erzeugten Zugang direkt an seine Strategie. Regel 4 - Module bekommen den Broker nicht per DI - war eingehalten, aber nicht erzwungen. Jetzt zwei Tests ueber alle vier Modul-Assemblies: der eine prueft per Reflection Konstruktorparameter, Felder und Eigenschaften auf IBrokerClient, IBrokerPortfolioReader und IExecutionService; der andere durchsucht die Modulquellen nach dem zweiten Weg, dem Griff in den Container. Beide Gegenproben mit einem absichtlichen Regelbruch gefahren: jeder Test schlaegt bei genau seinem Weg an. Getrennte Container je Modul waeren der harte Weg, aendern aber die Modularchitektur. Der Fall, um den es geht, ist ein NEUES Modul, das den Core versehentlich umgeht - und den faengt der Test beim Uebersetzen. 404 Tests gruen, auf Windows und auf der Test-VM. Behebt #474, damit ist auch #302 vollstaendig. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,20 @@ public interface IModule
|
||||
/// </summary>
|
||||
void RegisterServices(IServiceCollection services, IConfiguration configuration);
|
||||
|
||||
/// <summary>
|
||||
/// Wie oben, zusätzlich mit der Fabrik für den <b>an das Buch des Moduls gebundenen</b>
|
||||
/// Ausführungszugang. Der Host ruft diese Fassung; die Vorgabe reicht an die einfache weiter,
|
||||
/// damit ein Modul, das gar nicht handelt, nichts davon wissen muss.
|
||||
///
|
||||
/// <para>Ein handelndes Modul überschreibt sie und gibt den Zugang an seine Strategie weiter –
|
||||
/// er gehört ausdrücklich <b>nicht</b> in den Container. Was dort steht, kann jedes Modul
|
||||
/// auflösen, auch der Zugang eines anderen, und damit auf dessen Buch handeln
|
||||
/// (Kapitalmodell §2 Regel 3).</para>
|
||||
/// </summary>
|
||||
void RegisterServices(IServiceCollection services, IConfiguration configuration,
|
||||
Trading.ModuleExecutionFactory execution)
|
||||
=> RegisterServices(services, configuration);
|
||||
|
||||
/// <summary>
|
||||
/// Registriert die UI-Ansichten des Moduls bei der Shell (Launcher). Die Shell öffnet jede Ansicht
|
||||
/// auf Wunsch in einem eigenen Fenster; Module liefern eigene Forms, die Shell bleibt modul-agnostisch.
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
using IBKRTrader.Core.Capital;
|
||||
|
||||
namespace IBKRTrader.Core.Trading;
|
||||
|
||||
/// <summary>
|
||||
/// Der Ausführungszugang eines <b>einzelnen</b> Moduls – fest an dessen Buch gebunden.
|
||||
///
|
||||
/// <para><b>Warum nicht einfach <see cref="IExecutionService"/>.</b> Dort trägt das Modul seine
|
||||
/// Herkunft selbst im Signal ein (<c>TradeSignal.SourceModule</c>), und ein Feld, das der
|
||||
/// Absender füllt, ist kein Ausweis. Ein Modul könnte das Kürzel eines anderen eintragen; die
|
||||
/// Position liefe dann auf dessen Cap, dessen Konzentration und dessen GuV. Kein heutiges Modul
|
||||
/// tut das – aber das Kapitalmodell verlangt in §2 Regel 3 ausdrücklich, dass die Zuordnung
|
||||
/// <i>serverseitig</i> gesetzt wird, und eine Regel, auf deren Einhaltung man hofft, ist keine.</para>
|
||||
///
|
||||
/// <para>Der Host erzeugt je Modul genau eine Instanz und übergibt sie beim Registrieren. Sie
|
||||
/// steht <b>nicht</b> im gemeinsamen Container: Ein Modul kann den Zugang eines anderen deshalb
|
||||
/// nicht auflösen.</para>
|
||||
/// </summary>
|
||||
public interface IModuleExecution
|
||||
{
|
||||
/// <summary>Das Buch, auf das jede Ausführung dieses Zugangs läuft.</summary>
|
||||
BookId Book { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Übergibt ein Signal an die Ausführungs-Pipeline des Cores. Die Herkunft wird dabei
|
||||
/// überschrieben – was im Signal steht, spielt keine Rolle.
|
||||
/// </summary>
|
||||
Task<ExecutionResult> ExecuteAsync(TradeSignal signal, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bindet ein Modul an sein Buch: setzt die Herkunft und reicht weiter.
|
||||
///
|
||||
/// <para>Bewusst kein Filter und keine zweite Prüfung – der Zugang soll nichts anderes tun, als
|
||||
/// das eine Feld zu setzen, das ein Modul nicht selbst bestimmen darf. Alles Weitere entscheidet
|
||||
/// die Pipeline, für jedes Modul gleich.</para>
|
||||
/// </summary>
|
||||
public sealed class ModuleExecution : IModuleExecution
|
||||
{
|
||||
private readonly IExecutionService _inner;
|
||||
|
||||
public ModuleExecution(BookId book, IExecutionService inner)
|
||||
{
|
||||
Book = book;
|
||||
_inner = inner;
|
||||
}
|
||||
|
||||
public BookId Book { get; }
|
||||
|
||||
public Task<ExecutionResult> ExecuteAsync(TradeSignal signal, CancellationToken ct = default) =>
|
||||
_inner.ExecuteAsync(signal with { SourceModule = Book.Value }, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Erzeugt den gebundenen Ausführungszugang eines Moduls – erst dann, wenn der Container steht.
|
||||
///
|
||||
/// <para>Beim Registrieren gibt es noch keinen <see cref="IServiceProvider"/>; das Modul bekommt
|
||||
/// deshalb die Fabrik und ruft sie in seiner eigenen Dienst-Registrierung auf. Der Zugang landet
|
||||
/// so <b>in der Strategie des Moduls</b> statt im Container – und ist damit für kein anderes
|
||||
/// Modul auflösbar.</para>
|
||||
/// </summary>
|
||||
public sealed class ModuleExecutionFactory
|
||||
{
|
||||
public ModuleExecutionFactory(BookId book) => Book = book;
|
||||
|
||||
/// <summary>Das Buch, an das jeder erzeugte Zugang gebunden ist.</summary>
|
||||
public BookId Book { get; }
|
||||
|
||||
public IModuleExecution Create(IServiceProvider services) =>
|
||||
new ModuleExecution(Book, (IExecutionService)services.GetService(typeof(IExecutionService))!);
|
||||
}
|
||||
@@ -69,7 +69,11 @@ public static class AppHostBuilder
|
||||
foreach (var module in modules)
|
||||
{
|
||||
services.AddSingleton(module);
|
||||
module.RegisterServices(services, context.Configuration);
|
||||
// Der Ausführungszugang wird dem Modul übergeben, statt im Container zu
|
||||
// stehen: was dort steht, kann jedes Modul auflösen - auch den Zugang eines
|
||||
// anderen Moduls und damit dessen Buch.
|
||||
module.RegisterServices(services, context.Configuration,
|
||||
new ModuleExecutionFactory(BookId.ForModule(BookNameOf(module))));
|
||||
}
|
||||
configureExtra?.Invoke(services, context.Configuration);
|
||||
})
|
||||
|
||||
@@ -26,7 +26,18 @@ public sealed class CongressTradingModule : IModule
|
||||
public string Name => "CongressTrading";
|
||||
public string DbPrefix => "ct_";
|
||||
|
||||
public void RegisterServices(IServiceCollection services, IConfiguration configuration)
|
||||
public void RegisterServices(IServiceCollection services, IConfiguration configuration) =>
|
||||
Register(services, configuration, execution: null);
|
||||
|
||||
/// <summary>
|
||||
/// Der Weg, den der Host nimmt: mit dem an das Buch CT gebundenen Ausführungszugang.
|
||||
/// </summary>
|
||||
public void RegisterServices(IServiceCollection services, IConfiguration configuration,
|
||||
ModuleExecutionFactory execution) =>
|
||||
Register(services, configuration, execution);
|
||||
|
||||
private void Register(IServiceCollection services, IConfiguration configuration,
|
||||
ModuleExecutionFactory? execution)
|
||||
{
|
||||
// Modul-Persistenz: eigener DbContext (ct_-Tabellen) in derselben MariaDB.
|
||||
var conn = ServiceCollectionExtensions.EffectiveConnectionString(configuration["Database:MySqlConnectionString"]);
|
||||
@@ -34,7 +45,13 @@ public sealed class CongressTradingModule : IModule
|
||||
|
||||
services.AddSingleton<CongressRepository>();
|
||||
services.AddSingleton<CapitolTradesScraper>();
|
||||
services.AddSingleton<Strategy.CongressTradingStrategy>();
|
||||
// Die Strategie bekommt den an CT gebundenen Ausführungszugang - nicht den
|
||||
// gemeinsamen IExecutionService. Damit kann dieses Modul auf kein fremdes Buch handeln,
|
||||
// auch nicht durch einen Fehler beim Setzen von SourceModule.
|
||||
services.AddSingleton(sp => new Strategy.CongressTradingStrategy(
|
||||
execution?.Create(sp) ?? throw new InvalidOperationException(
|
||||
"CongressTrading braucht den gebundenen Ausführungszugang des Hosts."),
|
||||
sp.GetRequiredService<LoggingService>()));
|
||||
|
||||
services.AddSingleton<CongressHistoryImportWorker>();
|
||||
services.AddSingleton<CongressScrapeWorker>();
|
||||
|
||||
@@ -6,15 +6,16 @@ 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"/>.
|
||||
/// <see cref="TradeSignal"/> um und übergibt es dem an das Buch CT gebundenen
|
||||
/// <see cref="IModuleExecution"/> des Cores.
|
||||
/// Risiko, Kurs, Order und Buchung macht der Core – das Modul liefert nur die Absicht.
|
||||
/// </summary>
|
||||
public class CongressTradingStrategy
|
||||
{
|
||||
private readonly IExecutionService _execution;
|
||||
private readonly IModuleExecution _execution;
|
||||
private readonly LoggingService _logger;
|
||||
|
||||
public CongressTradingStrategy(IExecutionService execution, LoggingService logger)
|
||||
public CongressTradingStrategy(IModuleExecution execution, LoggingService logger)
|
||||
{
|
||||
_execution = execution;
|
||||
_logger = logger;
|
||||
@@ -31,6 +32,9 @@ public class CongressTradingStrategy
|
||||
{
|
||||
Symbol = trade.Ticker,
|
||||
Side = side.Value,
|
||||
// Pflichtfeld des Signals, aber NICHT massgeblich: der gebundene Zugang setzt die
|
||||
// Herkunft serverseitig und ueberschreibt, was hier steht. Solange TradeSignal das
|
||||
// Feld verlangt, bleibt es - entfallen kann es erst, wenn kein Aufrufer es mehr fuellt.
|
||||
SourceModule = CongressTradingModule.LogTag,
|
||||
Reason = $"Kongress-{trade.TradeType}: {trade.MemberBioId} / {trade.IssuerName}"
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user