|
|
|
|
@@ -51,6 +51,19 @@ public class TradingBotBase : ITradingBot
|
|
|
|
|
/// </summary>
|
|
|
|
|
public Dictionary<IndicatorType, IndicatorsResultBase> PreCalculatedIndicatorValues { get; set; }
|
|
|
|
|
|
|
|
|
|
// Cached properties for performance optimization
|
|
|
|
|
private bool? _isForBacktest;
|
|
|
|
|
private bool? _isForWatchingOnly;
|
|
|
|
|
private int? _maxLossStreak;
|
|
|
|
|
private int? _cooldownPeriod;
|
|
|
|
|
private bool? _flipPosition;
|
|
|
|
|
|
|
|
|
|
private bool IsForBacktest => _isForBacktest ??= Config.IsForBacktest;
|
|
|
|
|
private bool IsForWatchingOnly => _isForWatchingOnly ??= Config.IsForWatchingOnly;
|
|
|
|
|
private int MaxLossStreak => _maxLossStreak ??= Config.MaxLossStreak;
|
|
|
|
|
private int CooldownPeriod => _cooldownPeriod ??= Config.CooldownPeriod;
|
|
|
|
|
private bool FlipPosition => _flipPosition ??= Config.FlipPosition;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public TradingBotBase(
|
|
|
|
|
ILogger<TradingBotBase> logger,
|
|
|
|
|
@@ -70,7 +83,7 @@ public class TradingBotBase : ITradingBot
|
|
|
|
|
|
|
|
|
|
public async Task Start(BotStatus previousStatus)
|
|
|
|
|
{
|
|
|
|
|
if (!Config.IsForBacktest)
|
|
|
|
|
if (!IsForBacktest)
|
|
|
|
|
{
|
|
|
|
|
// Start async initialization in the background without blocking
|
|
|
|
|
try
|
|
|
|
|
@@ -94,17 +107,8 @@ public class TradingBotBase : ITradingBot
|
|
|
|
|
switch (previousStatus)
|
|
|
|
|
{
|
|
|
|
|
case BotStatus.Saved:
|
|
|
|
|
var indicatorNames = Config.Scenario.Indicators.Select(i => i.Type.ToString()).ToList();
|
|
|
|
|
var startupMessage = $"🚀 Bot Started Successfully\n\n" +
|
|
|
|
|
$"📊 Trading Setup:\n" +
|
|
|
|
|
$"🎯 Ticker: `{Config.Ticker}`\n" +
|
|
|
|
|
$"⏰ Timeframe: `{Config.Timeframe}`\n" +
|
|
|
|
|
$"🎮 Scenario: `{Config.Scenario?.Name ?? "Unknown"}`\n" +
|
|
|
|
|
$"💰 Balance: `${Config.BotTradingBalance:F2}`\n" +
|
|
|
|
|
$"👀 Mode: `{(Config.IsForWatchingOnly ? "Watch Only" : "Live Trading")}`\n\n" +
|
|
|
|
|
$"📈 Active Indicators: `{string.Join(", ", indicatorNames)}`\n\n" +
|
|
|
|
|
$"✅ Ready to monitor signals and execute trades\n" +
|
|
|
|
|
$"📢 Notifications will be sent when positions are triggered";
|
|
|
|
|
var indicatorNames = Config.Scenario.Indicators.Select(i => i.Type.ToString());
|
|
|
|
|
var startupMessage = $"🚀 Bot Started Successfully\n\n📊 Trading Setup:\n🎯 Ticker: `{Config.Ticker}`\n⏰ Timeframe: `{Config.Timeframe}`\n🎮 Scenario: `{Config.Scenario?.Name ?? "Unknown"}`\n💰 Balance: `${Config.BotTradingBalance:F2}`\n👀 Mode: `{(Config.IsForWatchingOnly ? "Watch Only" : "Live Trading")}`\n\n📈 Active Indicators: `{string.Join(", ", indicatorNames)}`\n\n✅ Ready to monitor signals and execute trades\n📢 Notifications will be sent when positions are triggered";
|
|
|
|
|
|
|
|
|
|
await LogInformation(startupMessage);
|
|
|
|
|
break;
|
|
|
|
|
@@ -172,7 +176,7 @@ public class TradingBotBase : ITradingBot
|
|
|
|
|
|
|
|
|
|
public async Task LoadAccount()
|
|
|
|
|
{
|
|
|
|
|
if (Config.IsForBacktest) return;
|
|
|
|
|
if (IsForBacktest) return;
|
|
|
|
|
await ServiceScopeHelpers.WithScopedService<IAccountService>(_scopeFactory, async accountService =>
|
|
|
|
|
{
|
|
|
|
|
var account = await accountService.GetAccountByAccountName(Config.AccountName, false, false);
|
|
|
|
|
@@ -186,7 +190,7 @@ public class TradingBotBase : ITradingBot
|
|
|
|
|
/// </summary>
|
|
|
|
|
public async Task VerifyAndUpdateBalance()
|
|
|
|
|
{
|
|
|
|
|
if (Config.IsForBacktest) return;
|
|
|
|
|
if (IsForBacktest) return;
|
|
|
|
|
if (Account == null)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning("Cannot verify balance: Account is null");
|
|
|
|
|
@@ -233,40 +237,85 @@ public class TradingBotBase : ITradingBot
|
|
|
|
|
|
|
|
|
|
public async Task Run()
|
|
|
|
|
{
|
|
|
|
|
// Update signals for live trading only
|
|
|
|
|
if (!Config.IsForBacktest)
|
|
|
|
|
// Fast path for backtests - skip live trading operations
|
|
|
|
|
if (IsForBacktest)
|
|
|
|
|
{
|
|
|
|
|
await UpdateSignals();
|
|
|
|
|
await LoadLastCandle();
|
|
|
|
|
if (!IsForWatchingOnly)
|
|
|
|
|
await ManagePositions();
|
|
|
|
|
|
|
|
|
|
UpdateWalletBalances();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!Config.IsForWatchingOnly)
|
|
|
|
|
// Live trading path
|
|
|
|
|
await UpdateSignals();
|
|
|
|
|
await LoadLastCandle();
|
|
|
|
|
|
|
|
|
|
if (!IsForWatchingOnly)
|
|
|
|
|
await ManagePositions();
|
|
|
|
|
|
|
|
|
|
UpdateWalletBalances();
|
|
|
|
|
if (!Config.IsForBacktest)
|
|
|
|
|
|
|
|
|
|
ExecutionCount++;
|
|
|
|
|
|
|
|
|
|
// Optimized logging - cache frequently used values
|
|
|
|
|
var serverDate = DateTime.UtcNow;
|
|
|
|
|
var lastCandleDate = LastCandle?.Date;
|
|
|
|
|
var signalCount = Signals.Count;
|
|
|
|
|
var positionCount = Positions.Count;
|
|
|
|
|
|
|
|
|
|
Logger.LogInformation(
|
|
|
|
|
"Bot Status {Name} - ServerDate: {ServerDate}, LastCandleDate: {LastCandleDate}, Signals: {SignalCount}, Executions: {ExecutionCount}, Positions: {PositionCount}",
|
|
|
|
|
Config.Name, serverDate, lastCandleDate, signalCount, ExecutionCount, positionCount);
|
|
|
|
|
|
|
|
|
|
// Optimize position logging - build string efficiently
|
|
|
|
|
if (positionCount > 0)
|
|
|
|
|
{
|
|
|
|
|
ExecutionCount++;
|
|
|
|
|
|
|
|
|
|
Logger.LogInformation(
|
|
|
|
|
"Bot Status {Name} - ServerDate: {ServerDate}, LastCandleDate: {LastCandleDate}, Signals: {SignalCount}, Executions: {ExecutionCount}, Positions: {PositionCount}",
|
|
|
|
|
Config.Name, DateTime.UtcNow, LastCandle?.Date, Signals.Count, ExecutionCount, Positions.Count);
|
|
|
|
|
|
|
|
|
|
var positionStrings = new string[positionCount];
|
|
|
|
|
var index = 0;
|
|
|
|
|
foreach (var position in Positions.Values)
|
|
|
|
|
{
|
|
|
|
|
positionStrings[index++] = $"{position.SignalIdentifier} - Status: {position.Status}";
|
|
|
|
|
}
|
|
|
|
|
Logger.LogInformation("[{Name}] Internal Positions : {Position}", Config.Name,
|
|
|
|
|
string.Join(", ",
|
|
|
|
|
Positions.Values.Select(p => $"{p.SignalIdentifier} - Status: {p.Status}")));
|
|
|
|
|
string.Join(", ", positionStrings));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task UpdateSignals(HashSet<Candle> candles = null)
|
|
|
|
|
{
|
|
|
|
|
// Fast path for backtests - skip live trading checks
|
|
|
|
|
if (IsForBacktest && candles != null)
|
|
|
|
|
{
|
|
|
|
|
var backtestSignal =
|
|
|
|
|
TradingBox.GetSignal(candles, Config.Scenario, Signals, Config.Scenario.LoopbackPeriod,
|
|
|
|
|
PreCalculatedIndicatorValues);
|
|
|
|
|
if (backtestSignal == null) return;
|
|
|
|
|
await AddSignal(backtestSignal);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Live trading path with checks
|
|
|
|
|
// Skip indicator checking if flipping is disabled and there's an open position
|
|
|
|
|
// This prevents unnecessary indicator calculations when we can't act on signals anyway
|
|
|
|
|
if (!Config.FlipPosition && Positions.Any(p => p.Value.IsOpen()))
|
|
|
|
|
if (!FlipPosition)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug(
|
|
|
|
|
$"Skipping signal update: Position open and flip disabled. Open positions: {Positions.Count(p => p.Value.IsOpen())}");
|
|
|
|
|
return;
|
|
|
|
|
var hasOpenPosition = false;
|
|
|
|
|
foreach (var position in Positions.Values)
|
|
|
|
|
{
|
|
|
|
|
if (position.IsOpen())
|
|
|
|
|
{
|
|
|
|
|
hasOpenPosition = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hasOpenPosition)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug(
|
|
|
|
|
$"Skipping signal update: Position open and flip disabled. Open positions: {Positions.Count(p => p.Value.IsOpen())}");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if we're in cooldown period for any direction
|
|
|
|
|
@@ -276,24 +325,13 @@ public class TradingBotBase : ITradingBot
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Config.IsForBacktest && candles != null)
|
|
|
|
|
await ServiceScopeHelpers.WithScopedService<IGrainFactory>(_scopeFactory, async grainFactory =>
|
|
|
|
|
{
|
|
|
|
|
var backtestSignal =
|
|
|
|
|
TradingBox.GetSignal(candles, Config.Scenario, Signals, Config.Scenario.LoopbackPeriod,
|
|
|
|
|
PreCalculatedIndicatorValues);
|
|
|
|
|
if (backtestSignal == null) return;
|
|
|
|
|
await AddSignal(backtestSignal);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
await ServiceScopeHelpers.WithScopedService<IGrainFactory>(_scopeFactory, async grainFactory =>
|
|
|
|
|
{
|
|
|
|
|
var scenarioRunnerGrain = grainFactory.GetGrain<IScenarioRunnerGrain>(Guid.NewGuid());
|
|
|
|
|
var signal = await scenarioRunnerGrain.GetSignals(Config, Signals, Account.Exchange, LastCandle);
|
|
|
|
|
if (signal == null) return;
|
|
|
|
|
await AddSignal(signal);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
var scenarioRunnerGrain = grainFactory.GetGrain<IScenarioRunnerGrain>(Guid.NewGuid());
|
|
|
|
|
var signal = await scenarioRunnerGrain.GetSignals(Config, Signals, Account.Exchange, LastCandle);
|
|
|
|
|
if (signal == null) return;
|
|
|
|
|
await AddSignal(signal);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<LightSignal> RecreateSignalFromPosition(Position position)
|
|
|
|
|
@@ -352,17 +390,40 @@ public class TradingBotBase : ITradingBot
|
|
|
|
|
private async Task ManagePositions()
|
|
|
|
|
{
|
|
|
|
|
// Early exit optimization - skip if no positions to manage
|
|
|
|
|
var hasOpenPositions = Positions.Values.Any(p => !p.IsFinished());
|
|
|
|
|
var hasWaitingSignals = Signals.Values.Any(s => s.Status == SignalStatus.WaitingForPosition);
|
|
|
|
|
|
|
|
|
|
var hasOpenPositions = false;
|
|
|
|
|
var hasWaitingSignals = false;
|
|
|
|
|
|
|
|
|
|
// Optimize: Use foreach instead of LINQ for better performance
|
|
|
|
|
foreach (var position in Positions.Values)
|
|
|
|
|
{
|
|
|
|
|
if (!position.IsFinished())
|
|
|
|
|
{
|
|
|
|
|
hasOpenPositions = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!hasOpenPositions)
|
|
|
|
|
{
|
|
|
|
|
foreach (var signal in Signals.Values)
|
|
|
|
|
{
|
|
|
|
|
if (signal.Status == SignalStatus.WaitingForPosition)
|
|
|
|
|
{
|
|
|
|
|
hasWaitingSignals = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!hasOpenPositions && !hasWaitingSignals)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
// First, process all existing positions that are not finished
|
|
|
|
|
foreach (var position in Positions.Values.Where(p => !p.IsFinished()))
|
|
|
|
|
foreach (var position in Positions.Values)
|
|
|
|
|
{
|
|
|
|
|
var signalForPosition = Signals[position.SignalIdentifier];
|
|
|
|
|
if (signalForPosition == null)
|
|
|
|
|
if (position.IsFinished()) continue;
|
|
|
|
|
|
|
|
|
|
if (!Signals.TryGetValue(position.SignalIdentifier, out var signalForPosition))
|
|
|
|
|
{
|
|
|
|
|
await LogInformation(
|
|
|
|
|
$"🔍 Signal Recovery\nSignal not found for position `{position.Identifier}`\nRecreating signal from position data...");
|
|
|
|
|
@@ -389,11 +450,9 @@ public class TradingBotBase : ITradingBot
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Then, open positions for signals waiting for a position open
|
|
|
|
|
// But first, check if we already have a position for any of these signals
|
|
|
|
|
var signalsWaitingForPosition = Signals.Values.Where(s => s.Status == SignalStatus.WaitingForPosition);
|
|
|
|
|
|
|
|
|
|
foreach (var signal in signalsWaitingForPosition)
|
|
|
|
|
foreach (var signal in Signals.Values)
|
|
|
|
|
{
|
|
|
|
|
if (signal.Status != SignalStatus.WaitingForPosition) continue;
|
|
|
|
|
if (LastCandle != null && signal.Date < LastCandle.Date)
|
|
|
|
|
{
|
|
|
|
|
await LogWarning(
|
|
|
|
|
@@ -432,23 +491,33 @@ public class TradingBotBase : ITradingBot
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!WalletBalances.ContainsKey(date))
|
|
|
|
|
// Optimize: Use TryGetValue instead of ContainsKey + First()
|
|
|
|
|
if (!WalletBalances.TryGetValue(date, out _))
|
|
|
|
|
{
|
|
|
|
|
var previousBalance = WalletBalances.First().Value;
|
|
|
|
|
WalletBalances[date] = previousBalance + GetProfitAndLoss();
|
|
|
|
|
// Cache the calculation to avoid repeated computation
|
|
|
|
|
var profitAndLoss = GetProfitAndLoss();
|
|
|
|
|
var previousBalance = WalletBalances.Count > 0 ? WalletBalances.First().Value : Config.BotTradingBalance;
|
|
|
|
|
WalletBalances[date] = previousBalance + profitAndLoss;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task UpdatePosition(LightSignal signal, Position positionForSignal)
|
|
|
|
|
{
|
|
|
|
|
// Skip processing if position is already canceled or rejected (never filled)
|
|
|
|
|
if (positionForSignal.Status == PositionStatus.Canceled ||
|
|
|
|
|
positionForSignal.Status == PositionStatus.Rejected)
|
|
|
|
|
{
|
|
|
|
|
await LogDebug(
|
|
|
|
|
$"Skipping update for position {positionForSignal.Identifier} - status is {positionForSignal.Status} (never filled)");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
// Skip processing if position is already canceled or rejected (never filled)
|
|
|
|
|
if (positionForSignal.Status == PositionStatus.Canceled ||
|
|
|
|
|
positionForSignal.Status == PositionStatus.Rejected)
|
|
|
|
|
// Fast path for backtests - simplified position handling
|
|
|
|
|
if (IsForBacktest)
|
|
|
|
|
{
|
|
|
|
|
await LogDebug(
|
|
|
|
|
$"Skipping update for position {positionForSignal.Identifier} - status is {positionForSignal.Status} (never filled)");
|
|
|
|
|
await UpdatePositionForBacktest(signal, positionForSignal);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -456,23 +525,14 @@ public class TradingBotBase : ITradingBot
|
|
|
|
|
var brokerPositions = await ServiceScopeHelpers.WithScopedService<ITradingService, List<Position>>(
|
|
|
|
|
_scopeFactory, async tradingService =>
|
|
|
|
|
{
|
|
|
|
|
internalPosition = Config.IsForBacktest
|
|
|
|
|
? positionForSignal
|
|
|
|
|
: await tradingService.GetPositionByIdentifierAsync(positionForSignal.Identifier);
|
|
|
|
|
internalPosition = await tradingService.GetPositionByIdentifierAsync(positionForSignal.Identifier);
|
|
|
|
|
|
|
|
|
|
if (Config.IsForBacktest)
|
|
|
|
|
{
|
|
|
|
|
return new List<Position> { internalPosition };
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
return await ServiceScopeHelpers.WithScopedService<IExchangeService, List<Position>>(
|
|
|
|
|
_scopeFactory,
|
|
|
|
|
async exchangeService =>
|
|
|
|
|
{
|
|
|
|
|
return [.. await exchangeService.GetBrokerPositions(Account)];
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return await ServiceScopeHelpers.WithScopedService<IExchangeService, List<Position>>(
|
|
|
|
|
_scopeFactory,
|
|
|
|
|
async exchangeService =>
|
|
|
|
|
{
|
|
|
|
|
return [.. await exchangeService.GetBrokerPositions(Account)];
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!Config.IsForBacktest)
|
|
|
|
|
@@ -963,6 +1023,30 @@ public class TradingBotBase : ITradingBot
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Optimized position update method for backtests - skips live trading overhead
|
|
|
|
|
/// </summary>
|
|
|
|
|
private async Task UpdatePositionForBacktest(LightSignal signal, Position positionForSignal)
|
|
|
|
|
{
|
|
|
|
|
// For backtests, positions are filled immediately
|
|
|
|
|
if (positionForSignal.Status == PositionStatus.New)
|
|
|
|
|
{
|
|
|
|
|
positionForSignal.Status = PositionStatus.Filled;
|
|
|
|
|
await SetPositionStatus(signal.Identifier, PositionStatus.Filled);
|
|
|
|
|
SetSignalStatus(signal.Identifier, SignalStatus.PositionOpen);
|
|
|
|
|
}
|
|
|
|
|
else if (positionForSignal.Status == PositionStatus.Filled)
|
|
|
|
|
{
|
|
|
|
|
// Handle position closing logic for backtests
|
|
|
|
|
await HandleClosedPosition(positionForSignal);
|
|
|
|
|
}
|
|
|
|
|
else if (positionForSignal.Status == PositionStatus.Finished ||
|
|
|
|
|
positionForSignal.Status == PositionStatus.Flipped)
|
|
|
|
|
{
|
|
|
|
|
await HandleClosedPosition(positionForSignal);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task UpdatePositionDatabase(Position position)
|
|
|
|
|
{
|
|
|
|
|
await ServiceScopeHelpers.WithScopedService<ITradingService>(_scopeFactory,
|
|
|
|
|
@@ -1131,20 +1215,20 @@ public class TradingBotBase : ITradingBot
|
|
|
|
|
|
|
|
|
|
private async Task<bool> CanOpenPosition(LightSignal signal)
|
|
|
|
|
{
|
|
|
|
|
// Early return if we're in backtest mode and haven't executed yet
|
|
|
|
|
// TODO : check if its a startup cycle
|
|
|
|
|
if (!Config.IsForBacktest && ExecutionCount == 0)
|
|
|
|
|
// Fast path for backtests - skip live trading checks
|
|
|
|
|
if (IsForBacktest)
|
|
|
|
|
{
|
|
|
|
|
return !await IsInCooldownPeriodAsync() && await CheckLossStreak(signal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Live trading path
|
|
|
|
|
// Early return if we haven't executed yet
|
|
|
|
|
if (ExecutionCount == 0)
|
|
|
|
|
{
|
|
|
|
|
await LogInformation("⏳ Bot Not Ready\nCannot open position\nBot hasn't executed first cycle yet");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if we're in backtest mode
|
|
|
|
|
if (Config.IsForBacktest)
|
|
|
|
|
{
|
|
|
|
|
return !await IsInCooldownPeriodAsync() && await CheckLossStreak(signal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check broker positions for live trading
|
|
|
|
|
var canOpenPosition = await CheckBrokerPositions();
|
|
|
|
|
if (!canOpenPosition)
|
|
|
|
|
@@ -1158,18 +1242,15 @@ public class TradingBotBase : ITradingBot
|
|
|
|
|
decimal currentPrice = 0;
|
|
|
|
|
await ServiceScopeHelpers.WithScopedService<IExchangeService>(_scopeFactory, async exchangeService =>
|
|
|
|
|
{
|
|
|
|
|
currentPrice = Config.IsForBacktest
|
|
|
|
|
? LastCandle?.Close ?? 0
|
|
|
|
|
: await exchangeService.GetCurrentPrice(Account, Config.Ticker);
|
|
|
|
|
currentPrice = await exchangeService.GetCurrentPrice(Account, Config.Ticker);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bool synthRisk = false;
|
|
|
|
|
await ServiceScopeHelpers.WithScopedService<ITradingService>(_scopeFactory, async tradingService =>
|
|
|
|
|
{
|
|
|
|
|
synthRisk = await tradingService.AssessSynthPositionRiskAsync(Config.Ticker, signal.Direction,
|
|
|
|
|
currentPrice,
|
|
|
|
|
Config, Config.IsForBacktest);
|
|
|
|
|
Config, false);
|
|
|
|
|
});
|
|
|
|
|
if (!synthRisk)
|
|
|
|
|
{
|
|
|
|
|
@@ -1184,38 +1265,69 @@ public class TradingBotBase : ITradingBot
|
|
|
|
|
private async Task<bool> CheckLossStreak(LightSignal signal)
|
|
|
|
|
{
|
|
|
|
|
// If MaxLossStreak is 0, there's no limit
|
|
|
|
|
if (Config.MaxLossStreak <= 0)
|
|
|
|
|
if (MaxLossStreak <= 0)
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get the last N finished positions regardless of direction
|
|
|
|
|
var recentPositions = Positions
|
|
|
|
|
.Values
|
|
|
|
|
.Where(p => p.IsFinished())
|
|
|
|
|
.OrderByDescending(p => p.Open.Date)
|
|
|
|
|
.Take(Config.MaxLossStreak)
|
|
|
|
|
.ToList();
|
|
|
|
|
// Optimize: Pre-allocate array and use manual sorting for better performance
|
|
|
|
|
var maxStreak = MaxLossStreak;
|
|
|
|
|
var recentPositions = new Position[maxStreak];
|
|
|
|
|
var count = 0;
|
|
|
|
|
|
|
|
|
|
// Collect recent finished positions manually for better performance
|
|
|
|
|
foreach (var position in Positions.Values)
|
|
|
|
|
{
|
|
|
|
|
if (!position.IsFinished()) continue;
|
|
|
|
|
|
|
|
|
|
// Simple insertion sort by date (descending)
|
|
|
|
|
var insertIndex = 0;
|
|
|
|
|
while (insertIndex < count && recentPositions[insertIndex].Open.Date > position.Open.Date)
|
|
|
|
|
{
|
|
|
|
|
insertIndex++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (insertIndex < maxStreak)
|
|
|
|
|
{
|
|
|
|
|
// Shift elements
|
|
|
|
|
for (var i = Math.Min(count, maxStreak - 1); i > insertIndex; i--)
|
|
|
|
|
{
|
|
|
|
|
recentPositions[i] = recentPositions[i - 1];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
recentPositions[insertIndex] = position;
|
|
|
|
|
if (count < maxStreak) count++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we don't have enough positions to form a streak, we can open
|
|
|
|
|
if (recentPositions.Count < Config.MaxLossStreak)
|
|
|
|
|
if (count < maxStreak)
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if all recent positions were losses
|
|
|
|
|
var allLosses = recentPositions.All(p => p.ProfitAndLoss?.Realized < 0);
|
|
|
|
|
var allLosses = true;
|
|
|
|
|
for (var i = 0; i < count; i++)
|
|
|
|
|
{
|
|
|
|
|
if (recentPositions[i].ProfitAndLoss?.Realized >= 0)
|
|
|
|
|
{
|
|
|
|
|
allLosses = false;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!allLosses)
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we have a loss streak, check if the last position was in the same direction as the signal
|
|
|
|
|
var lastPosition = recentPositions.First();
|
|
|
|
|
var lastPosition = recentPositions[0]; // First element is most recent due to descending sort
|
|
|
|
|
if (lastPosition.OriginDirection == signal.Direction)
|
|
|
|
|
{
|
|
|
|
|
await LogWarning(
|
|
|
|
|
$"🔥 Loss Streak Limit\nCannot open position\nMax loss streak: `{Config.MaxLossStreak}` reached\n📉 Last `{recentPositions.Count}` trades were losses\n🎯 Last position: `{lastPosition.OriginDirection}`\nWaiting for opposite direction signal");
|
|
|
|
|
$"🔥 Loss Streak Limit\nCannot open position\nMax loss streak: `{maxStreak}` reached\n📉 Last `{count}` trades were losses\n🎯 Last position: `{lastPosition.OriginDirection}`\nWaiting for opposite direction signal");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -2030,8 +2142,21 @@ public class TradingBotBase : ITradingBot
|
|
|
|
|
|
|
|
|
|
public int GetWinRate()
|
|
|
|
|
{
|
|
|
|
|
var succeededPositions = Positions.Values.Where(p => p.IsValidForMetrics()).Count(p => p.IsInProfit());
|
|
|
|
|
var total = Positions.Values.Where(p => p.IsValidForMetrics()).Count();
|
|
|
|
|
// Optimize: Single pass through positions
|
|
|
|
|
var succeededPositions = 0;
|
|
|
|
|
var total = 0;
|
|
|
|
|
|
|
|
|
|
foreach (var position in Positions.Values)
|
|
|
|
|
{
|
|
|
|
|
if (position.IsValidForMetrics())
|
|
|
|
|
{
|
|
|
|
|
total++;
|
|
|
|
|
if (position.IsInProfit())
|
|
|
|
|
{
|
|
|
|
|
succeededPositions++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (total == 0)
|
|
|
|
|
return 0;
|
|
|
|
|
@@ -2041,9 +2166,15 @@ public class TradingBotBase : ITradingBot
|
|
|
|
|
|
|
|
|
|
public decimal GetProfitAndLoss()
|
|
|
|
|
{
|
|
|
|
|
// Calculate net PnL after deducting fees for each position
|
|
|
|
|
var netPnl = Positions.Values.Where(p => p.IsValidForMetrics() && p.ProfitAndLoss != null)
|
|
|
|
|
.Sum(p => p.GetPnLBeforeFees());
|
|
|
|
|
// Optimize: Manual loop instead of LINQ for better performance
|
|
|
|
|
var netPnl = 0m;
|
|
|
|
|
foreach (var position in Positions.Values)
|
|
|
|
|
{
|
|
|
|
|
if (position.IsValidForMetrics() && position.ProfitAndLoss != null)
|
|
|
|
|
{
|
|
|
|
|
netPnl += position.GetPnLBeforeFees();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return netPnl;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -2055,11 +2186,15 @@ public class TradingBotBase : ITradingBot
|
|
|
|
|
/// <returns>Returns the total fees paid as a decimal value.</returns>
|
|
|
|
|
public decimal GetTotalFees()
|
|
|
|
|
{
|
|
|
|
|
decimal totalFees = 0;
|
|
|
|
|
// Optimize: Manual loop instead of LINQ
|
|
|
|
|
var totalFees = 0m;
|
|
|
|
|
|
|
|
|
|
foreach (var position in Positions.Values.Where(p => p.IsValidForMetrics()))
|
|
|
|
|
foreach (var position in Positions.Values)
|
|
|
|
|
{
|
|
|
|
|
totalFees += TradingHelpers.CalculatePositionFees(position);
|
|
|
|
|
if (position.IsValidForMetrics())
|
|
|
|
|
{
|
|
|
|
|
totalFees += TradingHelpers.CalculatePositionFees(position);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return totalFees;
|
|
|
|
|
@@ -2580,8 +2715,8 @@ public class TradingBotBase : ITradingBot
|
|
|
|
|
|
|
|
|
|
// Calculate cooldown end time based on last position closing time
|
|
|
|
|
var baseIntervalSeconds = CandleHelpers.GetBaseIntervalInSeconds(Config.Timeframe);
|
|
|
|
|
var cooldownEndTime = LastPositionClosingTime.Value.AddSeconds(baseIntervalSeconds * Config.CooldownPeriod);
|
|
|
|
|
var isInCooldown = (Config.IsForBacktest ? LastCandle.Date : DateTime.UtcNow) < cooldownEndTime;
|
|
|
|
|
var cooldownEndTime = LastPositionClosingTime.Value.AddSeconds(baseIntervalSeconds * CooldownPeriod);
|
|
|
|
|
var isInCooldown = (IsForBacktest ? LastCandle.Date : DateTime.UtcNow) < cooldownEndTime;
|
|
|
|
|
|
|
|
|
|
if (isInCooldown)
|
|
|
|
|
{
|
|
|
|
|
|