Add isLiveTrading helper to fix bug

This commit is contained in:
2025-12-04 23:42:09 +07:00
parent b44e1f66a7
commit 78edd850fe
8 changed files with 123 additions and 106 deletions

View File

@@ -14,6 +14,7 @@ using Managing.Domain.Bots;
using Managing.Domain.Indicators; using Managing.Domain.Indicators;
using Managing.Domain.MoneyManagements; using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios; using Managing.Domain.Scenarios;
using Managing.Domain.Shared.Helpers;
using Managing.Domain.Strategies; using Managing.Domain.Strategies;
using Managing.Domain.Trades; using Managing.Domain.Trades;
using Managing.Domain.Users; using Managing.Domain.Users;
@@ -955,13 +956,6 @@ public class BotController : BaseController
} }
} }
var tradingType = request.Config.TradingType switch
{
TradingType.BacktestFutures => TradingType.Futures,
TradingType.BacktestSpot => TradingType.Spot,
_ => TradingType.Futures
};
// Map the request to the full TradingBotConfig // Map the request to the full TradingBotConfig
var config = new TradingBotConfig var config = new TradingBotConfig
{ {
@@ -985,7 +979,7 @@ public class BotController : BaseController
// Set computed/default properties // Set computed/default properties
FlipPosition = request.Config.FlipPosition, FlipPosition = request.Config.FlipPosition,
Name = request.Config.Name, Name = request.Config.Name,
TradingType = tradingType TradingType = TradingBox.GetLiveTradingType(request.Config.TradingType)
}; };
return (config, user); return (config, user);

View File

@@ -13,8 +13,8 @@ public interface ICandleStoreGrain : IGrainWithStringKey
/// <summary> /// <summary>
/// Gets the current list of historical candles (up to 500 most recent) /// Gets the current list of historical candles (up to 500 most recent)
/// </summary> /// </summary>
/// <returns>List of candles ordered by date</returns> /// <returns>Read-only list of candles ordered by date</returns>
Task<HashSet<Candle>> GetCandlesAsync(); Task<IReadOnlyList<Candle>> GetCandlesAsync();
/// <summary> /// <summary>
/// Gets the X latest candles from the store /// Gets the X latest candles from the store
/// </summary> /// </summary>

View File

