Files
managing-apps/src/Managing.Application/Trading/TradingService.cs
Oda 082ae8714b Trading bot grain (#33)
* Trading bot Grain

* Fix a bit more of the trading bot

* Advance on the tradingbot grain

* Fix build

* Fix db script

* Fix user login

* Fix a bit backtest

* Fix cooldown and backtest

* start fixing bot start

* Fix startup

* Setup local db

* Fix build and update candles and scenario

* Add bot registry

* Add reminder

* Updateing the grains

* fix bootstraping

* Save stats on tick

* Save bot data every tick

* Fix serialization

* fix save bot stats

* Fix get candles

* use dict instead of list for position

* Switch hashset to dict

* Fix a bit

* Fix bot launch and bot view

* add migrations

* Remove the tolist

* Add agent grain

* Save agent summary

* clean

* Add save bot

* Update get bots

* Add get bots

* Fix stop/restart

* fix Update config

* Update scanner table on new backtest saved

* Fix backtestRowDetails.tsx

* Fix agentIndex

* Update agentIndex

* Fix more things

* Update user cache

* Fix

* Fix account load/start/restart/run
2025-08-05 04:07:06 +07:00

424 lines
16 KiB
C#

using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Domain.Accounts;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.Indicators;
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.Synth.Models;
using Managing.Domain.Trades;
using Managing.Domain.Users;
using Managing.Infrastructure.Evm.Models.Privy;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Application.Trading;
public class TradingService : ITradingService
{
private readonly ITradingRepository _tradingRepository;
private readonly IExchangeService _exchangeService;
private readonly IAccountService _accountService;
private readonly ICacheService _cacheService;
private readonly IMessengerService _messengerService;
private readonly IStatisticRepository _statisticRepository;
private readonly IEvmManager _evmManager;
private readonly ILogger<TradingService> _logger;
private readonly ISynthPredictionService _synthPredictionService;
public TradingService(
ITradingRepository tradingRepository,
IExchangeService exchangeService,
ILogger<TradingService> logger,
IAccountService accountService,
ICacheService cacheService,
IMessengerService messengerService,
IStatisticRepository statisticRepository,
IEvmManager evmManager,
ISynthPredictionService synthPredictionService)
{
_tradingRepository = tradingRepository;
_exchangeService = exchangeService;
_logger = logger;
_accountService = accountService;
_cacheService = cacheService;
_messengerService = messengerService;
_statisticRepository = statisticRepository;
_evmManager = evmManager;
_synthPredictionService = synthPredictionService;
}
public async Task DeleteScenarioAsync(string name)
{
await _tradingRepository.DeleteScenarioAsync(name);
}
public async Task DeleteIndicatorAsync(string name)
{
await _tradingRepository.DeleteIndicatorAsync(name);
}
public async Task<Position> GetPositionByIdentifierAsync(Guid identifier)
{
return await _tradingRepository.GetPositionByIdentifierAsync(identifier);
}
public async Task<IEnumerable<Position>> GetPositionsAsync(PositionInitiator positionInitiator)
{
return await _tradingRepository.GetPositionsAsync(positionInitiator);
}
public async Task<IEnumerable<Position>> GetPositionsByStatusAsync(PositionStatus postionStatus)
{
return await _tradingRepository.GetPositionsByStatusAsync(postionStatus);
}
public async Task<Scenario> GetScenarioByNameAsync(string scenario)
{
return await _tradingRepository.GetScenarioByNameAsync(scenario);
}
public async Task<IEnumerable<Scenario>> GetScenariosAsync()
{
return await _tradingRepository.GetScenariosAsync();
}
public async Task<IEnumerable<IndicatorBase>> GetIndicatorsAsync()
{
return await _tradingRepository.GetStrategiesAsync();
}
public async Task<IndicatorBase> GetIndicatorByNameAsync(string strategy)
{
return await _tradingRepository.GetStrategyByNameAsync(strategy);
}
public async Task InsertPositionAsync(Position position)
{
await _tradingRepository.InsertPositionAsync(position);
}
public async Task InsertScenarioAsync(Scenario scenario)
{
await _tradingRepository.InsertScenarioAsync(scenario);
}
public async Task InsertIndicatorAsync(IndicatorBase indicatorBase)
{
await _tradingRepository.InsertIndicatorAsync(indicatorBase);
}
public async Task<Position> ManagePosition(Account account, Position position)
{
var lastPrice = await _exchangeService.GetPrice(account, position.Ticker, DateTime.UtcNow);
var quantityInPosition = await _exchangeService.GetQuantityInPosition(account, position.Ticker);
var orders = await _exchangeService.GetOpenOrders(account, position.Ticker);
if (quantityInPosition > 0)
{
// Position still open
position.ProfitAndLoss =
TradingBox.GetProfitAndLoss(position, position.Open.Quantity, lastPrice, position.Open.Leverage);
_logger.LogInformation($"Position is still open - PNL : {position.ProfitAndLoss.Realized} $");
_logger.LogInformation($"Requested trades : {orders.Count}");
}
else
{
// No quantity in position = SL/TP hit
if (orders.All(o => o.TradeType != TradeType.StopLoss))
{
// SL hit
_logger.LogInformation($"Stop loss is filled on exchange.");
position.StopLoss.SetStatus(TradeStatus.Filled);
position.ProfitAndLoss = TradingBox.GetProfitAndLoss(position, position.StopLoss.Quantity,
position.StopLoss.Price, position.Open.Leverage);
_ = _exchangeService.CancelOrder(account, position.Ticker);
}
else if (orders.All(o => o.TradeType != TradeType.TakeProfit))
{
// TP Hit
if (position.TakeProfit1.Status == TradeStatus.Filled && position.TakeProfit2 != null)
{
position.TakeProfit2.SetStatus(TradeStatus.Filled);
position.ProfitAndLoss = TradingBox.GetProfitAndLoss(position, position.TakeProfit2.Quantity,
position.TakeProfit2.Price, 1);
_logger.LogInformation($"TakeProfit 2 is filled on exchange.");
}
else
{
position.TakeProfit1.SetStatus(TradeStatus.Filled);
position.ProfitAndLoss = TradingBox.GetProfitAndLoss(position, position.TakeProfit1.Quantity,
position.TakeProfit1.Price, 1);
_logger.LogInformation($"TakeProfit 1 is filled on exchange.");
}
}
else
{
_logger.LogInformation(
$"Position closed manually or forced close by exchange because quantity in position is below 0.");
position.Status = PositionStatus.Finished;
if (orders.Any()) await _exchangeService.CancelOrder(account, position.Ticker);
}
}
return position;
}
public async Task UpdatePositionAsync(Position position)
{
await _tradingRepository.UpdatePositionAsync(position);
}
public async Task<IEnumerable<Position>> GetPositionsAsync()
{
var positions = new List<Position>();
positions.AddRange(await GetPositionsByStatusAsync(PositionStatus.New));
positions.AddRange(await GetPositionsByStatusAsync(PositionStatus.Filled));
positions.AddRange(await GetPositionsByStatusAsync(PositionStatus.PartiallyFilled));
return positions;
}
public async Task WatchTrader()
{
var availableTickers = new List<Ticker> { Ticker.BTC, Ticker.ETH, Ticker.UNI, Ticker.LINK };
var watchAccount = await GetTradersWatch();
var key = $"AccountsQuantityInPosition";
var aqip = _cacheService.GetValue<List<TraderFollowup>>(key);
if (aqip == null)
{
aqip = GetAccountsQuantityInPosition(watchAccount);
}
else
{
foreach (var a in watchAccount.Where(w => !aqip.Any(a => a.Account.Address == w.Address)))
{
var newAccount = SetupFollowUp(a);
aqip.Add(newAccount);
}
foreach (var a in aqip)
{
await ManageTrader(a, availableTickers);
}
}
_cacheService.SaveValue(key, aqip, TimeSpan.FromMinutes(10));
}
public async Task<IEnumerable<Trader>> GetTradersWatch()
{
var watchAccount = await _statisticRepository.GetBestTradersAsync();
var customWatchAccount = (await _accountService.GetAccountsAsync(true, false))
.Where(a => a.Type == AccountType.Watch)
.ToList().MapToTraders();
watchAccount.AddRange(customWatchAccount.Where(a =>
!watchAccount.Any(w => w.Address.Equals(a.Address, StringComparison.InvariantCultureIgnoreCase))));
return watchAccount;
}
public void UpdateDeltaNeutralOpportunities()
{
var fundingRates = _exchangeService.GetFundingRates();
}
public async Task UpdateScenarioAsync(Scenario scenario)
{
await _tradingRepository.UpdateScenarioAsync(scenario);
}
public async Task UpdateIndicatorAsync(IndicatorBase indicatorBase)
{
await _tradingRepository.UpdateStrategyAsync(indicatorBase);
}
public async Task<IEnumerable<Position>> GetBrokerPositions(Account account)
{
return await _exchangeService.GetBrokerPositions(account);
}
private async Task ManageTrader(TraderFollowup a, List<Ticker> tickers)
{
var shortAddress = a.Account.Address.Substring(0, 6);
foreach (var ticker in tickers)
{
try
{
var newTrade = await _exchangeService.GetTrade(a.Account.Address, "", ticker);
var oldTrade = a.Trades.SingleOrDefault(t => t.Ticker == ticker);
if (newTrade == null)
{
if (oldTrade != null)
{
_logger.LogInformation(
$"[{shortAddress}][{ticker}] Trader previously got a position open but the position was close by trader");
await _messengerService.SendClosedPosition(a.Account.Address, oldTrade);
a.Trades.Remove(oldTrade);
}
}
else if ((newTrade != null && oldTrade == null) || (newTrade.Quantity > oldTrade.Quantity))
{
_logger.LogInformation(
$"[{shortAddress}][{ticker}] Trader increase {newTrade.Direction} by {newTrade.Quantity - (oldTrade?.Quantity ?? 0)} with leverage {newTrade.Leverage} at {newTrade.Price} leverage.");
var index = a.Trades.IndexOf(oldTrade);
if (index != -1)
{
a.Trades[index] = newTrade;
}
else
{
a.Trades.Add(newTrade);
}
// Open position
await _messengerService.SendIncreasePosition(a.Account.Address, newTrade, "Test6", oldTrade);
// Save position to cache
}
else if (newTrade.Quantity < oldTrade.Quantity && newTrade.Quantity > 0)
{
var decreaseAmount = oldTrade.Quantity - newTrade.Quantity;
var index = a.Trades.IndexOf(oldTrade);
a.Trades[index] = newTrade;
_logger.LogInformation(
$"[{a.Account.Address.Substring(0, 6)}][{ticker}] Trader decrease position but didnt close it {decreaseAmount}");
await _messengerService.SendDecreasePosition(a.Account.Address, newTrade, decreaseAmount);
}
else
{
_logger.LogInformation(
$"[{shortAddress}][{ticker}] No change - Quantity still {newTrade.Quantity}");
}
}
catch (Exception ex)
{
_logger.LogError($"[{shortAddress}][{ticker}] Impossible to fetch trader");
}
}
}
private List<TraderFollowup> GetAccountsQuantityInPosition(IEnumerable<Trader> watchAccount)
{
var result = new List<TraderFollowup>();
foreach (var account in watchAccount)
{
var trader = SetupFollowUp(account);
result.Add(trader);
}
return result;
}
private static TraderFollowup SetupFollowUp(Trader account)
{
var trader = new TraderFollowup
{
Account = account,
Trades = new List<Trade>(),
PositionIdentifiers = new List<string>()
};
return trader;
}
public class TraderFollowup
{
public Trader Account { get; set; }
public List<Trade> Trades { get; set; }
public List<string> PositionIdentifiers { get; set; }
}
public async Task<PrivyInitAddressResponse> InitPrivyWallet(string publicAddress)
{
try
{
if (string.IsNullOrEmpty(publicAddress))
{
_logger.LogWarning("Attempted to initialize Privy wallet with null or empty public address");
return new PrivyInitAddressResponse
{ Success = false, Error = "Public address cannot be null or empty" };
}
return await _evmManager.InitAddress(publicAddress);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error initializing Privy wallet for address {PublicAddress}", publicAddress);
return new PrivyInitAddressResponse { Success = false, Error = ex.Message };
}
}
// Synth API integration methods
public async Task<SignalValidationResult> ValidateSynthSignalAsync(LightSignal signal, decimal currentPrice,
TradingBotConfig botConfig, bool isBacktest)
{
return await _synthPredictionService.ValidateSignalAsync(signal, currentPrice, botConfig, isBacktest);
}
public async Task<bool> AssessSynthPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice,
TradingBotConfig botConfig, bool isBacktest)
{
return await _synthPredictionService.AssessPositionRiskAsync(ticker, direction, currentPrice,
botConfig, isBacktest);
}
public async Task<SynthRiskResult> MonitorSynthPositionRiskAsync(Ticker ticker, TradeDirection direction,
decimal currentPrice, decimal liquidationPrice, Guid positionIdentifier, TradingBotConfig botConfig)
{
return await _synthPredictionService.MonitorPositionRiskAsync(ticker, direction, currentPrice, liquidationPrice,
positionIdentifier, botConfig);
}
/// <summary>
/// Calculates indicators values for a given scenario and candles.
/// </summary>
/// <param name="scenario">The scenario containing indicators.</param>
/// <param name="candles">The candles to calculate indicators for.</param>
/// <returns>A dictionary of indicator types to their calculated values.</returns>
public Dictionary<IndicatorType, IndicatorsResultBase> CalculateIndicatorsValuesAsync(
Scenario scenario,
HashSet<Candle> candles)
{
var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
if (scenario?.Indicators == null || scenario.Indicators.Count == 0)
{
return indicatorsValues;
}
// Build indicators from scenario
foreach (var indicator in scenario.Indicators)
{
try
{
indicatorsValues[indicator.Type] = indicator.GetIndicatorValues(candles);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error calculating indicator {IndicatorName}: {ErrorMessage}",
indicator.Name, ex.Message);
}
}
return indicatorsValues;
}
public async Task<IndicatorBase?> GetIndicatorByNameUserAsync(string name, User user)
{
return await _tradingRepository.GetStrategyByNameUserAsync(name, user);
}
public async Task<Scenario?> GetScenarioByNameUserAsync(string scenarioName, User user)
{
return await _tradingRepository.GetScenarioByNameUserAsync(scenarioName, user);
}
}