Fix spot bot

This commit is contained in:
2025-12-04 21:21:48 +07:00
parent a07d7ede18
commit b44e1f66a7
13 changed files with 117 additions and 47 deletions

View File

@@ -814,7 +814,7 @@ public class BotController : BaseController
UseForSignalFiltering = request.Config.UseForSignalFiltering, UseForSignalFiltering = request.Config.UseForSignalFiltering,
UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss, UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss,
// Set computed/default properties // Set computed/default properties
TradingType = TradingType.Futures, TradingType = request.Config.TradingType,
FlipPosition = request.Config.FlipPosition, FlipPosition = request.Config.FlipPosition,
Name = request.Config.Name Name = request.Config.Name
}; };
@@ -955,6 +955,13 @@ 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
{ {
@@ -976,9 +983,9 @@ public class BotController : BaseController
UseForSignalFiltering = request.Config.UseForSignalFiltering, UseForSignalFiltering = request.Config.UseForSignalFiltering,
UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss, UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss,
// Set computed/default properties // Set computed/default properties
TradingType = TradingType.Futures,
FlipPosition = request.Config.FlipPosition, FlipPosition = request.Config.FlipPosition,
Name = request.Config.Name Name = request.Config.Name,
TradingType = tradingType
}; };
return (config, user); return (config, user);

View File

@@ -197,7 +197,7 @@ public class JobService
Timeframe = backtestRequest.Config.Timeframe, Timeframe = backtestRequest.Config.Timeframe,
IsForWatchingOnly = backtestRequest.Config.IsForWatchingOnly, IsForWatchingOnly = backtestRequest.Config.IsForWatchingOnly,
BotTradingBalance = backtestRequest.Config.BotTradingBalance, BotTradingBalance = backtestRequest.Config.BotTradingBalance,
TradingType = TradingType.BacktestFutures, TradingType = backtestRequest.Config.TradingType,
CooldownPeriod = backtestRequest.Config.CooldownPeriod ?? 1, CooldownPeriod = backtestRequest.Config.CooldownPeriod ?? 1,
MaxLossStreak = backtestRequest.Config.MaxLossStreak, MaxLossStreak = backtestRequest.Config.MaxLossStreak,
MaxPositionTimeHours = backtestRequest.Config.MaxPositionTimeHours, MaxPositionTimeHours = backtestRequest.Config.MaxPositionTimeHours,

View File

@@ -447,7 +447,8 @@ namespace Managing.Application.Backtests
UseSynthApi = universalConfig.UseSynthApi, UseSynthApi = universalConfig.UseSynthApi,
UseForPositionSizing = universalConfig.UseForPositionSizing, UseForPositionSizing = universalConfig.UseForPositionSizing,
UseForSignalFiltering = universalConfig.UseForSignalFiltering, UseForSignalFiltering = universalConfig.UseForSignalFiltering,
UseForDynamicStopLoss = universalConfig.UseForDynamicStopLoss UseForDynamicStopLoss = universalConfig.UseForDynamicStopLoss,
TradingType = universalConfig.TradingType
}; };
var backtestRequest = new RunBacktestRequest var backtestRequest = new RunBacktestRequest

View File

