добавлена защита от потерь на падениях
test / deploy_trader_prod (push) Successful in 11m34s Details

dev
vlad zverzhkhovskiy 2025-09-12 09:46:08 +03:00
parent bd607b6c74
commit 32072e57f9
6 changed files with 244 additions and 21 deletions

View File

@ -63,5 +63,36 @@
{
return System.Math.Sqrt(System.Math.Pow(x2 - x1, 2) + System.Math.Pow(y2 - y1, 2));
}
public static bool TryGetAreasRelation(DateTime[] times, decimal[] values, decimal currentValue, TimeSpan boundTimeSpan, out double relation)
{
var upArea = 0d;
var downArea = 0d;
var startTime = times[times.Length - 1];
for (int i = 1; i < times.Length - 2; i++)
{
var k = values.Length - i;
if (startTime - times[k] > boundTimeSpan)
{
break;
}
var point = (double)(values[k] - currentValue);
var time = times[k];
var timePrev = times[k - 1];
var dt = (time - timePrev).TotalSeconds;
var ar = dt * point;
if (ar > 0)
{
upArea += ar;
}
else
{
downArea += System.Math.Abs(ar);
}
}
var area = downArea + upArea;
relation = area != 0 ? (double)upArea / area * 100 : 0;
return area != 0;
}
}
}

View File

@ -94,7 +94,7 @@ namespace KLHZ.Trader.Core.Tests
[Test]
public void CalcProfitTest()
{
var profit = TradingCalculator.CaclProfit(2990, 2991.5m, 0.0025m, 10.3m, false);
var profit = TradingCalculator.CaclProfit(2990, 2985m, 0.0025m, 10.3m, false);
}
}
}

View File

