Skip to content

Commit c8faeb4

Browse files
konardclaude
andcommitted
Implement Kelly Criterion for optimal position sizing in TraderBot
This implementation adds the Kelly Criterion algorithm to maximize long-term profit while managing risk through optimal capital allocation. ## Key Features - Kelly Criterion calculator with mathematical accuracy - Dynamic learning from trade history (10+ trades minimum) - Configurable risk limits and safety parameters - Integration with existing trading strategy - Comprehensive test suite with real-world scenarios ## Configuration - UseKellyCriterion: Enable/disable Kelly position sizing - WinProbability: Initial win rate estimate (0.0-1.0) - ProfitLossRatio: Initial profit/loss ratio estimate - KellyFractionLimit: Maximum risk fraction (default 0.25) ## Formula: f = (bp - q) / b Where f = fraction of capital, b = profit/loss ratio, p = win probability, q = loss probability 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 2eecc41 commit c8faeb4

File tree

9 files changed

+542
-5
lines changed

9 files changed

+542
-5
lines changed

csharp/TraderBot/KellyCriterion.cs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
namespace TraderBot;
2+
3+
public static class KellyCriterion
4+
{
5+
/// <summary>
6+
/// Calculates the optimal bet size fraction using the Kelly Criterion formula.
7+
/// Formula: f = (bp - q) / b
8+
/// Where:
9+
/// - f = fraction of capital to bet
10+
/// - b = profit/loss ratio (odds)
11+
/// - p = probability of winning
12+
/// - q = probability of losing (1-p)
13+
/// </summary>
14+
/// <param name="winProbability">Probability of winning (0.0 to 1.0)</param>
15+
/// <param name="profitLossRatio">The ratio of profit to loss (e.g., 2.0 means profit is 2x the loss)</param>
16+
/// <param name="maxFraction">Maximum fraction to limit risk (default 0.25)</param>
17+
/// <returns>The optimal fraction of capital to bet (0.0 to maxFraction)</returns>
18+
public static double CalculateOptimalBetSize(double winProbability, double profitLossRatio, double maxFraction = 0.25)
19+
{
20+
if (winProbability < 0 || winProbability > 1)
21+
throw new ArgumentException("Win probability must be between 0 and 1", nameof(winProbability));
22+
23+
if (profitLossRatio <= 0)
24+
throw new ArgumentException("Profit/loss ratio must be positive", nameof(profitLossRatio));
25+
26+
if (maxFraction <= 0 || maxFraction > 1)
27+
throw new ArgumentException("Max fraction must be between 0 and 1", nameof(maxFraction));
28+
29+
double lossProbability = 1.0 - winProbability;
30+
31+
// Kelly Criterion formula: f = (bp - q) / b
32+
double kellyFraction = (profitLossRatio * winProbability - lossProbability) / profitLossRatio;
33+
34+
// Return 0 if Kelly suggests negative betting (negative expected value)
35+
if (kellyFraction <= 0)
36+
return 0.0;
37+
38+
// Cap at maximum fraction to limit risk
39+
return Math.Min(kellyFraction, maxFraction);
40+
}
41+
42+
/// <summary>
43+
/// Calculates the win probability and profit/loss ratio from historical operations
44+
/// </summary>
45+
/// <param name="operations">List of completed operations</param>
46+
/// <returns>Tuple containing (winProbability, profitLossRatio)</returns>
47+
public static (double WinProbability, double ProfitLossRatio) CalculateHistoricalMetrics(
48+
IEnumerable<(DateTime Date, decimal BuyPrice, decimal SellPrice)> operations)
49+
{
50+
var operationsList = operations.ToList();
51+
if (operationsList.Count < 10) // Need minimum historical data
52+
return (0.5, 1.0); // Default conservative values
53+
54+
var wins = 0;
55+
var totalProfit = 0.0m;
56+
var totalLoss = 0.0m;
57+
58+
foreach (var op in operationsList)
59+
{
60+
var profit = op.SellPrice - op.BuyPrice;
61+
if (profit > 0)
62+
{
63+
wins++;
64+
totalProfit += profit;
65+
}
66+
else if (profit < 0)
67+
{
68+
totalLoss += Math.Abs(profit);
69+
}
70+
}
71+
72+
var winProbability = (double)wins / operationsList.Count;
73+
var avgProfit = wins > 0 ? (double)(totalProfit / wins) : 0.0;
74+
var avgLoss = (operationsList.Count - wins) > 0 ? (double)(totalLoss / (operationsList.Count - wins)) : 1.0;
75+
var profitLossRatio = avgLoss > 0 ? avgProfit / avgLoss : 1.0;
76+
77+
return (winProbability, Math.Max(profitLossRatio, 0.1)); // Minimum ratio to avoid division issues
78+
}
79+
}