@@ -544,6 +544,18 @@ public class SpotBot : TradingBotBase, ITradingBot
return; return;
} }
// Ensure account is loaded before accessing Account.Exchange
if (Account == null)
{
Logger.LogWarning("Cannot update signals: Account is null. Loading account...");
await LoadAccountAsync();
if (Account == null)
{
Logger.LogError("Cannot update signals: Account failed to load");
return;
}
}
// Live trading: use ScenarioRunnerGrain to get signals // Live trading: use ScenarioRunnerGrain to get signals
await ServiceScopeHelpers.WithScopedService<IGrainFactory>(_scopeFactory, async grainFactory => await ServiceScopeHelpers.WithScopedService<IGrainFactory>(_scopeFactory, async grainFactory =>
{ {

View File

@@ -187,7 +187,7 @@ namespace Managing.Application.ManageBot
MasterBotUserId = masterBot.User.Id, MasterBotUserId = masterBot.User.Id,
// Set computed/default properties // Set computed/default properties
TradingType = TradingType.Futures, TradingType = masterConfig.TradingType,
Name = masterConfig.Name Name = masterConfig.Name
}; };

View File

@@ -60,11 +60,24 @@ public class ScenarioRunnerGrain : Grain, IScenarioRunnerGrain
{ {
try try
{ {
if (candle == null)
{
_logger.LogWarning($"Cannot generate signals: candle is null for {config.Ticker} for {config.Name}");
return null;
}
var candlesHashSet = await GetCandlesAsync(tradingExchanges, config); var candlesHashSet = await GetCandlesAsync(tradingExchanges, config);
// Convert to ordered List to preserve chronological order for indicators // Convert to ordered List to preserve chronological order for indicators
var candlesList = candlesHashSet.OrderBy(c => c.Date).ToList(); var candlesList = candlesHashSet.OrderBy(c => c.Date).ToList();
if (candlesList.LastOrDefault()!.Date <= candle.Date) if (candlesList.Count == 0)
{
_logger.LogWarning($"No candles available for {config.Ticker} for {config.Name}");
return null; // No candles available, cannot generate signal
}
var lastCandle = candlesList.Last();
if (lastCandle.Date <= candle.Date)
{ {
_logger.LogWarning($"No new candles for {config.Ticker} for {config.Name}"); _logger.LogWarning($"No new candles for {config.Ticker} for {config.Name}");
return null; // No new candles, no need to generate a signal return null; // No new candles, no need to generate a signal

View File

@@ -22,7 +22,10 @@ public class BundleBacktestHealthCheckWorker : BackgroundService
private readonly ILogger<BundleBacktestHealthCheckWorker> _logger; private readonly ILogger<BundleBacktestHealthCheckWorker> _logger;
private readonly TimeSpan _checkInterval = TimeSpan.FromMinutes(30); private readonly TimeSpan _checkInterval = TimeSpan.FromMinutes(30);
private readonly TimeSpan _inactiveThreshold = TimeSpan.FromMinutes(2); // Check bundles inactive for 2+ minutes private readonly TimeSpan _inactiveThreshold = TimeSpan.FromMinutes(2); // Check bundles inactive for 2+ minutes
private readonly TimeSpan _stuckThreshold = TimeSpan.FromHours(2); // Consider bundle stuck if no progress for 2 hours
private readonly TimeSpan
_stuckThreshold = TimeSpan.FromHours(2); // Consider bundle stuck if no progress for 2 hours
private readonly IMessengerService _messengerService; private readonly IMessengerService _messengerService;
public BundleBacktestHealthCheckWorker( public BundleBacktestHealthCheckWorker(
@@ -80,8 +83,10 @@ public class BundleBacktestHealthCheckWorker : BackgroundService
_logger.LogInformation("Starting bundle health check..."); _logger.LogInformation("Starting bundle health check...");
// Check bundles in Pending and Running status // Check bundles in Pending and Running status
var pendingBundles = await backtestRepository.GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus.Pending); var pendingBundles =
var runningBundles = await backtestRepository.GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus.Running); await backtestRepository.GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus.Pending);
var runningBundles =
await backtestRepository.GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus.Running);
// Only check bundles that haven't been updated in more than the inactive threshold // Only check bundles that haven't been updated in more than the inactive threshold
var inactiveThresholdTime = DateTime.UtcNow.Add(-_inactiveThreshold); var inactiveThresholdTime = DateTime.UtcNow.Add(-_inactiveThreshold);
@@ -138,7 +143,8 @@ public class BundleBacktestHealthCheckWorker : BackgroundService
_logger.LogDebug( _logger.LogDebug(
"Bundle {BundleRequestId} ({Status}): Expected {Expected} jobs, Found {Actual} jobs, Completed {Completed}/{Total}", "Bundle {BundleRequestId} ({Status}): Expected {Expected} jobs, Found {Actual} jobs, Completed {Completed}/{Total}",
bundle.RequestId, bundle.Status, expectedJobCount, actualJobCount, bundle.CompletedBacktests, bundle.TotalBacktests); bundle.RequestId, bundle.Status, expectedJobCount, actualJobCount, bundle.CompletedBacktests,
bundle.TotalBacktests);
// Check 1: Missing jobs - bundle has no jobs or fewer jobs than expected // Check 1: Missing jobs - bundle has no jobs or fewer jobs than expected
if (actualJobCount == 0 || actualJobCount < expectedJobCount) if (actualJobCount == 0 || actualJobCount < expectedJobCount)
@@ -172,7 +178,8 @@ public class BundleBacktestHealthCheckWorker : BackgroundService
if (allJobsPending || hasFailedJobs) if (allJobsPending || hasFailedJobs)
{ {
await HandleStalePendingBundleAsync(bundle, timeSinceCreation, jobs, backtestRepository, jobRepository); await HandleStalePendingBundleAsync(bundle, timeSinceCreation, jobs, backtestRepository,
jobRepository);
return (StuckCount: 1, MissingJobsCount: 0, HealthyCount: 0); return (StuckCount: 1, MissingJobsCount: 0, HealthyCount: 0);
} }
} }
@@ -184,7 +191,8 @@ public class BundleBacktestHealthCheckWorker : BackgroundService
var totalProcessedJobs = completedJobs + failedJobs; var totalProcessedJobs = completedJobs + failedJobs;
if (totalProcessedJobs == bundle.TotalBacktests && if (totalProcessedJobs == bundle.TotalBacktests &&
(bundle.Status == BundleBacktestRequestStatus.Running || bundle.Status == BundleBacktestRequestStatus.Pending)) (bundle.Status == BundleBacktestRequestStatus.Running ||
bundle.Status == BundleBacktestRequestStatus.Pending))
{ {
await HandleCompletedBundleAsync(bundle, completedJobs, failedJobs, backtestRepository); await HandleCompletedBundleAsync(bundle, completedJobs, failedJobs, backtestRepository);
return (StuckCount: 0, MissingJobsCount: 0, HealthyCount: 1); return (StuckCount: 0, MissingJobsCount: 0, HealthyCount: 1);
@@ -244,7 +252,8 @@ public class BundleBacktestHealthCheckWorker : BackgroundService
var missingRequests = allBacktestRequests var missingRequests = allBacktestRequests
.Where(req => .Where(req =>
{ {
var signature = $"{req.Config.Ticker}_{req.Config.Timeframe}_{req.Config.Name}_{req.StartDate:yyyyMMdd}_{req.EndDate:yyyyMMdd}"; var signature =
$"{req.Config.Ticker}_{req.Config.Timeframe}_{req.Config.Name}_{req.StartDate:yyyyMMdd}_{req.EndDate:yyyyMMdd}";
return !existingJobSignatures.Contains(signature); return !existingJobSignatures.Contains(signature);
}) })
.ToList(); .ToList();
@@ -324,7 +333,8 @@ public class BundleBacktestHealthCheckWorker : BackgroundService
// Get the first account for the user // Get the first account for the user
using var scope = _scopeFactory.CreateScope(); using var scope = _scopeFactory.CreateScope();
var accountService = scope.ServiceProvider.GetRequiredService<IAccountService>(); var accountService = scope.ServiceProvider.GetRequiredService<IAccountService>();
var accounts = await accountService.GetAccountsByUserAsync(bundleRequest.User, hideSecrets: true, getBalance: false); var accounts =
await accountService.GetAccountsByUserAsync(bundleRequest.User, hideSecrets: true, getBalance: false);
var firstAccount = accounts.FirstOrDefault(); var firstAccount = accounts.FirstOrDefault();
if (firstAccount == null) if (firstAccount == null)
@@ -362,7 +372,8 @@ public class BundleBacktestHealthCheckWorker : BackgroundService
UseSynthApi = universalConfig.UseSynthApi, UseSynthApi = universalConfig.UseSynthApi,
UseForPositionSizing = universalConfig.UseForPositionSizing, UseForPositionSizing = universalConfig.UseForPositionSizing,
UseForSignalFiltering = universalConfig.UseForSignalFiltering, UseForSignalFiltering = universalConfig.UseForSignalFiltering,
UseForDynamicStopLoss = universalConfig.UseForDynamicStopLoss UseForDynamicStopLoss = universalConfig.UseForDynamicStopLoss,
TradingType = universalConfig.TradingType
}; };
var backtestRequest = new RunBacktestRequest var backtestRequest = new RunBacktestRequest
@@ -435,7 +446,9 @@ public class BundleBacktestHealthCheckWorker : BackgroundService
"Bundle {BundleRequestId} has all jobs completed ({Completed} completed, {Failed} failed). Updating bundle status.", "Bundle {BundleRequestId} has all jobs completed ({Completed} completed, {Failed} failed). Updating bundle status.",
bundle.RequestId, completedJobs, failedJobs); bundle.RequestId, completedJobs, failedJobs);
bundle.Status = failedJobs == 0 ? BundleBacktestRequestStatus.Completed : BundleBacktestRequestStatus.Completed; bundle.Status = failedJobs == 0
? BundleBacktestRequestStatus.Completed
: BundleBacktestRequestStatus.Completed;
bundle.CompletedBacktests = completedJobs; bundle.CompletedBacktests = completedJobs;
bundle.FailedBacktests = failedJobs; bundle.FailedBacktests = failedJobs;
bundle.CompletedAt = DateTime.UtcNow; bundle.CompletedAt = DateTime.UtcNow;
@@ -473,7 +486,8 @@ public class BundleBacktestHealthCheckWorker : BackgroundService
// Update bundle timestamp to give it another chance // Update bundle timestamp to give it another chance
bundle.UpdatedAt = DateTime.UtcNow; bundle.UpdatedAt = DateTime.UtcNow;
bundle.ErrorMessage = $"Bundle was stuck. Reset {runningJobs.Count(j => j.Status == JobStatus.Pending)} stale jobs to pending."; bundle.ErrorMessage =
$"Bundle was stuck. Reset {runningJobs.Count(j => j.Status == JobStatus.Pending)} stale jobs to pending.";
} }
await backtestRepository.UpdateBundleBacktestRequestAsync(bundle); await backtestRepository.UpdateBundleBacktestRequestAsync(bundle);
@@ -585,4 +599,3 @@ public class BundleBacktestHealthCheckWorker : BackgroundService
} }
} }
} }

