diff --git a/src/Managing.Application.Abstractions/Repositories/IEvmManager.cs b/src/Managing.Application.Abstractions/Repositories/IEvmManager.cs index 627a7012..980b80f9 100644 --- a/src/Managing.Application.Abstractions/Repositories/IEvmManager.cs +++ b/src/Managing.Application.Abstractions/Repositories/IEvmManager.cs @@ -94,6 +94,24 @@ public interface IEvmManager int pageIndex = 0, int pageSize = 20); + /// + /// Gets the spot position history (swap executions) for a specific ticker and account from GMX + /// + /// The trading account + /// The ticker to get history for + /// Optional start date for filtering + /// Optional end date for filtering + /// Page index for pagination (default: 0) + /// Page size for pagination (default: 20) + /// Spot position history response containing swap executions + Task> GetSpotPositionHistory( + Account account, + Ticker ticker, + DateTime? fromDate = null, + DateTime? toDate = null, + int pageIndex = 0, + int pageSize = 20); + /// /// Gets the staked KUDAI balance for a specific address /// diff --git a/src/Managing.Application.Abstractions/Services/IExchangeService.cs b/src/Managing.Application.Abstractions/Services/IExchangeService.cs index e86f9b5c..213225cd 100644 --- a/src/Managing.Application.Abstractions/Services/IExchangeService.cs +++ b/src/Managing.Application.Abstractions/Services/IExchangeService.cs @@ -73,4 +73,12 @@ public interface IExchangeService Ticker ticker, DateTime? fromDate = null, DateTime? toDate = null); + + Task> GetSpotPositionHistory( + Account account, + Ticker ticker, + DateTime? fromDate = null, + DateTime? toDate = null, + int pageIndex = 0, + int pageSize = 20); } \ No newline at end of file diff --git a/src/Managing.Application.Abstractions/Services/IWeb3ProxyService.cs b/src/Managing.Application.Abstractions/Services/IWeb3ProxyService.cs index 7c0b6f11..eb1b27df 100644 --- a/src/Managing.Application.Abstractions/Services/IWeb3ProxyService.cs +++ b/src/Managing.Application.Abstractions/Services/IWeb3ProxyService.cs @@ -31,5 +31,13 @@ namespace Managing.Application.Abstractions.Services string? ticker = null, DateTime? fromDate = null, DateTime? toDate = null); + + Task> GetGmxSpotPositionHistoryAsync( + string account, + int pageIndex = 0, + int pageSize = 20, + string? ticker = null, + DateTime? fromDate = null, + DateTime? toDate = null); } } \ No newline at end of file diff --git a/src/Managing.Application/Bots/SpotBot.cs b/src/Managing.Application/Bots/SpotBot.cs index 7f9dc8d2..944dab89 100644 --- a/src/Managing.Application/Bots/SpotBot.cs +++ b/src/Managing.Application/Bots/SpotBot.cs @@ -337,11 +337,31 @@ public class SpotBot : TradingBotBase, ITradingBot // No token balance found - check if position was closed if (internalPosition.Status == PositionStatus.Filled) { + 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) + { + internalPosition.Status = PositionStatus.Finished; + await HandleClosedPosition(internalPosition); + return; + } + await LogDebugAsync( $"āš ļø Position Status Check\n" + $"Internal position `{internalPosition.Identifier}` shows Filled\n" + - $"But no token balance found on broker\n" + - $"Position may have been closed"); + $"But no token balance found on broker or in history\n" + + $"Will retry verification on next cycle"); } } } @@ -351,6 +371,56 @@ public class SpotBot : TradingBotBase, ITradingBot } } + private async Task<(bool found, bool hadError)> CheckSpotPositionInExchangeHistory(Position position) + { + try + { + await LogDebugAsync( + $"šŸ” Checking Spot Position History for Position: `{position.Identifier}`\nTicker: `{Config.Ticker}`"); + + List positionHistory = null; + await ServiceScopeHelpers.WithScopedService(_scopeFactory, + async exchangeService => + { + var fromDate = DateTime.UtcNow.AddHours(-24); + var toDate = DateTime.UtcNow; + positionHistory = + await exchangeService.GetSpotPositionHistory(Account, Config.Ticker, fromDate, toDate); + }); + + if (positionHistory != null && positionHistory.Any()) + { + var recentPosition = positionHistory + .OrderByDescending(p => p.Date) + .FirstOrDefault(); + + if (recentPosition != null) + { + await LogDebugAsync( + $"āœ… Spot Position Found in History\n" + + $"Position: `{position.Identifier}`\n" + + $"Ticker: `{recentPosition.Ticker}`\n" + + $"Date: `{recentPosition.Date}`"); + return (true, false); + } + } + + await LogDebugAsync( + $"āŒ No Spot Position Found in Exchange History\nPosition: `{position.Identifier}`\nPosition may still be open or data is delayed"); + return (false, false); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error checking spot position history for position {PositionId}", position.Identifier); + await LogWarningAsync( + $"āš ļø Web3Proxy Error During Spot Position History Check\n" + + $"Position: `{position.Identifier}`\n" + + $"Error: {ex.Message}\n" + + $"Will retry on next execution cycle"); + return (false, true); + } + } + protected override async Task HandleOrderManagementAndPositionStatus(LightSignal signal, Position internalPosition, Position positionForSignal) { diff --git a/src/Managing.Application/Workers/BundleBacktestHealthCheckWorker.cs b/src/Managing.Application/Workers/BundleBacktestHealthCheckWorker.cs index b27e5105..90b32330 100644 --- a/src/Managing.Application/Workers/BundleBacktestHealthCheckWorker.cs +++ b/src/Managing.Application/Workers/BundleBacktestHealthCheckWorker.cs @@ -464,6 +464,7 @@ public class BundleBacktestHealthCheckWorker : BackgroundService // Some jobs are still pending or running - bundle is genuinely stuck // Reset any stale running jobs back to pending var runningJobs = jobs.Where(j => j.Status == JobStatus.Running).ToList(); + var resetJobCount = 0; foreach (var job in runningJobs) { @@ -481,13 +482,14 @@ public class BundleBacktestHealthCheckWorker : BackgroundService job.AssignedWorkerId = null; job.LastHeartbeat = null; await jobRepository.UpdateAsync(job); + resetJobCount++; } } // Update bundle timestamp to give it another chance bundle.UpdatedAt = DateTime.UtcNow; bundle.ErrorMessage = - $"Bundle was stuck. Reset {runningJobs.Count(j => j.Status == JobStatus.Pending)} stale jobs to pending."; + $"Bundle was stuck. Reset {resetJobCount} stale jobs to pending."; } await backtestRepository.UpdateBundleBacktestRequestAsync(bundle); diff --git a/src/Managing.Infrastructure.Exchanges/Abstractions/IExchangeProcessor.cs b/src/Managing.Infrastructure.Exchanges/Abstractions/IExchangeProcessor.cs index cf30a793..442ffa3b 100644 --- a/src/Managing.Infrastructure.Exchanges/Abstractions/IExchangeProcessor.cs +++ b/src/Managing.Infrastructure.Exchanges/Abstractions/IExchangeProcessor.cs @@ -47,4 +47,12 @@ public interface IExchangeProcessor Ticker ticker, DateTime? fromDate = null, DateTime? toDate = null); + + Task> GetSpotPositionHistory( + Account account, + Ticker ticker, + DateTime? fromDate = null, + DateTime? toDate = null, + int pageIndex = 0, + int pageSize = 20); } diff --git a/src/Managing.Infrastructure.Exchanges/ExchangeService.cs b/src/Managing.Infrastructure.Exchanges/ExchangeService.cs index f3fb3d01..7cf7c009 100644 --- a/src/Managing.Infrastructure.Exchanges/ExchangeService.cs +++ b/src/Managing.Infrastructure.Exchanges/ExchangeService.cs @@ -193,6 +193,18 @@ namespace Managing.Infrastructure.Exchanges return processor.GetPositionHistory(account, ticker, fromDate, toDate); } + public Task> GetSpotPositionHistory( + Account account, + Ticker ticker, + DateTime? fromDate = null, + DateTime? toDate = null, + int pageIndex = 0, + int pageSize = 20) + { + var processor = GetProcessor(account); + return processor.GetSpotPositionHistory(account, ticker, fromDate, toDate, pageIndex, pageSize); + } + public async Task> GetTrades(Account account, Ticker ticker) { var processor = GetProcessor(account); diff --git a/src/Managing.Infrastructure.Exchanges/Exchanges/BaseProcessor.cs b/src/Managing.Infrastructure.Exchanges/Exchanges/BaseProcessor.cs index 2c490b7e..4e5fe258 100644 --- a/src/Managing.Infrastructure.Exchanges/Exchanges/BaseProcessor.cs +++ b/src/Managing.Infrastructure.Exchanges/Exchanges/BaseProcessor.cs @@ -48,5 +48,13 @@ namespace Managing.Infrastructure.Exchanges.Exchanges Ticker ticker, DateTime? fromDate = null, DateTime? toDate = null); + + public abstract Task> GetSpotPositionHistory( + Account account, + Ticker ticker, + DateTime? fromDate = null, + DateTime? toDate = null, + int pageIndex = 0, + int pageSize = 20); } } diff --git a/src/Managing.Infrastructure.Exchanges/Exchanges/EvmProcessor.cs b/src/Managing.Infrastructure.Exchanges/Exchanges/EvmProcessor.cs index 2238ab41..a153c333 100644 --- a/src/Managing.Infrastructure.Exchanges/Exchanges/EvmProcessor.cs +++ b/src/Managing.Infrastructure.Exchanges/Exchanges/EvmProcessor.cs @@ -217,6 +217,17 @@ public class EvmProcessor : BaseProcessor return await _evmManager.GetPositionHistory(account, ticker, fromDate, toDate); } + public override async Task> GetSpotPositionHistory( + Account account, + Ticker ticker, + DateTime? fromDate = null, + DateTime? toDate = null, + int pageIndex = 0, + int pageSize = 20) + { + return await _evmManager.GetSpotPositionHistory(account, ticker, fromDate, toDate, pageIndex, pageSize); + } + #region Not implemented public override void LoadClient(Account account) diff --git a/src/Managing.Infrastructure.Web3/EvmManager.cs b/src/Managing.Infrastructure.Web3/EvmManager.cs index 85b8e4cf..f679db90 100644 --- a/src/Managing.Infrastructure.Web3/EvmManager.cs +++ b/src/Managing.Infrastructure.Web3/EvmManager.cs @@ -1002,6 +1002,25 @@ public class EvmManager : IEvmManager return result; } + public async Task> GetSpotPositionHistory( + Account account, + Ticker ticker, + DateTime? fromDate = null, + DateTime? toDate = null, + int pageIndex = 0, + int pageSize = 20) + { + var result = await _web3ProxyService.GetGmxSpotPositionHistoryAsync( + account.Key, + pageIndex, + pageSize, + ticker.ToString(), + fromDate, + toDate); + + return result; + } + public async Task GetKudaiStakedBalance(string address) { try diff --git a/src/Managing.Infrastructure.Web3/Services/Web3ProxyService.cs b/src/Managing.Infrastructure.Web3/Services/Web3ProxyService.cs index e82e96d7..e9b9e4f7 100644 --- a/src/Managing.Infrastructure.Web3/Services/Web3ProxyService.cs +++ b/src/Managing.Infrastructure.Web3/Services/Web3ProxyService.cs @@ -697,5 +697,98 @@ namespace Managing.Infrastructure.Evm.Services return positions; } + + public async Task> GetGmxSpotPositionHistoryAsync( + string account, + int pageIndex = 0, + int pageSize = 20, + string? ticker = null, + DateTime? fromDate = null, + DateTime? toDate = null) + { + var payload = new + { + account, + pageIndex, + pageSize, + ticker, + fromDateTime = fromDate?.ToString("O"), + toDateTime = toDate?.ToString("O") + }; + + var response = await GetGmxServiceAsync("/spot-position-history", payload); + + if (response == null) + { + throw new Web3ProxyException("Spot position history response is null"); + } + + if (!response.Success) + { + throw new Web3ProxyException($"Spot position history request failed: {response.Error}"); + } + + var positions = new List(); + if (response.Positions != null) + { + foreach (var g in response.Positions) + { + try + { + var tickerEnum = MiscExtensions.ParseEnum(g.Ticker); + var directionEnum = MiscExtensions.ParseEnum(g.Direction); + + var position = new Position( + Guid.NewGuid(), + 0, + directionEnum, + tickerEnum, + null, + PositionInitiator.Bot, + g.Date, + null + ) + { + Status = MiscExtensions.ParseEnum(g.Status) + }; + + if (g.Open != null) + { + position.Open = new Trade( + g.Open.Date, + MiscExtensions.ParseEnum(g.Open.Direction), + MiscExtensions.ParseEnum(g.Open.Status), + TradeType.Market, + tickerEnum, + (decimal)g.Open.Quantity, + (decimal)g.Open.Price, + (decimal)g.Open.Leverage, + g.Open.ExchangeOrderId, + string.Empty + ); + } + + position.ProfitAndLoss = new ProfitAndLoss + { + Realized = g.ProfitAndLoss?.Realized != null + ? (decimal)g.ProfitAndLoss.Realized + : (decimal)g.Pnl, + Net = g.ProfitAndLoss?.Net != null ? (decimal)g.ProfitAndLoss.Net : (decimal)g.Pnl + }; + + position.UiFees = g.UiFees.HasValue ? (decimal)g.UiFees.Value : 0; + position.GasFees = g.GasFees.HasValue ? (decimal)g.GasFees.Value : 0; + + positions.Add(position); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to map GMX spot position history entry"); + } + } + } + + return positions; + } } } \ No newline at end of file diff --git a/src/Managing.Nswag/Program.cs b/src/Managing.Nswag/Program.cs index 72154b45..3796ba53 100644 --- a/src/Managing.Nswag/Program.cs +++ b/src/Managing.Nswag/Program.cs @@ -1,4 +1,4 @@ -// See https://aka.ms/new-console-template for more information +// See https://aka.ms/new-console-template for more information using NJsonSchema.CodeGeneration.TypeScript; using NSwag; @@ -23,8 +23,11 @@ for (int i = 0; i < 10; i++) solutionDirectory = parent.FullName; } -var targetDirectory = Path.Combine(solutionDirectory, "src", "Managing.WebApp", "src", "generated"); -Directory.CreateDirectory(targetDirectory); // Ensure the directory exists +var targetWebAppDirectory = Path.Combine(solutionDirectory, "src", "Managing.WebApp", "src", "generated"); +Directory.CreateDirectory(targetWebAppDirectory); // Ensure the directory exists + +var targetWeb3ProxyDirectory = Path.Combine(solutionDirectory, "src", "Managing.Web3Proxy", "src", "generated"); +Directory.CreateDirectory(targetWeb3ProxyDirectory); var settings = new TypeScriptClientGeneratorSettings { @@ -69,7 +72,7 @@ if (autoGeneratedEndIndex != -1) } } -File.WriteAllText(Path.Combine(targetDirectory, "ManagingApi.ts"), codeApiClient); +File.WriteAllText(Path.Combine(targetWebAppDirectory, "ManagingApi.ts"), codeApiClient); var settingsTypes = new TypeScriptClientGeneratorSettings { @@ -92,4 +95,6 @@ var settingsTypes = new TypeScriptClientGeneratorSettings var generatorTypes = new TypeScriptClientGenerator(document, settingsTypes); var codeTypes = generatorTypes.GenerateFile(); -File.WriteAllText(Path.Combine(targetDirectory, "ManagingApiTypes.ts"), codeTypes); \ No newline at end of file + +File.WriteAllText(Path.Combine(targetWebAppDirectory, "ManagingApiTypes.ts"), codeTypes); +File.WriteAllText(Path.Combine(targetWeb3ProxyDirectory, "ManagingApiTypes.ts"), codeTypes); \ No newline at end of file diff --git a/src/Managing.Web3Proxy/src/generated/ManagingApiTypes.ts b/src/Managing.Web3Proxy/src/generated/ManagingApiTypes.ts index 14e59cac..fefca04e 100644 --- a/src/Managing.Web3Proxy/src/generated/ManagingApiTypes.ts +++ b/src/Managing.Web3Proxy/src/generated/ManagingApiTypes.ts @@ -11,6 +11,7 @@ export interface Account { + id?: number; name: string; exchange: TradingExchanges; type: AccountType; @@ -45,6 +46,9 @@ export interface User { agentName?: string | null; avatarUrl?: string | null; telegramChannel?: string | null; + ownerWalletAddress?: string | null; + isAdmin?: boolean; + lastConnectionDate?: Date | null; } export interface Balance { @@ -219,9 +223,100 @@ export interface SendTokenRequest { chainId?: number | null; } -export interface ExchangeApprovalStatus { +export interface ExchangeInitializedStatus { exchange?: TradingExchanges; - isApproved?: boolean; + isInitialized?: boolean; +} + +export interface PaginatedBundleBacktestRequestsResponse { + bundleRequests?: BundleBacktestRequestListItemResponse[]; + totalCount?: number; + currentPage?: number; + pageSize?: number; + totalPages?: number; + hasNextPage?: boolean; + hasPreviousPage?: boolean; +} + +export interface BundleBacktestRequestListItemResponse { + requestId?: string; + name?: string; + version?: number; + status?: string; + createdAt?: Date; + completedAt?: Date | null; + updatedAt?: Date; + totalBacktests?: number; + completedBacktests?: number; + failedBacktests?: number; + progressPercentage?: number; + userId?: number | null; + userName?: string | null; + errorMessage?: string | null; + currentBacktest?: string | null; + estimatedTimeRemainingSeconds?: number | null; +} + +export enum BundleBacktestRequestSortableColumn { + RequestId = "RequestId", + Name = "Name", + Status = "Status", + CreatedAt = "CreatedAt", + CompletedAt = "CompletedAt", + TotalBacktests = "TotalBacktests", + CompletedBacktests = "CompletedBacktests", + FailedBacktests = "FailedBacktests", + ProgressPercentage = "ProgressPercentage", + UserId = "UserId", + UserName = "UserName", + UpdatedAt = "UpdatedAt", +} + +export enum BundleBacktestRequestStatus { + Pending = "Pending", + Running = "Running", + Completed = "Completed", + Failed = "Failed", + Cancelled = "Cancelled", + Saved = "Saved", +} + +export interface BundleBacktestRequestSummaryResponse { + statusSummary?: BundleBacktestRequestStatusSummary[]; + totalRequests?: number; +} + +export interface BundleBacktestRequestStatusSummary { + status?: string; + count?: number; +} + +export interface PaginatedUsersResponse { + users?: UserListItemResponse[]; + totalCount?: number; + currentPage?: number; + pageSize?: number; + totalPages?: number; + hasNextPage?: boolean; + hasPreviousPage?: boolean; +} + +export interface UserListItemResponse { + id?: number; + name?: string; + agentName?: string; + avatarUrl?: string; + telegramChannel?: string; + ownerWalletAddress?: string; + isAdmin?: boolean; + lastConnectionDate?: Date | null; +} + +export enum UserSortableColumn { + Id = "Id", + Name = "Name", + OwnerWalletAddress = "OwnerWalletAddress", + AgentName = "AgentName", } export interface Backtest { @@ -233,17 +328,18 @@ export interface Backtest { config: TradingBotConfig; positions: { [key: string]: Position; }; signals: { [key: string]: LightSignal; }; - candles: Candle[]; startDate: Date; endDate: Date; statistics: PerformanceMetrics; fees: number; - walletBalances: KeyValuePairOfDateTimeAndDecimal[]; user: User; score: number; requestId?: string; metadata?: any | null; scoreMessage?: string; + initialBalance: number; + netPnl: number; + positionCount: number; } export interface TradingBotConfig { @@ -253,7 +349,7 @@ export interface TradingBotConfig { timeframe: Timeframe; isForWatchingOnly: boolean; botTradingBalance: number; - isForBacktest: boolean; + tradingType: TradingType; cooldownPeriod: number; maxLossStreak: number; flipPosition: boolean; @@ -268,6 +364,9 @@ export interface TradingBotConfig { useForPositionSizing?: boolean; useForSignalFiltering?: boolean; useForDynamicStopLoss?: boolean; + isForCopyTrading?: boolean; + masterBotIdentifier?: string | null; + masterBotUserId?: number | null; } export interface LightMoneyManagement { @@ -288,6 +387,13 @@ export enum Timeframe { OneMinute = "OneMinute", } +export enum TradingType { + Futures = "Futures", + BacktestFutures = "BacktestFutures", + BacktestSpot = "BacktestSpot", + Spot = "Spot", +} + export interface RiskManagement { adverseProbabilityThreshold: number; favorableProbabilityThreshold: number; @@ -327,9 +433,18 @@ export interface LightIndicator { slowPeriods?: number | null; signalPeriods?: number | null; multiplier?: number | null; + stDev?: number | null; smoothPeriods?: number | null; stochPeriods?: number | null; cyclePeriods?: number | null; + kFactor?: number | null; + dFactor?: number | null; + tenkanPeriods?: number | null; + kijunPeriods?: number | null; + senkouBPeriods?: number | null; + offsetPeriods?: number | null; + senkouOffset?: number | null; + chikouOffset?: number | null; } export enum IndicatorType { @@ -343,11 +458,15 @@ export enum IndicatorType { EmaTrend = "EmaTrend", Composite = "Composite", StochRsiTrend = "StochRsiTrend", + StochasticCross = "StochasticCross", Stc = "Stc", StDev = "StDev", LaggingStc = "LaggingStc", SuperTrendCrossEma = "SuperTrendCrossEma", DualEmaCross = "DualEmaCross", + BollingerBandsPercentBMomentumBreakout = "BollingerBandsPercentBMomentumBreakout", + BollingerBandsVolatilityProtection = "BollingerBandsVolatilityProtection", + IchimokuKumoTrend = "IchimokuKumoTrend", } export enum SignalType { @@ -357,8 +476,8 @@ export enum SignalType { } export interface Position { - accountName: string; date: Date; + accountId: number; originDirection: TradeDirection; ticker: Ticker; moneyManagement: LightMoneyManagement; @@ -367,12 +486,16 @@ export interface Position { TakeProfit1: Trade; TakeProfit2?: Trade | null; ProfitAndLoss?: ProfitAndLoss | null; + uiFees?: number; + gasFees?: number; status: PositionStatus; signalIdentifier?: string | null; identifier: string; initiator: PositionInitiator; user: User; initiatorIdentifier: string; + recoveryAttempted?: boolean; + tradingType?: TradingType; } export enum TradeDirection { @@ -430,7 +553,6 @@ export enum PositionStatus { Canceled = "Canceled", Rejected = "Rejected", Updating = "Updating", - PartiallyFilled = "PartiallyFilled", Filled = "Filled", Flipped = "Flipped", Finished = "Finished", @@ -495,25 +617,10 @@ export interface PerformanceMetrics { totalPnL?: number; } -export interface KeyValuePairOfDateTimeAndDecimal { - key?: Date; - value?: number; -} - export interface DeleteBacktestsRequest { backtestIds: string[]; } -export interface PaginatedBacktestsResponse { - backtests?: LightBacktestResponse[] | null; - totalCount?: number; - currentPage?: number; - pageSize?: number; - totalPages?: number; - hasNextPage?: boolean; - hasPreviousPage?: boolean; -} - export interface LightBacktestResponse { id: string; config: TradingBotConfig; @@ -528,6 +635,36 @@ export interface LightBacktestResponse { sharpeRatio: number; score: number; scoreMessage: string; + initialBalance: number; + netPnl: number; + positionCount: number; +} + +export interface PaginatedBacktestsResponse { + backtests?: LightBacktestResponse[] | null; + totalCount?: number; + currentPage?: number; + pageSize?: number; + totalPages?: number; + hasNextPage?: boolean; + hasPreviousPage?: boolean; +} + +export enum BacktestSortableColumn { + Score = "Score", + FinalPnl = "FinalPnl", + NetPnl = "NetPnl", + WinRate = "WinRate", + GrowthPercentage = "GrowthPercentage", + HodlPercentage = "HodlPercentage", + Duration = "Duration", + Timeframe = "Timeframe", + IndicatorsCount = "IndicatorsCount", + MaxDrawdown = "MaxDrawdown", + Fees = "Fees", + SharpeRatio = "SharpeRatio", + Ticker = "Ticker", + Name = "Name", } export interface LightBacktest { @@ -544,18 +681,27 @@ export interface LightBacktest { sharpeRatio?: number | null; score?: number; scoreMessage?: string | null; + metadata?: any | null; + ticker?: string | null; + initialBalance?: number; + netPnl?: number; + positionCount?: number; } export interface RunBacktestRequest { config?: TradingBotConfigRequest | null; startDate?: Date; endDate?: Date; + balance?: number; + watchOnly?: boolean; save?: boolean; withCandles?: boolean; + moneyManagementName?: string | null; + moneyManagement?: MoneyManagement | null; } export interface TradingBotConfigRequest { - accountName: string; + accountName?: string | null; ticker: Ticker; timeframe: Timeframe; isForWatchingOnly: boolean; @@ -575,6 +721,7 @@ export interface TradingBotConfigRequest { useForPositionSizing?: boolean; useForSignalFiltering?: boolean; useForDynamicStopLoss?: boolean; + tradingType?: TradingType; } export interface ScenarioRequest { @@ -593,9 +740,18 @@ export interface IndicatorRequest { slowPeriods?: number | null; signalPeriods?: number | null; multiplier?: number | null; + stDev?: number | null; smoothPeriods?: number | null; stochPeriods?: number | null; cyclePeriods?: number | null; + kFactor?: number | null; + dFactor?: number | null; + tenkanPeriods?: number | null; + kijunPeriods?: number | null; + senkouBPeriods?: number | null; + offsetPeriods?: number | null; + senkouOffset?: number | null; + chikouOffset?: number | null; } export interface MoneyManagementRequest { @@ -606,6 +762,10 @@ export interface MoneyManagementRequest { leverage: number; } +export interface MoneyManagement extends LightMoneyManagement { + user?: User | null; +} + export interface BundleBacktestRequest { requestId: string; user: User; @@ -613,8 +773,76 @@ export interface BundleBacktestRequest { completedAt?: Date | null; status: BundleBacktestRequestStatus; name: string; - backtestRequestsJson: string; - results?: string[] | null; + version: number; + universalConfigJson: string; + dateTimeRangesJson: string; + moneyManagementVariantsJson: string; + tickerVariantsJson: string; + results?: string[]; + totalBacktests: number; + completedBacktests: number; + failedBacktests: number; + progressPercentage?: number; + errorMessage?: string | null; + progressInfo?: string | null; + currentBacktest?: string | null; + estimatedTimeRemainingSeconds?: number | null; + updatedAt: Date; +} + +export interface RunBundleBacktestRequest { + name: string; + universalConfig: BundleBacktestUniversalConfig; + dateTimeRanges: DateTimeRange[]; + moneyManagementVariants: MoneyManagementVariant[]; + tickerVariants: Ticker[]; + saveAsTemplate: boolean; +} + +export interface BundleBacktestUniversalConfig { + timeframe: Timeframe; + isForWatchingOnly: boolean; + botTradingBalance: number; + botName: string; + tradingType: TradingType; + flipPosition: boolean; + cooldownPeriod?: number | null; + maxLossStreak?: number; + scenario?: ScenarioRequest | null; + scenarioName?: string | null; + maxPositionTimeHours?: number | null; + closeEarlyWhenProfitable?: boolean; + flipOnlyWhenInProfit?: boolean; + useSynthApi?: boolean; + useForPositionSizing?: boolean; + useForSignalFiltering?: boolean; + useForDynamicStopLoss?: boolean; + watchOnly?: boolean; + save?: boolean; + withCandles?: boolean; +} + +export interface DateTimeRange { + startDate: Date; + endDate: Date; +} + +export interface MoneyManagementVariant { + moneyManagement?: MoneyManagementRequest; +} + +export interface BundleBacktestRequestViewModel { + requestId: string; + createdAt: Date; + completedAt?: Date | null; + status: BundleBacktestRequestStatus; + name: string; + version: number; + universalConfig: BundleBacktestUniversalConfig; + dateTimeRanges: DateTimeRange[]; + moneyManagementVariants: MoneyManagementVariant[]; + tickerVariants: Ticker[]; + results?: string[]; totalBacktests: number; completedBacktests: number; failedBacktests: number; @@ -625,17 +853,18 @@ export interface BundleBacktestRequest { estimatedTimeRemainingSeconds?: number | null; } -export enum BundleBacktestRequestStatus { - Pending = "Pending", - Running = "Running", - Completed = "Completed", - Failed = "Failed", - Cancelled = "Cancelled", -} - -export interface RunBundleBacktestRequest { - name: string; - requests: RunBacktestRequest[]; +export interface BundleBacktestStatusResponse { + bundleRequestId?: string; + status?: string | null; + totalJobs?: number; + completedJobs?: number; + failedJobs?: number; + runningJobs?: number; + pendingJobs?: number; + progressPercentage?: number; + createdAt?: Date; + completedAt?: Date | null; + errorMessage?: string | null; } export interface GeneticRequest { @@ -726,14 +955,15 @@ export interface RunGeneticRequest { eligibleIndicators?: IndicatorType[] | null; } -export interface MoneyManagement extends LightMoneyManagement { - user?: User | null; -} - export interface StartBotRequest { config?: TradingBotConfigRequest | null; } +export interface StartCopyTradingRequest { + masterBotIdentifier?: string; + botTradingBalance?: number; +} + export interface SaveBotRequest extends StartBotRequest { } @@ -750,12 +980,14 @@ export interface TradingBotResponse { candles: Candle[]; winRate: number; profitAndLoss: number; + roi: number; identifier: string; agentName: string; createDate: Date; startupTime: Date; name: string; ticker: Ticker; + masterAgentName?: string | null; } export interface PaginatedResponseOfTradingBotResponse { @@ -768,7 +1000,19 @@ export interface PaginatedResponseOfTradingBotResponse { hasNextPage?: boolean; } -export interface OpenPositionManuallyRequest { +export enum BotSortableColumn { + CreateDate = "CreateDate", + Name = "Name", + Ticker = "Ticker", + Status = "Status", + StartupTime = "StartupTime", + Roi = "Roi", + Pnl = "Pnl", + WinRate = "WinRate", + AgentName = "AgentName", +} + +export interface CreateManualSignalRequest { identifier?: string; direction?: TradeDirection; } @@ -788,6 +1032,7 @@ export interface UpdateBotConfigRequest { export interface TickerInfos { ticker?: Ticker; imageUrl?: string | null; + name?: string | null; } export interface SpotlightOverview { @@ -819,9 +1064,18 @@ export interface IndicatorBase { slowPeriods?: number | null; signalPeriods?: number | null; multiplier?: number | null; + stDev?: number | null; smoothPeriods?: number | null; stochPeriods?: number | null; cyclePeriods?: number | null; + kFactor?: number | null; + dFactor?: number | null; + tenkanPeriods?: number | null; + kijunPeriods?: number | null; + senkouBPeriods?: number | null; + offsetPeriods?: number | null; + senkouOffset?: number | null; + chikouOffset?: number | null; user?: User | null; } @@ -853,6 +1107,7 @@ export interface IndicatorsResultBase { stdDev?: StdDevResult[] | null; superTrend?: SuperTrendResult[] | null; chandelierLong?: ChandelierResult[] | null; + ichimoku?: IchimokuResult[] | null; } export interface ResultBase { @@ -929,6 +1184,14 @@ export interface SuperTrendResult extends ResultBase { lowerBand?: number | null; } +export interface IchimokuResult extends ResultBase { + tenkanSen?: number | null; + kijunSen?: number | null; + senkouSpanA?: number | null; + senkouSpanB?: number | null; + chikouSpan?: number | null; +} + export interface GetCandlesWithIndicatorsRequest { ticker?: Ticker; startDate?: Date; @@ -949,6 +1212,7 @@ export interface TopStrategiesViewModel { export interface StrategyPerformance { strategyName?: string | null; pnL?: number; + netPnL?: number; agentName?: string | null; } @@ -960,65 +1224,75 @@ export interface StrategyRoiPerformance { strategyName?: string | null; roi?: number; pnL?: number; + netPnL?: number; volume?: number; } -export interface TopAgentsByPnLViewModel { - topAgentsByPnL?: AgentPerformance[] | null; -} - -export interface AgentPerformance { - agentName?: string | null; - pnL?: number; - totalROI?: number; - totalVolume?: number; - activeStrategiesCount?: number; - totalBalance?: number; -} - export interface UserStrategyDetailsViewModel { name?: string | null; state?: BotStatus; pnL?: number; + netPnL?: number; roiPercentage?: number; - roiLast24H?: number; - runtime?: Date; + runtime?: Date | null; + totalRuntimeSeconds?: number; + lastStartTime?: Date | null; + lastStopTime?: Date | null; + accumulatedRunTimeSeconds?: number; winRate?: number; totalVolumeTraded?: number; volumeLast24H?: number; wins?: number; losses?: number; - positions?: Position[] | null; + positions?: PositionViewModel[] | null; identifier?: string; walletBalances?: { [key: string]: number; } | null; ticker?: Ticker; + masterAgentName?: string | null; +} + +export interface PositionViewModel { + date: Date; + accountId: number; + originDirection: TradeDirection; + ticker: Ticker; + Open: Trade; + StopLoss: Trade; + TakeProfit1: Trade; + ProfitAndLoss?: ProfitAndLoss | null; + uiFees?: number; + gasFees?: number; + status: PositionStatus; + signalIdentifier?: string | null; + identifier: string; } export interface PlatformSummaryViewModel { + lastUpdated?: Date; + lastSnapshot?: Date; + hasPendingChanges?: boolean; totalAgents?: number; totalActiveStrategies?: number; totalPlatformPnL?: number; totalPlatformVolume?: number; - totalPlatformVolumeLast24h?: number; - totalOpenInterest?: number; + openInterest?: number; totalPositionCount?: number; - agentsChange24h?: number; - strategiesChange24h?: number; - pnLChange24h?: number; - volumeChange24h?: number; - openInterestChange24h?: number; - positionCountChange24h?: number; + totalPlatformFees?: number; + dailySnapshots?: DailySnapshot[] | null; volumeByAsset?: { [key in keyof typeof Ticker]?: number; } | null; positionCountByAsset?: { [key in keyof typeof Ticker]?: number; } | null; positionCountByDirection?: { [key in keyof typeof TradeDirection]?: number; } | null; - lastUpdated?: Date; - last24HourSnapshot?: Date; - volumeHistory?: VolumeHistoryPoint[] | null; } -export interface VolumeHistoryPoint { +export interface DailySnapshot { date?: Date; - volume?: number; + totalAgents?: number; + totalStrategies?: number; + totalVolume?: number; + totalPnL?: number; + netPnL?: number; + totalOpenInterest?: number; + totalPositionCount?: number; } export interface PaginatedAgentIndexResponse { @@ -1038,16 +1312,19 @@ export interface PaginatedAgentIndexResponse { export interface AgentSummaryViewModel { agentName?: string | null; totalPnL?: number; + netPnL?: number; totalROI?: number; wins?: number; losses?: number; activeStrategiesCount?: number; totalVolume?: number; totalBalance?: number; + totalFees?: number; + backtestCount?: number; } export enum SortableFields { - TotalPnL = "TotalPnL", + NetPnL = "NetPnL", TotalROI = "TotalROI", Wins = "Wins", Losses = "Losses", @@ -1059,25 +1336,82 @@ export enum SortableFields { } export interface AgentBalanceHistory { + userId?: number; agentName?: string | null; agentBalances?: AgentBalance[] | null; } export interface AgentBalance { - agentName?: string | null; - totalValue?: number; - totalAccountUsdValue?: number; + userId?: number; + totalBalanceValue?: number; + usdcWalletValue?: number; + usdcInPositionsValue?: number; botsAllocationUsdValue?: number; pnL?: number; time?: Date; } -export interface BestAgentsResponse { - agents?: AgentBalanceHistory[] | null; +export interface JobStatusResponse { + jobId?: string; + status?: string | null; + progressPercentage?: number; + createdAt?: Date; + startedAt?: Date | null; + completedAt?: Date | null; + errorMessage?: string | null; + result?: LightBacktest | null; +} + +export interface PaginatedJobsResponse { + jobs?: JobListItemResponse[]; totalCount?: number; currentPage?: number; pageSize?: number; totalPages?: number; + hasNextPage?: boolean; + hasPreviousPage?: boolean; +} + +export interface JobListItemResponse { + jobId?: string; + status?: string; + jobType?: string; + progressPercentage?: number; + priority?: number; + userId?: number; + bundleRequestId?: string | null; + geneticRequestId?: string | null; + assignedWorkerId?: string | null; + createdAt?: Date; + startedAt?: Date | null; + completedAt?: Date | null; + lastHeartbeat?: Date | null; + errorMessage?: string | null; + startDate?: Date; + endDate?: Date; +} + +export interface JobSummaryResponse { + statusSummary?: JobStatusSummary[]; + jobTypeSummary?: JobTypeSummary[]; + statusTypeSummary?: JobStatusTypeSummary[]; + totalJobs?: number; +} + +export interface JobStatusSummary { + status?: string; + count?: number; +} + +export interface JobTypeSummary { + jobType?: string; + count?: number; +} + +export interface JobStatusTypeSummary { + status?: string; + jobType?: string; + count?: number; } export interface ScenarioViewModel { @@ -1120,11 +1454,69 @@ export interface PrivyInitAddressResponse { isAlreadyInitialized?: boolean; } +export interface IndicatorRequestDto { + indicatorName: string; + strategyDescription: string; + documentationUrl?: string | null; + imageUrl?: string | null; + requesterName: string; +} + export interface LoginRequest { name: string; address: string; signature: string; message: string; + ownerWalletAddress?: string | null; +} + +export interface PaginatedWhitelistAccountsResponse { + accounts?: WhitelistAccount[] | null; + totalCount?: number; + pageNumber?: number; + pageSize?: number; + totalPages?: number; +} + +export interface WhitelistAccount { + id?: number; + privyId?: string | null; + privyCreationDate?: Date; + embeddedWallet?: string | null; + externalEthereumAccount?: string | null; + twitterAccount?: string | null; + isWhitelisted?: boolean; + createdAt?: Date; + updatedAt?: Date | null; +} + +export interface PrivyWebhookDto { + type?: string | null; + user?: PrivyUserDto | null; + wallet?: PrivyWalletDto | null; +} + +export interface PrivyUserDto { + created_at?: number; + has_accepted_terms?: boolean; + id?: string | null; + is_guest?: boolean; + linked_accounts?: PrivyLinkedAccountDto[] | null; + mfa_methods?: any[] | null; +} + +export interface PrivyLinkedAccountDto { + address?: string | null; + first_verified_at?: number | null; + latest_verified_at?: number | null; + type?: string | null; + verified_at?: number | null; +} + +export interface PrivyWalletDto { + type?: string | null; + address?: string | null; + chain_type?: string | null; } export interface FileResponse { diff --git a/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts b/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts index e42e5eab..112408f1 100644 --- a/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts +++ b/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts @@ -40,6 +40,7 @@ import { TradeStatus, TradeType } from '../../generated/ManagingApiTypes.js'; +import {TradeActionType} from '../../generated/gmxsdk/types/tradeHistory.js'; // Cache implementation for markets info data interface CacheEntry { @@ -1532,6 +1533,134 @@ export const getPositionHistoryImpl = async ( ); }; +/** + * Implementation function to get spot position (swap) history on GMX + * This returns swap executions (MarketSwap) so we can reconcile spot positions. + */ +export const getSpotPositionHistoryImpl = async ( + sdk: GmxSdk, + pageIndex: number = 0, + pageSize: number = 20, + ticker?: string, + fromDateTime?: string, + toDateTime?: string +): Promise => { + return executeWithFallback( + async (sdk, retryCount) => { + const fromTimestamp = fromDateTime ? Math.floor(new Date(fromDateTime).getTime() / 1000) : undefined; + const toTimestamp = toDateTime ? Math.floor(new Date(toDateTime).getTime() / 1000) : undefined; + + const {marketsInfoData, tokensData} = await getMarketsInfoWithCache(sdk); + + if (!marketsInfoData || !tokensData) { + throw new Error("No markets or tokens info data"); + } + + const tradeActions = await sdk.trades.getTradeHistory({ + pageIndex, + pageSize, + fromTxTimestamp: fromTimestamp, + toTxTimestamp: toTimestamp, + marketsInfoData, + tokensData, + marketsDirectionsFilter: undefined, + orderEventCombinations: [{ + eventName: TradeActionType.OrderExecuted, + orderType: OrderType.MarketSwap, + }], + }); + + let positions: Position[] = []; + + for (const action of tradeActions) { + console.log(`šŸ“Š Action:`, action); + // Some swap actions don't carry marketInfo; derive from target/initial collateral token instead. + const initialToken = (action as any).initialCollateralToken; + const targetToken = (action as any).targetCollateralToken || initialToken; + if (!targetToken && !initialToken) { + continue; + } + + const targetSymbol = targetToken?.symbol || 'UNKNOWN'; + const initialSymbol = initialToken?.symbol || targetSymbol; + const isSellToUsdc = (targetSymbol?.toUpperCase?.() === 'USDC'); + const direction = isSellToUsdc ? TradeDirection.Short : TradeDirection.Long; + + const targetDecimals = targetToken?.decimals ?? 18; + const initialDecimals = initialToken?.decimals ?? 18; + + const executionAmountOut = (action as any).executionAmountOut ?? 0n; // output (target token) + const initialAmount = (action as any).initialCollateralDeltaAmount ?? 0n; // input (initial token) + + const outputQty = Number(executionAmountOut) / Math.pow(10, targetDecimals); + const inputQty = Number(initialAmount) / Math.pow(10, initialDecimals); + + // Base ticker & quantity: Long -> target token received; Short -> initial token sold + const baseSymbol = direction === TradeDirection.Short ? initialSymbol : targetSymbol; + const tickerSymbol = (ticker as any) || baseSymbol; + const quantity = direction === TradeDirection.Short ? inputQty : outputQty; + + let price = 0; + if (quantity > 0) { + // Quote per base: input value / base qty (Long: USDC/BTC; Short: USDC/BTC) + const numerator = direction === TradeDirection.Short ? outputQty : inputQty; + price = numerator / quantity; + } + + const pnlUsd = (action as any).pnlUsd ? Number((action as any).pnlUsd) / 1e30 : 0; + + const timestampSec = (action as any).timestamp || 0; + const date = new Date(Number(timestampSec) * 1000); + const exchangeOrderId = action.transaction?.hash || (action as any).id; + + const position: Position = { + ticker: tickerSymbol as any, + direction, + price, + quantity, + leverage: 1, + status: PositionStatus.Finished, + tradeType: TradeType.Market, + date, + exchangeOrderId, + pnl: pnlUsd, + ProfitAndLoss: { + realized: pnlUsd, + net: pnlUsd, + averageOpenPrice: undefined + }, + Open: { + ticker: tickerSymbol as any, + direction, + price, + quantity, + leverage: 1, + status: TradeStatus.Filled, + tradeType: TradeType.Market, + date, + exchangeOrderId, + fee: 0, + message: "Spot swap execution" + } as Trade + } as any; + + positions.push(position); + } + + console.log(`šŸ“Š Positions:`, positions); + + if (ticker) { + positions = positions.filter(p => + p.ticker === (ticker as any) || + p.Open?.ticker === (ticker as any) + ); + } + + return positions; + }, sdk, 0 + ); +}; + /** * Implementation function to get positions on GMX with fallback RPC support * @param sdk The GMX SDK client @@ -1816,6 +1945,61 @@ export async function getPositionHistory( } } +/** + * Gets spot position (swap) history on GMX + * @param this The FastifyRequest instance + * @param reply The FastifyReply instance + * @param account The wallet address of the user + * @param pageIndex The page index for pagination (default: 0) + * @param pageSize The number of items per page (default: 20) + * @param ticker Optional ticker filter + * @param fromDateTime Optional start datetime (ISO 8601 format) + * @param toDateTime Optional end datetime (ISO 8601 format) + * @returns The response object with success status and positions array + */ +export async function getSpotPositionHistory( + this: FastifyRequest, + reply: FastifyReply, + account: string, + pageIndex?: number, + pageSize?: number, + ticker?: string, + fromDateTime?: string, + toDateTime?: string +) { + try { + getPositionHistorySchema.parse({ + account, + pageIndex: pageIndex ?? 0, + pageSize: pageSize ?? 20, + ticker, + fromDateTime, + toDateTime + }); + + const sdk = await this.getClientForAddress(account); + + const positions = await getSpotPositionHistoryImpl( + sdk, + pageIndex ?? 0, + pageSize ?? 20, + ticker, + fromDateTime, + toDateTime + ); + + return { + success: true, + positions, + pageIndex: pageIndex ?? 0, + pageSize: pageSize ?? 20, + count: positions.length + }; + } catch (error) { + return handleError(this, reply, error, 'gmx/spot-position-history'); + } +} + // Helper to pre-populate and refresh the markets cache async function getMarketsData() { // Use a dummy zero address for the account @@ -1849,6 +2033,7 @@ export default fp(async (fastify) => { fastify.decorateRequest('getGmxTrade', getGmxTrade) fastify.decorateRequest('getGmxPositions', getGmxPositions) fastify.decorateRequest('getPositionHistory', getPositionHistory) + fastify.decorateRequest('getSpotPositionHistory', getSpotPositionHistory) fastify.decorateRequest('getGmxRebateStats', getGmxRebateStats) fastify.decorateRequest('getClaimableFundingFees', getClaimableFundingFees) fastify.decorateRequest('claimGmxFundingFees', claimGmxFundingFees) diff --git a/src/Managing.Web3Proxy/test/plugins/get-spot-position-history.test.ts b/src/Managing.Web3Proxy/test/plugins/get-spot-position-history.test.ts new file mode 100644 index 00000000..c966520c --- /dev/null +++ b/src/Managing.Web3Proxy/test/plugins/get-spot-position-history.test.ts @@ -0,0 +1,53 @@ +import {test} from 'node:test' +import assert from 'node:assert' +import {getClientForAddress, getSpotPositionHistoryImpl} from '../../src/plugins/custom/gmx.js' +import {Ticker} from '../../src/generated/ManagingApiTypes.js' + +test('GMX get spot position history - Market swaps', async (t) => { + await t.test('should get spot swap executions', async () => { + const sdk = await getClientForAddress('0x932167388dD9aad41149b3cA23eBD489E2E2DD78') + + const result = await getSpotPositionHistoryImpl( + sdk, + 0, // pageIndex + 100, // pageSize + Ticker.BTC, // ticker + '2025-12-04T00:00:00.000Z', // fromDateTime + '2025-12-07T00:00:00.000Z' // toDateTime + ) + + console.log('\nšŸ“Š Spot Swap History Summary:') + console.log(`Total swaps: ${result.length}`) + console.log(`šŸ“Š Result:`, result); + + assert.ok(result, 'Spot position history result should be defined') + assert.ok(Array.isArray(result), 'Spot position history should be an array') + }) + + await t.test('should get spot swaps within date range', async () => { + const sdk = await getClientForAddress('0x932167388dD9aad41149b3cA23eBD489E2E2DD78') + + const toDate = new Date() + const fromDate = new Date(toDate.getTime() - (60 * 60 * 1000)) // last 1 hour + + const fromDateTime = fromDate.toISOString() + const toDateTime = toDate.toISOString() + + const result = await getSpotPositionHistoryImpl( + sdk, + 0, + 50, + Ticker.BTC, + fromDateTime, + toDateTime + ) + + console.log(`\nšŸ“… Spot swaps in last 1 hour: ${result.length}`) + console.log(`From: ${fromDateTime}`) + console.log(`To: ${toDateTime}`) + + assert.ok(result, 'Spot position history result should be defined') + assert.ok(Array.isArray(result), 'Spot position history should be an array') + }) +}) + diff --git a/src/Managing.WebApp/src/generated/ManagingApi.ts b/src/Managing.WebApp/src/generated/ManagingApi.ts index 4094764f..84909337 100644 --- a/src/Managing.WebApp/src/generated/ManagingApi.ts +++ b/src/Managing.WebApp/src/generated/ManagingApi.ts @@ -5216,6 +5216,7 @@ export interface TradingBotConfigRequest { useForPositionSizing?: boolean; useForSignalFiltering?: boolean; useForDynamicStopLoss?: boolean; + tradingType?: TradingType; } export interface ScenarioRequest { diff --git a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts index 6771774c..fefca04e 100644 --- a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts +++ b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts @@ -721,6 +721,7 @@ export interface TradingBotConfigRequest { useForPositionSizing?: boolean; useForSignalFiltering?: boolean; useForDynamicStopLoss?: boolean; + tradingType?: TradingType; } export interface ScenarioRequest {