csharp/TraderBot/TradingService.cs

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public class TradingService : BackgroundService
3939
protected readonly ConcurrentDictionary<string, OrderState> ActiveSellOrders;
4040
protected readonly ConcurrentDictionary<decimal, long> LotsSets;
4141
protected readonly ConcurrentDictionary<string, decimal> ActiveSellOrderSourcePrice;
42+
protected readonly List<(DateTime Date, decimal BuyPrice, decimal SellPrice)> CompletedOperations;
4243

4344
public TradingService(ILogger<TradingService> logger, InvestApiClient investApi, IHostApplicationLifetime lifetime, TradingSettings settings)
4445
{
@@ -111,6 +112,7 @@ public TradingService(ILogger<TradingService> logger, InvestApiClient investApi,
111112
ActiveSellOrders = new ConcurrentDictionary<string, OrderState>();
112113
LotsSets = new ConcurrentDictionary<decimal, long>();
113114
ActiveSellOrderSourcePrice = new ConcurrentDictionary<string, decimal>();
115+
CompletedOperations = new List<(DateTime, decimal, decimal)>();
114116
LastOperationsCheckpoint = settings.LoadOperationsFrom;
115117
}
116118

@@ -290,7 +292,15 @@ protected void TrySubtractTradesFromOrder(ConcurrentDictionary<string, OrderStat
290292
if (activeOrder.LotsRequested == 0)
291293
{
292294
orders.TryRemove(orderTrades.OrderId, out activeOrder);
293-
ActiveSellOrderSourcePrice.TryRemove(orderTrades.OrderId, out decimal sourcePrice);
295+
296+
// Track completed buy-sell cycle for Kelly Criterion
297+
if (orders == ActiveSellOrders && ActiveSellOrderSourcePrice.TryGetValue(orderTrades.OrderId, out decimal sourcePrice))
298+
{
299+
var sellPrice = MoneyValueToDecimal(activeOrder.InitialSecurityPrice);
300+
TrackCompletedOperation(sourcePrice, sellPrice);
301+
}
302+
303+
ActiveSellOrderSourcePrice.TryRemove(orderTrades.OrderId, out decimal _);
294304
Logger.LogInformation($"Active order removed: {activeOrder}");
295305
}
296306
}
@@ -477,7 +487,7 @@ await marketDataStream.RequestStream.WriteAsync(new MarketDataRequest
477487
{
478488
Logger.LogInformation($"buy activated");
479489
Logger.LogInformation($"bid: {bestBid}, ask: {bestAsk}.");
480-
var lots = (long)(cashBalance / lotPrice);
490+
var lots = CalculateOptimalLotSize(cashBalance, lotPrice);
481491
var marketLotsAtTargetPrice = orderBook.Bids.FirstOrDefault(o => o.Price == bestBid)?.Quantity ?? 0;
482492
Logger.LogInformation($"marketLotsAtTargetPrice: {marketLotsAtTargetPrice}");
483493
var response = await PlaceBuyOrder(lots, bestBid);
@@ -544,7 +554,7 @@ await marketDataStream.RequestStream.WriteAsync(new MarketDataRequest
544554
var lotPrice = bestBid * LotSize;
545555
if (cashBalance > lotPrice)
546556
{
547-
var lots = (long)(cashBalance / lotPrice);
557+
var lots = CalculateOptimalLotSize(cashBalance, lotPrice);
548558
var marketLotsAtTargetPrice = orderBook.Bids.FirstOrDefault(o => o.Price == bestBid)?.Quantity ?? 0;
549559
Logger.LogInformation($"marketLotsAtTargetPrice: {marketLotsAtTargetPrice}");
550560
var response = await PlaceBuyOrder(lots, bestBid);
@@ -652,6 +662,57 @@ private bool IsTimeToBuy()
652662
{
653663
var currentTime = DateTime.UtcNow.TimeOfDay;
654664
return currentTime > MinimumTimeToBuy && currentTime < MaximumTimeToBuy;
665+
}
666+
667+
private long CalculateOptimalLotSize(decimal cashBalance, decimal lotPrice)
668+
{
669+
if (!Settings.UseKellyCriterion)
670+
{
671+
// Use traditional sizing: all available cash
672+
return (long)(cashBalance / lotPrice);
673+
}
674+
675+
double winProbability = Settings.WinProbability;
676+
double profitLossRatio = Settings.ProfitLossRatio;
677+
678+
// If we have enough historical data, calculate metrics dynamically
679+
if (CompletedOperations.Count >= 10)
680+
{
681+
var (historicalWinProb, historicalRatio) = KellyCriterion.CalculateHistoricalMetrics(CompletedOperations);
682+
winProbability = historicalWinProb;
683+
profitLossRatio = historicalRatio;
684+
Logger.LogInformation($"Using historical metrics - Win Probability: {winProbability:F3}, Profit/Loss Ratio: {profitLossRatio:F3}");
685+
}
686+
else
687+
{
688+
Logger.LogInformation($"Using configured metrics - Win Probability: {winProbability:F3}, Profit/Loss Ratio: {profitLossRatio:F3}");
689+
}
690+
691+
var kellyFraction = KellyCriterion.CalculateOptimalBetSize(winProbability, profitLossRatio, Settings.KellyFractionLimit);
692+
var optimalCashToUse = cashBalance * (decimal)kellyFraction;
693+
var lots = (long)Math.Max(1, optimalCashToUse / lotPrice); // Ensure at least 1 lot
694+
695+
Logger.LogInformation($"Kelly Criterion: Fraction={kellyFraction:F3}, OptimalCash={optimalCashToUse:F2}, Lots={lots}");
696+
697+
return lots;
698+
}
699+
700+
private void TrackCompletedOperation(decimal buyPrice, decimal sellPrice)
701+
{
702+
lock (CompletedOperations)
703+
{
704+
CompletedOperations.Add((DateTime.UtcNow, buyPrice, sellPrice));
705+
706+
// Keep only last 100 operations to prevent memory growth
707+
if (CompletedOperations.Count > 100)
708+
{
709+
CompletedOperations.RemoveAt(0);
710+
}
711+
}
712+
713+
var profit = sellPrice - buyPrice;
714+
var profitPercent = (profit / buyPrice) * 100;
715+
Logger.LogInformation($"Operation completed: Buy={buyPrice}, Sell={sellPrice}, Profit={profit:F4} ({profitPercent:F2}%)");
655716
}
656717

657718
private async Task<(decimal, decimal)> GetCashBalance(bool forceRemote = false)

csharp/TraderBot/TradingSettings.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,8 @@ public class TradingSettings
1717
public long EarlySellOwnedLotsDelta { get; set; }
1818
public decimal EarlySellOwnedLotsMultiplier { get; set; }
1919
public DateTime LoadOperationsFrom { get; set; }
20+
public bool UseKellyCriterion { get; set; }
21+
public double WinProbability { get; set; }
22+
public double ProfitLossRatio { get; set; }
23+
public double KellyFractionLimit { get; set; } = 0.25;
2024
}

csharp/TraderBot/appsettings.TMON.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
"MaximumTimeToBuy": "23:59:59",
2525
"EarlySellOwnedLotsDelta": 300000,
2626
"EarlySellOwnedLotsMultiplier": 0,
27-
"LoadOperationsFrom": "2025-03-01T00:00:01.3389860Z"
27+
"LoadOperationsFrom": "2025-03-01T00:00:01.3389860Z",
28+
"UseKellyCriterion": true,
29+
"WinProbability": 0.55,
30+
"ProfitLossRatio": 1.2,
31+
"KellyFractionLimit": 0.25
2832
}
2933
}

csharp/TraderBot/appsettings.TRUR.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
"MaximumTimeToBuy": "14:45:00",
2525
"EarlySellOwnedLotsDelta": 300000,
2626
"EarlySellOwnedLotsMultiplier": 0,
27-
"LoadOperationsFrom": "2025-03-01T00:00:01.3389860Z"
27+
"LoadOperationsFrom": "2025-03-01T00:00:01.3389860Z",
28+
"UseKellyCriterion": true,
29+
"WinProbability": 0.52,
30+
"ProfitLossRatio": 1.1,
31+
"KellyFractionLimit": 0.2
2832
}
2933
}

0 commit comments

Comments
 (0)