View File

@@ -403,7 +403,8 @@ public class BundleBacktestWorker : BaseWorker<BundleBacktestWorker>
UseSynthApi = universalConfig.UseSynthApi, UseSynthApi = universalConfig.UseSynthApi,
UseForPositionSizing = universalConfig.UseForPositionSizing, UseForPositionSizing = universalConfig.UseForPositionSizing,
UseForSignalFiltering = universalConfig.UseForSignalFiltering, UseForSignalFiltering = universalConfig.UseForSignalFiltering,
UseForDynamicStopLoss = universalConfig.UseForDynamicStopLoss UseForDynamicStopLoss = universalConfig.UseForDynamicStopLoss,
TradingType = universalConfig.TradingType
}; };
var backtestRequest = new RunBacktestRequest var backtestRequest = new RunBacktestRequest

View File

@@ -110,4 +110,6 @@ public class TradingBotConfigRequest
/// Whether to use Synth predictions for dynamic stop-loss/take-profit adjustments /// Whether to use Synth predictions for dynamic stop-loss/take-profit adjustments
/// </summary> /// </summary>
public bool UseForDynamicStopLoss { get; set; } = true; public bool UseForDynamicStopLoss { get; set; } = true;
public TradingType TradingType { get; set; }
} }

View File

@@ -25,6 +25,7 @@ import {
Ticker, Ticker,
Timeframe, Timeframe,
TradingBotConfigRequest, TradingBotConfigRequest,
TradingType,
UpdateBotConfigRequest, UpdateBotConfigRequest,
} from '../../../generated/ManagingApi' } from '../../../generated/ManagingApi'
import type {IUnifiedTradingConfigInput, UnifiedTradingModalProps} from '../../../global/type' import type {IUnifiedTradingConfigInput, UnifiedTradingModalProps} from '../../../global/type'
@@ -83,6 +84,7 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
flipOnlyWhenInProfit: true, flipOnlyWhenInProfit: true,
balance: 10000, balance: 10000,
closeEarlyWhenProfitable: false, closeEarlyWhenProfitable: false,
tradingType: TradingType.BacktestSpot,
useSynthApi: false, useSynthApi: false,
useForPositionSizing: true, useForPositionSizing: true,
useForSignalFiltering: true, useForSignalFiltering: true,
@@ -217,6 +219,7 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
setValue('useForPositionSizing', backtest.config.useForPositionSizing ?? true); setValue('useForPositionSizing', backtest.config.useForPositionSizing ?? true);
setValue('useForSignalFiltering', backtest.config.useForSignalFiltering ?? true); setValue('useForSignalFiltering', backtest.config.useForSignalFiltering ?? true);
setValue('useForDynamicStopLoss', backtest.config.useForDynamicStopLoss ?? true); setValue('useForDynamicStopLoss', backtest.config.useForDynamicStopLoss ?? true);
setValue('tradingType', (backtest.config as any).tradingType || TradingType.BacktestSpot);
// Use backtest's money management as custom // Use backtest's money management as custom
if (backtest.config.moneyManagement) { if (backtest.config.moneyManagement) {
@@ -259,6 +262,7 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
setValue('useForPositionSizing', backtest.config.useForPositionSizing ?? true); setValue('useForPositionSizing', backtest.config.useForPositionSizing ?? true);
setValue('useForSignalFiltering', backtest.config.useForSignalFiltering ?? true); setValue('useForSignalFiltering', backtest.config.useForSignalFiltering ?? true);
setValue('useForDynamicStopLoss', backtest.config.useForDynamicStopLoss ?? true); setValue('useForDynamicStopLoss', backtest.config.useForDynamicStopLoss ?? true);
setValue('tradingType', (backtest.config as any).tradingType || TradingType.BacktestSpot);
// Set tickers for backtest (array) // Set tickers for backtest (array)
if (backtest.config.ticker) { if (backtest.config.ticker) {
@@ -318,6 +322,7 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
setValue('useForPositionSizing', config.useForPositionSizing ?? true); setValue('useForPositionSizing', config.useForPositionSizing ?? true);
setValue('useForSignalFiltering', config.useForSignalFiltering ?? true); setValue('useForSignalFiltering', config.useForSignalFiltering ?? true);
setValue('useForDynamicStopLoss', config.useForDynamicStopLoss ?? true); setValue('useForDynamicStopLoss', config.useForDynamicStopLoss ?? true);
setValue('tradingType', (config as any).tradingType || TradingType.BacktestSpot);
// Handle money management - if it exists, treat as custom for update mode // Handle money management - if it exists, treat as custom for update mode
if (config.moneyManagement) { if (config.moneyManagement) {
@@ -664,6 +669,7 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
useForPositionSizing: form.useForPositionSizing ?? true, useForPositionSizing: form.useForPositionSizing ?? true,
useForSignalFiltering: form.useForSignalFiltering ?? true, useForSignalFiltering: form.useForSignalFiltering ?? true,
useForDynamicStopLoss: form.useForDynamicStopLoss ?? true, useForDynamicStopLoss: form.useForDynamicStopLoss ?? true,
tradingType: form.tradingType || TradingType.BacktestSpot,
moneyManagementName: showCustomMoneyManagement ? undefined : selectedMoneyManagement, moneyManagementName: showCustomMoneyManagement ? undefined : selectedMoneyManagement,
moneyManagement: moneyManagement, moneyManagement: moneyManagement,
}; };
@@ -889,6 +895,18 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
))} ))}
</select> </select>
</FormInput> </FormInput>
{(mode === 'createBot' || mode === 'updateBot') && (
<FormInput label="Trading Type" htmlFor="tradingType">
<select
className="select select-bordered w-full"
{...register('tradingType', { required: true })}
>
<option value={TradingType.BacktestSpot}>Spot</option>
<option value={TradingType.BacktestFutures}>Futures</option>
</select>
</FormInput>
)}
</div> </div>
{/* Money Management */} {/* Money Management */}

