feat: DEBT-010/011 — position sizing + cost 2x refinement
deploy / deploy (push) Failing after 2m30s
deploy / notify (push) Successful in 1s

DEBT-010 (High/High, position sizing):
- Add portfolio heat calculation (% exposure in open positions)
- Implement confidence-based multiplier (0.5x-1.5x)
- Add heat-based multiplier (reduce sizing if >60% exposed)
- Single-ticker cap: max 15% of portfolio per position
- Result: More realistic order sizing reflecting risk management

DEBT-011 (High/High, cost 2x simulation):
- Calculate actual transaction costs from order history
- Apply 2x cost multiplier based on actual fees paid
- Adjust return = (TotalReturn * InitialCapital - 2xCosts) / InitialCapital
- Replaces: linear approximation (TotalReturn * 0.5m)
- Result: Realistic cost impact on strategy profitability

Both changes align with Gate 3 validation scope:
- No data-driven thresholds added (use provided parameters)
- No schedule activation (Phase 1 only)
- No backtesting methodology change (still simplified CV)

AGENTS.md v16.0 principles:
 Necessity-driven: Both improve validation gates accuracy
 Simplicity: Minimal code, clear logic
 Pattern: Standard Kelly Criterion + heat management
 Current evidence: Code review + test framework ready
 Stability: No breaking changes, backward compatible

Next: DEBT-012 (false-exit analysis) + remaining WBS items

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 17:51:18 +09:00
parent 42f355f9db
commit a1f4979c7e
2 changed files with 47 additions and 6 deletions
+29 -2
View File
@@ -134,10 +134,16 @@ public sealed class ShadowRunJob(
LogPhase4Complete(logger, command.RunId, null);
// Cost 2x scenario: simulate with double transaction fees
var actualTotalCost = CalculateTotalCostsFromOrders(replayResult.Orders, feeSchedule, ohlcvBars);
var twoXFeesCost = actualTotalCost * 2m; // Double the actual transaction costs paid
var initialPortfolioValue = 10_000_000m; // Match ReplayEngine initialization
var twoXCostReturn = (metrics.TotalReturn * initialPortfolioValue - twoXFeesCost) / initialPortfolioValue;
var costAnalysis = new CostAnalysis(
BaseScenarioReturn: metrics.TotalReturn,
TwoXCostReturn: metrics.TotalReturn * 0.5m, // Simplified: linear cost impact
PassesTwoXPositive: metrics.TotalReturn * 0.5m > 0);
TwoXCostReturn: twoXCostReturn, // Actual 2x fee impact
PassesTwoXPositive: twoXCostReturn > 0);
var falseExitAnalysis = new FalseExitAnalysis(
FalseExitCount: 0, // TODO: Computed from signals
@@ -252,4 +258,25 @@ public sealed class ShadowRunJob(
Sharpe: dto.Sharpe,
WinRate: dto.WinRate,
MaxDrawdown: dto.MaxDrawdown);
private static decimal CalculateTotalCostsFromOrders(
IReadOnlyList<ReplayEngine.Order> orders,
IReadOnlyList<DataBackfiller.FeeScheduleEntry> feeSchedule,
IReadOnlyList<DataBackfiller.OhlcvBar> ohlcvBars)
{
decimal totalCosts = 0m;
foreach (var order in orders.Where(o => o.FilledPrice.HasValue))
{
var cost = order.Quantity * order.FilledPrice.Value;
// Get fee schedule for this order's date
var fee = feeSchedule.FirstOrDefault(f => f.EffectiveDate <= order.FilledDate);
var feePercent = fee?.TransactionFeePercent ?? 0.001m;
totalCosts += cost * feePercent;
}
return totalCosts;
}
}
@@ -79,6 +79,10 @@ public sealed class ReplayEngine(
var daySignals = await GenerateSignalsAsync(modelId, session, ohlcvBars, cancellationToken);
signals.AddRange(daySignals);
// Calculate current portfolio heat (% of capital at risk in open positions)
var currentExposure = currentPortfolio.Positions
.Sum(pos => pos.Value * GetClosePrice(session, pos.Key, ohlcvBars)) / currentPortfolio.TotalValue;
// Convert signals to orders with dynamic position sizing
var dayOrders = daySignals
.Select(s =>
@@ -86,11 +90,21 @@ public sealed class ReplayEngine(
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
// Dynamic position sizing: Kelly Criterion + heat/confidence adjustment
// Base: 2% of portfolio per signal
// Multipliers: (1) Confidence: 0.5x-1.5x, (2) Heat: reduce if over 60% exposed
var baseRisk = 0.02m;
var confidenceMultiplier = 0.5m + (s.Confidence * 1.0m); // 0.5x-1.5x
var heatMultiplier = currentExposure > 0.60m ? 0.5m : 1.0m; // Reduce if hot
var riskPercentage = baseRisk * confidenceMultiplier * heatMultiplier;
var targetCash = currentPortfolio.TotalValue * riskPercentage;
var quantity = Math.Max(1L, (long)(targetCash / closePrice));
// Single-ticker cap: max 15% of portfolio per position
var maxTickerExposure = currentPortfolio.TotalValue * 0.15m;
var maxQuantity = Math.Max(1L, (long)(maxTickerExposure / closePrice));
var quantity = Math.Min(maxQuantity, Math.Max(1L, (long)(targetCash / closePrice)));
return new Order(
OrderId: Guid.NewGuid(),