@ -3,8 +3,8 @@
public static class BotModeSwitcher
{
private readonly static object _locker = new();
private static bool _canSell = false;
private static bool _canPurchase = false;
private static bool _canSell = true;
private static bool _canPurchase = true;
public static bool CanSell()
{

View File

@ -77,10 +77,17 @@ namespace KLHZ.Trader.Core.Exchange.Services
private async Task ProcessPrices()
{
var buffer = new LinkedList<(DateTime, double)>();
var tradesBufferBuys = new LinkedList<(DateTime, double)>();
var tradesBufferSells = new LinkedList<(DateTime, double)>();
var tradesRelBuffer = new LinkedList<(DateTime, decimal)>();
while (await _pricesChannel.Reader.WaitToReadAsync())
{
var message = await _pricesChannel.Reader.ReadAsync();
if (message.IsHistoricalData)
{
await _tradeDataProvider.AddData(message, TimeSpan.FromHours(6));
}
if (_tradingInstrumentsFigis.Contains(message.Figi))
{
var currentTime = message.IsHistoricalData ? message.Time : DateTime.UtcNow;
@ -92,14 +99,18 @@ namespace KLHZ.Trader.Core.Exchange.Services
if (message.Figi == "FUTIMOEXF000")
{
var windowMaxSize = 1000;
await SellAssetsIfNeed(message);
var data = await _tradeDataProvider.GetData(message.Figi, windowMaxSize);
if (data.timestamps.Length <= 1)
{
buffer.Clear();
}
var state = ExchangeScheduler.GetCurrentState(message.Time);
await ProcessClearing(data, state, message);
//await SellOldAssetsIfCan(message);
ProcessOpeningStops(message, currentTime);
await ProcessNewPriceIMOEXF(data, state, message, windowMaxSize);
await ProcessNewPriceIMOEXF(data, state, message, windowMaxSize, buffer, tradesBufferBuys, tradesBufferSells, tradesRelBuffer);
}
}
catch (Exception ex)
@ -110,18 +121,19 @@ namespace KLHZ.Trader.Core.Exchange.Services
}
}
private async Task SellOldAssetsIfCan(INewPrice message)
private async Task SellAssetsIfNeed(INewPrice message)
{
var accounts = _tradeDataProvider.Accounts.Values.ToArray();
var assetType = _tradeDataProvider.GetAssetTypeByFigi(message.Figi);
foreach (var acc in accounts)
{
var assets = acc.Assets.Values.Where(a => a.Figi == message.Figi && (DateTime.UtcNow - a.BoughtAt > TimeSpan.FromHours(4))).ToArray();
var assets = acc.Assets.Values.Where(a => a.Figi == message.Figi).ToArray();
foreach (var asset in assets)
{
var profit = TradingCalculator.CaclProfit(asset.BoughtPrice, message.Value,
GetComission(assetType), GetLeverage(message.Figi, asset.Count < 0), asset.Count < 0);
if (profit > 0)
if (message.Time - asset.BoughtAt > TimeSpan.FromMinutes(4) && profit<-66m)
{
await _dataBus.Broadcast(new TradeCommand()
{
@ -131,6 +143,23 @@ namespace KLHZ.Trader.Core.Exchange.Services
: Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketSell,
Count = (long)asset.Count,
RecomendPrice = null,
EnableMargin = false,
});
OpeningStops[message.Figi] = DateTime.UtcNow.AddMinutes(10);
await LogDeclision(DeclisionTradeAction.CloseLong, message, profit);
}
if (message.Time - asset.BoughtAt > TimeSpan.FromHours(4) && profit> 100)
{
await _dataBus.Broadcast(new TradeCommand()
{
AccountId = asset.AccountId,
Figi = message.Figi,
CommandType = asset.Count < 0 ? Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketBuy
: Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketSell,
Count = (long)asset.Count,
RecomendPrice = null,
EnableMargin = false,
});
await LogDeclision(DeclisionTradeAction.CloseLong, message, profit);
}
@ -138,9 +167,11 @@ namespace KLHZ.Trader.Core.Exchange.Services
}
}
private async Task ProcessNewPriceIMOEXF((DateTime[] timestamps, decimal[] prices, decimal bidsCount, decimal asksCount) data,
private async Task ProcessNewPriceIMOEXF((DateTime[] timestamps, decimal[] prices) data,
ExchangeState state,
INewPrice message, int windowMaxSize)
INewPrice message, int windowMaxSize, LinkedList<(DateTime time, double val)> areasBuffer,
LinkedList<(DateTime time, double val)> tradesBufferBuys, LinkedList<(DateTime time, double val)> tradesBufferSells,
LinkedList<(DateTime time, decimal val)> tradesRelBufferSells)
{
var res = TradingEvent.None;
var resultMoveAvFull = MovingAverage.CheckByWindowAverageMean(data.timestamps, data.prices,
@ -154,6 +185,70 @@ namespace KLHZ.Trader.Core.Exchange.Services
await LogPrice(message, _smallWindowProcessor, resultMoveAvFull.smallWindowAv);
}
//var oldTotalSales = (decimal)tradesBufferSells.Sum(s => s.val);
//var oldTotalBuys = (decimal)tradesBufferBuys.Sum(s => s.val);
//var oldTotalTrades = oldTotalSales + oldTotalBuys;
//if (message.Direction == 1)
//{
// tradesBufferBuys.AddLast((message.Time, message.Count));
// if (tradesBufferBuys.Last != null && tradesBufferBuys.First != null
// && tradesBufferBuys.Last.Value.time - tradesBufferBuys.First.Value.time > TimeSpan.FromSeconds(60))
// {
// tradesBufferBuys.RemoveFirst();
// }
//}
//if (message.Direction == 2)
//{
// tradesBufferSells.AddLast((message.Time, message.Count));
// if (tradesBufferSells.Last != null && tradesBufferSells.First != null
// && tradesBufferSells.Last.Value.time - tradesBufferSells.First.Value.time > TimeSpan.FromSeconds(60))
// {
// tradesBufferSells.RemoveFirst();
// }
//}
//var totalSales = (decimal)tradesBufferSells.Sum(s => s.val);
//var totalBuys = (decimal)tradesBufferBuys.Sum(s => s.val);
//var totalTrades = totalSales + totalBuys;
//var tradesRelation = -100m;
//var oldTradesRelation = -100m;
//await LogPrice(message, "tradesvolume", totalTrades);
//if (totalTrades > 0 && oldTotalTrades > 0)
//{
// tradesRelation = (totalBuys - totalSales) / totalTrades;
// oldTradesRelation = (oldTotalBuys - oldTotalSales) / oldTotalTrades;
// tradesRelBufferSells.AddLast((message.Time, tradesRelation - oldTradesRelation));
// if (tradesRelBufferSells.Last != null && tradesRelBufferSells.First != null
// && tradesRelBufferSells.Last.Value.time - tradesRelBufferSells.First.Value.time > TimeSpan.FromSeconds(10))
// {
// tradesRelBufferSells.RemoveFirst();
// }
// if (tradesRelBufferSells.Count > 0)
// {
// await LogPrice(message, "tradesrelation", tradesRelBufferSells.Sum(e => e.val) / tradesRelBufferSells.Count);
// }
//}
var areasRel = -1m;
if (ShapeAreaCalculator.TryGetAreasRelation(data.timestamps, data.prices, message.Value, TimeSpan.FromMinutes(15), out var rel))
{
areasBuffer.AddLast((message.Time, rel));
if (areasBuffer.Last != null && areasBuffer.First != null
&& areasBuffer.Last.Value.time - areasBuffer.First.Value.time > TimeSpan.FromMinutes(1))
{
areasBuffer.RemoveFirst();
}
areasRel = (decimal)areasBuffer.Sum(a => a.val) / areasBuffer.Count;
await LogPrice(message, "balancescalc30min", areasRel);
}
if ((res & TradingEvent.UptrendStart) == TradingEvent.UptrendStart
&& !OpeningStops.TryGetValue(message.Figi, out _)
&& state == ExchangeState.Open
@ -161,8 +256,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
&& (data.timestamps[data.timestamps.Length - 1] - data.timestamps[data.timestamps.Length - 2] < TimeSpan.FromMinutes(1))
)
{
var fullData = await _tradeDataProvider.GetData(message.Figi, TimeSpan.FromMinutes(30));
if (fullData.isFullIntervalExists && fullData.prices.Last() - fullData.prices.First() > -8)
if (areasRel >= 20 && areasRel < 75)
{
if (!message.IsHistoricalData && BotModeSwitcher.CanPurchase())
{
@ -196,6 +290,10 @@ namespace KLHZ.Trader.Core.Exchange.Services
}
await LogDeclision(DeclisionTradeAction.OpenLong, message);
}
//else if (areasRel >=75)
//{
// await LogDeclision(DeclisionTradeAction.OpenShort, message);
//}
}
if ((res & TradingEvent.UptrendEnd) == TradingEvent.UptrendEnd)
@ -245,7 +343,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
}
}
private async Task ProcessClearing((DateTime[] timestamps, decimal[] prices, decimal bidsCount, decimal asksCount) data, ExchangeState state, INewPrice message)
private async Task ProcessClearing((DateTime[] timestamps, decimal[] prices) data, ExchangeState state, INewPrice message)
{
if (state == ExchangeState.ClearingTime
&& !message.IsHistoricalData

View File

@ -64,14 +64,14 @@ namespace KLHZ.Trader.Core.Exchange.Services
return (Array.Empty<DateTime>(), Array.Empty<decimal>(), false);
}
public async ValueTask<(DateTime[] timestamps, decimal[] prices, decimal bidsCount, decimal asksCount)> GetData(string figi, int? length = null)
public async ValueTask<(DateTime[] timestamps, decimal[] prices)> GetData(string figi, int? length = null)
{
if (_historyCash.TryGetValue(figi, out var unit))
{
var res = await unit.GetData(length);
return (res.timestamps, res.prices, unit.BidsCount, unit.AsksCount);
return (res.timestamps, res.prices);
}
return (Array.Empty<DateTime>(), Array.Empty<decimal>(), 1, 1);
return (Array.Empty<DateTime>(), Array.Empty<decimal>());
}
public async ValueTask AddData(INewPrice message, TimeSpan? clearingInterval = null)

View File

@ -3,6 +3,7 @@ using KLHZ.Trader.Core.Contracts.Messaging.Interfaces;
using KLHZ.Trader.Core.DataLayer;
using KLHZ.Trader.Core.DataLayer.Entities.Orders;
using KLHZ.Trader.Core.DataLayer.Entities.Prices;
using KLHZ.Trader.Core.Exchange.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
@ -12,13 +13,15 @@ namespace KLHZ.Trader.Service.Controllers
[Route("[controller]/[action]")]
public class PlayController : ControllerBase
{
private readonly TraderDataProvider _traderDataProvider;
private readonly IDataBus _dataBus;
private readonly IDbContextFactory<TraderDbContext> _dbContextFactory;
public PlayController(IDataBus dataBus, IDbContextFactory<TraderDbContext> dbContextFactory)
public PlayController(IDataBus dataBus, IDbContextFactory<TraderDbContext> dbContextFactory, TraderDataProvider traderDataProvider)
{
_dbContextFactory = dbContextFactory;
_dataBus = dataBus;
_traderDataProvider = traderDataProvider;
}
[HttpGet]
@ -26,8 +29,8 @@ namespace KLHZ.Trader.Service.Controllers
{
try
{
var time1 = DateTime.UtcNow.AddDays(-7);
var time2 = DateTime.UtcNow.AddMinutes(-30);
var time1 = DateTime.UtcNow.AddDays(-30);
var time2 = DateTime.UtcNow.AddMinutes(30);
using var context1 = await _dbContextFactory.CreateDbContextAsync();
context1.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var data = await context1.PriceChanges
@ -39,7 +42,9 @@ namespace KLHZ.Trader.Service.Controllers
Ticker = c.Ticker,
Time = c.Time,
Value = c.Value,
IsHistoricalData = true
IsHistoricalData = true,
Direction = c.Direction,
Count = c.Count,
})
.ToArrayAsync();
@ -193,6 +198,95 @@ namespace KLHZ.Trader.Service.Controllers
}
}
//[HttpGet]
//public async Task GetBalance(string figi)
//{
// try
// {
// var time1 = DateTime.UtcNow.AddDays(-7);
// using var context1 = await _dbContextFactory.CreateDbContextAsync();
// context1.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
// var data = await context1.PriceChanges
// .Where(c => c.Figi == figi && c.Time >= time1)
// .OrderBy(c => c.Time)
// .Select(c => new NewPriceMessage()
// {
// Figi = figi,
// Ticker = c.Ticker,
// Time = c.Time,
// Value = c.Value,
// IsHistoricalData = true
// })
// .ToArrayAsync();
// var buffer = new List<ProcessedPrice>();
// var list = new LinkedList<(DateTime time, double value)>();
// var previous = -1000d;
// foreach (var mess in data)
// {
// await _traderDataProvider.AddData(mess, TimeSpan.FromHours(6));
// var dataFromCache = await _traderDataProvider.GetData(figi, TimeSpan.FromMinutes(15));
// if (dataFromCache.isFullIntervalExists)
// {
// if (ShapeAreaCalculator.TryGetAreasRelation(dataFromCache.timestamps, dataFromCache.prices,mess.Value,out var res))
// {
// if (list.Count > 0 && mess.Time - list.Last().time > TimeSpan.FromMinutes(5))
// {
// list.Clear();
// }
// list.AddLast((mess.Time, res));
// if (list.Last().time - list.First().time > TimeSpan.FromMinutes(1))
// {
// list.RemoveFirst();
// }
// var newRes = (decimal)(list.Sum(i => i.value) / list.Count);
// if (list.Count > 0)
// {
// try
// {
// buffer.Add(new ProcessedPrice()
// {
// Figi = figi,
// Processor = "balancescalc30min",
// Ticker = mess.Ticker,
// Time = mess.Time,
// Value = newRes,
// //Value = (decimal)res,
// });
// }
// catch(Exception ex)
// {
// }
// }
// }
// }
// else
// {
// previous = -1d;
// }
// if (buffer.Count > 10000)
// {
// await context1.ProcessedPrices.AddRangeAsync(buffer);
// await context1.SaveChangesAsync();
// buffer.Clear();
// }
// }
// if (buffer.Count > 0)
// {
// await context1.ProcessedPrices.AddRangeAsync(buffer);
// await context1.SaveChangesAsync();
// buffer.Clear();
// }
// }
// catch (Exception ex)
// {
// }
//}
////[HttpGet]
//public async Task LoadTradesToHistory(string figi)
//{