Compare commits
10 Commits
bdc5ba2db7
...
vk/9ef2-st
| Author | SHA1 | Date | |
|---|---|---|---|
| 0428775abf | |||
| 452c274073 | |||
| 1bb736ff70 | |||
| 1d33c6c2ee | |||
|
|
c4204f7264 | ||
| 07fb67c535 | |||
| ae353aa0d5 | |||
| 8d4be59d10 | |||
| 3f1d102452 | |||
| efb1f2edce |
@@ -539,7 +539,7 @@ public class BotController : BaseController
|
||||
WinRate = (item.TradeWins + item.TradeLosses) != 0
|
||||
? item.TradeWins / (item.TradeWins + item.TradeLosses)
|
||||
: 0,
|
||||
ProfitAndLoss = item.Pnl,
|
||||
ProfitAndLoss = item.NetPnL,
|
||||
Roi = item.Roi,
|
||||
Identifier = item.Identifier.ToString(),
|
||||
AgentName = item.User.AgentName,
|
||||
|
||||
@@ -42,6 +42,7 @@ public class DataController : ControllerBase
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly IBotService _botService;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IBacktester _backtester;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DataController"/> class.
|
||||
@@ -57,6 +58,7 @@ public class DataController : ControllerBase
|
||||
/// <param name="serviceScopeFactory">Service scope factory for creating scoped services.</param>
|
||||
/// <param name="botService">Service for bot operations.</param>
|
||||
/// <param name="configuration">Configuration for accessing environment variables.</param>
|
||||
/// <param name="backtester">Service for backtest operations.</param>
|
||||
public DataController(
|
||||
IExchangeService exchangeService,
|
||||
IAccountService accountService,
|
||||
@@ -68,7 +70,8 @@ public class DataController : ControllerBase
|
||||
IGrainFactory grainFactory,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
IBotService botService,
|
||||
IConfiguration configuration)
|
||||
IConfiguration configuration,
|
||||
IBacktester backtester)
|
||||
{
|
||||
_exchangeService = exchangeService;
|
||||
_accountService = accountService;
|
||||
@@ -81,6 +84,7 @@ public class DataController : ControllerBase
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_botService = botService;
|
||||
_configuration = configuration;
|
||||
_backtester = backtester;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -918,6 +922,7 @@ public class DataController : ControllerBase
|
||||
public async Task<ActionResult<PaginatedResponse<TradingBotResponse>>> GetStrategiesPaginated(
|
||||
int pageNumber = 1,
|
||||
int pageSize = 10,
|
||||
BotStatus? status = null,
|
||||
string? name = null,
|
||||
string? ticker = null,
|
||||
string? agentName = null,
|
||||
@@ -943,11 +948,14 @@ public class DataController : ControllerBase
|
||||
// Check environment variable for filtering profitable strategies only
|
||||
var showOnlyProfitable = _configuration.GetValue<bool>("showOnlyProfitable", false);
|
||||
|
||||
// Get paginated bots excluding Saved status
|
||||
// Default to Running status if not provided
|
||||
var statusFilter = status ?? BotStatus.Running;
|
||||
|
||||
// Get paginated bots with status filter
|
||||
var (bots, totalCount) = await _botService.GetBotsPaginatedAsync(
|
||||
pageNumber,
|
||||
pageSize,
|
||||
null, // No specific status filter - we'll exclude Saved in the service call
|
||||
statusFilter,
|
||||
name,
|
||||
ticker,
|
||||
agentName,
|
||||
@@ -957,9 +965,9 @@ public class DataController : ControllerBase
|
||||
sortDirection,
|
||||
showOnlyProfitable);
|
||||
|
||||
// Filter out Saved status bots
|
||||
var filteredBots = bots.Where(bot => bot.Status != BotStatus.Saved).ToList();
|
||||
var filteredCount = totalCount - bots.Count(bot => bot.Status == BotStatus.Saved);
|
||||
// No additional filtering needed since we're using the status filter directly
|
||||
var filteredBots = bots.ToList();
|
||||
var filteredCount = totalCount;
|
||||
|
||||
// Map to response objects
|
||||
var tradingBotResponses = MapBotsToTradingBotResponse(filteredBots);
|
||||
@@ -1028,7 +1036,7 @@ public class DataController : ControllerBase
|
||||
WinRate = (item.TradeWins + item.TradeLosses) != 0
|
||||
? item.TradeWins / (item.TradeWins + item.TradeLosses)
|
||||
: 0,
|
||||
ProfitAndLoss = item.Pnl,
|
||||
ProfitAndLoss = item.NetPnL,
|
||||
Roi = item.Roi,
|
||||
Identifier = item.Identifier.ToString(),
|
||||
AgentName = item.User.AgentName,
|
||||
@@ -1044,4 +1052,50 @@ public class DataController : ControllerBase
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves only the statistical information for a specific backtest by ID.
|
||||
/// This endpoint returns only the performance metrics without positions, signals, or candles.
|
||||
/// Useful for displaying backtest stats when starting a bot from a backtest.
|
||||
/// No authentication required.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the backtest to retrieve stats for.</param>
|
||||
/// <returns>The backtest statistics without detailed position/signal data.</returns>
|
||||
[HttpGet("GetBacktestStats/{id}")]
|
||||
public async Task<ActionResult<object>> GetBacktestStats(int id)
|
||||
{
|
||||
var backtest = await _backtester.GetBacktestByIdAsync(id.ToString());
|
||||
|
||||
if (backtest == null)
|
||||
{
|
||||
return NotFound($"Backtest with ID {id} not found.");
|
||||
}
|
||||
|
||||
// Return only the statistical information
|
||||
var stats = new
|
||||
{
|
||||
id = backtest.Id,
|
||||
name = backtest.Config.Name,
|
||||
ticker = backtest.Config.Ticker,
|
||||
timeframe = backtest.Config.Timeframe,
|
||||
tradingType = backtest.Config.TradingType,
|
||||
startDate = backtest.StartDate,
|
||||
endDate = backtest.EndDate,
|
||||
initialBalance = backtest.InitialBalance,
|
||||
finalPnl = backtest.FinalPnl,
|
||||
netPnl = backtest.NetPnl,
|
||||
growthPercentage = backtest.GrowthPercentage,
|
||||
hodlPercentage = backtest.HodlPercentage,
|
||||
winRate = backtest.WinRate,
|
||||
sharpeRatio = backtest.Statistics?.SharpeRatio ?? 0,
|
||||
maxDrawdown = backtest.Statistics?.MaxDrawdown ?? 0,
|
||||
maxDrawdownRecoveryTime = backtest.Statistics?.MaxDrawdownRecoveryTime ?? TimeSpan.Zero,
|
||||
fees = backtest.Fees,
|
||||
score = backtest.Score,
|
||||
scoreMessage = backtest.ScoreMessage,
|
||||
positionCount = backtest.PositionCount
|
||||
};
|
||||
|
||||
return Ok(stats);
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ public interface IBacktestRepository
|
||||
BacktestsFilter? filter = null);
|
||||
|
||||
Task<Backtest> GetBacktestByIdForUserAsync(User user, string id);
|
||||
Task<Backtest> GetBacktestByIdAsync(string id);
|
||||
Task DeleteBacktestByIdForUserAsync(User user, string id);
|
||||
Task DeleteBacktestsByIdsForUserAsync(User user, IEnumerable<string> ids);
|
||||
void DeleteAllBacktestsForUser(User user);
|
||||
|
||||
@@ -64,6 +64,7 @@ namespace Managing.Application.Abstractions.Services
|
||||
(IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(Guid requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc");
|
||||
Task<(IEnumerable<LightBacktest> Backtests, int TotalCount)> GetBacktestsByRequestIdPaginatedAsync(Guid requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc");
|
||||
Task<Backtest> GetBacktestByIdForUserAsync(User user, string id);
|
||||
Task<Backtest> GetBacktestByIdAsync(string id);
|
||||
Task<bool> DeleteBacktestByUserAsync(User user, string id);
|
||||
Task<bool> DeleteBacktestsByIdsForUserAsync(User user, IEnumerable<string> ids);
|
||||
bool DeleteBacktestsByUser(User user);
|
||||
|
||||
@@ -218,6 +218,16 @@ namespace Managing.Application.Backtests
|
||||
return backtest;
|
||||
}
|
||||
|
||||
public async Task<Backtest> GetBacktestByIdAsync(string id)
|
||||
{
|
||||
var backtest = await _backtestRepository.GetBacktestByIdAsync(id);
|
||||
|
||||
if (backtest == null)
|
||||
return null;
|
||||
|
||||
return backtest;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteBacktestByUserAsync(User user, string id)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -593,6 +593,18 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
|
||||
// Load state into the trading bot instance
|
||||
LoadStateIntoTradingBot(tradingBot);
|
||||
|
||||
// Set up callback for immediate balance sync and save when balance is updated
|
||||
tradingBot.OnBalanceUpdatedCallback = async () =>
|
||||
{
|
||||
SyncStateFromBase();
|
||||
await _state.WriteStateAsync();
|
||||
|
||||
// Save to database immediately so GetStrategiesPaginated shows correct balance
|
||||
var botRegistry = GrainFactory.GetGrain<ILiveBotRegistryGrain>(0);
|
||||
var status = await botRegistry.GetBotStatus(this.GetPrimaryKey());
|
||||
await SaveBotAsync(status);
|
||||
};
|
||||
|
||||
return tradingBot;
|
||||
}
|
||||
|
||||
@@ -1063,6 +1075,7 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
|
||||
Volume = 0,
|
||||
Fees = 0,
|
||||
BotTradingBalance = _state.State.Config.BotTradingBalance,
|
||||
BacktestId = _state.State.Config.BacktestId,
|
||||
MasterBotUserId = _state.State.Config.MasterBotUserId
|
||||
};
|
||||
}
|
||||
@@ -1129,6 +1142,7 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
|
||||
LongPositionCount = longPositionCount,
|
||||
ShortPositionCount = shortPositionCount,
|
||||
BotTradingBalance = _state.State.Config.BotTradingBalance,
|
||||
BacktestId = _state.State.Config.BacktestId,
|
||||
MasterBotUserId = _state.State.Config.MasterBotUserId
|
||||
};
|
||||
}
|
||||
|
||||
@@ -454,6 +454,75 @@ public class SpotBot : TradingBotBase
|
||||
|
||||
if (positionQuantity > 0)
|
||||
{
|
||||
// Check if token balance is very low (dust) compared to position quantity
|
||||
// This likely means the position was closed but status wasn't updated
|
||||
var dustThreshold = Config.Ticker == Ticker.ETH
|
||||
? 0.01m // ETH: 0.01 ETH is likely gas reserve or dust
|
||||
: 0.0001m; // Other tokens: very small amount is dust
|
||||
|
||||
var isDustAmount = tokenBalanceAmount <= dustThreshold;
|
||||
var balanceIsVeryLow = tokenBalanceAmount < positionQuantity * 0.1m; // Less than 10% of expected
|
||||
|
||||
// If balance is dust or very low, check if opening swap failed or position was closed
|
||||
if (isDustAmount || balanceIsVeryLow)
|
||||
{
|
||||
if (internalPosition.Status == PositionStatus.Filled)
|
||||
{
|
||||
// First, check if the opening swap actually succeeded (exists in history)
|
||||
// If not, the swap likely failed and position should be marked as Canceled
|
||||
var openingSwapConfirmed = await VerifyOpeningSwapInHistory(internalPosition);
|
||||
|
||||
if (!openingSwapConfirmed)
|
||||
{
|
||||
// Opening swap not found in history - likely failed
|
||||
// Mark position as Canceled since it never actually opened
|
||||
var previousStatus = internalPosition.Status;
|
||||
internalPosition.Status = PositionStatus.Canceled;
|
||||
internalPosition.Open.SetStatus(TradeStatus.Cancelled);
|
||||
positionForSignal.Open.SetStatus(TradeStatus.Cancelled);
|
||||
await SetPositionStatus(internalPosition.SignalIdentifier, PositionStatus.Canceled);
|
||||
await UpdatePositionInDatabaseAsync(internalPosition);
|
||||
|
||||
await LogWarningAsync(
|
||||
$"❌ Position Opening Failed - Swap Not Found in History\n" +
|
||||
$"Position: `{internalPosition.Identifier}`\n" +
|
||||
$"Signal: `{internalPosition.SignalIdentifier}`\n" +
|
||||
$"Ticker: {Config.Ticker}\n" +
|
||||
$"Expected Quantity: `{positionQuantity:F5}`\n" +
|
||||
$"Token Balance: `{tokenBalanceAmount:F5}` (very low)\n" +
|
||||
$"Status Changed: `{previousStatus}` → `Canceled`\n" +
|
||||
$"The opening swap (USDC → {Config.Ticker}) was not found in exchange history\n" +
|
||||
$"This indicates the swap failed and position never opened\n" +
|
||||
$"Position will not be tracked or managed");
|
||||
|
||||
await NotifyAgentAndPlatformAsync(NotificationEventType.PositionClosed, internalPosition);
|
||||
return; // Exit - position failed to open
|
||||
}
|
||||
|
||||
// Opening swap exists, so position did open - check if it was closed
|
||||
var (positionFoundInHistory, hadWeb3ProxyError) =
|
||||
await CheckSpotPositionInExchangeHistory(internalPosition);
|
||||
|
||||
if (hadWeb3ProxyError)
|
||||
{
|
||||
await LogWarningAsync(
|
||||
$"⏳ Web3Proxy Error During Spot Position Verification\n" +
|
||||
$"Position: `{internalPosition.Identifier}`\n" +
|
||||
$"Cannot verify if position is closed\n" +
|
||||
$"Will retry on next execution cycle");
|
||||
return;
|
||||
}
|
||||
|
||||
if (positionFoundInHistory)
|
||||
{
|
||||
// Position was closed - mark as Finished
|
||||
internalPosition.Status = PositionStatus.Finished;
|
||||
await HandleClosedPosition(internalPosition);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only check tolerance if token balance is LESS than position quantity
|
||||
// If balance is greater, it could be orphaned tokens from previous positions
|
||||
if (tokenBalanceAmount < positionQuantity)
|
||||
@@ -463,15 +532,19 @@ public class SpotBot : TradingBotBase
|
||||
|
||||
if (difference > tolerance)
|
||||
{
|
||||
await LogWarningAsync(
|
||||
$"⚠️ Token Balance Below Position Quantity\n" +
|
||||
$"Position: `{internalPosition.Identifier}`\n" +
|
||||
$"Position Quantity: `{positionQuantity:F5}`\n" +
|
||||
$"Token Balance: `{tokenBalanceAmount:F5}`\n" +
|
||||
$"Difference: `{difference:F5}`\n" +
|
||||
$"Tolerance (0.7%): `{tolerance:F5}`\n" +
|
||||
$"Token balance is significantly lower than expected\n" +
|
||||
$"Skipping position synchronization");
|
||||
// Only log warning if this is not a dust amount (already handled above)
|
||||
if (!isDustAmount && !balanceIsVeryLow)
|
||||
{
|
||||
await LogWarningAsync(
|
||||
$"⚠️ Token Balance Below Position Quantity\n" +
|
||||
$"Position: `{internalPosition.Identifier}`\n" +
|
||||
$"Position Quantity: `{positionQuantity:F5}`\n" +
|
||||
$"Token Balance: `{tokenBalanceAmount:F5}`\n" +
|
||||
$"Difference: `{difference:F5}`\n" +
|
||||
$"Tolerance (0.7%): `{tolerance:F5}`\n" +
|
||||
$"Token balance is significantly lower than expected\n" +
|
||||
$"Skipping position synchronization");
|
||||
}
|
||||
return; // Skip processing if balance is too low
|
||||
}
|
||||
}
|
||||
@@ -490,25 +563,68 @@ public class SpotBot : TradingBotBase
|
||||
}
|
||||
}
|
||||
|
||||
// Token balance exists - verify the opening swap actually succeeded in history before marking as Filled
|
||||
// Token balance exists - ALWAYS verify the opening swap actually succeeded in history
|
||||
// This is critical because Web3Proxy may return success when transaction is sent,
|
||||
// but the swap might fail on-chain. We must verify it actually executed.
|
||||
var previousPositionStatus = internalPosition.Status;
|
||||
|
||||
// Only check history if position is not already Filled
|
||||
if (internalPosition.Status != PositionStatus.Filled)
|
||||
// Always verify the opening swap exists in history, even if position is already marked as Filled
|
||||
// This catches cases where the position was prematurely marked as Filled by the command handler
|
||||
bool swapConfirmedInHistory = await VerifyOpeningSwapInHistory(internalPosition);
|
||||
|
||||
if (!swapConfirmedInHistory)
|
||||
{
|
||||
// Verify the opening swap actually executed successfully
|
||||
bool swapConfirmedInHistory = await VerifyOpeningSwapInHistory(internalPosition);
|
||||
// Swap not found in history - check if this is a failed swap or just delayed
|
||||
// If balance is very low, the swap likely failed
|
||||
var dustThreshold = Config.Ticker == Ticker.ETH
|
||||
? 0.01m // ETH: 0.01 ETH is likely gas reserve or dust
|
||||
: 0.0001m; // Other tokens: very small amount is dust
|
||||
|
||||
if (!swapConfirmedInHistory)
|
||||
var isDustAmount = tokenBalanceAmount <= dustThreshold;
|
||||
var balanceIsVeryLow = tokenBalanceAmount < positionQuantity * 0.1m; // Less than 10% of expected
|
||||
|
||||
if (isDustAmount || balanceIsVeryLow)
|
||||
{
|
||||
await LogDebugAsync(
|
||||
$"⏳ Opening Swap Not Yet Confirmed in History\n" +
|
||||
// Balance is very low and swap not in history - swap likely failed
|
||||
// Mark position as Canceled since swap failed on-chain
|
||||
var previousStatus = internalPosition.Status;
|
||||
internalPosition.Status = PositionStatus.Canceled;
|
||||
internalPosition.Open.SetStatus(TradeStatus.Cancelled);
|
||||
positionForSignal.Open.SetStatus(TradeStatus.Cancelled);
|
||||
await SetPositionStatus(internalPosition.SignalIdentifier, PositionStatus.Canceled);
|
||||
await UpdatePositionInDatabaseAsync(internalPosition);
|
||||
|
||||
await LogWarningAsync(
|
||||
$"❌ Position Opening Failed - Swap Not Found in History\n" +
|
||||
$"Position: `{internalPosition.Identifier}`\n" +
|
||||
$"Token Balance: `{tokenBalanceAmount:F5}`\n" +
|
||||
$"Status: `{internalPosition.Status}`\n" +
|
||||
$"Waiting for swap to appear in exchange history...\n" +
|
||||
$"Will retry on next cycle");
|
||||
return; // Don't mark as Filled yet, wait for history confirmation
|
||||
$"Signal: `{internalPosition.SignalIdentifier}`\n" +
|
||||
$"Ticker: {Config.Ticker}\n" +
|
||||
$"Expected Quantity: `{positionQuantity:F5}`\n" +
|
||||
$"Token Balance: `{tokenBalanceAmount:F5}` (very low)\n" +
|
||||
$"Status Changed: `{previousStatus}` → `Canceled`\n" +
|
||||
$"The opening swap (USDC → {Config.Ticker}) was not found in exchange history\n" +
|
||||
$"This indicates the swap transaction failed on-chain even though it was sent\n" +
|
||||
$"Position will not be tracked or managed");
|
||||
|
||||
await NotifyAgentAndPlatformAsync(NotificationEventType.PositionClosed, internalPosition);
|
||||
return; // Exit - swap failed
|
||||
}
|
||||
else
|
||||
{
|
||||
// Balance exists but swap not yet in history - might be delayed
|
||||
// Only wait if position is not already Filled (to avoid repeated checks)
|
||||
if (internalPosition.Status != PositionStatus.Filled)
|
||||
{
|
||||
await LogDebugAsync(
|
||||
$"⏳ Opening Swap Not Yet Confirmed in History\n" +
|
||||
$"Position: `{internalPosition.Identifier}`\n" +
|
||||
$"Token Balance: `{tokenBalanceAmount:F5}`\n" +
|
||||
$"Status: `{internalPosition.Status}`\n" +
|
||||
$"Waiting for swap to appear in exchange history...\n" +
|
||||
$"Will retry on next cycle");
|
||||
return; // Don't mark as Filled yet, wait for history confirmation
|
||||
}
|
||||
// If already Filled, continue - might be a timing issue with history indexing
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ public abstract class TradingBotBase : ITradingBot
|
||||
public Dictionary<string, LightSignal> Signals { get; set; }
|
||||
public Dictionary<Guid, Position> Positions { get; set; }
|
||||
public Dictionary<DateTime, decimal> WalletBalances { get; set; }
|
||||
private decimal _currentBalance;
|
||||
public DateTime PreloadSince { get; set; }
|
||||
public int PreloadedCandlesCount { get; set; }
|
||||
public long ExecutionCount { get; set; } = 0;
|
||||
@@ -51,6 +50,12 @@ public abstract class TradingBotBase : ITradingBot
|
||||
// OPTIMIZATION 2: Cache open position state to avoid expensive Positions.Any() calls
|
||||
private bool _hasOpenPosition = false;
|
||||
|
||||
/// <summary>
|
||||
/// Callback to notify the grain when balance is updated (for immediate sync and save to database).
|
||||
/// Set by the grain after creating the bot instance.
|
||||
/// </summary>
|
||||
public Func<Task>? OnBalanceUpdatedCallback { get; set; }
|
||||
|
||||
public TradingBotBase(
|
||||
ILogger<TradingBotBase> logger,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
@@ -65,7 +70,6 @@ public abstract class TradingBotBase : ITradingBot
|
||||
Signals = new Dictionary<string, LightSignal>();
|
||||
Positions = new Dictionary<Guid, Position>();
|
||||
WalletBalances = new Dictionary<DateTime, decimal>();
|
||||
_currentBalance = config.BotTradingBalance;
|
||||
PreloadSince = CandleHelpers.GetBotPreloadSinceFromTimeframe(config.Timeframe);
|
||||
}
|
||||
|
||||
@@ -433,13 +437,13 @@ public abstract class TradingBotBase : ITradingBot
|
||||
|
||||
if (WalletBalances.Count == 0)
|
||||
{
|
||||
WalletBalances[date] = _currentBalance;
|
||||
WalletBalances[date] = Config.BotTradingBalance;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!WalletBalances.ContainsKey(date))
|
||||
{
|
||||
WalletBalances[date] = _currentBalance;
|
||||
WalletBalances[date] = Config.BotTradingBalance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1270,13 +1274,15 @@ public abstract class TradingBotBase : ITradingBot
|
||||
|
||||
if (position.ProfitAndLoss != null)
|
||||
{
|
||||
// Update the current balance when position closes
|
||||
_currentBalance += position.ProfitAndLoss.Net;
|
||||
// Update the balance when position closes
|
||||
Config.BotTradingBalance += position.ProfitAndLoss.Net;
|
||||
|
||||
await LogDebugAsync(
|
||||
string.Format("💰 Balance Updated\nNew bot trading balance: `${0:F2}`",
|
||||
Config.BotTradingBalance));
|
||||
|
||||
// For live trading, immediately sync and save to database
|
||||
await OnBalanceUpdatedAsync();
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -2060,6 +2066,18 @@ public abstract class TradingBotBase : ITradingBot
|
||||
await CancelAllOrders();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the bot trading balance is updated (e.g., after a position closes).
|
||||
/// Calls the OnBalanceUpdatedCallback if set (by the grain for live trading).
|
||||
/// </summary>
|
||||
protected virtual async Task OnBalanceUpdatedAsync()
|
||||
{
|
||||
if (OnBalanceUpdatedCallback != null)
|
||||
{
|
||||
await OnBalanceUpdatedCallback();
|
||||
}
|
||||
}
|
||||
|
||||
// Interface implementation
|
||||
public async Task LogInformation(string message)
|
||||
{
|
||||
|
||||
@@ -123,7 +123,7 @@ namespace Managing.Application.Trading.Handlers
|
||||
position.TakeProfit1 = exchangeService.BuildEmptyTrade(
|
||||
request.Ticker,
|
||||
takeProfitPrice,
|
||||
quantity,
|
||||
position.Open.Quantity, // Use same quantity as StopLoss for consistency
|
||||
closeDirection,
|
||||
request.MoneyManagement.Leverage,
|
||||
TradeType.TakeProfit,
|
||||
|
||||
@@ -121,7 +121,7 @@ public class OpenSpotPositionCommandHandler(
|
||||
position.TakeProfit1 = exchangeService.BuildEmptyTrade(
|
||||
request.Ticker,
|
||||
takeProfitPrice,
|
||||
quantity,
|
||||
position.Open.Quantity, // Use same quantity as StopLoss for consistency
|
||||
TradeDirection.Short,
|
||||
1, // Spot trading has no leverage
|
||||
TradeType.TakeProfit,
|
||||
|
||||
@@ -31,6 +31,11 @@ namespace Managing.Domain.Bots
|
||||
|
||||
public decimal BotTradingBalance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The backtest ID associated with this bot (nullable for bots not created from backtests)
|
||||
/// </summary>
|
||||
public int? BacktestId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user ID of the master bot's owner when this bot is for copy trading
|
||||
/// </summary>
|
||||
|
||||
@@ -123,4 +123,10 @@ public class TradingBotConfig
|
||||
/// </summary>
|
||||
[Id(23)]
|
||||
public int? MasterBotUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The backtest ID associated with this bot configuration (nullable for bots not created from backtests)
|
||||
/// </summary>
|
||||
[Id(24)]
|
||||
public int? BacktestId { get; set; }
|
||||
}
|
||||
@@ -112,4 +112,9 @@ public class TradingBotConfigRequest
|
||||
public bool UseForDynamicStopLoss { get; set; } = true;
|
||||
|
||||
public TradingType TradingType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The backtest ID associated with this bot configuration (nullable for bots not created from backtests)
|
||||
/// </summary>
|
||||
public int? BacktestId { get; set; }
|
||||
}
|
||||
1799
src/Managing.Infrastructure.Database/Migrations/20260109112209_AddBacktestIdToBots.Designer.cs
generated
Normal file
1799
src/Managing.Infrastructure.Database/Migrations/20260109112209_AddBacktestIdToBots.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Managing.Infrastructure.Databases.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddBacktestIdToBots : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "BacktestId",
|
||||
table: "Bots",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "BacktestId",
|
||||
table: "Bots");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -305,6 +305,9 @@ namespace Managing.Infrastructure.Databases.Migrations
|
||||
b.Property<long>("AccumulatedRunTimeSeconds")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int?>("BacktestId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<decimal>("BotTradingBalance")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
|
||||
@@ -276,7 +276,8 @@ public class AgentSummaryRepository : IAgentSummaryRepository
|
||||
entity.TotalVolume = domain.TotalVolume;
|
||||
entity.TotalBalance = domain.TotalBalance;
|
||||
entity.TotalFees = domain.TotalFees;
|
||||
entity.BacktestCount = domain.BacktestCount;
|
||||
// BacktestCount is NOT updated here - it's managed independently via IncrementBacktestCountAsync
|
||||
// This prevents other update operations from overwriting the BacktestCount
|
||||
}
|
||||
|
||||
private static AgentSummary MapToDomain(AgentSummaryEntity entity)
|
||||
|
||||
@@ -41,6 +41,11 @@ public class BotEntity
|
||||
|
||||
public decimal BotTradingBalance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The backtest ID associated with this bot (nullable for bots not created from backtests)
|
||||
/// </summary>
|
||||
public int? BacktestId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user ID of the master bot's owner when this bot is for copy trading
|
||||
/// </summary>
|
||||
|
||||
@@ -298,6 +298,17 @@ public class PostgreSqlBacktestRepository : IBacktestRepository
|
||||
return entity != null ? PostgreSqlMappers.Map(entity) : null;
|
||||
}
|
||||
|
||||
public async Task<Backtest> GetBacktestByIdAsync(string id)
|
||||
{
|
||||
var entity = await _context.Backtests
|
||||
.AsNoTracking()
|
||||
.Include(b => b.User)
|
||||
.FirstOrDefaultAsync(b => b.Identifier == id)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return entity != null ? PostgreSqlMappers.Map(entity) : null;
|
||||
}
|
||||
|
||||
public void DeleteBacktestByIdForUser(User user, string id)
|
||||
{
|
||||
var entity = _context.Backtests
|
||||
|
||||
@@ -83,6 +83,7 @@ public class PostgreSqlBotRepository : IBotRepository
|
||||
existingEntity.AccumulatedRunTimeSeconds = bot.AccumulatedRunTimeSeconds;
|
||||
existingEntity.MasterBotUserId =
|
||||
bot.MasterBotUserId ?? existingEntity.MasterBotUserId;
|
||||
existingEntity.BotTradingBalance = bot.BotTradingBalance;
|
||||
|
||||
await _context.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -796,6 +796,7 @@ public static class PostgreSqlMappers
|
||||
LongPositionCount = entity.LongPositionCount,
|
||||
ShortPositionCount = entity.ShortPositionCount,
|
||||
BotTradingBalance = entity.BotTradingBalance,
|
||||
BacktestId = entity.BacktestId,
|
||||
MasterBotUserId = entity.MasterBotUserId,
|
||||
MasterBotUser = entity.MasterBotUser != null ? Map(entity.MasterBotUser) : null
|
||||
};
|
||||
@@ -830,6 +831,7 @@ public static class PostgreSqlMappers
|
||||
LongPositionCount = bot.LongPositionCount,
|
||||
ShortPositionCount = bot.ShortPositionCount,
|
||||
BotTradingBalance = bot.BotTradingBalance,
|
||||
BacktestId = bot.BacktestId,
|
||||
MasterBotUserId = bot.MasterBotUserId,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user