1001 lines
44 KiB
C#
1001 lines
44 KiB
C#
using Managing.Api.Models.Requests;
|
|
using Managing.Api.Models.Responses;
|
|
using Managing.Application.Abstractions;
|
|
using Managing.Application.Abstractions.Grains;
|
|
using Managing.Application.Abstractions.Services;
|
|
using Managing.Application.ManageBot.Commands;
|
|
using Managing.Core;
|
|
using Managing.Domain.Backtests;
|
|
using Managing.Domain.Bots;
|
|
using Managing.Domain.Candles;
|
|
using Managing.Domain.Scenarios;
|
|
using Managing.Domain.Shared.Helpers;
|
|
using Managing.Domain.Statistics;
|
|
using Managing.Domain.Strategies;
|
|
using Managing.Domain.Strategies.Base;
|
|
using Managing.Domain.Trades;
|
|
using MediatR;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using static Managing.Common.Enums;
|
|
using DailySnapshot = Managing.Api.Models.Responses.DailySnapshot;
|
|
|
|
namespace Managing.Api.Controllers;
|
|
|
|
/// <summary>
|
|
/// Controller for handling data-related operations such as retrieving tickers, spotlight data, and candles.
|
|
/// Requires authorization for access.
|
|
/// </summary>
|
|
[AllowAnonymous]
|
|
[ApiController]
|
|
[Route("[controller]")]
|
|
public class DataController : ControllerBase
|
|
{
|
|
private readonly IExchangeService _exchangeService;
|
|
private readonly IAccountService _accountService;
|
|
private readonly ICacheService _cacheService;
|
|
private readonly IStatisticService _statisticService;
|
|
private readonly IAgentService _agentService;
|
|
private readonly IMediator _mediator;
|
|
private readonly ITradingService _tradingService;
|
|
private readonly IGrainFactory _grainFactory;
|
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
|
private readonly IBotService _botService;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="DataController"/> class.
|
|
/// </summary>
|
|
/// <param name="exchangeService">Service for interacting with exchanges.</param>
|
|
/// <param name="accountService">Service for account management.</param>
|
|
/// <param name="cacheService">Service for caching data.</param>
|
|
/// <param name="statisticService">Service for statistical analysis.</param>
|
|
/// <param name="agentService">Service for agents</param>
|
|
/// <param name="hubContext">SignalR hub context for real-time communication.</param>
|
|
/// <param name="mediator">Mediator for handling commands and queries.</param>
|
|
/// <param name="tradingService">Service for trading operations.</param>
|
|
/// <param name="grainFactory">Orleans grain factory for accessing grains.</param>
|
|
/// <param name="serviceScopeFactory">Service scope factory for creating scoped services.</param>
|
|
/// <param name="botService">Service for bot operations.</param>
|
|
public DataController(
|
|
IExchangeService exchangeService,
|
|
IAccountService accountService,
|
|
ICacheService cacheService,
|
|
IStatisticService statisticService,
|
|
IAgentService agentService,
|
|
IMediator mediator,
|
|
ITradingService tradingService,
|
|
IGrainFactory grainFactory,
|
|
IServiceScopeFactory serviceScopeFactory,
|
|
IBotService botService)
|
|
{
|
|
_exchangeService = exchangeService;
|
|
_accountService = accountService;
|
|
_cacheService = cacheService;
|
|
_statisticService = statisticService;
|
|
_agentService = agentService;
|
|
_mediator = mediator;
|
|
_tradingService = tradingService;
|
|
_grainFactory = grainFactory;
|
|
_serviceScopeFactory = serviceScopeFactory;
|
|
_botService = botService;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves tickers for a given account and timeframe, utilizing caching to improve performance.
|
|
/// </summary>
|
|
/// <param name="timeframe">The timeframe for which to retrieve tickers.</param>
|
|
/// <returns>An array of tickers.</returns>
|
|
[HttpGet("GetTickers")]
|
|
public async Task<ActionResult<List<TickerInfos>>> GetTickers([FromQuery] Timeframe timeframe)
|
|
{
|
|
var cacheKey = string.Concat(timeframe.ToString());
|
|
var tickers = _cacheService.GetValue<List<TickerInfos>>(cacheKey);
|
|
|
|
if (tickers == null || tickers.Count == 0)
|
|
{
|
|
var availableTicker = await _exchangeService.GetTickers(timeframe);
|
|
|
|
tickers = MapTickerToTickerInfos(availableTicker);
|
|
|
|
_cacheService.SaveValue(cacheKey, tickers, TimeSpan.FromHours(2));
|
|
}
|
|
|
|
return Ok(tickers);
|
|
}
|
|
|
|
private List<TickerInfos> MapTickerToTickerInfos(List<Ticker> availableTicker)
|
|
{
|
|
var tickerInfos = new List<TickerInfos>();
|
|
var tokens = new Dictionary<string, string>
|
|
{
|
|
{ "AAVE", "https://assets.coingecko.com/coins/images/12645/standard/AAVE.png?1696512452" },
|
|
{ "ADA", "https://assets.coingecko.com/coins/images/975/standard/cardano.png?1696502090" },
|
|
{ "APE", "https://assets.coingecko.com/coins/images/24383/standard/apecoin.jpg?1696523566" },
|
|
{
|
|
"ARB", "https://assets.coingecko.com/coins/images/16547/small/photo_2023-03-29_21.47.00.jpeg?1680097630"
|
|
},
|
|
{ "ATOM", "https://assets.coingecko.com/coins/images/1481/standard/cosmos_hub.png?1696502525" },
|
|
{ "AVAX", "https://assets.coingecko.com/coins/images/12559/small/coin-round-red.png?1604021818" },
|
|
{ "BNB", "https://assets.coingecko.com/coins/images/825/standard/bnb-icon2_2x.png?1696501970" },
|
|
{ "BTC", "https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1547033579" },
|
|
{ "DOGE", "https://assets.coingecko.com/coins/images/5/small/dogecoin.png?1547792256" },
|
|
{
|
|
"DOT",
|
|
"https://static.coingecko.com/s/polkadot-73b0c058cae10a2f076a82dcade5cbe38601fad05d5e6211188f09eb96fa4617.gif"
|
|
},
|
|
{ "ETH", "https://assets.coingecko.com/coins/images/279/small/ethereum.png?1595348880" },
|
|
{ "FIL", "https://assets.coingecko.com/coins/images/12817/standard/filecoin.png?1696512609" },
|
|
{ "GMX", "https://assets.coingecko.com/coins/images/18323/small/arbit.png?1631532468" },
|
|
{ "LINK", "https://assets.coingecko.com/coins/images/877/thumb/chainlink-new-logo.png?1547034700" },
|
|
{ "LTC", "https://assets.coingecko.com/coins/images/2/small/litecoin.png?1547033580" },
|
|
{ "MATIC", "https://assets.coingecko.com/coins/images/32440/standard/polygon.png?1698233684" },
|
|
{ "NEAR", "https://assets.coingecko.com/coins/images/10365/standard/near.jpg?1696510367" },
|
|
{ "OP", "https://assets.coingecko.com/coins/images/25244/standard/Optimism.png?1696524385" },
|
|
{ "PEPE", "https://assets.coingecko.com/coins/images/29850/standard/pepe-token.jpeg?1696528776" },
|
|
{ "SOL", "https://assets.coingecko.com/coins/images/4128/small/solana.png?1640133422" },
|
|
{ "UNI", "https://assets.coingecko.com/coins/images/12504/thumb/uniswap-uni.png?1600306604" },
|
|
{ "USDC", "https://assets.coingecko.com/coins/images/6319/thumb/USD_Coin_icon.png?1547042389" },
|
|
{ "USDT", "https://assets.coingecko.com/coins/images/325/thumb/Tether-logo.png?1598003707" },
|
|
{ "WIF", "https://assets.coingecko.com/coins/images/33566/standard/dogwifhat.jpg?1702499428" },
|
|
{ "XRP", "https://assets.coingecko.com/coins/images/44/small/xrp-symbol-white-128.png?1605778731" },
|
|
{ "SHIB", "https://assets.coingecko.com/coins/images/11939/standard/shiba.png?1696511800" },
|
|
{ "STX", "https://assets.coingecko.com/coins/images/2069/standard/Stacks_Logo_png.png?1709979332" },
|
|
{ "ORDI", "https://assets.coingecko.com/coins/images/30162/standard/ordi.png?1696529082" },
|
|
{ "APT", "https://assets.coingecko.com/coins/images/26455/standard/aptos_round.png?1696525528" },
|
|
{ "BOME", "https://assets.coingecko.com/coins/images/36071/standard/bome.png?1710407255" },
|
|
{ "MEME", "https://assets.coingecko.com/coins/images/32528/standard/memecoin_%282%29.png?1698912168" },
|
|
{ "FLOKI", "https://assets.coingecko.com/coins/images/16746/standard/PNG_image.png?1696516318" },
|
|
{ "MEW", "https://assets.coingecko.com/coins/images/36440/standard/MEW.png?1711442286" },
|
|
{ "TAO", "https://assets.coingecko.com/coins/images/28452/standard/ARUsPeNQ_400x400.jpeg?1696527447" },
|
|
{ "BONK", "https://assets.coingecko.com/coins/images/28600/standard/bonk.jpg?1696527587" },
|
|
{ "WLD", "https://assets.coingecko.com/coins/images/31069/standard/worldcoin.jpeg?1696529903" },
|
|
{
|
|
"tBTC",
|
|
"https://assets.coingecko.com/coins/images/11224/standard/0x18084fba666a33d37592fa2633fd49a74dd93a88.png?1696511155"
|
|
},
|
|
{ "EIGEN", "https://assets.coingecko.com/coins/images/37441/standard/eigen.jpg?1728023974" },
|
|
{ "SUI", "https://assets.coingecko.com/coins/images/26375/standard/sui-ocean-square.png?1727791290" },
|
|
{ "SEI", "https://assets.coingecko.com/coins/images/28205/standard/Sei_Logo_-_Transparent.png?1696527207" },
|
|
{ "DAI", "https://assets.coingecko.com/coins/images/9956/thumb/4943.png?1636636734" },
|
|
{ "TIA", "https://assets.coingecko.com/coins/images/31967/standard/tia.jpg?1696530772" },
|
|
{ "TRX", "https://assets.coingecko.com/coins/images/1094/standard/tron-logo.png?1696502193" },
|
|
{
|
|
"TON",
|
|
"https://assets.coingecko.com/coins/images/17980/standard/photo_2024-09-10_17.09.00.jpeg?1725963446"
|
|
},
|
|
{
|
|
"PENDLE",
|
|
"https://assets.coingecko.com/coins/images/15069/standard/Pendle_Logo_Normal-03.png?1696514728"
|
|
},
|
|
{ "wstETH", "https://assets.coingecko.com/coins/images/18834/standard/wstETH.png?1696518295" },
|
|
{ "USDe", "https://assets.coingecko.com/coins/images/33613/standard/USDE.png?1716355685" },
|
|
{ "SATS", "https://assets.coingecko.com/coins/images/30666/standard/_dD8qr3M_400x400.png?1702913020" },
|
|
{ "POL", "https://assets.coingecko.com/coins/images/32440/standard/polygon.png?1698233684" },
|
|
{ "XLM", "https://assets.coingecko.com/coins/images/100/standard/Stellar_symbol_black_RGB.png?1696501482" },
|
|
{ "BCH", "https://assets.coingecko.com/coins/images/780/standard/bitcoin-cash-circle.png?1696501932" },
|
|
{ "ICP", "https://assets.coingecko.com/coins/images/14495/standard/Internet_Computer_logo.png?1696514180" },
|
|
{ "RENDER", "https://assets.coingecko.com/coins/images/11636/standard/rndr.png?1696511529" },
|
|
{ "INJ", "https://assets.coingecko.com/coins/images/12882/standard/Secondary_Symbol.png?1696512670" },
|
|
{ "TRUMP", "https://assets.coingecko.com/coins/images/53746/standard/trump.png?1737171561" },
|
|
{ "MELANIA", "https://assets.coingecko.com/coins/images/53775/standard/melania-meme.png?1737329885" },
|
|
{ "ENA", "https://assets.coingecko.com/coins/images/36530/standard/ethena.png?1711701436" },
|
|
{ "FARTCOIN", "https://assets.coingecko.com/coins/images/50891/standard/fart.jpg?1729503972" },
|
|
{ "AI16Z", "https://assets.coingecko.com/coins/images/51090/standard/AI16Z.jpg?1730027175" },
|
|
{ "ANIME", "https://assets.coingecko.com/coins/images/53575/standard/anime.jpg?1736748703" },
|
|
{ "BERA", "https://assets.coingecko.com/coins/images/25235/standard/BERA.png?1738822008" },
|
|
{ "VIRTUAL", "https://assets.coingecko.com/coins/images/34057/standard/LOGOMARK.png?1708356054" },
|
|
{
|
|
"PENGU",
|
|
"https://assets.coingecko.com/coins/images/52622/standard/PUDGY_PENGUINS_PENGU_PFP.png?1733809110"
|
|
},
|
|
{ "FET", "https://assets.coingecko.com/coins/images/5681/standard/ASI.png?1719827289" },
|
|
{ "ONDO", "https://assets.coingecko.com/coins/images/26580/standard/ONDO.png?1696525656" },
|
|
{ "AIXBT", "https://assets.coingecko.com/coins/images/51784/standard/3.png?1731981138" },
|
|
{
|
|
"CAKE",
|
|
"https://assets.coingecko.com/coins/images/12632/standard/pancakeswap-cake-logo_%281%29.png?1696512440"
|
|
},
|
|
{ "S", "https://assets.coingecko.com/coins/images/38108/standard/200x200_Sonic_Logo.png?1734679256" },
|
|
{ "JUP", "https://assets.coingecko.com/coins/images/34188/standard/jup.png?1704266489" },
|
|
{ "HYPE", "https://assets.coingecko.com/coins/images/50882/standard/hyperliquid.jpg?1729431300" },
|
|
{ "OM", "https://assets.coingecko.com/coins/images/12151/standard/OM_Token.png?1696511991" }
|
|
};
|
|
|
|
var tokenNames = new Dictionary<string, string>
|
|
{
|
|
{ "AAVE", "Aave" },
|
|
{ "ADA", "Cardano" },
|
|
{ "APE", "ApeCoin" },
|
|
{ "ARB", "Arbitrum" },
|
|
{ "ATOM", "Cosmos" },
|
|
{ "AVAX", "Avalanche" },
|
|
{ "BNB", "BNB" },
|
|
{ "BTC", "Bitcoin" },
|
|
{ "DOGE", "Dogecoin" },
|
|
{ "DOT", "Polkadot" },
|
|
{ "ETH", "Ethereum" },
|
|
{ "FIL", "Filecoin" },
|
|
{ "GMX", "GMX" },
|
|
{ "LINK", "Chainlink" },
|
|
{ "LTC", "Litecoin" },
|
|
{ "MATIC", "Polygon" },
|
|
{ "NEAR", "NEAR Protocol" },
|
|
{ "OP", "Optimism" },
|
|
{ "PEPE", "Pepe" },
|
|
{ "SOL", "Solana" },
|
|
{ "UNI", "Uniswap" },
|
|
{ "USDC", "USD Coin" },
|
|
{ "USDT", "Tether" },
|
|
{ "WIF", "dogwifhat" },
|
|
{ "XRP", "XRP" },
|
|
{ "SHIB", "Shiba Inu" },
|
|
{ "STX", "Stacks" },
|
|
{ "ORDI", "ORDI" },
|
|
{ "APT", "Aptos" },
|
|
{ "BOME", "BOOK OF MEME" },
|
|
{ "MEME", "Memecoin" },
|
|
{ "FLOKI", "Floki" },
|
|
{ "MEW", "cat in a dogs world" },
|
|
{ "TAO", "Bittensor" },
|
|
{ "BONK", "Bonk" },
|
|
{ "WLD", "Worldcoin" },
|
|
{ "tBTC", "tBTC" },
|
|
{ "EIGEN", "Eigenlayer" },
|
|
{ "SUI", "Sui" },
|
|
{ "SEI", "Sei" },
|
|
{ "DAI", "Dai" },
|
|
{ "TIA", "Celestia" },
|
|
{ "TRX", "TRON" },
|
|
{ "TON", "Toncoin" },
|
|
{ "PENDLE", "Pendle" },
|
|
{ "wstETH", "Wrapped stETH" },
|
|
{ "USDe", "Ethena USDe" },
|
|
{ "SATS", "1000SATS" },
|
|
{ "POL", "Polygon Ecosystem Token" },
|
|
{ "XLM", "Stellar" },
|
|
{ "BCH", "Bitcoin Cash" },
|
|
{ "ICP", "Internet Computer" },
|
|
{ "RENDER", "Render" },
|
|
{ "INJ", "Injective" },
|
|
{ "TRUMP", "TRUMP" },
|
|
{ "MELANIA", "MELANIA" },
|
|
{ "ENA", "Ethena" },
|
|
{ "FARTCOIN", "FARTCOIN" },
|
|
{ "AI16Z", "AI16Z" },
|
|
{ "ANIME", "ANIME" },
|
|
{ "BERA", "Berachain" },
|
|
{ "VIRTUAL", "Virtual Protocol" },
|
|
{ "PENGU", "Pudgy Penguins" },
|
|
{ "FET", "Artificial Superintelligence Alliance" },
|
|
{ "ONDO", "Ondo" },
|
|
{ "AIXBT", "AIXBT" },
|
|
{ "CAKE", "PancakeSwap" },
|
|
{ "S", "Sonic" },
|
|
{ "JUP", "Jupiter" },
|
|
{ "HYPE", "Hyperliquid" },
|
|
{ "OM", "MANTRA" }
|
|
};
|
|
|
|
foreach (var ticker in availableTicker)
|
|
{
|
|
var tickerInfo = new TickerInfos
|
|
{
|
|
Ticker = ticker,
|
|
ImageUrl = tokens.GetValueOrDefault(ticker.ToString(),
|
|
"https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1547033579"), // Default to BTC image if not found
|
|
Name = tokenNames.GetValueOrDefault(ticker.ToString(), ticker.ToString())
|
|
};
|
|
tickerInfos.Add(tickerInfo);
|
|
}
|
|
|
|
return tickerInfos;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves the latest spotlight overview, using caching to enhance response times.
|
|
/// </summary>
|
|
/// <returns>A <see cref="SpotlightOverview"/> object containing spotlight data.</returns>
|
|
[Authorize]
|
|
[HttpGet("Spotlight")]
|
|
public async Task<ActionResult<SpotlightOverview>> GetSpotlight()
|
|
{
|
|
var cacheKey = $"Spotlight_{DateTime.Now.AddDays(-2).ToString("yyyy-MM-dd")}";
|
|
var overview = _cacheService.GetValue<SpotlightOverview>(cacheKey);
|
|
if (overview == null)
|
|
{
|
|
overview = await _statisticService.GetLastSpotlight(DateTime.Now.AddDays(-2));
|
|
_cacheService.SaveValue(cacheKey, overview, TimeSpan.FromMinutes(2));
|
|
}
|
|
|
|
return Ok(overview);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves candles with indicators values for backtest details display.
|
|
/// </summary>
|
|
/// <param name="exchange">The trading exchange.</param>
|
|
/// <param name="ticker">The ticker symbol.</param>
|
|
/// <param name="startDate">The start date for the candles.</param>
|
|
/// <param name="endDate">The end date for the candles.</param>
|
|
/// <param name="timeframe">The timeframe for the candles.</param>
|
|
/// <param name="scenario">The scenario object to calculate indicators values (optional).</param>
|
|
/// <returns>A response containing candles and indicators values.</returns>
|
|
[Authorize]
|
|
[HttpPost("GetCandlesWithIndicators")]
|
|
public async Task<ActionResult<CandlesWithIndicatorsResponse>> GetCandlesWithIndicators(
|
|
[FromBody] GetCandlesWithIndicatorsRequest request)
|
|
{
|
|
try
|
|
{
|
|
// Get candles for the specified period
|
|
var candles = await _exchangeService.GetCandlesInflux(TradingExchanges.Evm, request.Ticker,
|
|
request.StartDate, request.Timeframe, request.EndDate);
|
|
|
|
if (candles == null || candles.Count == 0)
|
|
{
|
|
return Ok(new CandlesWithIndicatorsResponse
|
|
{
|
|
Candles = new HashSet<Candle>(),
|
|
IndicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>()
|
|
});
|
|
}
|
|
|
|
// Calculate indicators values if scenario is provided
|
|
Dictionary<IndicatorType, IndicatorsResultBase> indicatorsValues = null;
|
|
if (request.Scenario != null && request.Scenario.Indicators != null &&
|
|
request.Scenario.Indicators.Count > 0)
|
|
{
|
|
// Map ScenarioRequest to domain Scenario object
|
|
var domainScenario = MapScenarioRequestToScenario(request.Scenario);
|
|
indicatorsValues = TradingBox.CalculateIndicatorsValues(domainScenario, candles);
|
|
}
|
|
|
|
return Ok(new CandlesWithIndicatorsResponse
|
|
{
|
|
Candles = candles,
|
|
IndicatorsValues = indicatorsValues ?? new Dictionary<IndicatorType, IndicatorsResultBase>()
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return StatusCode(500, $"Error retrieving candles with indicators: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves statistics about currently running bots and their change in the last 24 hours.
|
|
/// </summary>
|
|
/// <returns>A <see cref="StrategiesStatisticsViewModel"/> containing bot statistics.</returns>
|
|
[HttpGet("GetStrategiesStatistics")]
|
|
public async Task<ActionResult<StrategiesStatisticsViewModel>> GetStrategiesStatistics()
|
|
{
|
|
const string cacheKey = "StrategiesStatistics";
|
|
const string previousCountKey = "PreviousBotsCount";
|
|
|
|
// Check if the statistics are already cached
|
|
var cachedStats = _cacheService.GetValue<StrategiesStatisticsViewModel>(cacheKey);
|
|
|
|
if (cachedStats != null)
|
|
{
|
|
return Ok(cachedStats);
|
|
}
|
|
|
|
// Get active bots
|
|
var activeBots = await _mediator.Send(new GetBotsByStatusCommand(BotStatus.Running));
|
|
var currentCount = activeBots.Count();
|
|
|
|
// Get previous count from cache
|
|
var previousCount = _cacheService.GetValue<int>(previousCountKey);
|
|
|
|
// Calculate change - if no previous value, set current count as the change (all are new)
|
|
int change;
|
|
if (previousCount == 0)
|
|
{
|
|
// First time running - assume all bots are new (positive change)
|
|
change = currentCount;
|
|
}
|
|
else
|
|
{
|
|
// Calculate actual difference between current and previous count
|
|
change = currentCount - previousCount;
|
|
}
|
|
|
|
// Create the response
|
|
var botsStatistics = new StrategiesStatisticsViewModel
|
|
{
|
|
TotalStrategiesRunning = currentCount,
|
|
ChangeInLast24Hours = change
|
|
};
|
|
|
|
// Store current count for future comparison (with 24 hour expiration)
|
|
_cacheService.SaveValue(previousCountKey, currentCount, TimeSpan.FromHours(24));
|
|
|
|
// Cache the statistics for 5 minutes
|
|
_cacheService.SaveValue(cacheKey, botsStatistics, TimeSpan.FromMinutes(5));
|
|
|
|
return Ok(botsStatistics);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves the top 3 performing strategies based on PnL.
|
|
/// </summary>
|
|
/// <returns>A <see cref="TopStrategiesViewModel"/> containing the top performing strategies.</returns>
|
|
[HttpGet("GetTopStrategies")]
|
|
public async Task<ActionResult<TopStrategiesViewModel>> GetTopStrategies()
|
|
{
|
|
// Get top 3 bots by PnL directly from database (both running and stopped)
|
|
var bots = await _mediator.Send(new GetTopBotsByPnLCommand(new[] { BotStatus.Running, BotStatus.Stopped }, 3));
|
|
|
|
// Map to view model
|
|
var topStrategies = new TopStrategiesViewModel
|
|
{
|
|
TopStrategies = bots
|
|
.Select(bot => new StrategyPerformance
|
|
{
|
|
StrategyName = bot.Name,
|
|
PnL = bot.Pnl,
|
|
NetPnL = bot.NetPnL,
|
|
AgentName = bot.User.AgentName,
|
|
})
|
|
.ToList()
|
|
};
|
|
|
|
return Ok(topStrategies);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves the top 3 performing strategies based on ROI percentage.
|
|
/// </summary>
|
|
/// <returns>A <see cref="TopStrategiesByRoiViewModel"/> containing the top performing strategies by ROI.</returns>
|
|
[HttpGet("GetTopStrategiesByRoi")]
|
|
public async Task<ActionResult<TopStrategiesByRoiViewModel>> GetTopStrategiesByRoi()
|
|
{
|
|
// Get top 3 bots by ROI directly from database (both running and stopped)
|
|
var bots = await _mediator.Send(new GetTopBotsByRoiCommand(new[] { BotStatus.Running, BotStatus.Stopped }, 3));
|
|
|
|
// Map to view model
|
|
var topStrategiesByRoi = new TopStrategiesByRoiViewModel
|
|
{
|
|
TopStrategiesByRoi = bots
|
|
.Select(bot => new StrategyRoiPerformance
|
|
{
|
|
StrategyName = bot.Name,
|
|
Roi = bot.Roi,
|
|
PnL = bot.Pnl,
|
|
NetPnL = bot.NetPnL,
|
|
Volume = bot.Volume
|
|
})
|
|
.ToList()
|
|
};
|
|
|
|
return Ok(topStrategiesByRoi);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Retrieves list of the active strategies for a user with detailed information
|
|
/// </summary>
|
|
/// <param name="agentName">The agentName to retrieve strategies for</param>
|
|
/// <returns>A list of detailed strategy information</returns>
|
|
[HttpGet("GetUserStrategies")]
|
|
public async Task<ActionResult<List<UserStrategyDetailsViewModel>>> GetUserStrategies(string agentName)
|
|
{
|
|
if (string.IsNullOrEmpty(agentName))
|
|
{
|
|
return BadRequest("Agent name cannot be null or empty.");
|
|
}
|
|
|
|
// Get all strategies for the specified user
|
|
var userStrategies = await _mediator.Send(new GetUserStrategiesCommand(agentName));
|
|
|
|
// Get agent balance history for the last 30 days
|
|
var startDate = DateTime.UtcNow.AddDays(-30);
|
|
var endDate = DateTime.UtcNow;
|
|
var agentBalanceHistory = await _agentService.GetAgentBalances(agentName, startDate, endDate);
|
|
|
|
// Convert to detailed view model with additional information using separate scopes to avoid DbContext concurrency
|
|
var result = await Task.WhenAll(
|
|
userStrategies.Select(strategy =>
|
|
ServiceScopeHelpers.WithScopedService<ITradingService, UserStrategyDetailsViewModel>(
|
|
_serviceScopeFactory,
|
|
async tradingService =>
|
|
await MapStrategyToViewModelAsync(strategy, agentBalanceHistory, tradingService)))
|
|
);
|
|
|
|
return Ok(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves a specific strategy for a user by strategy name
|
|
/// </summary>
|
|
/// <param name="agentName">The agent/user name to retrieve the strategy for</param>
|
|
/// <param name="strategyName">The name of the strategy to retrieve</param>
|
|
/// <returns>Detailed information about the requested strategy</returns>
|
|
[HttpGet("GetUserStrategy")]
|
|
public async Task<ActionResult<UserStrategyDetailsViewModel>> GetUserStrategy(string agentName, string strategyName)
|
|
{
|
|
if (string.IsNullOrEmpty(agentName))
|
|
{
|
|
return BadRequest("Agent name cannot be null or empty.");
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(strategyName))
|
|
{
|
|
return BadRequest("Strategy name cannot be null or empty.");
|
|
}
|
|
|
|
// Get the specific strategy for the user
|
|
var strategy = await _mediator.Send(new GetUserStrategyCommand(agentName, strategyName));
|
|
|
|
if (strategy == null)
|
|
{
|
|
return NotFound($"Strategy '{strategyName}' not found for user '{agentName}'");
|
|
}
|
|
|
|
// Get agent balance history for the last 30 days
|
|
var startDate = DateTime.UtcNow.AddDays(-30);
|
|
var endDate = DateTime.UtcNow;
|
|
var agentBalanceHistory = await _agentService.GetAgentBalances(agentName, startDate, endDate);
|
|
|
|
// Map the strategy to a view model using the shared method
|
|
var result = await MapStrategyToViewModelAsync(strategy, agentBalanceHistory, _tradingService);
|
|
|
|
return Ok(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps a trading bot to a strategy view model with detailed statistics
|
|
/// </summary>
|
|
/// <param name="strategy">The trading bot to map</param>
|
|
/// <param name="agentBalanceHistory">Agent balance history data</param>
|
|
/// <param name="tradingService">Trading service for fetching positions</param>
|
|
/// <returns>A view model with detailed strategy information</returns>
|
|
private async Task<UserStrategyDetailsViewModel> MapStrategyToViewModelAsync(Bot strategy,
|
|
AgentBalanceHistory agentBalanceHistory, ITradingService tradingService)
|
|
{
|
|
// Calculate volume statistics
|
|
decimal totalVolume = strategy.Volume;
|
|
decimal volumeLast24h = strategy.Volume;
|
|
|
|
// Use caching for position data in UI context (not critical trading operations)
|
|
var cacheKey = $"positions_{strategy.Identifier}";
|
|
var cachedPositions = _cacheService.GetValue<List<Position>>(cacheKey);
|
|
|
|
List<Position> positions;
|
|
if (cachedPositions != null)
|
|
{
|
|
positions = cachedPositions;
|
|
}
|
|
else
|
|
{
|
|
// Fetch positions associated with this bot using the provided trading service
|
|
positions = (await tradingService.GetPositionsByInitiatorIdentifierAsync(strategy.Identifier)).ToList();
|
|
|
|
// Cache positions for 2 minutes for UI display purposes
|
|
_cacheService.SaveValue(cacheKey, positions, TimeSpan.FromMinutes(2));
|
|
}
|
|
|
|
var positionsForMetrics = positions.Where(p => p.IsValidForMetrics());
|
|
// Calculate win/loss statistics from actual positions (including open positions)
|
|
int wins = positionsForMetrics.Count(p => p.ProfitAndLoss != null && p.ProfitAndLoss.Realized > 0);
|
|
int losses = positionsForMetrics.Count(p => p.ProfitAndLoss != null && p.ProfitAndLoss.Realized <= 0);
|
|
|
|
int winRate = wins + losses > 0 ? (wins * 100) / (wins + losses) : 0;
|
|
|
|
// Convert positions to view models
|
|
var positionViewModels = positions.Select(MapPositionToViewModel).ToList();
|
|
|
|
// Convert agent balance history to wallet balances dictionary
|
|
var walletBalances = agentBalanceHistory?.AgentBalances?
|
|
.ToDictionary(b => b.Time, b => b.TotalBalanceValue) ?? new Dictionary<DateTime, decimal>();
|
|
|
|
return new UserStrategyDetailsViewModel
|
|
{
|
|
Name = strategy.Name,
|
|
State = strategy.Status,
|
|
PnL = strategy.Pnl,
|
|
NetPnL = strategy.NetPnL,
|
|
ROIPercentage = strategy.Roi,
|
|
Runtime = strategy.Status == BotStatus.Running ? strategy.LastStartTime : null,
|
|
LastStartTime = strategy.LastStartTime,
|
|
LastStopTime = strategy.LastStopTime,
|
|
AccumulatedRunTimeSeconds = strategy.AccumulatedRunTimeSeconds,
|
|
TotalRuntimeSeconds = strategy.GetTotalRuntimeSeconds(),
|
|
WinRate = winRate,
|
|
TotalVolumeTraded = totalVolume,
|
|
VolumeLast24H = volumeLast24h,
|
|
Wins = wins,
|
|
Losses = losses,
|
|
Positions = positionViewModels,
|
|
Identifier = strategy.Identifier,
|
|
WalletBalances = walletBalances,
|
|
Ticker = strategy.Ticker
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves a summary of platform activity across all agents (platform-level data only)
|
|
/// Uses Orleans grain for efficient caching and real-time updates
|
|
/// </summary>
|
|
/// <returns>A summary of platform activity without individual agent details</returns>
|
|
[HttpGet("GetPlatformSummary")]
|
|
public async Task<ActionResult<PlatformSummaryViewModel>> GetPlatformSummary()
|
|
{
|
|
try
|
|
{
|
|
// Get the platform summary grain
|
|
var platformSummaryGrain = _grainFactory.GetGrain<IPlatformSummaryGrain>("platform-summary");
|
|
|
|
// Get the platform summary from the grain (handles caching and real-time updates)
|
|
var state = await platformSummaryGrain.GetPlatformSummaryAsync();
|
|
|
|
// Map the state to the view model
|
|
var summary = MapPlatformSummaryStateToViewModel(state);
|
|
|
|
return Ok(summary);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Log the error and return a fallback response
|
|
// In production, you might want to return cached data or partial data
|
|
return StatusCode(500, $"Error retrieving platform summary: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Retrieves a paginated list of agent summaries for the agent index page
|
|
/// </summary>
|
|
/// <param name="page">Page number (defaults to 1)</param>
|
|
/// <param name="pageSize">Number of items per page (defaults to 10, max 100)</param>
|
|
/// <param name="sortBy">Field to sort by (TotalPnL, TotalROI, Wins, Losses, AgentName, CreatedAt, UpdatedAt)</param>
|
|
/// <param name="sortOrder">Sort order - "asc" or "desc" (defaults to "desc")</param>
|
|
/// <param name="agentNames">Optional comma-separated list of agent names to filter by</param>
|
|
/// <returns>A paginated list of agent summaries sorted by the specified field</returns>
|
|
[AllowAnonymous]
|
|
[HttpGet("GetAgentIndexPaginated")]
|
|
public async Task<ActionResult<PaginatedAgentIndexResponse>> GetAgentIndexPaginated(
|
|
int page = 1,
|
|
int pageSize = 10,
|
|
SortableFields sortBy = SortableFields.NetPnL,
|
|
string sortOrder = "desc",
|
|
string? agentNames = null)
|
|
{
|
|
// Validate pagination parameters
|
|
if (page < 1)
|
|
{
|
|
return BadRequest("Page must be greater than 0");
|
|
}
|
|
|
|
if (pageSize < 1 || pageSize > 100)
|
|
{
|
|
return BadRequest("Page size must be between 1 and 100");
|
|
}
|
|
|
|
// Validate sort order
|
|
if (sortOrder != "asc" && sortOrder != "desc")
|
|
{
|
|
return BadRequest("Sort order must be 'asc' or 'desc'");
|
|
}
|
|
|
|
// Parse agent names filter
|
|
IEnumerable<string>? agentNamesList = null;
|
|
if (!string.IsNullOrWhiteSpace(agentNames))
|
|
{
|
|
agentNamesList = agentNames.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(name => name.Trim())
|
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
|
.ToList();
|
|
}
|
|
|
|
// Get paginated results from database
|
|
var command = new GetPaginatedAgentSummariesCommand(page, pageSize, sortBy, sortOrder, agentNamesList);
|
|
var result = await _mediator.Send(command);
|
|
var agentSummaries = result.Results;
|
|
var totalCount = result.TotalCount;
|
|
|
|
// Map to view models
|
|
var agentSummaryViewModels = new List<AgentSummaryViewModel>();
|
|
foreach (var agentSummary in agentSummaries)
|
|
{
|
|
// Calculate win rate
|
|
int averageWinRate = 0;
|
|
if (agentSummary.Wins + agentSummary.Losses > 0)
|
|
{
|
|
averageWinRate = (agentSummary.Wins * 100) / (agentSummary.Wins + agentSummary.Losses);
|
|
}
|
|
|
|
// Map to view model
|
|
var agentSummaryViewModel = new AgentSummaryViewModel
|
|
{
|
|
AgentName = agentSummary.AgentName,
|
|
TotalPnL = agentSummary.TotalPnL,
|
|
NetPnL = agentSummary.NetPnL,
|
|
TotalROI = agentSummary.TotalROI,
|
|
Wins = agentSummary.Wins,
|
|
Losses = agentSummary.Losses,
|
|
ActiveStrategiesCount = agentSummary.ActiveStrategiesCount,
|
|
TotalVolume = agentSummary.TotalVolume,
|
|
TotalBalance = agentSummary.TotalBalance,
|
|
TotalFees = agentSummary.TotalFees,
|
|
BacktestCount = agentSummary.BacktestCount,
|
|
};
|
|
|
|
agentSummaryViewModels.Add(agentSummaryViewModel);
|
|
}
|
|
|
|
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
|
|
|
|
var response = new PaginatedAgentIndexResponse
|
|
{
|
|
AgentSummaries = agentSummaryViewModels,
|
|
TotalCount = totalCount,
|
|
CurrentPage = page,
|
|
PageSize = pageSize,
|
|
TotalPages = totalPages,
|
|
HasNextPage = page < totalPages,
|
|
HasPreviousPage = page > 1,
|
|
SortBy = sortBy,
|
|
SortOrder = sortOrder,
|
|
FilteredAgentNames = agentNames
|
|
};
|
|
|
|
return Ok(response);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves balance history for a specific agent within a date range
|
|
/// </summary>
|
|
/// <param name="agentName">The name of the agent to retrieve balances for</param>
|
|
/// <param name="startDate">The start date for the balance history</param>
|
|
/// <param name="endDate">Optional end date for the balance history (defaults to current time)</param>
|
|
/// <returns>A list of agent balances within the specified date range</returns>
|
|
[HttpGet("GetAgentBalances")]
|
|
public async Task<ActionResult<AgentBalanceHistory>> GetAgentBalances(
|
|
string agentName,
|
|
DateTime startDate,
|
|
DateTime? endDate = null)
|
|
{
|
|
var balances = await _agentService.GetAgentBalances(agentName, startDate, endDate);
|
|
return Ok(balances);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves an array of online agent names
|
|
/// </summary>
|
|
/// <returns>An array of online agent names</returns>
|
|
[HttpGet("GetOnlineAgent")]
|
|
public async Task<ActionResult<IEnumerable<string>>> GetOnlineAgent()
|
|
{
|
|
const string cacheKey = "OnlineAgentNames";
|
|
|
|
// Check if the online agent names are already cached
|
|
var cachedAgentNames = _cacheService.GetValue<List<string>>(cacheKey);
|
|
|
|
if (cachedAgentNames != null)
|
|
{
|
|
return Ok(cachedAgentNames);
|
|
}
|
|
|
|
// Get only online agent names
|
|
var onlineAgentNames = await _mediator.Send(new GetOnlineAgentNamesCommand());
|
|
|
|
// Cache the results for 2 minutes
|
|
_cacheService.SaveValue(cacheKey, onlineAgentNames, TimeSpan.FromMinutes(2));
|
|
|
|
return Ok(onlineAgentNames);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps a ScenarioRequest to a domain Scenario object.
|
|
/// </summary>
|
|
/// <param name="scenarioRequest">The scenario request to map.</param>
|
|
/// <returns>A domain Scenario object.</returns>
|
|
private Scenario MapScenarioRequestToScenario(ScenarioRequest scenarioRequest)
|
|
{
|
|
var scenario = new Scenario(scenarioRequest.Name, scenarioRequest.LoopbackPeriod);
|
|
|
|
foreach (var indicatorRequest in scenarioRequest.Indicators)
|
|
{
|
|
var indicator = new IndicatorBase(indicatorRequest.Name, indicatorRequest.Type)
|
|
{
|
|
SignalType = indicatorRequest.SignalType,
|
|
MinimumHistory = indicatorRequest.MinimumHistory,
|
|
Period = indicatorRequest.Period,
|
|
FastPeriods = indicatorRequest.FastPeriods,
|
|
SlowPeriods = indicatorRequest.SlowPeriods,
|
|
SignalPeriods = indicatorRequest.SignalPeriods,
|
|
Multiplier = indicatorRequest.Multiplier,
|
|
SmoothPeriods = indicatorRequest.SmoothPeriods,
|
|
StochPeriods = indicatorRequest.StochPeriods,
|
|
CyclePeriods = indicatorRequest.CyclePeriods
|
|
};
|
|
|
|
scenario.AddIndicator(indicator);
|
|
}
|
|
|
|
return scenario;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps PlatformSummaryGrainState to PlatformSummaryViewModel
|
|
/// </summary>
|
|
/// <param name="state">The platform summary grain state</param>
|
|
/// <returns>A mapped platform summary view model</returns>
|
|
private PlatformSummaryViewModel MapPlatformSummaryStateToViewModel(PlatformSummaryGrainState state)
|
|
{
|
|
return new PlatformSummaryViewModel
|
|
{
|
|
// Metadata
|
|
LastUpdated = state.LastUpdated,
|
|
LastSnapshot = state.LastSnapshot,
|
|
HasPendingChanges = state.HasPendingChanges,
|
|
|
|
// Current metrics
|
|
TotalAgents = state.TotalAgents,
|
|
TotalActiveStrategies = state.TotalActiveStrategies,
|
|
TotalPlatformPnL = state.TotalPlatformPnL,
|
|
TotalPlatformVolume = state.TotalPlatformVolume,
|
|
OpenInterest = state.OpenInterest,
|
|
TotalPositionCount = state.TotalLifetimePositionCount,
|
|
TotalPlatformFees = state.TotalPlatformFees,
|
|
|
|
// Historical snapshots
|
|
DailySnapshots = state.DailySnapshots
|
|
.OrderBy(s => s.Date)
|
|
.Select(s => new DailySnapshot
|
|
{
|
|
Date = s.Date,
|
|
TotalAgents = s.TotalAgents,
|
|
TotalStrategies = s.TotalStrategies,
|
|
TotalVolume = s.TotalVolume,
|
|
TotalPnL = s.TotalPnL,
|
|
NetPnL = s.NetPnL,
|
|
TotalOpenInterest = s.TotalOpenInterest,
|
|
TotalPositionCount = s.TotalLifetimePositionCount
|
|
})
|
|
.ToList(),
|
|
|
|
// Breakdowns
|
|
VolumeByAsset = state.VolumeByAsset ?? new Dictionary<Ticker, decimal>(),
|
|
PositionCountByAsset = state.PositionCountByAsset ?? new Dictionary<Ticker, int>(),
|
|
PositionCountByDirection = state.PositionCountByDirection ?? new Dictionary<TradeDirection, int>()
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves a paginated list of strategies (bots) excluding those with Saved status
|
|
/// </summary>
|
|
/// <param name="pageNumber">Page number (1-based, defaults to 1)</param>
|
|
/// <param name="pageSize">Number of items per page (defaults to 10, max 100)</param>
|
|
/// <param name="name">Filter by name (partial match, case-insensitive)</param>
|
|
/// <param name="ticker">Filter by ticker (partial match, case-insensitive)</param>
|
|
/// <param name="agentName">Filter by agent name (partial match, case-insensitive)</param>
|
|
/// <param name="sortBy">Sort field (defaults to CreateDate)</param>
|
|
/// <param name="sortDirection">Sort direction - "Asc" or "Desc" (defaults to "Desc")</param>
|
|
/// <returns>A paginated list of strategies excluding Saved status bots</returns>
|
|
[HttpGet("GetStrategiesPaginated")]
|
|
public async Task<ActionResult<PaginatedResponse<TradingBotResponse>>> GetStrategiesPaginated(
|
|
int pageNumber = 1,
|
|
int pageSize = 10,
|
|
string? name = null,
|
|
string? ticker = null,
|
|
string? agentName = null,
|
|
BotSortableColumn sortBy = BotSortableColumn.CreateDate,
|
|
string sortDirection = "Desc")
|
|
{
|
|
// Validate pagination parameters
|
|
if (pageNumber < 1)
|
|
{
|
|
return BadRequest("Page number must be greater than 0");
|
|
}
|
|
|
|
if (pageSize < 1 || pageSize > 100)
|
|
{
|
|
return BadRequest("Page size must be between 1 and 100");
|
|
}
|
|
|
|
// Validate sort direction
|
|
if (sortDirection != "Asc" && sortDirection != "Desc")
|
|
{
|
|
return BadRequest("Sort direction must be 'Asc' or 'Desc'");
|
|
}
|
|
|
|
try
|
|
{
|
|
// Get paginated bots excluding Saved status
|
|
var (bots, totalCount) = await _botService.GetBotsPaginatedAsync(
|
|
pageNumber,
|
|
pageSize,
|
|
null, // No specific status filter - we'll exclude Saved in the service call
|
|
name,
|
|
ticker,
|
|
agentName,
|
|
sortBy,
|
|
sortDirection);
|
|
|
|
// Filter out Saved status bots
|
|
var filteredBots = bots.Where(bot => bot.Status != BotStatus.Saved).ToList();
|
|
var filteredCount = totalCount - bots.Count(bot => bot.Status == BotStatus.Saved);
|
|
|
|
// Map to response objects
|
|
var tradingBotResponses = MapBotsToTradingBotResponse(filteredBots);
|
|
|
|
// Calculate pagination metadata
|
|
var totalPages = (int)Math.Ceiling((double)filteredCount / pageSize);
|
|
|
|
var response = new PaginatedResponse<TradingBotResponse>
|
|
{
|
|
Items = tradingBotResponses.ToList(),
|
|
TotalCount = filteredCount,
|
|
PageNumber = pageNumber,
|
|
PageSize = pageSize,
|
|
TotalPages = totalPages,
|
|
HasPreviousPage = pageNumber > 1,
|
|
HasNextPage = pageNumber < totalPages
|
|
};
|
|
|
|
return Ok(response);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return StatusCode(500, $"Error retrieving strategies: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps a Position domain object to a PositionViewModel
|
|
/// </summary>
|
|
/// <param name="position">The position domain object to map</param>
|
|
/// <returns>A PositionViewModel with the same properties</returns>
|
|
private static PositionViewModel MapPositionToViewModel(Position position)
|
|
{
|
|
return new PositionViewModel
|
|
{
|
|
Date = position.Date,
|
|
AccountId = position.AccountId,
|
|
OriginDirection = position.OriginDirection,
|
|
Ticker = position.Ticker,
|
|
Open = position.Open,
|
|
StopLoss = position.StopLoss,
|
|
TakeProfit1 = position.TakeProfit1,
|
|
ProfitAndLoss = position.ProfitAndLoss,
|
|
UiFees = position.UiFees,
|
|
GasFees = position.GasFees,
|
|
Status = position.Status,
|
|
SignalIdentifier = position.SignalIdentifier,
|
|
Identifier = position.Identifier,
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps a collection of Bot entities to TradingBotResponse objects.
|
|
/// </summary>
|
|
/// <param name="bots">The collection of bots to map</param>
|
|
/// <returns>A list of TradingBotResponse objects</returns>
|
|
private static List<TradingBotResponse> MapBotsToTradingBotResponse(IEnumerable<Bot> bots)
|
|
{
|
|
var list = new List<TradingBotResponse>();
|
|
|
|
foreach (var item in bots)
|
|
{
|
|
list.Add(new TradingBotResponse
|
|
{
|
|
Status = item.Status.ToString(),
|
|
WinRate = (item.TradeWins + item.TradeLosses) != 0
|
|
? item.TradeWins / (item.TradeWins + item.TradeLosses)
|
|
: 0,
|
|
ProfitAndLoss = item.Pnl,
|
|
Roi = item.Roi,
|
|
Identifier = item.Identifier.ToString(),
|
|
AgentName = item.User.AgentName,
|
|
CreateDate = item.CreateDate,
|
|
StartupTime = item.StartupTime,
|
|
Name = item.Name,
|
|
Ticker = item.Ticker,
|
|
});
|
|
}
|
|
|
|
return list;
|
|
}
|
|
} |