View File

@@ -5298,10 +5298,10 @@ export interface BundleBacktestUniversalConfig {
isForWatchingOnly: boolean; isForWatchingOnly: boolean;
botTradingBalance: number; botTradingBalance: number;
botName: string; botName: string;
tradingType: TradingType;
flipPosition: boolean; flipPosition: boolean;
cooldownPeriod?: number | null; cooldownPeriod?: number | null;
maxLossStreak?: number; maxLossStreak?: number;
tradingType?: TradingType;
scenario?: ScenarioRequest | null; scenario?: ScenarioRequest | null;
scenarioName?: string | null; scenarioName?: string | null;
maxPositionTimeHours?: number | null; maxPositionTimeHours?: number | null;

View File

@@ -803,6 +803,7 @@ export interface BundleBacktestUniversalConfig {
isForWatchingOnly: boolean; isForWatchingOnly: boolean;
botTradingBalance: number; botTradingBalance: number;
botName: string; botName: string;
tradingType: TradingType;
flipPosition: boolean; flipPosition: boolean;
cooldownPeriod?: number | null; cooldownPeriod?: number | null;
maxLossStreak?: number; maxLossStreak?: number;

View File

@@ -15,7 +15,8 @@ import type {
Timeframe, Timeframe,
TradeDirection, TradeDirection,
TradingBotResponse, TradingBotResponse,
TradingExchanges TradingExchanges,
TradingType
} from '../generated/ManagingApi' } from '../generated/ManagingApi'
import {FC, ReactNode} from 'react' import {FC, ReactNode} from 'react'
@@ -71,6 +72,7 @@ export type IBacktestsFormInput = {
maxPositionTimeHours?: number | null maxPositionTimeHours?: number | null
flipOnlyWhenInProfit?: boolean flipOnlyWhenInProfit?: boolean
closeEarlyWhenProfitable?: boolean closeEarlyWhenProfitable?: boolean
tradingType?: TradingType
// Synth API fields // Synth API fields
useSynthApi?: boolean useSynthApi?: boolean
useForPositionSizing?: boolean useForPositionSizing?: boolean