diff --git a/src/Managing.Api/Controllers/BotController.cs b/src/Managing.Api/Controllers/BotController.cs
index 1bd68680..c9600c0b 100644
--- a/src/Managing.Api/Controllers/BotController.cs
+++ b/src/Managing.Api/Controllers/BotController.cs
@@ -14,6 +14,7 @@ using Managing.Domain.Bots;
using Managing.Domain.Indicators;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios;
+using Managing.Domain.Shared.Helpers;
using Managing.Domain.Strategies;
using Managing.Domain.Trades;
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
var config = new TradingBotConfig
{
@@ -985,7 +979,7 @@ public class BotController : BaseController
// Set computed/default properties
FlipPosition = request.Config.FlipPosition,
Name = request.Config.Name,
- TradingType = tradingType
+ TradingType = TradingBox.GetLiveTradingType(request.Config.TradingType)
};
return (config, user);
diff --git a/src/Managing.Application.Abstractions/Grains/ICandleStoreGrain.cs b/src/Managing.Application.Abstractions/Grains/ICandleStoreGrain.cs
index acfd954a..2a834f63 100644
--- a/src/Managing.Application.Abstractions/Grains/ICandleStoreGrain.cs
+++ b/src/Managing.Application.Abstractions/Grains/ICandleStoreGrain.cs
@@ -13,8 +13,8 @@ public interface ICandleStoreGrain : IGrainWithStringKey
///
/// Gets the current list of historical candles (up to 500 most recent)
///
- /// List of candles ordered by date
- Task> GetCandlesAsync();
+ /// Read-only list of candles ordered by date
+ Task> GetCandlesAsync();
///
/// Gets the X latest candles from the store
///
diff --git a/src/Managing.Application/Bots/TradingBotBase.cs b/src/Managing.Application/Bots/TradingBotBase.cs
index f931697d..d7c39374 100644
--- a/src/Managing.Application/Bots/TradingBotBase.cs
+++ b/src/Managing.Application/Bots/TradingBotBase.cs
@@ -68,7 +68,7 @@ public abstract class TradingBotBase : ITradingBot
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
try
@@ -111,8 +111,8 @@ public abstract class TradingBotBase : ITradingBot
case BotStatus.Stopped:
// If status was Stopped we log a message to inform the user that the bot is restarting
await LogInformationAsync($"🔄 Bot Restarted\n" +
- $"📊 Resuming operations with {Signals.Count} signals and {Positions.Count} positions\n" +
- $"✅ Ready to continue trading");
+ $"📊 Resuming operations with {Signals.Count} signals and {Positions.Count} positions\n" +
+ $"✅ Ready to continue trading");
break;
default:
@@ -235,7 +235,7 @@ public abstract class TradingBotBase : ITradingBot
await ManagePositions();
UpdateWalletBalances();
- if (Config.TradingType == TradingType.Futures)
+ if (TradingBox.IsLiveTrading(Config.TradingType))
{
ExecutionCount++;
@@ -436,7 +436,9 @@ public abstract class TradingBotBase : ITradingBot
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)
{
@@ -474,7 +476,7 @@ public abstract class TradingBotBase : ITradingBot
// Common position status handling
if (internalPosition.Status == PositionStatus.Finished ||
- internalPosition.Status == PositionStatus.Flipped)
+ internalPosition.Status == PositionStatus.Flipped)
{
await HandleClosedPosition(positionForSignal);
}
@@ -636,7 +638,7 @@ public abstract class TradingBotBase : ITradingBot
}
// 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)
{
await MonitorSynthRisk(signal, positionForSignal);
@@ -644,19 +646,22 @@ public abstract class TradingBotBase : ITradingBot
}
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);
return;
}
}
// Virtual methods for trading mode-specific behavior
- protected virtual async Task SynchronizeWithBrokerPositions(Position internalPosition, Position positionForSignal, List brokerPositions)
+ protected virtual async Task SynchronizeWithBrokerPositions(Position internalPosition, Position positionForSignal,
+ List brokerPositions)
{
// 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)
}
@@ -957,44 +962,44 @@ public abstract class TradingBotBase : ITradingBot
LightSignal previousSignal, decimal lastPrice)
{
// Default implementation - subclasses should override
- if (Config.FlipPosition)
- {
- var isPositionInProfit = (openedPosition.ProfitAndLoss?.Realized ?? 0) > 0;
- var shouldFlip = !Config.FlipOnlyWhenInProfit || isPositionInProfit;
+ if (Config.FlipPosition)
+ {
+ var isPositionInProfit = (openedPosition.ProfitAndLoss?.Realized ?? 0) > 0;
+ var shouldFlip = !Config.FlipOnlyWhenInProfit || isPositionInProfit;
- if (shouldFlip)
- {
- var flipReason = Config.FlipOnlyWhenInProfit
- ? "current position is in profit"
- : "FlipOnlyWhenInProfit is disabled";
+ if (shouldFlip)
+ {
+ var flipReason = Config.FlipOnlyWhenInProfit
+ ? "current position is in profit"
+ : "FlipOnlyWhenInProfit is disabled";
- await LogInformation(
- $"🔄 Position Flip Initiated\nFlipping position due to opposite signal\nReason: {flipReason}");
- await CloseTrade(previousSignal, openedPosition, openedPosition.Open, lastPrice, true);
- await SetPositionStatus(previousSignal.Identifier, PositionStatus.Flipped);
- var newPosition = await OpenPosition(signal);
- await LogInformation(
- $"✅ Position Flipped\nPosition: `{previousSignal.Identifier}` → `{signal.Identifier}`\nPrice: `${lastPrice}`");
- return newPosition;
- }
- else
- {
- var currentPnl = openedPosition.ProfitAndLoss?.Realized ?? 0;
- await LogInformation(
- $"💸 Flip Blocked - Not Profitable\nPosition `{previousSignal.Identifier}` PnL: `${currentPnl:F2}`\nSignal `{signal.Identifier}` will wait for profitability");
-
- SetSignalStatus(signal.Identifier, SignalStatus.Expired);
- return null;
- }
- }
- else
- {
- await LogInformation(
- $"🚫 Flip Disabled\nPosition already open for: `{previousSignal.Identifier}`\nFlipping disabled, new signal expired");
- SetSignalStatus(signal.Identifier, SignalStatus.Expired);
- return null;
- }
+ await LogInformation(
+ $"🔄 Position Flip Initiated\nFlipping position due to opposite signal\nReason: {flipReason}");
+ await CloseTrade(previousSignal, openedPosition, openedPosition.Open, lastPrice, true);
+ await SetPositionStatus(previousSignal.Identifier, PositionStatus.Flipped);
+ var newPosition = await OpenPosition(signal);
+ await LogInformation(
+ $"✅ Position Flipped\nPosition: `{previousSignal.Identifier}` → `{signal.Identifier}`\nPrice: `${lastPrice}`");
+ return newPosition;
}
+ else
+ {
+ var currentPnl = openedPosition.ProfitAndLoss?.Realized ?? 0;
+ await LogInformation(
+ $"💸 Flip Blocked - Not Profitable\nPosition `{previousSignal.Identifier}` PnL: `${currentPnl:F2}`\nSignal `{signal.Identifier}` will wait for profitability");
+
+ SetSignalStatus(signal.Identifier, SignalStatus.Expired);
+ return null;
+ }
+ }
+ else
+ {
+ await LogInformation(
+ $"🚫 Flip Disabled\nPosition already open for: `{previousSignal.Identifier}`\nFlipping disabled, new signal expired");
+ SetSignalStatus(signal.Identifier, SignalStatus.Expired);
+ return null;
+ }
+ }
///
/// Executes the actual position opening logic.
@@ -1003,34 +1008,34 @@ public abstract class TradingBotBase : ITradingBot
protected virtual async Task ExecuteOpenPosition(LightSignal signal, decimal lastPrice)
{
// Default implementation - subclasses should override
- // Verify actual balance before opening position
+ // Verify actual balance before opening position
await VerifyAndUpdateBalanceAsync();
- var command = new OpenPositionRequest(
- Config.AccountName,
- Config.MoneyManagement,
- signal.Direction,
- Config.Ticker,
- PositionInitiator.Bot,
- signal.Date,
- Account.User,
- Config.BotTradingBalance,
+ var command = new OpenPositionRequest(
+ Config.AccountName,
+ Config.MoneyManagement,
+ signal.Direction,
+ Config.Ticker,
+ PositionInitiator.Bot,
+ signal.Date,
+ Account.User,
+ Config.BotTradingBalance,
Config.TradingType == TradingType.BacktestFutures,
- lastPrice,
- signalIdentifier: signal.Identifier,
+ lastPrice,
+ signalIdentifier: signal.Identifier,
initiatorIdentifier: Identifier,
tradingType: Config.TradingType);
- var position = await ServiceScopeHelpers
- .WithScopedServices(
- _scopeFactory,
- async (exchangeService, accountService, tradingService) =>
- {
- return await new OpenPositionCommandHandler(exchangeService, accountService, tradingService)
- .Handle(command);
- });
+ var position = await ServiceScopeHelpers
+ .WithScopedServices(
+ _scopeFactory,
+ async (exchangeService, accountService, tradingService) =>
+ {
+ return await new OpenPositionCommandHandler(exchangeService, accountService, tradingService)
+ .Handle(command);
+ });
- return position;
+ return position;
}
private async Task SendPositionToCopyTrading(Position position)
@@ -1175,7 +1180,7 @@ public abstract class TradingBotBase : ITradingBot
var brokerHistoryReconciled = await ReconcileWithBrokerHistory(position, currentCandle);
if (brokerHistoryReconciled && !forceMarketClose)
{
- goto SkipCandleBasedCalculation;
+ goto SkipCandleBasedCalculation;
}
// Calculate position closing details using subclass-specific logic
@@ -1217,7 +1222,7 @@ public abstract class TradingBotBase : ITradingBot
$"Total Fees: `${position.GasFees + position.UiFees:F2}`\n" +
$"Net P&L (after fees): `${position.ProfitAndLoss.Net:F2}`";
- if (Config.TradingType == TradingType.Futures)
+ if (TradingBox.IsLiveTrading(Config.TradingType))
{
await LogDebugAsync(logMessage);
}
@@ -1227,7 +1232,7 @@ public abstract class TradingBotBase : ITradingBot
await SetPositionStatus(position.SignalIdentifier, PositionStatus.Finished);
// Update position in database with all trade changes
- if (Config.TradingType == TradingType.Futures)
+ if (TradingBox.IsLiveTrading(Config.TradingType))
{
position.Status = PositionStatus.Finished;
await UpdatePositionDatabase(position);
@@ -1240,7 +1245,9 @@ public abstract class TradingBotBase : ITradingBot
// Update the last position closing time for cooldown period tracking
// 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
{
@@ -1283,7 +1290,7 @@ public abstract class TradingBotBase : ITradingBot
private async Task CancelAllOrders()
{
- if (Config.TradingType == TradingType.Futures && !Config.IsForWatchingOnly)
+ if (TradingBox.IsLiveTrading(Config.TradingType) && !Config.IsForWatchingOnly)
{
try
{
@@ -1465,7 +1472,7 @@ public abstract class TradingBotBase : ITradingBot
try
{
// 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;
}
@@ -1480,7 +1487,7 @@ public abstract class TradingBotBase : ITradingBot
$"🆔 Signal ID: `{signal.Identifier}`";
// 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(_scopeFactory,
async (tradingService, exchangeService) =>
@@ -1513,7 +1520,7 @@ public abstract class TradingBotBase : ITradingBot
await LogInformation(signalText);
- if (Config.IsForWatchingOnly && Config.TradingType == TradingType.Futures && ExecutionCount > 0)
+ if (Config.IsForWatchingOnly && TradingBox.IsLiveTrading(Config.TradingType) && ExecutionCount > 0)
{
await ServiceScopeHelpers.WithScopedService(_scopeFactory, async messengerService =>
{
@@ -1852,7 +1859,8 @@ public abstract class TradingBotBase : ITradingBot
// Calculate cooldown end time based on last position closing time
var cooldownEndTime =
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)
{
@@ -1937,7 +1945,8 @@ public abstract class TradingBotBase : ITradingBot
await agentGrain.OnPositionOpenedAsync(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;
case NotificationEventType.PositionClosed:
@@ -1952,7 +1961,8 @@ public abstract class TradingBotBase : ITradingBot
await agentGrain.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;
case NotificationEventType.PositionUpdated:
@@ -2109,7 +2119,7 @@ public abstract class TradingBotBase : ITradingBot
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 messageWithBotName = $"🤖 {user.AgentName} - {Config.Name}\n{message}";
diff --git a/src/Managing.Application/Grains/CandleStoreGrain.cs b/src/Managing.Application/Grains/CandleStoreGrain.cs
index a767ebee..45647a20 100644
--- a/src/Managing.Application/Grains/CandleStoreGrain.cs
+++ b/src/Managing.Application/Grains/CandleStoreGrain.cs
@@ -121,7 +121,7 @@ public class CandleStoreGrain : Grain, ICandleStoreGrain, IAsyncObserver
await base.OnDeactivateAsync(reason, cancellationToken);
}
- public Task> GetCandlesAsync()
+ public Task> GetCandlesAsync()
{
try
{
@@ -130,15 +130,16 @@ public class CandleStoreGrain : Grain, ICandleStoreGrain, IAsyncObserver
{
_logger.LogWarning("State not initialized for grain {GrainKey}, returning empty list",
this.GetPrimaryKeyString());
- return Task.FromResult(new HashSet());
+ return Task.FromResult>(new List());
}
- return Task.FromResult(_state.State.Candles.ToHashSet());
+ // Return a readonly wrapper to preserve order and prevent external modifications
+ return Task.FromResult>(_state.State.Candles.AsReadOnly());
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving candles for grain {GrainKey}", this.GetPrimaryKeyString());
- return Task.FromResult(new HashSet());
+ return Task.FromResult>(new List());
}
}
diff --git a/src/Managing.Application/Scenarios/ScenarioRunnerGrain.cs b/src/Managing.Application/Scenarios/ScenarioRunnerGrain.cs
index 5e14b003..b872b5ca 100644
--- a/src/Managing.Application/Scenarios/ScenarioRunnerGrain.cs
+++ b/src/Managing.Application/Scenarios/ScenarioRunnerGrain.cs
@@ -32,11 +32,11 @@ public class ScenarioRunnerGrain : Grain, IScenarioRunnerGrain
_scopeFactory = scopeFactory;
}
- private async Task> GetCandlesAsync(TradingExchanges tradingExchange, TradingBotConfig config)
+ private async Task> GetCandlesAsync(TradingExchanges tradingExchange, TradingBotConfig config)
{
try
{
- var newCandles = await ServiceScopeHelpers.WithScopedService>(
+ var newCandles = await ServiceScopeHelpers.WithScopedService>(
_scopeFactory, async grainFactory =>
{
var priceGrainKey =
@@ -66,9 +66,8 @@ public class ScenarioRunnerGrain : Grain, IScenarioRunnerGrain
return null;
}
- var candlesHashSet = await GetCandlesAsync(tradingExchanges, config);
- // Convert to ordered List to preserve chronological order for indicators
- var candlesList = candlesHashSet.OrderBy(c => c.Date).ToList();
+ // Get candles as ordered List (already ordered by date from CandleStoreGrain)
+ var candlesList = await GetCandlesAsync(tradingExchanges, config);
if (candlesList.Count == 0)
{
diff --git a/src/Managing.Domain/Backtests/Backtest.cs b/src/Managing.Domain/Backtests/Backtest.cs
index 35982d92..64454ea0 100644
--- a/src/Managing.Domain/Backtests/Backtest.cs
+++ b/src/Managing.Domain/Backtests/Backtest.cs
@@ -66,7 +66,7 @@ public class Backtest
Timeframe = Config.Timeframe,
IsForWatchingOnly = false, // Always start as active bot
BotTradingBalance = initialTradingBalance,
- TradingType = TradingType.Futures, // Always Futures for live bots
+ TradingType = Config.TradingType,
CooldownPeriod = Config.CooldownPeriod,
MaxLossStreak = Config.MaxLossStreak,
MaxPositionTimeHours = Config.MaxPositionTimeHours, // Properly copy nullable value
diff --git a/src/Managing.Domain/Shared/Helpers/TradingBox.cs b/src/Managing.Domain/Shared/Helpers/TradingBox.cs
index 56a074df..9c181653 100644
--- a/src/Managing.Domain/Shared/Helpers/TradingBox.cs
+++ b/src/Managing.Domain/Shared/Helpers/TradingBox.cs
@@ -69,12 +69,6 @@ public static class TradingBox
preCalculatedIndicatorValues);
}
- public static LightSignal GetSignal(IReadOnlyList newCandles, LightScenario lightScenario,
- Dictionary previousSignal, IndicatorComboConfig config, int? loopbackPeriod = 1)
- {
- return GetSignal(newCandles, lightScenario, previousSignal, config, loopbackPeriod, null);
- }
-
public static LightSignal GetSignal(IReadOnlyList newCandles, LightScenario lightScenario,
Dictionary previousSignal, IndicatorComboConfig config, int? loopbackPeriod,
Dictionary preCalculatedIndicatorValues)
@@ -1343,4 +1337,23 @@ public static class TradingBox
}
#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}")
+ };
+ }
}
\ No newline at end of file
diff --git a/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts b/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts
index 65a2cc2b..0a039c9c 100644
--- a/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts
+++ b/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts
@@ -13,9 +13,9 @@ describe('swap tokens implementation', () => {
console.log('Account', sdk.account)
const result = await swapGmxTokensImpl(
sdk,
- Ticker.ETH,
+ Ticker.BTC,
Ticker.USDC,
- 0.0042
+ 0.00006733
)
assert.strictEqual(typeof result, 'string')