feat(bot): add FinlyticBot autonomous paper trading microservice with Alpaca Markets API integration
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FinlyticBot.Util;
|
||||
using FinlyticCore.Models.Trades;
|
||||
using FinlyticCore.Services;
|
||||
|
||||
namespace FinlyticBot.Services;
|
||||
|
||||
public class BotRiskSizingService : IBotRiskSizingService
|
||||
{
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IFinlyticLogger<BotRiskSizingService> _finlyticLogger;
|
||||
|
||||
public BotRiskSizingService(
|
||||
ISettingsService settingsService,
|
||||
IFinlyticLogger<BotRiskSizingService> finlyticLogger)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_finlyticLogger = finlyticLogger;
|
||||
}
|
||||
|
||||
public async Task<SizingResult> EvaluateAndSizeTradeAsync(
|
||||
TradeProposalDto proposal,
|
||||
decimal accountEquity,
|
||||
int currentOpenTradesCount,
|
||||
decimal todayRealizedLossPercent,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(proposal);
|
||||
|
||||
// 1. Check Master Bot Switch
|
||||
bool isEnabled = await _settingsService.GetSettingAsync(SettingKeys.IsEnabled, ct);
|
||||
if (!isEnabled)
|
||||
{
|
||||
return new SizingResult(false, "FinlyticBot is currently disabled in settings.", 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
// 2. Check Daily Drawdown Circuit Breaker
|
||||
double dailyLossLimit = await _settingsService.GetSettingAsync(SettingKeys.DailyLossLimitPercent, ct);
|
||||
if (todayRealizedLossPercent >= (decimal)dailyLossLimit)
|
||||
{
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.BotChannel,
|
||||
"[RiskEngine] Circuit breaker triggered! Today's realized loss {Loss:F2}% >= limit {Limit:F2}%. Rejecting trade {TradeId}.",
|
||||
todayRealizedLossPercent, dailyLossLimit, proposal.TradeId);
|
||||
return new SizingResult(false, $"Daily loss limit reached ({todayRealizedLossPercent:F2}% >= {dailyLossLimit:F2}%).", 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
// 3. Check Max Open Trades Limit
|
||||
int maxOpenTrades = await _settingsService.GetSettingAsync(SettingKeys.MaxOpenTrades, ct);
|
||||
if (currentOpenTradesCount >= maxOpenTrades)
|
||||
{
|
||||
return new SizingResult(false, $"Max concurrent open positions reached ({currentOpenTradesCount}/{maxOpenTrades}).", 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
// 4. Validate CRV (Chance-Risiko-Verhältnis)
|
||||
double minCrv = await _settingsService.GetSettingAsync(SettingKeys.MinCrv, ct);
|
||||
decimal calculatedCrv = proposal.RiskRewardRatio ?? 0;
|
||||
if (calculatedCrv <= 0 && proposal.EntryPrice > 0 && proposal.StopLoss > 0 && proposal.TakeProfit > 0)
|
||||
{
|
||||
decimal slDist = Math.Abs(proposal.EntryPrice - proposal.StopLoss);
|
||||
decimal tpDist = Math.Abs(proposal.TakeProfit - proposal.EntryPrice);
|
||||
if (slDist > 0)
|
||||
{
|
||||
calculatedCrv = Math.Round(tpDist / slDist, 2);
|
||||
}
|
||||
}
|
||||
|
||||
if (calculatedCrv < (decimal)minCrv)
|
||||
{
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.BotChannel,
|
||||
"[RiskEngine] Trade {Symbol} rejected: CRV {Crv:F2} below threshold {MinCrv:F2}",
|
||||
proposal.Symbol, calculatedCrv, minCrv);
|
||||
return new SizingResult(false, $"CRV {calculatedCrv:F2} below minimum threshold {minCrv:F2}.", 0, 0, 0, calculatedCrv);
|
||||
}
|
||||
|
||||
// 5. Validate Win-Rate
|
||||
double minWinRate = await _settingsService.GetSettingAsync(SettingKeys.MinWinRate, ct);
|
||||
if (proposal.WinRate < minWinRate)
|
||||
{
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.BotChannel,
|
||||
"[RiskEngine] Trade {Symbol} rejected: Win-Rate {WinRate:F1}% below threshold {MinWinRate:F1}%",
|
||||
proposal.Symbol, proposal.WinRate, minWinRate);
|
||||
return new SizingResult(false, $"Win-Rate {proposal.WinRate:F1}% below minimum threshold {minWinRate:F1}%.", 0, 0, 0, calculatedCrv);
|
||||
}
|
||||
|
||||
// 6. Validate VIX Threshold
|
||||
double maxVix = await _settingsService.GetSettingAsync(SettingKeys.MaxVixThreshold, ct);
|
||||
if (proposal.VixValue > (decimal)maxVix)
|
||||
{
|
||||
return new SizingResult(false, $"VIX {proposal.VixValue:F1} exceeds maximum volatility threshold {maxVix:F1}.", 0, 0, 0, calculatedCrv);
|
||||
}
|
||||
|
||||
// 7. Calculate Position Sizing (Fixed Fractional Sizing)
|
||||
if (accountEquity <= 0)
|
||||
{
|
||||
return new SizingResult(false, "Account equity is zero or negative.", 0, 0, 0, calculatedCrv);
|
||||
}
|
||||
|
||||
double riskPercent = await _settingsService.GetSettingAsync(SettingKeys.RiskPerTradePercent, ct);
|
||||
decimal maxRiskAmount = accountEquity * ((decimal)riskPercent / 100m);
|
||||
decimal priceRiskPerUnit = Math.Abs(proposal.EntryPrice - proposal.StopLoss);
|
||||
|
||||
if (priceRiskPerUnit <= 0)
|
||||
{
|
||||
return new SizingResult(false, "Stop loss cannot be identical to entry price.", 0, 0, 0, calculatedCrv);
|
||||
}
|
||||
|
||||
decimal calculatedQty = Math.Floor(maxRiskAmount / priceRiskPerUnit);
|
||||
if (calculatedQty <= 0)
|
||||
{
|
||||
// Allow fractional share if total position is at least 10$
|
||||
calculatedQty = Math.Round(maxRiskAmount / priceRiskPerUnit, 2);
|
||||
if (calculatedQty <= 0)
|
||||
{
|
||||
return new SizingResult(false, "Calculated order quantity is 0 (account equity too small for Stop Loss distance).", 0, 0, 0, calculatedCrv);
|
||||
}
|
||||
}
|
||||
|
||||
decimal totalPositionValue = calculatedQty * proposal.EntryPrice;
|
||||
|
||||
// 8. Cap against Max Single Position Cap
|
||||
double maxCap = await _settingsService.GetSettingAsync(SettingKeys.MaxSinglePositionCap, ct);
|
||||
if (totalPositionValue > (decimal)maxCap && proposal.EntryPrice > 0)
|
||||
{
|
||||
calculatedQty = Math.Floor((decimal)maxCap / proposal.EntryPrice);
|
||||
totalPositionValue = calculatedQty * proposal.EntryPrice;
|
||||
if (calculatedQty <= 0)
|
||||
{
|
||||
return new SizingResult(false, "Position size exceeds maximum position cap.", 0, 0, 0, calculatedCrv);
|
||||
}
|
||||
}
|
||||
|
||||
decimal actualRiskAmount = calculatedQty * priceRiskPerUnit;
|
||||
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.BotChannel,
|
||||
"[RiskEngine] Sizing APPROVED for {Symbol}: Qty={Qty}, PositionVal=${PosVal:F2}, Risk=${Risk:F2} ({RiskPct:F1}%), CRV={Crv:F2}",
|
||||
proposal.Symbol, calculatedQty, totalPositionValue, actualRiskAmount, riskPercent, calculatedCrv);
|
||||
|
||||
return new SizingResult(true, null, calculatedQty, totalPositionValue, actualRiskAmount, calculatedCrv);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user