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 {