@@ -68,7 +68,7 @@ public abstract class TradingBotBase : ITradingBot
public virtual async Task Start(BotStatus previousStatus) public virtual async Task Start(BotStatus previousStatus)
{ {
if (Config.TradingType == TradingType.Futures) if (TradingBox.IsLiveTrading(Config.TradingType))
{ {
// Start async initialization in the background without blocking // Start async initialization in the background without blocking
try try
@@ -235,7 +235,7 @@ public abstract class TradingBotBase : ITradingBot
await ManagePositions(); await ManagePositions();
UpdateWalletBalances(); UpdateWalletBalances();
if (Config.TradingType == TradingType.Futures) if (TradingBox.IsLiveTrading(Config.TradingType))
{ {
ExecutionCount++; ExecutionCount++;
@@ -436,7 +436,9 @@ public abstract class TradingBotBase : ITradingBot
protected void UpdateWalletBalances() protected void UpdateWalletBalances()
{ {
var date = Config.TradingType == TradingType.BacktestFutures ? LastCandle?.Date ?? DateTime.UtcNow : DateTime.UtcNow; var date = Config.TradingType == TradingType.BacktestFutures
? LastCandle?.Date ?? DateTime.UtcNow
: DateTime.UtcNow;
if (WalletBalances.Count == 0) if (WalletBalances.Count == 0)
{ {
@@ -636,7 +638,7 @@ public abstract class TradingBotBase : ITradingBot
} }
// Synth risk monitoring (only for live trading) // Synth risk monitoring (only for live trading)
if (Config.UseSynthApi && Config.TradingType == TradingType.Futures && if (Config.UseSynthApi && TradingBox.IsLiveTrading(Config.TradingType) &&
positionForSignal.Status == PositionStatus.Filled) positionForSignal.Status == PositionStatus.Filled)
{ {
await MonitorSynthRisk(signal, positionForSignal); await MonitorSynthRisk(signal, positionForSignal);
@@ -644,19 +646,22 @@ public abstract class TradingBotBase : ITradingBot
} }
catch (Exception ex) catch (Exception ex)
{ {
await LogWarningAsync($"Cannot update position {positionForSignal.Identifier}: {ex.Message}, {ex.StackTrace}"); await LogWarningAsync(
$"Cannot update position {positionForSignal.Identifier}: {ex.Message}, {ex.StackTrace}");
SentrySdk.CaptureException(ex); SentrySdk.CaptureException(ex);
return; return;
} }
} }
// Virtual methods for trading mode-specific behavior // Virtual methods for trading mode-specific behavior
protected virtual async Task SynchronizeWithBrokerPositions(Position internalPosition, Position positionForSignal, List<Position> brokerPositions) protected virtual async Task SynchronizeWithBrokerPositions(Position internalPosition, Position positionForSignal,
List<Position> brokerPositions)
{ {
// Default implementation: do nothing (for backtest) // Default implementation: do nothing (for backtest)
} }
protected virtual async Task HandleOrderManagementAndPositionStatus(LightSignal signal, Position internalPosition, Position positionForSignal) protected virtual async Task HandleOrderManagementAndPositionStatus(LightSignal signal, Position internalPosition,
Position positionForSignal)
{ {
// Default implementation: do nothing (for backtest) // Default implementation: do nothing (for backtest)
} }
@@ -1217,7 +1222,7 @@ public abstract class TradingBotBase : ITradingBot
$"Total Fees: `${position.GasFees + position.UiFees:F2}`\n" + $"Total Fees: `${position.GasFees + position.UiFees:F2}`\n" +
$"Net P&L (after fees): `${position.ProfitAndLoss.Net:F2}`"; $"Net P&L (after fees): `${position.ProfitAndLoss.Net:F2}`";
if (Config.TradingType == TradingType.Futures) if (TradingBox.IsLiveTrading(Config.TradingType))
{ {
await LogDebugAsync(logMessage); await LogDebugAsync(logMessage);
} }
@@ -1227,7 +1232,7 @@ public abstract class TradingBotBase : ITradingBot
await SetPositionStatus(position.SignalIdentifier, PositionStatus.Finished); await SetPositionStatus(position.SignalIdentifier, PositionStatus.Finished);
// Update position in database with all trade changes // Update position in database with all trade changes
if (Config.TradingType == TradingType.Futures) if (TradingBox.IsLiveTrading(Config.TradingType))
{ {
position.Status = PositionStatus.Finished; position.Status = PositionStatus.Finished;
await UpdatePositionDatabase(position); await UpdatePositionDatabase(position);
@@ -1240,7 +1245,9 @@ public abstract class TradingBotBase : ITradingBot
// Update the last position closing time for cooldown period tracking // Update the last position closing time for cooldown period tracking
// Only update if position was actually filled // Only update if position was actually filled
LastPositionClosingTime = Config.TradingType == TradingType.BacktestFutures ? currentCandle.Date : DateTime.UtcNow; LastPositionClosingTime = Config.TradingType == TradingType.BacktestFutures
? currentCandle.Date
: DateTime.UtcNow;
} }
else else
{ {
@@ -1283,7 +1290,7 @@ public abstract class TradingBotBase : ITradingBot
private async Task CancelAllOrders() private async Task CancelAllOrders()
{ {
if (Config.TradingType == TradingType.Futures && !Config.IsForWatchingOnly) if (TradingBox.IsLiveTrading(Config.TradingType) && !Config.IsForWatchingOnly)
{ {
try try
{ {
@@ -1465,7 +1472,7 @@ public abstract class TradingBotBase : ITradingBot
try try
{ {
// Set signal status based on configuration // Set signal status based on configuration
if (Config.IsForWatchingOnly || (ExecutionCount < 1 && Config.TradingType == TradingType.Futures)) if (Config.IsForWatchingOnly || (ExecutionCount < 1 && TradingBox.IsLiveTrading(Config.TradingType)))
{ {
signal.Status = SignalStatus.Expired; signal.Status = SignalStatus.Expired;
} }
@@ -1480,7 +1487,7 @@ public abstract class TradingBotBase : ITradingBot
$"🆔 Signal ID: `{signal.Identifier}`"; $"🆔 Signal ID: `{signal.Identifier}`";
// Apply Synth-based signal filtering if enabled // Apply Synth-based signal filtering if enabled
if (Config.UseSynthApi && Config.TradingType == TradingType.Futures && ExecutionCount > 0) if (Config.UseSynthApi && TradingBox.IsLiveTrading(Config.TradingType) && ExecutionCount > 0)
{ {
await ServiceScopeHelpers.WithScopedServices<ITradingService, IExchangeService>(_scopeFactory, await ServiceScopeHelpers.WithScopedServices<ITradingService, IExchangeService>(_scopeFactory,
async (tradingService, exchangeService) => async (tradingService, exchangeService) =>
@@ -1513,7 +1520,7 @@ public abstract class TradingBotBase : ITradingBot
await LogInformation(signalText); await LogInformation(signalText);
if (Config.IsForWatchingOnly && Config.TradingType == TradingType.Futures && ExecutionCount > 0) if (Config.IsForWatchingOnly && TradingBox.IsLiveTrading(Config.TradingType) && ExecutionCount > 0)
{ {
await ServiceScopeHelpers.WithScopedService<IMessengerService>(_scopeFactory, async messengerService => await ServiceScopeHelpers.WithScopedService<IMessengerService>(_scopeFactory, async messengerService =>
{ {
@@ -1852,7 +1859,8 @@ public abstract class TradingBotBase : ITradingBot
// Calculate cooldown end time based on last position closing time // Calculate cooldown end time based on last position closing time
var cooldownEndTime = var cooldownEndTime =
TradingBox.CalculateCooldownEndTime(LastPositionClosingTime.Value, Config.Timeframe, Config.CooldownPeriod); TradingBox.CalculateCooldownEndTime(LastPositionClosingTime.Value, Config.Timeframe, Config.CooldownPeriod);
var isInCooldown = (Config.TradingType == TradingType.BacktestFutures ? LastCandle.Date : DateTime.UtcNow) < cooldownEndTime; var isInCooldown = (Config.TradingType == TradingType.BacktestFutures ? LastCandle.Date : DateTime.UtcNow) <
cooldownEndTime;
if (isInCooldown) if (isInCooldown)
{ {
@@ -1937,7 +1945,8 @@ public abstract class TradingBotBase : ITradingBot
await agentGrain.OnPositionOpenedAsync(positionOpenEvent); await agentGrain.OnPositionOpenedAsync(positionOpenEvent);
await platformGrain.OnPositionOpenAsync(positionOpenEvent); await platformGrain.OnPositionOpenAsync(positionOpenEvent);
await LogDebugAsync($"Sent position opened event to both grains for position {position.Identifier}"); await LogDebugAsync(
$"Sent position opened event to both grains for position {position.Identifier}");
break; break;
case NotificationEventType.PositionClosed: case NotificationEventType.PositionClosed:
@@ -1952,7 +1961,8 @@ public abstract class TradingBotBase : ITradingBot
await agentGrain.OnPositionClosedAsync(positionClosedEvent); await agentGrain.OnPositionClosedAsync(positionClosedEvent);
await platformGrain.OnPositionClosedAsync(positionClosedEvent); await platformGrain.OnPositionClosedAsync(positionClosedEvent);
await LogDebugAsync($"Sent position closed event to both grains for position {position.Identifier}"); await LogDebugAsync(
$"Sent position closed event to both grains for position {position.Identifier}");
break; break;
case NotificationEventType.PositionUpdated: case NotificationEventType.PositionUpdated:
@@ -2109,7 +2119,7 @@ public abstract class TradingBotBase : ITradingBot
protected virtual async Task SendTradeMessageAsync(string message, bool isBadBehavior = false) protected virtual async Task SendTradeMessageAsync(string message, bool isBadBehavior = false)
{ {
if (Config.TradingType == TradingType.Futures) if (TradingBox.IsLiveTrading(Config.TradingType))
{ {
var user = Account.User; var user = Account.User;
var messageWithBotName = $"🤖 {user.AgentName} - {Config.Name}\n{message}"; var messageWithBotName = $"🤖 {user.AgentName} - {Config.Name}\n{message}";

View File

@@ -121,7 +121,7 @@ public class CandleStoreGrain : Grain, ICandleStoreGrain, IAsyncObserver<Candle>
await base.OnDeactivateAsync(reason, cancellationToken); await base.OnDeactivateAsync(reason, cancellationToken);
} }
public Task<HashSet<Candle>> GetCandlesAsync() public Task<IReadOnlyList<Candle>> GetCandlesAsync()
{ {
try try
{ {
@@ -130,15 +130,16 @@ public class CandleStoreGrain : Grain, ICandleStoreGrain, IAsyncObserver<Candle>
{ {
_logger.LogWarning("State not initialized for grain {GrainKey}, returning empty list", _logger.LogWarning("State not initialized for grain {GrainKey}, returning empty list",
this.GetPrimaryKeyString()); this.GetPrimaryKeyString());
return Task.FromResult(new HashSet<Candle>()); return Task.FromResult<IReadOnlyList<Candle>>(new List<Candle>());
} }
return Task.FromResult(_state.State.Candles.ToHashSet()); // Return a readonly wrapper to preserve order and prevent external modifications
return Task.FromResult<IReadOnlyList<Candle>>(_state.State.Candles.AsReadOnly());
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error retrieving candles for grain {GrainKey}", this.GetPrimaryKeyString()); _logger.LogError(ex, "Error retrieving candles for grain {GrainKey}", this.GetPrimaryKeyString());
return Task.FromResult(new HashSet<Candle>()); return Task.FromResult<IReadOnlyList<Candle>>(new List<Candle>());
} }
} }

View File

@@ -32,11 +32,11 @@ public class ScenarioRunnerGrain : Grain, IScenarioRunnerGrain
_scopeFactory = scopeFactory; _scopeFactory = scopeFactory;
} }
private async Task<HashSet<Candle>> GetCandlesAsync(TradingExchanges tradingExchange, TradingBotConfig config) private async Task<IReadOnlyList<Candle>> GetCandlesAsync(TradingExchanges tradingExchange, TradingBotConfig config)
{ {
try try
{ {
var newCandles = await ServiceScopeHelpers.WithScopedService<IGrainFactory, HashSet<Candle>>( var newCandles = await ServiceScopeHelpers.WithScopedService<IGrainFactory, IReadOnlyList<Candle>>(
_scopeFactory, async grainFactory => _scopeFactory, async grainFactory =>
{ {
var priceGrainKey = var priceGrainKey =
@@ -66,9 +66,8 @@ public class ScenarioRunnerGrain : Grain, IScenarioRunnerGrain
return null; return null;
} }
var candlesHashSet = await GetCandlesAsync(tradingExchanges, config); // Get candles as ordered List (already ordered by date from CandleStoreGrain)
// Convert to ordered List to preserve chronological order for indicators var candlesList = await GetCandlesAsync(tradingExchanges, config);
var candlesList = candlesHashSet.OrderBy(c => c.Date).ToList();
if (candlesList.Count == 0) if (candlesList.Count == 0)
{ {

View File

@@ -66,7 +66,7 @@ public class Backtest
Timeframe = Config.Timeframe, Timeframe = Config.Timeframe,
IsForWatchingOnly = false, // Always start as active bot IsForWatchingOnly = false, // Always start as active bot
BotTradingBalance = initialTradingBalance, BotTradingBalance = initialTradingBalance,
TradingType = TradingType.Futures, // Always Futures for live bots TradingType = Config.TradingType,
CooldownPeriod = Config.CooldownPeriod, CooldownPeriod = Config.CooldownPeriod,
MaxLossStreak = Config.MaxLossStreak, MaxLossStreak = Config.MaxLossStreak,
MaxPositionTimeHours = Config.MaxPositionTimeHours, // Properly copy nullable value MaxPositionTimeHours = Config.MaxPositionTimeHours, // Properly copy nullable value

View File

@@ -69,12 +69,6 @@ public static class TradingBox
preCalculatedIndicatorValues); preCalculatedIndicatorValues);
} }
public static LightSignal GetSignal(IReadOnlyList<Candle> newCandles, LightScenario lightScenario,
Dictionary<string, LightSignal> previousSignal, IndicatorComboConfig config, int? loopbackPeriod = 1)
{
return GetSignal(newCandles, lightScenario, previousSignal, config, loopbackPeriod, null);
}
public static LightSignal GetSignal(IReadOnlyList<Candle> newCandles, LightScenario lightScenario, public static LightSignal GetSignal(IReadOnlyList<Candle> newCandles, LightScenario lightScenario,
Dictionary<string, LightSignal> previousSignal, IndicatorComboConfig config, int? loopbackPeriod, Dictionary<string, LightSignal> previousSignal, IndicatorComboConfig config, int? loopbackPeriod,
Dictionary<IndicatorType, IndicatorsResultBase> preCalculatedIndicatorValues) Dictionary<IndicatorType, IndicatorsResultBase> preCalculatedIndicatorValues)
@@ -1343,4 +1337,23 @@ public static class TradingBox
} }
#endregion #endregion
public static bool IsLiveTrading(TradingType tradingType)
{
return tradingType switch
{
TradingType.Futures => true,
TradingType.Spot => true,
_ => false
};
}
public static TradingType GetLiveTradingType(TradingType tradingType)
{
return tradingType switch {
TradingType.BacktestFutures => TradingType.Futures,
TradingType.BacktestSpot => TradingType.Spot,
_ => throw new InvalidOperationException($"Unsupported TradingType for live trading: {tradingType}")
};
}
} }

View File

@@ -13,9 +13,9 @@ describe('swap tokens implementation', () => {
console.log('Account', sdk.account) console.log('Account', sdk.account)
const result = await swapGmxTokensImpl( const result = await swapGmxTokensImpl(
sdk, sdk,
Ticker.ETH, Ticker.BTC,
Ticker.USDC, Ticker.USDC,
0.0042 0.00006733
) )
assert.strictEqual(typeof result, 'string') assert.strictEqual(typeof result, 'string')