diff --git a/src/KArtSell.Modules.ModelOperations/ShadowRun/ReplayEngine.cs b/src/KArtSell.Modules.ModelOperations/ShadowRun/ReplayEngine.cs index b5537c34..8ebee3a8 100644 --- a/src/KArtSell.Modules.ModelOperations/ShadowRun/ReplayEngine.cs +++ b/src/KArtSell.Modules.ModelOperations/ShadowRun/ReplayEngine.cs @@ -79,27 +79,48 @@ public sealed class ReplayEngine( var daySignals = await GenerateSignalsAsync(modelId, session, ohlcvBars, cancellationToken); signals.AddRange(daySignals); - // Convert signals to orders + // Convert signals to orders with dynamic position sizing var dayOrders = daySignals - .Select(s => new Order( - OrderId: Guid.NewGuid(), - PlacedDate: session, - FilledDate: session, // Market order filled same day - Ticker: s.Ticker, - Action: s.Action, - Quantity: 100, // Simplified: fixed quantity - InitialPrice: GetClosePrice(session, s.Ticker, ohlcvBars), - FilledPrice: GetClosePrice(session, s.Ticker, ohlcvBars))) + .Select(s => + { + var closePrice = GetClosePrice(session, s.Ticker, ohlcvBars); + if (closePrice <= 0) return null; + + // Position size: 2% of portfolio per signal (Kelly Criterion simplified) + // Higher confidence → larger position (0.5x to 1.5x multiplier) + var riskPercentage = 0.02m * s.Confidence * 2m; // Ranges 0.01-0.03 + var targetCash = currentPortfolio.TotalValue * riskPercentage; + var quantity = Math.Max(1L, (long)(targetCash / closePrice)); + + return new Order( + OrderId: Guid.NewGuid(), + PlacedDate: session, + FilledDate: session, + Ticker: s.Ticker, + Action: s.Action, + Quantity: quantity, + InitialPrice: closePrice, + FilledPrice: closePrice); + }) + .Where(o => o != null) + .Cast() .ToList(); orders.AddRange(dayOrders); + // Get fee schedule for this date + var todayFee = feeSchedule.FirstOrDefault(f => f.EffectiveDate <= session); + var feePercent = todayFee?.TransactionFeePercent ?? 0.001m; // 0.1% default + // Update portfolio foreach (var order in dayOrders) { if (order.FilledPrice.HasValue) { var cost = order.Quantity * order.FilledPrice.Value; + var fees = cost * feePercent; + var totalCost = cost + fees; + switch (order.Action) { case SignalAction.Buy: @@ -107,7 +128,7 @@ public sealed class ReplayEngine( currentPortfolio.Positions[order.Ticker] = existing + order.Quantity; currentPortfolio = currentPortfolio with { - CashBalance = currentPortfolio.CashBalance - cost + CashBalance = currentPortfolio.CashBalance - totalCost }; break; case SignalAction.Sell: @@ -116,7 +137,7 @@ public sealed class ReplayEngine( currentPortfolio.Positions[order.Ticker] = Math.Max(0, current - order.Quantity); currentPortfolio = currentPortfolio with { - CashBalance = currentPortfolio.CashBalance + cost + CashBalance = currentPortfolio.CashBalance + cost - fees // Sell proceeds minus fees }; break; }