Compare commits

...

4 Commits

Author SHA1 Message Date
vlad zverzhkhovskiy 8edb4351dd добавил миграцию
test / deploy_trader_prod (push) Successful in 4m33s Details
2025-09-02 14:43:43 +03:00
vlad zverzhkhovskiy 62de4169b8 Сократил объем информации по стаканам 2025-09-02 14:43:00 +03:00
vlad zverzhkhovskiy 45b34b4509 добавил сбор данных по стаканам. 2025-09-02 14:25:46 +03:00
vlad zverzhkhovskiy d634cfac73 удалил калмана 2025-09-02 12:17:19 +03:00
56 changed files with 584 additions and 482 deletions

View File

@ -0,0 +1,13 @@
namespace KLHZ.Trader.Core.Contracts.Declisions.Dtos.Enums
{
[Flags]
public enum TradingEvent
{
None = 0,
StopBuy = 1,
LongOpen = 2,
ShortClose = 4,
LongClose = 8,
ShortOpen = 16,
}
}

View File

@ -1,6 +1,6 @@
namespace KLHZ.Trader.Core.Declisions.Dtos
namespace KLHZ.Trader.Core.Contracts.Declisions.Dtos
{
internal readonly struct TwoPeriodsProcessingDto
public readonly struct TwoPeriodsResultDto
{
public readonly int Start;
public readonly int Bound;
@ -11,7 +11,7 @@
public readonly TimeSpan PeriodStart;
public readonly TimeSpan PeriodEnd;
public TwoPeriodsProcessingDto(bool success, float diffStart, float diffEnd, int start, int bound, int end,
public TwoPeriodsResultDto(bool success, float diffStart, float diffEnd, int start, int bound, int end,
TimeSpan periodStart, TimeSpan periodEnd)
{
Success = success;

View File

@ -1,4 +1,4 @@
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces;
namespace KLHZ.Trader.Core.Contracts.Declisions.Interfaces
{
@ -8,5 +8,8 @@ namespace KLHZ.Trader.Core.Contracts.Declisions.Interfaces
public int Length { get; }
public ValueTask AddData(INewPrice priceChange);
public ValueTask<(DateTime[] timestamps, float[] prices)> GetData();
public ValueTask AddOrderbook(IOrderbook orderbook);
public long AsksCount { get; }
public long BidsCount { get; }
}
}

View File

@ -1,9 +1,9 @@
using KLHZ.Trader.Core.Contracts.Declisions.Dtos;
using KLHZ.Trader.Core.Contracts.Declisions.Dtos.Enums;
namespace KLHZ.Trader.Core.Contracts.Declisions.Interfaces
{
public interface ITradingEventsDetector
{
public ValueTask<TradingEventsDto> Detect(IPriceHistoryCacheUnit unit);
public ValueTask<TradingEvent> Detect(IPriceHistoryCacheUnit unit);
}
}

View File

@ -1,4 +1,4 @@
namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces
namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces
{
public interface IMessage
{

View File

@ -1,4 +1,4 @@
namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces
namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces
{
public interface INewCandle
{

View File

@ -1,4 +1,4 @@
namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces
namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces
{
public interface INewPrice
{

View File

@ -0,0 +1,13 @@
namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces
{
public interface IOrderbook
{
public string Ticker { get; }
public string Figi { get; }
public long AsksCount { get; }
public long BidsCount { get; }
public DateTime Time { get; }
public IOrderbookItem[] Asks { get; }
public IOrderbookItem[] Bids { get; }
}
}

View File

@ -0,0 +1,8 @@
namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces
{
public interface IOrderbookItem
{
public long Count { get; }
public decimal Price { get; }
}
}

View File

@ -1,4 +1,4 @@
namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces
namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces
{
public interface IProcessedPrice : INewPrice
{

View File

@ -1,4 +1,4 @@
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces;
namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos
{

View File

@ -0,0 +1,15 @@
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces;
namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos
{
public class NewOrderbookMessage : IOrderbook
{
public required string Ticker { get; init; }
public required string Figi { get; init; }
public DateTime Time { get; init; }
public IOrderbookItem[] Asks { get; init; } = [];
public IOrderbookItem[] Bids { get; init; } = [];
public long AsksCount { get; init; }
public long BidsCount { get; init; }
}
}

View File

@ -1,4 +1,4 @@
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces;
namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos
{

View File

@ -1,19 +1,21 @@
using KLHZ.Trader.Core.Contracts.Messaging.Dtos;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces;
using System.Threading.Channels;
namespace KLHZ.Trader.Core.Contracts.Messaging.Interfaces
{
public interface IDataBus
{
public bool AddChannel(string key, Channel<IOrderbook> channel);
public bool AddChannel(string key, Channel<IProcessedPrice> channel);
public bool AddChannel(string key, Channel<INewPrice> channel);
public bool AddChannel(string key, Channel<TradeCommand> channel);
public bool AddChannel(string key, Channel<IMessage> channel);
public bool AddChannel(string key, Channel<INewCandle> channel);
public Task BroadcastNewPrice(INewPrice newPriceMessage);
public Task BroadcastCommand(TradeCommand command);
public Task BroadcastNewCandle(INewCandle command);
public Task BroadcastProcessedPrice(IProcessedPrice command);
public Task Broadcast(INewPrice newPriceMessage);
public Task Broadcast(TradeCommand command);
public Task Broadcast(INewCandle command);
public Task Broadcast(IProcessedPrice command);
public Task Broadcast(IOrderbook orderbook);
}
}

View File

@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;
//Тесты
[assembly: InternalsVisibleTo("KLHZ.Trader.Core.Tests")]

View File

@ -1,4 +1,4 @@
namespace KLHZ.Trader.Core.Math.Utils
namespace KLHZ.Trader.Core.Math.Common
{
public static class Lines
{
@ -14,7 +14,7 @@
{
var dtime = (float)(time2 - time1).TotalSeconds;
var dval1 = (val1_2 - val1_1);
var dval1 = val1_2 - val1_1;
var k1 = dval1 / dtime;
var b1 = val1_1;

View File

@ -1,4 +1,4 @@
namespace KLHZ.Trader.Core.Declisions.Dtos
namespace KLHZ.Trader.Core.Math.Declisions.Dtos
{
internal readonly struct PeriodPricesInfoDto
{

View File

@ -1,4 +1,4 @@
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces;
namespace KLHZ.Trader.Core.Math.Declisions.Dtos
{

View File

@ -1,7 +1,7 @@
using KLHZ.Trader.Core.Contracts.Declisions.Interfaces;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces;
namespace KLHZ.Trader.Core.Math.Declisions.Services
namespace KLHZ.Trader.Core.Math.Declisions.Services.Cache
{
public class PriceHistoryCacheUnit : IPriceHistoryCacheUnit
{
@ -20,6 +20,9 @@ namespace KLHZ.Trader.Core.Math.Declisions.Services
}
}
public long AsksCount => 1;
public long BidsCount => 1;
private readonly object _locker = new();
private readonly float[] Prices = new float[CacheMaxLength];
private readonly DateTime[] Timestamps = new DateTime[CacheMaxLength];
@ -56,6 +59,11 @@ namespace KLHZ.Trader.Core.Math.Declisions.Services
}
}
public ValueTask AddOrderbook(IOrderbook orderbook)
{
return ValueTask.CompletedTask;
}
public PriceHistoryCacheUnit(string figi, params INewPrice[] priceChanges)
{
Figi = figi;

View File

@ -1,8 +1,7 @@
using KLHZ.Trader.Core.Contracts.Declisions.Interfaces;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces;
using System.Reflection;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces;
namespace KLHZ.Trader.Core.Math.Declisions.Services
namespace KLHZ.Trader.Core.Math.Declisions.Services.Cache
{
public class PriceHistoryCacheUnit2 : IPriceHistoryCacheUnit
{
@ -22,6 +21,28 @@ namespace KLHZ.Trader.Core.Math.Declisions.Services
}
}
public long AsksCount
{
get
{
lock (_locker)
{
return _asksCount;
}
}
}
public long BidsCount
{
get
{
lock (_locker)
{
return _bidsCount;
}
}
}
private readonly object _locker = new();
private readonly float[] Prices = new float[_arrayMaxLength];
private readonly DateTime[] Timestamps = new DateTime[_arrayMaxLength];
@ -29,8 +50,12 @@ namespace KLHZ.Trader.Core.Math.Declisions.Services
private int _length = 0;
private int _pointer = -1;
private long _asksCount = 1;
private long _bidsCount = 1;
public ValueTask AddData(INewPrice priceChange)
{
if (priceChange.Figi != Figi) return ValueTask.CompletedTask;
lock (_locker)
{
_pointer++;
@ -57,19 +82,30 @@ namespace KLHZ.Trader.Core.Math.Declisions.Services
{
if (_pointer < 0)
{
return ValueTask.FromResult((Array.Empty<DateTime>(), Array.Empty <float>()));
return ValueTask.FromResult((Array.Empty<DateTime>(), Array.Empty<float>()));
}
else
{
var prices = new float[_length];
var timestamps = new DateTime[_length];
Array.Copy(Prices,1+ _pointer - _length, prices, 0, prices.Length);
Array.Copy(Timestamps,1+ _pointer - _length, timestamps, 0, timestamps.Length);
Array.Copy(Prices, 1 + _pointer - _length, prices, 0, prices.Length);
Array.Copy(Timestamps, 1 + _pointer - _length, timestamps, 0, timestamps.Length);
return ValueTask.FromResult((timestamps, prices));
}
}
}
public ValueTask AddOrderbook(IOrderbook orderbook)
{
if (orderbook.Figi != Figi) return ValueTask.CompletedTask;
lock (_locker)
{
_asksCount = orderbook.AsksCount;
_bidsCount = orderbook.BidsCount;
}
return ValueTask.CompletedTask;
}
public PriceHistoryCacheUnit2(string figi, params INewPrice[] priceChanges)
{
Figi = figi;
@ -85,7 +121,7 @@ namespace KLHZ.Trader.Core.Math.Declisions.Services
.Skip(priceChanges.Length - CacheMaxLength)
.ToArray();
foreach ( var pc in selectedPriceChanges)
foreach (var pc in selectedPriceChanges)
{
AddData(pc);
}

View File

@ -0,0 +1,14 @@
using KLHZ.Trader.Core.Contracts.Declisions.Dtos.Enums;
using KLHZ.Trader.Core.Contracts.Declisions.Interfaces;
using KLHZ.Trader.Core.Math.Declisions.Utils;
namespace KLHZ.Trader.Core.Math.Declisions.Services.EventsDetection
{
public class IntervalsTradingEventsDetector : ITradingEventsDetector
{
public ValueTask<TradingEvent> Detect(IPriceHistoryCacheUnit unit)
{
return ValueTask.FromResult(TwoPeriods.Detect(unit));
}
}
}

View File

@ -1,319 +0,0 @@
using KLHZ.Trader.Core.Contracts.Declisions.Dtos;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces;
using KLHZ.Trader.Core.Contracts.Messaging.Interfaces;
using KLHZ.Trader.Core.Math.Declisions.Dtos;
using MathNet.Filtering.Kalman;
using MathNet.Numerics.LinearAlgebra;
using Microsoft.Extensions.Hosting;
using System.Threading.Channels;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace KLHZ.Trader.Core.Math.Declisions.Services
{
public class KalmanPredictor : IHostedService
{
private readonly double r = 1000.0; // Measurement covariance
private readonly PriceHistoryCacheUnit _cache = new PriceHistoryCacheUnit("");
private readonly Channel<INewPrice> _messages = Channel.CreateUnbounded<INewPrice>();
private readonly IDataBus _dataBus;
private DiscreteKalmanFilter? _dkf;
public KalmanPredictor(IDataBus bus)
{
_dataBus = bus;
bus.AddChannel(nameof(KalmanPredictor), _messages);
_ = ProcessMessages();
}
public Task StartAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
private async Task Init()
{
}
private async Task ProcessCalman(INewPrice message)
{
var H = Matrix<double>.Build.Dense(2, 2, new[] { 1d, 0d, 0d, 1d }); // Measurement matrix
var length = _cache.Length;
if (length > 2 && _dkf != null)
{
var data = await _cache.GetData();
var dt = (data.timestamps[data.prices.Length - 1] - data.timestamps[data.prices.Length - 2]).TotalSeconds;
if (dt < 0.0000001 || dt > 1800) return;
var F = Matrix<double>.Build.Dense(2, 2, new[] { 1d, 0d, dt, 1 }); // State transition matrix
var R = Matrix<double>.Build.Dense(2, 2, new[] { r, r / dt, r / dt, 2 * r / (dt * dt) });
_dkf.Predict(F);
var state = _dkf.State;
var value = state[0, 0] + dt * state[1, 0];
if (!double.IsNaN(value))
{
await _dataBus.BroadcastProcessedPrice(new ProcessedPrice()
{
Figi = message.Figi,
Processor = nameof(KalmanPredictor) + "_predict",
Ticker = message.Ticker,
IsHistoricalData = message.IsHistoricalData,
Time = message.Time,
Value = (decimal)value,
});
}
var dprice = data.prices[data.prices.Length - 1] - data.prices[data.prices.Length - 2];
var k = dprice / dt;
var b = data.prices[data.prices.Length - 2];
var z = Matrix<double>.Build.Dense(2, 1, new[] { b, k });
_dkf.Update(z, H, R);
var state2 = _dkf.State;
var value2 = state2[0, 0] + dt * state2[1, 0];
if (!double.IsNaN(value))
{
await _dataBus.BroadcastProcessedPrice(new ProcessedPrice()
{
Figi = message.Figi,
Processor = nameof(KalmanPredictor) + "_state",
Ticker = message.Ticker,
IsHistoricalData = message.IsHistoricalData,
Time = message.Time,
Value = (decimal)value,
});
}
}
else if (length >= 2)
{
var data = await _cache.GetData();
var dprice = data.prices[data.prices.Length - 1] - data.prices[data.prices.Length - 2];
var dt = (data.timestamps[data.prices.Length - 1] - data.timestamps[data.prices.Length - 2]).TotalSeconds;
if (dt < 0.0000001) return;
var k = dprice / dt;
var b = data.prices[data.prices.Length - 2];
Matrix<double> x0 = Matrix<double>.Build.Dense(2, 1, new[] { b, k });
Matrix<double> P0 = Matrix<double>.Build.Dense(2, 2, new[] { r, r / dt, r / dt, 2 * r / (dt * dt) });
_dkf = new DiscreteKalmanFilter(x0, P0);
}
}
private async Task ProcessMovAv(INewPrice message, int window)
{
var data = await _cache.GetData();
if (data.prices.Length < window) return;
var sum = 0f;
var count = 0;
for (int i = 1; i <= window; i++)
{
sum += data.prices[data.prices.Length - i];
count++;
}
await _dataBus.BroadcastProcessedPrice(new ProcessedPrice()
{
Figi = message.Figi,
Processor = nameof(KalmanPredictor) + "_mov_av_window" + window.ToString(),
Ticker = message.Ticker,
IsHistoricalData = message.IsHistoricalData,
Time = message.Time,
Value = (decimal)(sum / count),
});
}
private static (DateTime time, float value) CalcTimeWindowAverageValue(DateTime[] timestamps, float[] values, int window, int shift = 0)
{
var sum = values[values.Length - 1 - shift];
var count = 1;
var startTime = timestamps[timestamps.Length - 1 - shift];
for (int i = 2; i < values.Length && startTime - timestamps[values.Length - i - shift] < TimeSpan.FromSeconds(window); i++)
{
sum += values[values.Length - i - shift];
count++;
}
return (startTime, sum / count);
}
private static TradingEventsDto CheckByWindowAverageMean(DateTime[] timestamps, float[] prices, int size, float meanfullStep = 3f)
{
var twav15s = new float[size];
var twav120s = new float[size];
var times = new DateTime[size];
for (int shift = 0; shift < size; shift++)
{
var twav15 = CalcTimeWindowAverageValue(timestamps, prices, 15, shift);
var twav120 = CalcTimeWindowAverageValue(timestamps, prices, 120, shift);
twav15s[size - 1 - shift] = twav15.value;
twav120s[size - 1 - shift] = twav120.value;
times[size - 1 - shift] = twav120.time;
if (shift > 0)
{
var isCrossing = Utils.Lines.IsLinesCrossing(
times[size - 1 - shift],
times[size - 2 - shift],
twav15s[size - 1 - shift],
twav15s[size - 2 - shift],
twav120s[size - 1 - shift],
twav120s[size - 2 - shift]);
if (shift == 1 && !isCrossing) //если нет пересечения скользящих средний с окном 120 и 15 секунд между
//текущей и предыдущей точкой - можно не продолжать выполнение.
{
break;
}
if (shift > 1 && isCrossing)
{
// если фильтрация окном 120 наползает на окно 15 сверху, потенциальное время открытия лонга и закрытия шорта
if (twav120s[size - 1] <= twav15s[size - 1] && twav120s[size - 2] > twav15s[size - 2] )
{
if (twav15s[size - 1 - shift] - twav15s[size - 1] >= meanfullStep)
{
return new TradingEventsDto(false, true);
}
}
// если фильтрация окном 15 наползает на окно 120 сверху, потенциальное время закрытия лонга и возможно открытия шорта
if (twav15s[size - 1] <= twav120s[size - 1] && twav15s[size - 2] > twav120s[size - 2])
{
if (twav15s[size - 1 - shift] - twav15s[size - 1] <= - meanfullStep)
{
return new TradingEventsDto(true, false);
}
}
}
}
}
return new TradingEventsDto(false, false);
}
private async Task ProcessTimeWindow(INewPrice message, int window)
{
var data = await _cache.GetData();
var sum = data.prices[data.prices.Length - 1];
var count = 1;
var startTime = data.timestamps[data.prices.Length - 1];
for (int i = 2; i < data.prices.Length && startTime - data.timestamps[data.prices.Length - i] < TimeSpan.FromSeconds(window); i++)
{
sum += data.prices[data.prices.Length - i];
count++;
}
await _dataBus.BroadcastProcessedPrice(new ProcessedPrice()
{
Figi = message.Figi,
Processor = nameof(KalmanPredictor) + "_timeWindow" + window.ToString(),
Ticker = message.Ticker,
IsHistoricalData = message.IsHistoricalData,
Time = message.Time,
Value = (decimal)(sum / count),
});
var diffValue = System.Math.Abs((decimal)(sum / count) - message.Value);
await _dataBus.BroadcastProcessedPrice(new ProcessedPrice()
{
Figi = message.Figi,
Processor = nameof(KalmanPredictor) + "_diff" + window.ToString(),
Ticker = message.Ticker,
IsHistoricalData = message.IsHistoricalData,
Time = message.Time,
Value = diffValue > 3 ? diffValue : 0,
});
}
private async Task ProcessMessages()
{
while (await _messages.Reader.WaitToReadAsync())
{
var message = await _messages.Reader.ReadAsync();
await _cache.AddData(message);
try
{
var data = await _cache.GetData();
var size = 50;
var twav15s = new float[size];
var twav120s = new float[size];
var times = new DateTime[size];
for (int shift = 0; shift < size; shift++)
{
var twav15 = CalcTimeWindowAverageValue(data.timestamps, data.prices, 15, shift);
var twav120 = CalcTimeWindowAverageValue(data.timestamps, data.prices, 120, shift);
twav15s[size - 1-shift] = twav15.value;
twav120s[size - 1 - shift] = twav120.value;
times[size - 1 - shift] = twav120.time;
if (shift>0)
{
var isCrossing = Utils.Lines.IsLinesCrossing(
times[size - 1 - shift],
times[size - 2 - shift],
twav15s[size - 1 - shift],
twav15s[size - 2 - shift],
twav120s[size - 1 - shift],
twav120s[size - 2 - shift]);
if (shift == 1 && !isCrossing) //если нет пересечения скользящих средний с окном 120 и 15 секунд между
//текущей и предыдущей точкой - можно не продолжать выполнение.
{
break;
}
if (shift>1 && isCrossing)
{
// если фильтрация окном 120 наползает на окно 15 сверху, потенциальное время открытия лонга
if (twav120s[size - 1] <= twav15s[size - 1] && twav120s[size - 2] > twav15s[size - 2])
{
}
// если фильтрация окном 15 наползает на окно 120 сверху, потенциальное время закрытия лонга и открытия шорта
if (twav15s[size - 1] <= twav120s[size - 1] && twav15s[size - 2] > twav120s[size - 2])
{
}
}
}
}
//if (isCrossing && twav120_2 <= twav15_2)// если филтрация окном 120 наползает на окно 15 сверху, потенциальное время открытия лонга
//{
// for (int i=shift;i<111; i++)
// {
// }
//}
//if (isCrossing && twav15_2 <= twav120_2)// если филтрация окном 15 наползает на окно 120 сверху, потенциальное время закрытия лонга
//{
//}
//await ProcessCalman(message);
await ProcessMovAv(message, 3);
await ProcessTimeWindow(message, 5);
await ProcessTimeWindow(message, 15);
await ProcessTimeWindow(message, 120);
}
catch (Exception ex)
{
}
}
}
}
}

View File

@ -0,0 +1,76 @@
using KLHZ.Trader.Core.Contracts.Declisions.Dtos;
using KLHZ.Trader.Core.Math.Common;
namespace KLHZ.Trader.Core.Math.Declisions.Utils
{
public static class MovingAverage
{
private static (DateTime time, float value) CalcTimeWindowAverageValue(DateTime[] timestamps, float[] values, int window, int shift = 0)
{
var sum = values[values.Length - 1 - shift];
var count = 1;
var startTime = timestamps[timestamps.Length - 1 - shift];
for (int i = 2; i < values.Length && startTime - timestamps[values.Length - i - shift] < TimeSpan.FromSeconds(window); i++)
{
sum += values[values.Length - i - shift];
count++;
}
return (startTime, sum / count);
}
public static TradingEventsDto CheckByWindowAverageMean(DateTime[] timestamps, float[] prices, int size, float meanfullStep = 3f)
{
var twav15s = new float[size];
var twav120s = new float[size];
var times = new DateTime[size];
for (int shift = 0; shift < size; shift++)
{
var twav15 = CalcTimeWindowAverageValue(timestamps, prices, 15, shift);
var twav120 = CalcTimeWindowAverageValue(timestamps, prices, 120, shift);
twav15s[size - 1 - shift] = twav15.value;
twav120s[size - 1 - shift] = twav120.value;
times[size - 1 - shift] = twav120.time;
if (shift > 0)
{
var isCrossing = Lines.IsLinesCrossing(
times[size - 1 - shift],
times[size - 2 - shift],
twav15s[size - 1 - shift],
twav15s[size - 2 - shift],
twav120s[size - 1 - shift],
twav120s[size - 2 - shift]);
if (shift == 1 && !isCrossing) //если нет пересечения скользящих средний с окном 120 и 15 секунд между
//текущей и предыдущей точкой - можно не продолжать выполнение.
{
break;
}
if (shift > 1 && isCrossing)
{
// если фильтрация окном 120 наползает на окно 15 сверху, потенциальное время открытия лонга и закрытия шорта
if (twav120s[size - 1] <= twav15s[size - 1] && twav120s[size - 2] > twav15s[size - 2])
{
if (twav15s[size - 1 - shift] - twav15s[size - 1] >= meanfullStep)
{
return new TradingEventsDto(false, true);
}
}
// если фильтрация окном 15 наползает на окно 120 сверху, потенциальное время закрытия лонга и возможно открытия шорта
if (twav15s[size - 1] <= twav120s[size - 1] && twav15s[size - 2] > twav120s[size - 2])
{
if (twav15s[size - 1 - shift] - twav15s[size - 1] <= -meanfullStep)
{
return new TradingEventsDto(true, false);
}
}
}
}
}
return new TradingEventsDto(false, false);
}
}
}

View File

@ -1,10 +1,15 @@
using KLHZ.Trader.Core.Contracts.Declisions.Interfaces;
using KLHZ.Trader.Core.Declisions.Dtos;
using KLHZ.Trader.Core.Math.Declisions.Services;
using KLHZ.Trader.Core.Contracts.Declisions.Dtos;
using KLHZ.Trader.Core.Contracts.Declisions.Dtos.Enums;
using KLHZ.Trader.Core.Contracts.Declisions.Interfaces;
using KLHZ.Trader.Core.Math.Declisions.Dtos;
using KLHZ.Trader.Core.Math.Declisions.Services.Cache;
namespace KLHZ.Trader.Core.Declisions.Utils
namespace KLHZ.Trader.Core.Math.Declisions.Utils
{
internal static class HistoryProcessingInstruments
/// <summary>
/// Обработка последних интервалов истории изменения цен.
/// </summary>
public static class TwoPeriods
{
internal static PeriodPricesInfoDto GetPriceDiffForTimeSpan(this IPriceHistoryCacheUnit unit, TimeSpan timeShift, TimeSpan timeSpan, int? pointsShift = null)
{
@ -25,7 +30,7 @@ namespace KLHZ.Trader.Core.Declisions.Utils
int count = 0;
for (int i = times.Length - 1; i > -1; i--)
{
if ((times[i] <= intervalEnd || (pointsShift.HasValue && count < pointsShift.Value)) && intervaEndIndex < 0)
if ((times[i] <= intervalEnd || pointsShift.HasValue && count < pointsShift.Value) && intervaEndIndex < 0)
{
intervaEndIndex = i;
}
@ -88,7 +93,7 @@ namespace KLHZ.Trader.Core.Declisions.Utils
if (k2 == 0 && k1 != 0) return 1000;
return (float)(k1 / k2);
}
internal static float CalcTrendRelationAbs(TwoPeriodsProcessingDto data)
internal static float CalcTrendRelationAbs(TwoPeriodsResultDto data)
{
var k1 = System.Math.Abs(data.DiffStart) / System.Math.Abs(data.PeriodStart.TotalSeconds);
var k2 = System.Math.Abs(data.DiffEnd) / System.Math.Abs(data.PeriodEnd.TotalSeconds);
@ -127,7 +132,7 @@ namespace KLHZ.Trader.Core.Declisions.Utils
var trendRelation = CalcTrendRelationAbs(startDiff, endDiff);
var isEndLocal = endDiff.PeriodDiff == 0;
var res = totalDiff.Success && isStartGrows && (isEndStable || isEndFalls) && (trendRelation >= 2 && !isEndLocal);
var res = totalDiff.Success && isStartGrows && (isEndStable || isEndFalls) && trendRelation >= 2 && !isEndLocal;
if (res)
{
@ -180,9 +185,9 @@ namespace KLHZ.Trader.Core.Declisions.Utils
return res;
}
internal static TwoPeriodsProcessingDto GetTwoPeriodsProcessingData(this (DateTime[] timestamps, float[] prices) data, TimeSpan shift, int shiftPointsStart, int shiftPointsEnd, TimeSpan firstPeriod, float meanfullDiff)
internal static TwoPeriodsResultDto GetTwoPeriodsProcessingData(this (DateTime[] timestamps, float[] prices) data, TimeSpan shift, int shiftPointsStart, int shiftPointsEnd, TimeSpan firstPeriod, float meanfullDiff)
{
var res = new TwoPeriodsProcessingDto(success: false, 0, 0, 0, 0, 0, TimeSpan.Zero, TimeSpan.Zero);
var res = new TwoPeriodsResultDto(success: false, 0, 0, 0, 0, 0, TimeSpan.Zero, TimeSpan.Zero);
var time = data.timestamps;
var prices = data.prices;
int count = -1;
@ -198,7 +203,7 @@ namespace KLHZ.Trader.Core.Declisions.Utils
bound = i;
shift = lastTime - time[i];
}
if (((lastTime - time[i]) >= shift + firstPeriod))
if (lastTime - time[i] >= shift + firstPeriod)
{
start = i;
@ -212,12 +217,12 @@ namespace KLHZ.Trader.Core.Declisions.Utils
{
var diff1 = prices[bound] - prices[start];
var diff2 = prices[end] - prices[bound];
res = new TwoPeriodsProcessingDto(true, diff1, diff2, start, bound, end, time[bound] - time[start], time[end] - time[bound]);
res = new TwoPeriodsResultDto(true, diff1, diff2, start, bound, end, time[bound] - time[start], time[end] - time[bound]);
}
return res;
}
internal static bool CheckLongClose(this IPriceHistoryCacheUnit unit, TimeSpan firstPeriod, TimeSpan secondPeriod, float meanfullDiff, int pointsStart, int pointsEnd)
public static bool CheckLongClose(this IPriceHistoryCacheUnit unit, TimeSpan firstPeriod, TimeSpan secondPeriod, float meanfullDiff, int pointsStart, int pointsEnd)
{
var data = unit.GetData().Result;
var periodStat = data.GetTwoPeriodsProcessingData(secondPeriod, pointsStart, pointsEnd, firstPeriod, meanfullDiff);
@ -239,7 +244,7 @@ namespace KLHZ.Trader.Core.Declisions.Utils
{
}
return isStartOk && isEndOk && (data.prices[periodStat.End] - data.prices[periodStat.Start] >= meanfullDiff);
return isStartOk && isEndOk && data.prices[periodStat.End] - data.prices[periodStat.Start] >= meanfullDiff;
}
internal static bool CheckUptrendStarting2(this IPriceHistoryCacheUnit unit, TimeSpan firstPeriod, TimeSpan secondPeriod, float meanfullDiff)
@ -293,7 +298,7 @@ namespace KLHZ.Trader.Core.Declisions.Utils
}
internal static bool CheckLongOpen(this IPriceHistoryCacheUnit unit, TimeSpan firstPeriod, TimeSpan secondPeriod, float meanfullDiff, int pointsStart, int pointsEnd)
public static bool CheckLongOpen(this IPriceHistoryCacheUnit unit, TimeSpan firstPeriod, TimeSpan secondPeriod, float meanfullDiff, int pointsStart, int pointsEnd)
{
var data = unit.GetData().Result;
var periodStat = data.GetTwoPeriodsProcessingData(secondPeriod, pointsStart, pointsEnd, firstPeriod, meanfullDiff);
@ -315,7 +320,46 @@ namespace KLHZ.Trader.Core.Declisions.Utils
{
}
return isStartOk && isEndOk && (data.prices[periodStat.Start] - data.prices[periodStat.End] >= meanfullDiff);
return isStartOk && isEndOk && data.prices[periodStat.Start] - data.prices[periodStat.End] >= meanfullDiff;
}
public static TradingEvent Detect(IPriceHistoryCacheUnit data)
{
float meanfullDiff;
if (data.Figi == "BBG004730N88")
{
meanfullDiff = 0.05f;
}
else if (data.Figi == "FUTIMOEXF000")
{
meanfullDiff = 1f;
}
else
{
return TradingEvent.None;
}
var res = TradingEvent.None;
//var downtrendStarts = data.CheckDowntrendStarting(TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(7), meanfullDiff);
var uptrendStarts = data.CheckLongOpen(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(7), meanfullDiff, 8, 3);
var uptrendStarts2 = data.CheckLongOpen(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(3), meanfullDiff, 15, 2);
var downtrendEnds = data.CheckLongOpen(TimeSpan.FromSeconds(120), TimeSpan.FromSeconds(10), meanfullDiff, 15, 5);
uptrendStarts |= downtrendEnds;
uptrendStarts |= uptrendStarts2;
if (uptrendStarts)
{
res |= TradingEvent.LongClose;
}
//var downtrendEnds = data.CheckDowntrendEnding(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(15), meanfullDiff);
var uptrendEnds = data.CheckLongClose(TimeSpan.FromSeconds(15), TimeSpan.FromSeconds(20), meanfullDiff * 1.5f, 8, 8);
var uptrendEnds2 = data.CheckLongClose(TimeSpan.FromSeconds(120), TimeSpan.FromSeconds(30), meanfullDiff, 15, 8);
uptrendEnds |= uptrendEnds2;
if (uptrendEnds)
{
res |= TradingEvent.LongOpen;
}
return res;
}
}
}

View File

@ -7,8 +7,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MathNet.Filtering.Kalman" Version="0.7.0" />
<PackageReference Include="MathNet.Numerics" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.8" />
</ItemGroup>

View File

@ -1,5 +1,5 @@
using KLHZ.Trader.Core.DataLayer.Entities.Prices;
using KLHZ.Trader.Core.Math.Declisions.Services;
using KLHZ.Trader.Core.Math.Declisions.Services.Cache;
namespace KLHZ.Trader.Core.Tests
{
@ -208,7 +208,7 @@ namespace KLHZ.Trader.Core.Tests
public void Test9()
{
var cacheUnit = new PriceHistoryCacheUnit2("");
for(int i= 0; i < 5*PriceHistoryCacheUnit2.CacheMaxLength; i++)
for (int i = 0; i < 5 * PriceHistoryCacheUnit2.CacheMaxLength; i++)
{
cacheUnit.AddData(new PriceChange() { Figi = "", Ticker = "", Value = i, Time = DateTime.UtcNow });
if (i >= 500)

View File

@ -1,5 +1,5 @@
using KLHZ.Trader.Core.DataLayer.Entities.Prices;
using KLHZ.Trader.Core.Math.Declisions.Services;
using KLHZ.Trader.Core.Math.Declisions.Services.Cache;
namespace KLHZ.Trader.Core.Tests
{

View File

@ -1,6 +1,6 @@
using KLHZ.Trader.Core.DataLayer.Entities.Prices;
using KLHZ.Trader.Core.Declisions.Utils;
using KLHZ.Trader.Core.Math.Declisions.Services;
using KLHZ.Trader.Core.Math.Declisions.Services.Cache;
using KLHZ.Trader.Core.Math.Declisions.Utils;
namespace KLHZ.Trader.Core.Tests
{
@ -45,7 +45,7 @@ namespace KLHZ.Trader.Core.Tests
var periodLength = 4;
var shift = 0;
var result = HistoryProcessingInstruments.GetPriceDiffForTimeSpan(unit, TimeSpan.FromSeconds(shift), TimeSpan.FromSeconds(periodLength));
var result = TwoPeriods.GetPriceDiffForTimeSpan(unit, TimeSpan.FromSeconds(shift), TimeSpan.FromSeconds(periodLength));
var maxValue = startValue + (step > 0 ? (step * count) - step * shift : (step * count) - (step * (shift + periodLength)));
var minValue = startValue + (step > 0 ? (step * count) - (step * (shift + periodLength)) : (step * count) - step * shift);
@ -79,7 +79,7 @@ namespace KLHZ.Trader.Core.Tests
var periodLength = 4;
var shift = 0;
var result = HistoryProcessingInstruments.GetPriceDiffForTimeSpan(unit, TimeSpan.FromSeconds(shift), TimeSpan.FromSeconds(periodLength));
var result = TwoPeriods.GetPriceDiffForTimeSpan(unit, TimeSpan.FromSeconds(shift), TimeSpan.FromSeconds(periodLength));
var maxValue = startValue + (step > 0 ? (step * count) - step * shift : (step * count) - (step * (shift + periodLength)));
var minValue = startValue + (step > 0 ? (step * count) - (step * (shift + periodLength)) : (step * count) - step * shift);
@ -114,7 +114,7 @@ namespace KLHZ.Trader.Core.Tests
var periodLength = 4;
var shift = 1;
var result = HistoryProcessingInstruments.GetPriceDiffForTimeSpan(unit, TimeSpan.FromSeconds(shift), TimeSpan.FromSeconds(periodLength));
var result = TwoPeriods.GetPriceDiffForTimeSpan(unit, TimeSpan.FromSeconds(shift), TimeSpan.FromSeconds(periodLength));
var maxValue = startValue + (step > 0 ? (step * count) - step * shift : (step * count) - (step * (shift + periodLength)));
var minValue = startValue + (step > 0 ? (step * count) - (step * (shift + periodLength)) : (step * count) - step * shift);
@ -149,7 +149,7 @@ namespace KLHZ.Trader.Core.Tests
var periodLength = 4;
var shift = 0;
var result = HistoryProcessingInstruments.GetPriceDiffForTimeSpan(unit, TimeSpan.FromSeconds(shift), TimeSpan.FromSeconds(periodLength));
var result = TwoPeriods.GetPriceDiffForTimeSpan(unit, TimeSpan.FromSeconds(shift), TimeSpan.FromSeconds(periodLength));
var maxValue = startValue + (step > 0 ? (step * count) - step * shift : (step * count) - (step * (shift + periodLength)));
var minValue = startValue + (step > 0 ? (step * count) - (step * (shift + periodLength)) : (step * count) - step * shift);
@ -184,7 +184,7 @@ namespace KLHZ.Trader.Core.Tests
var periodLength = 4;
var shift = 3;
var result = HistoryProcessingInstruments.GetPriceDiffForTimeSpan(unit, TimeSpan.FromSeconds(shift), TimeSpan.FromSeconds(periodLength));
var result = TwoPeriods.GetPriceDiffForTimeSpan(unit, TimeSpan.FromSeconds(shift), TimeSpan.FromSeconds(periodLength));
var maxValue = startValue + (step > 0 ? (step * count) - step * shift : (step * count) - (step * (shift + periodLength)));
var minValue = startValue + (step > 0 ? (step * count) - (step * (shift + periodLength)) : (step * count) - step * shift);
@ -219,7 +219,7 @@ namespace KLHZ.Trader.Core.Tests
var periodLength = 4;
var shift = 3;
var result = HistoryProcessingInstruments.GetPriceDiffForTimeSpan(unit, TimeSpan.FromSeconds(shift), TimeSpan.FromSeconds(periodLength));
var result = TwoPeriods.GetPriceDiffForTimeSpan(unit, TimeSpan.FromSeconds(shift), TimeSpan.FromSeconds(periodLength));
var maxValue1 = startValue + (step > 0 ? (step * count) - step * shift : (step * count) - (step * (shift + periodLength)));
var minValue1 = startValue + (step > 0 ? (step * count) - (step * (shift + periodLength)) : (step * count) - step * shift);
@ -260,7 +260,7 @@ namespace KLHZ.Trader.Core.Tests
}
var result2 = HistoryProcessingInstruments.GetPriceDiffForTimeSpan(unit2, TimeSpan.FromSeconds(shift), TimeSpan.FromSeconds(periodLength));
var result2 = TwoPeriods.GetPriceDiffForTimeSpan(unit2, TimeSpan.FromSeconds(shift), TimeSpan.FromSeconds(periodLength));
var maxValue2 = 100;
var minValue2 = -100;

View File

@ -1,3 +1,5 @@
using KLHZ.Trader.Core.Math.Common;
namespace KLHZ.Trader.Core.Tests
{
public class LinesProcessingTest
@ -14,7 +16,7 @@ namespace KLHZ.Trader.Core.Tests
var val2_1 = -0.5f;
var val2_2 = 0.5f;
Assert.IsTrue(KLHZ.Trader.Core.Math.Utils.Lines.IsLinesCrossing(time1, time2, val1_1, val1_2, val2_1, val2_2));
Assert.IsTrue(Lines.IsLinesCrossing(time1, time2, val1_1, val1_2, val2_1, val2_2));
}
[Test]
@ -29,7 +31,7 @@ namespace KLHZ.Trader.Core.Tests
var val2_1 = 0.5f;
var val2_2 = -0.5f;
Assert.IsFalse(KLHZ.Trader.Core.Math.Utils.Lines.IsLinesCrossing(time1, time2, val1_1, val1_2, val2_1, val2_2));
Assert.IsFalse(Lines.IsLinesCrossing(time1, time2, val1_1, val1_2, val2_1, val2_2));
}
}
}

View File

@ -1,5 +1,5 @@
using KLHZ.Trader.Core.Contracts.Messaging.Dtos;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces;
using KLHZ.Trader.Core.Contracts.Messaging.Interfaces;
using System.Collections.Concurrent;
using System.Threading.Channels;
@ -8,6 +8,7 @@ namespace KLHZ.Trader.Core.Common.Messaging.Services
{
public class DataBus : IDataBus
{
private readonly ConcurrentDictionary<string, Channel<IOrderbook>> _orderbooksChannels = new();
private readonly ConcurrentDictionary<string, Channel<IMessage>> _messagesChannels = new();
private readonly ConcurrentDictionary<string, Channel<INewCandle>> _candlesChannels = new();
private readonly ConcurrentDictionary<string, Channel<INewPrice>> _priceChannels = new();
@ -39,7 +40,12 @@ namespace KLHZ.Trader.Core.Common.Messaging.Services
return _commandChannels.TryAdd(key, channel);
}
public async Task BroadcastNewPrice(INewPrice newPriceMessage)
public bool AddChannel(string key, Channel<IOrderbook> channel)
{
return _orderbooksChannels.TryAdd(key, channel);
}
public async Task Broadcast(INewPrice newPriceMessage)
{
foreach (var channel in _priceChannels.Values)
{
@ -47,7 +53,7 @@ namespace KLHZ.Trader.Core.Common.Messaging.Services
}
}
public async Task BroadcastProcessedPrice(IProcessedPrice mess)
public async Task Broadcast(IProcessedPrice mess)
{
foreach (var channel in _processedPricesChannels.Values)
{
@ -55,7 +61,7 @@ namespace KLHZ.Trader.Core.Common.Messaging.Services
}
}
public async Task BroadcastNewCandle(INewCandle newPriceMessage)
public async Task Broadcast(INewCandle newPriceMessage)
{
foreach (var channel in _candlesChannels.Values)
{
@ -63,12 +69,20 @@ namespace KLHZ.Trader.Core.Common.Messaging.Services
}
}
public async Task BroadcastCommand(TradeCommand command)
public async Task Broadcast(TradeCommand command)
{
foreach (var channel in _commandChannels.Values)
{
await channel.Writer.WriteAsync(command);
}
}
public async Task Broadcast(IOrderbook orderbook)
{
foreach (var channel in _orderbooksChannels.Values)
{
await channel.Writer.WriteAsync(orderbook);
}
}
}
}

View File

@ -1,4 +1,4 @@
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces;
using KLHZ.Trader.Core.Contracts.Messaging.Interfaces;
using KLHZ.Trader.Core.DataLayer;
using Microsoft.EntityFrameworkCore;
@ -6,7 +6,7 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Threading.Channels;
namespace KLHZ.Trader.Core.Declisions.Services
namespace KLHZ.Trader.Core.Common.Messaging.Services
{
public class ProcessedPricesLogger : IHostedService
{
@ -44,7 +44,7 @@ namespace KLHZ.Trader.Core.Declisions.Services
Value = message.Value,
});
if (buffer.Count > 10000 || (DateTime.UtcNow - lastWrite) > TimeSpan.FromSeconds(5) || _channel.Reader.Count == 0)
if (buffer.Count > 10000 || DateTime.UtcNow - lastWrite > TimeSpan.FromSeconds(5) || _channel.Reader.Count == 0)
{
lastWrite = DateTime.UtcNow;
using var context = await _dbContextFactory.CreateDbContextAsync();

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations.Schema;
using KLHZ.Trader.Core.DataLayer.Entities.Declisions.Enums;
using System.ComponentModel.DataAnnotations.Schema;
namespace KLHZ.Trader.Core.DataLayer.Entities.Declisions
{

View File

@ -1,4 +1,4 @@
namespace KLHZ.Trader.Core.DataLayer.Entities.Declisions
namespace KLHZ.Trader.Core.DataLayer.Entities.Declisions.Enums
{
public enum DeclisionTradeAction
{

View File

@ -0,0 +1,11 @@
namespace KLHZ.Trader.Core.DataLayer.Entities.Orders.Enums
{
public enum OrderbookItemType
{
Unknown = 0,
Ask = 1,
Bid = 2,
AsksSummary = 3,
BidsSummary = 4
}
}

View File

@ -0,0 +1,32 @@
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces;
using KLHZ.Trader.Core.DataLayer.Entities.Orders.Enums;
using System.ComponentModel.DataAnnotations.Schema;
namespace KLHZ.Trader.Core.DataLayer.Entities.Orders
{
[Table("orderbook_items")]
public class OrderbookItem : IOrderbookItem
{
[Column("id")]
public long Id { get; set; }
[Column("time")]
public DateTime Time { get; set; }
[Column("price")]
public decimal Price { get; set; }
[Column("count")]
public long Count { get; set; }
[Column("figi")]
public required string Figi { get; set; }
[Column("ticker")]
public required string Ticker { get; set; }
[Column("item_type")]
public OrderbookItemType ItemType { get; set; }
}
}

View File

@ -1,4 +1,4 @@
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces;
using System.ComponentModel.DataAnnotations.Schema;
namespace KLHZ.Trader.Core.DataLayer.Entities.Prices

View File

@ -1,4 +1,4 @@
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces;
using System.ComponentModel.DataAnnotations.Schema;
namespace KLHZ.Trader.Core.DataLayer.Entities.Prices

View File

@ -1,4 +1,4 @@
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces;
using System.ComponentModel.DataAnnotations.Schema;
namespace KLHZ.Trader.Core.DataLayer.Entities.Prices

View File

@ -1,4 +1,4 @@
namespace KLHZ.Trader.Core.DataLayer.Entities.Trades
namespace KLHZ.Trader.Core.DataLayer.Entities.Trades.Enums
{
public enum AssetType
{

View File

@ -1,4 +1,4 @@
namespace KLHZ.Trader.Core.DataLayer.Entities.Trades
namespace KLHZ.Trader.Core.DataLayer.Entities.Trades.Enums
{
public enum PositionType
{

View File

@ -1,4 +1,4 @@
namespace KLHZ.Trader.Core.DataLayer.Entities.Trades
namespace KLHZ.Trader.Core.DataLayer.Entities.Trades.Enums
{
public enum TradeDirection
{

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations.Schema;
using KLHZ.Trader.Core.DataLayer.Entities.Trades.Enums;
using System.ComponentModel.DataAnnotations.Schema;
namespace KLHZ.Trader.Core.DataLayer.Entities.Trades
{

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations.Schema;
using KLHZ.Trader.Core.DataLayer.Entities.Trades.Enums;
using System.ComponentModel.DataAnnotations.Schema;
namespace KLHZ.Trader.Core.DataLayer.Entities.Trades
{

View File

@ -1,4 +1,5 @@
using KLHZ.Trader.Core.DataLayer.Entities.Declisions;
using KLHZ.Trader.Core.DataLayer.Entities.Orders;
using KLHZ.Trader.Core.DataLayer.Entities.Prices;
using KLHZ.Trader.Core.DataLayer.Entities.Trades;
using Microsoft.EntityFrameworkCore;
@ -14,6 +15,7 @@ namespace KLHZ.Trader.Core.DataLayer
public DbSet<PriceChange> PriceChanges { get; set; }
public DbSet<ProcessedPrice> ProcessedPrices { get; set; }
public DbSet<Candle> Candles { get; set; }
public DbSet<OrderbookItem> OrderbookItems { get; set; }
public TraderDbContext(DbContextOptions<TraderDbContext> options)
: base(options)
{
@ -60,6 +62,15 @@ namespace KLHZ.Trader.Core.DataLayer
v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
});
modelBuilder.Entity<OrderbookItem>(entity =>
{
entity.HasKey(e1 => e1.Id);
entity.Property(e => e.Time)
.HasConversion(
v => v.ToUniversalTime(),
v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
});
modelBuilder.Entity<ProcessedPrice>(entity =>
{
entity.HasKey(e1 => e1.Id);

View File

@ -1,41 +0,0 @@
using KLHZ.Trader.Core.Contracts.Declisions.Dtos;
using KLHZ.Trader.Core.Contracts.Declisions.Interfaces;
using KLHZ.Trader.Core.Declisions.Utils;
namespace KLHZ.Trader.Core.Declisions.Services
{
public class TradingEventsDetector : ITradingEventsDetector
{
public async ValueTask<TradingEventsDto> Detect(IPriceHistoryCacheUnit data)
{
await Task.Delay(0);
float meanfullDiff;
if (data.Figi == "BBG004730N88")
{
meanfullDiff = 0.05f;
}
else if (data.Figi == "FUTIMOEXF000")
{
meanfullDiff = 1f;
}
else
{
return TradingEventsDto.Empty;
}
//var downtrendStarts = data.CheckDowntrendStarting(TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(7), meanfullDiff);
var uptrendStarts = data.CheckLongOpen(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(7), meanfullDiff, 8, 3);
var uptrendStarts2 = data.CheckLongOpen(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(3), meanfullDiff, 15, 2);
var downtrendEnds = data.CheckLongOpen(TimeSpan.FromSeconds(120), TimeSpan.FromSeconds(10), meanfullDiff, 15, 5);
uptrendStarts |= downtrendEnds;
uptrendStarts |= uptrendStarts2;
//var downtrendEnds = data.CheckDowntrendEnding(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(15), meanfullDiff);
var uptrendEnds = data.CheckLongClose(TimeSpan.FromSeconds(15), TimeSpan.FromSeconds(20), meanfullDiff * 1.5f, 8, 8);
var uptrendEnds2 = data.CheckLongClose(TimeSpan.FromSeconds(120), TimeSpan.FromSeconds(30), meanfullDiff, 15, 8);
uptrendEnds |= uptrendEnds2;
return new TradingEventsDto(uptrendEnds, uptrendStarts);
}
}
}

View File

@ -8,7 +8,8 @@
public decimal AccountCashPart { get; set; }
public decimal AccountCashPartFutures { get; set; }
public decimal DefaultBuyPartOfAccount { get; set; }
public string[] AllowedInstrumentsFigis { get; set; } = [];
public string[] DataRecievingInstrumentsFigis { get; set; } = [];
public string[] TradingInstrumentsFigis { get; set; } = [];
public string[] ManagingAccountNamePatterns { get; set; } = [];
}
}

View File

@ -1,6 +1,8 @@
using Grpc.Core;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos;
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.DataLayer.Entities.Trades;
using KLHZ.Trader.Core.Exchange.Extentions;
@ -33,7 +35,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
_eventBus = eventBus;
_dbContextFactory = dbContextFactory;
_investApiClient = investApiClient;
_instrumentsFigis = options.Value.AllowedInstrumentsFigis.ToArray();
_instrumentsFigis = options.Value.DataRecievingInstrumentsFigis.ToArray();
_logger = logger;
_managedAccountNamePatterns = options.Value.ManagingAccountNamePatterns.ToArray();
}
@ -51,7 +53,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
var shares = await _investApiClient.Instruments.SharesAsync();
foreach (var share in shares.Instruments)
{
//if (_instrumentsFigis.Contains(share.Figi))
if (_instrumentsFigis.Contains(share.Figi))
{
_tickersCache.TryAdd(share.Figi, share.Ticker);
}
@ -59,7 +61,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
var futures = await _investApiClient.Instruments.FuturesAsync();
foreach (var future in futures.Instruments)
{
//if (_instrumentsFigis.Contains(future.Figi))
if (_instrumentsFigis.Contains(future.Figi))
{
_tickersCache.TryAdd(future.Figi, future.Ticker);
}
@ -100,6 +102,11 @@ namespace KLHZ.Trader.Core.Exchange.Services
SubscriptionAction = SubscriptionAction.Subscribe
};
var bookRequest = new SubscribeOrderBookRequest
{
SubscriptionAction = SubscriptionAction.Subscribe
};
foreach (var f in _instrumentsFigis)
{
request.Instruments.Add(
@ -113,24 +120,31 @@ namespace KLHZ.Trader.Core.Exchange.Services
{
InstrumentId = f
});
bookRequest.Instruments.Add(
new OrderBookInstrument()
{
InstrumentId = f,
Depth = 10
});
}
await stream.RequestStream.WriteAsync(new MarketDataRequest
{
SubscribeLastPriceRequest = request,
});
await stream.RequestStream.WriteAsync(new MarketDataRequest
{
SubscribeTradesRequest = tradesRequest,
SubscribeOrderBookRequest = bookRequest
});
using var context = await _dbContextFactory.CreateDbContextAsync();
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var pricesBuffer = new List<PriceChange>();
var orderbookItemsBuffer = new List<OrderbookItem>();
var tradesBuffer = new List<InstrumentTrade>();
var lastWritePrices = DateTime.UtcNow;
var lastWriteOrderbooks = DateTime.UtcNow;
var lastWriteTrades = DateTime.UtcNow;
var lastWritePrices = DateTime.UtcNow;
var lastWrite = DateTime.UtcNow;
await foreach (var response in stream.ResponseStream.ReadAllAsync())
{
if (response.LastPrice != null)
@ -143,7 +157,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
Value = response.LastPrice.Price,
IsHistoricalData = false,
};
await _eventBus.BroadcastNewPrice(message);
await _eventBus.Broadcast(message);
pricesBuffer.Add(message);
}
@ -156,24 +170,62 @@ namespace KLHZ.Trader.Core.Exchange.Services
Ticker = GetTickerByFigi(response.Trade.Figi),
Price = response.Trade.Price,
Count = response.Trade.Quantity,
Direction = response.Trade.Direction == Tinkoff.InvestApi.V1.TradeDirection.Sell ? KLHZ.Trader.Core.DataLayer.Entities.Trades.TradeDirection.Sell : KLHZ.Trader.Core.DataLayer.Entities.Trades.TradeDirection.Buy,
Direction = response.Trade.Direction == Tinkoff.InvestApi.V1.TradeDirection.Sell ? DataLayer.Entities.Trades.Enums.TradeDirection.Sell : DataLayer.Entities.Trades.Enums.TradeDirection.Buy,
};
tradesBuffer.Add(trade);
}
//if (pricesBuffer.Count > 200 || (DateTime.UtcNow - lastWritePrices).TotalSeconds > 10)
if (response.Orderbook != null)
{
lastWritePrices = DateTime.UtcNow;
await context.PriceChanges.AddRangeAsync(pricesBuffer);
pricesBuffer.Clear();
await context.SaveChangesAsync();
var asksSummary = new OrderbookItem()
{
Figi = response.Orderbook.Figi,
Ticker = GetTickerByFigi(response.Orderbook.Figi),
Count = response.Orderbook.Asks.Sum(a => (int)a.Quantity),
ItemType = DataLayer.Entities.Orders.Enums.OrderbookItemType.AsksSummary,
Time = response.Orderbook.Time.ToDateTime().ToUniversalTime(),
};
var bidsSummary = new OrderbookItem()
{
Figi = response.Orderbook.Figi,
Ticker = GetTickerByFigi(response.Orderbook.Figi),
Count = response.Orderbook.Bids.Sum(a => (int)a.Quantity),
ItemType = DataLayer.Entities.Orders.Enums.OrderbookItemType.BidsSummary,
Time = response.Orderbook.Time.ToDateTime().ToUniversalTime(),
};
orderbookItemsBuffer.Add(asksSummary);
orderbookItemsBuffer.Add(bidsSummary);
var message = new NewOrderbookMessage()
{
Ticker = GetTickerByFigi(response.Orderbook.Figi),
Figi = response.Orderbook.Figi,
Time = response.Orderbook.Time.ToDateTime().ToUniversalTime(),
AsksCount = asksSummary.Count,
BidsCount = bidsSummary.Count,
};
await _eventBus.Broadcast(message);
}
//if (tradesBuffer.Count > 200 || (DateTime.UtcNow - lastWriteTrades).TotalSeconds > 10)
if (orderbookItemsBuffer.Count + pricesBuffer.Count + tradesBuffer.Count > 1000 || (DateTime.UtcNow - lastWrite).TotalSeconds > 10)
{
lastWriteTrades = DateTime.UtcNow;
await context.InstrumentTrades.AddRangeAsync(tradesBuffer);
tradesBuffer.Clear();
lastWrite = DateTime.UtcNow;
if (orderbookItemsBuffer.Count > 0)
{
await context.OrderbookItems.AddRangeAsync(orderbookItemsBuffer);
orderbookItemsBuffer.Clear();
}
if (pricesBuffer.Count > 0)
{
await context.PriceChanges.AddRangeAsync(pricesBuffer);
pricesBuffer.Clear();
}
if (tradesBuffer.Count > 0)
{
await context.InstrumentTrades.AddRangeAsync(tradesBuffer);
tradesBuffer.Clear();
}
await context.SaveChangesAsync();
}
}

View File

@ -236,9 +236,9 @@ namespace KLHZ.Trader.Core.Exchange.Services
BoughtAt = DateTime.UtcNow,
Count = res.LotsExecuted,
Price = res.ExecutedOrderPrice,
Position = DataLayer.Entities.Trades.PositionType.Long,
Direction = DataLayer.Entities.Trades.TradeDirection.Buy,
Asset = DataLayer.Entities.Trades.AssetType.Common,
Position = DataLayer.Entities.Trades.Enums.PositionType.Long,
Direction = DataLayer.Entities.Trades.Enums.TradeDirection.Buy,
Asset = DataLayer.Entities.Trades.Enums.AssetType.Common,
};
await context.Trades.AddAsync(newTrade);

View File

@ -1,25 +1,26 @@
using KLHZ.Trader.Core.Common;
using KLHZ.Trader.Core.Contracts.Declisions.Dtos.Enums;
using KLHZ.Trader.Core.Contracts.Declisions.Interfaces;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Enums;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces;
using KLHZ.Trader.Core.Contracts.Messaging.Interfaces;
using KLHZ.Trader.Core.DataLayer;
using KLHZ.Trader.Core.DataLayer.Entities.Declisions;
using KLHZ.Trader.Core.Exchange;
using KLHZ.Trader.Core.DataLayer.Entities.Declisions.Enums;
using KLHZ.Trader.Core.Exchange.Extentions;
using KLHZ.Trader.Core.Exchange.Models;
using KLHZ.Trader.Core.Exchange.Services;
using KLHZ.Trader.Core.Math.Declisions.Services;
using KLHZ.Trader.Core.Math.Declisions.Services.Cache;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Collections.Concurrent;
using System.Threading.Channels;
using Tinkoff.InvestApi;
using AssetType = KLHZ.Trader.Core.Exchange.Models.AssetType;
namespace KLHZ.Trader.Core.Declisions.Services
namespace KLHZ.Trader.Core.Exchange.Services
{
public class Trader : IHostedService
{
@ -31,6 +32,7 @@ namespace KLHZ.Trader.Core.Declisions.Services
private readonly ConcurrentDictionary<string, ManagedAccount> Accounts = new();
private readonly ConcurrentDictionary<string, IPriceHistoryCacheUnit> _historyCash = new();
private readonly ITradingEventsDetector _tradingEventsDetector;
private readonly ILogger<Trader> _logger;
private readonly decimal _futureComission;
@ -41,8 +43,10 @@ namespace KLHZ.Trader.Core.Declisions.Services
private readonly string[] _managedAccountsNamePatterns = [];
private readonly Channel<INewPrice> _pricesChannel = Channel.CreateUnbounded<INewPrice>();
private readonly Channel<IOrderbook> _ordersbookChannel = Channel.CreateUnbounded<IOrderbook>();
public Trader(
ILogger<Trader> logger,
ITradingEventsDetector tradingEventsDetector,
BotModeSwitcher botModeSwitcher,
IServiceProvider provider,
@ -51,6 +55,7 @@ namespace KLHZ.Trader.Core.Declisions.Services
IDbContextFactory<TraderDbContext> dbContextFactory,
InvestApiClient investApiClient)
{
_logger = logger;
_tradingEventsDetector = tradingEventsDetector;
_botModeSwitcher = botModeSwitcher;
_dataBus = dataBus;
@ -86,10 +91,12 @@ namespace KLHZ.Trader.Core.Declisions.Services
}
_dataBus.AddChannel(nameof(Trader), _pricesChannel);
_ = ProcessMessages();
_dataBus.AddChannel(nameof(Trader), _ordersbookChannel);
_ = ProcessPrices();
_ = ProcessOrdersbooks();
}
private async Task ProcessMessages()
private async Task ProcessPrices()
{
while (await _pricesChannel.Reader.WaitToReadAsync())
{
@ -107,7 +114,7 @@ namespace KLHZ.Trader.Core.Declisions.Services
try
{
if (result.LongOpen)
if ((result & TradingEvent.LongOpen) == TradingEvent.LongOpen)
{
using var context = await _dbContextFactory.CreateDbContextAsync();
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
@ -122,7 +129,7 @@ namespace KLHZ.Trader.Core.Declisions.Services
});
await context.SaveChangesAsync();
}
if (result.LongClose)
if ((result & TradingEvent.LongClose) == TradingEvent.LongClose)
{
using var context = await _dbContextFactory.CreateDbContextAsync();
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
@ -145,6 +152,20 @@ namespace KLHZ.Trader.Core.Declisions.Services
}
}
private async Task ProcessOrdersbooks()
{
while (await _ordersbookChannel.Reader.WaitToReadAsync())
{
var message = await _ordersbookChannel.Reader.ReadAsync();
if (!_historyCash.TryGetValue(message.Figi, out var data))
{
data = new PriceHistoryCacheUnit2(message.Figi);
_historyCash.TryAdd(message.Figi, data);
}
await data.AddOrderbook(message);
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;

View File

@ -82,7 +82,7 @@ namespace KLHZ.Trader.Core.TG.Services
RecomendPrice = null,
Figi = "BBG004730N88",
};
await _eventBus.BroadcastCommand(command);
await _eventBus.Broadcast(command);
break;
}
case "продать сбер":
@ -95,7 +95,7 @@ namespace KLHZ.Trader.Core.TG.Services
Count = 1,
LotsCount = 1,
};
await _eventBus.BroadcastCommand(command);
await _eventBus.Broadcast(command);
break;
}
case "купить сбер":
@ -107,7 +107,7 @@ namespace KLHZ.Trader.Core.TG.Services
Figi = "BBG004730N88",
Count = 1
};
await _eventBus.BroadcastCommand(command);
await _eventBus.Broadcast(command);
break;
}
}

View File

@ -1,4 +1,4 @@
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces;
using KLHZ.Trader.Core.Contracts.Messaging.Interfaces;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;

View File

@ -0,0 +1,14 @@
drop table if exists orderbook_items;
create table orderbook_items
(
id bigserial,
time timestamp default current_timestamp,
figi text not null,
ticker text not null,
price decimal not null,
count bigint not null,
item_type int not null default 1,
primary key (id)
);
CREATE INDEX orderbook_items_index ON orderbook_items USING btree(figi, time, item_type);

View File

@ -0,0 +1,66 @@
using Grpc.Core;
using Microsoft.AspNetCore.Mvc;
using Tinkoff.InvestApi;
using Tinkoff.InvestApi.V1;
namespace KLHZ.Trader.Service.Controllers
{
[ApiController]
[Route("[controller]/[action]")]
public class ExchangeDataController : ControllerBase
{
private readonly InvestApiClient _investApiClient;
public ExchangeDataController(InvestApiClient investApiClient)
{
_investApiClient = investApiClient;
}
[HttpGet]
public async Task<string?> GetFigi([FromQuery] string ticker, [FromQuery] string? classCode = "TQBR")
{
var req = new InstrumentRequest()
{
ClassCode = classCode,
IdType = InstrumentIdType.Ticker,
Id = ticker,
};
try
{
var res = await _investApiClient.Instruments.GetInstrumentByAsync(req);
return res.Instrument.Figi;
}
catch (RpcException ex)
{
if (ex.StatusCode == Grpc.Core.StatusCode.NotFound && classCode == "TQBR")
{
return await GetFigi(ticker, "SPBFUT");
}
}
return null;
}
[HttpGet]
public async Task<string?> GetTicker([FromQuery] string figi)
{
var req = new InstrumentRequest()
{
ClassCode = "figi",
IdType = InstrumentIdType.Figi,
Id = figi,
};
try
{
var res = await _investApiClient.Instruments.GetInstrumentByAsync(req);
return res.Instrument.Ticker;
}
catch (Exception ex)
{
}
return null;
}
}
}

View File

@ -41,7 +41,7 @@ namespace KLHZ.Trader.Service.Controllers
foreach (var mess in data)
{
await _dataBus.BroadcastNewPrice(mess);
await _dataBus.Broadcast(mess);
}
}
catch (Exception ex)

View File

@ -3,9 +3,9 @@ using KLHZ.Trader.Core.Common.Messaging.Services;
using KLHZ.Trader.Core.Contracts.Declisions.Interfaces;
using KLHZ.Trader.Core.Contracts.Messaging.Interfaces;
using KLHZ.Trader.Core.DataLayer;
using KLHZ.Trader.Core.Declisions.Services;
using KLHZ.Trader.Core.Exchange;
using KLHZ.Trader.Core.Exchange.Services;
using KLHZ.Trader.Core.Math.Declisions.Services.EventsDetection;
using KLHZ.Trader.Core.TG;
using KLHZ.Trader.Core.TG.Services;
using KLHZ.Trader.Service.Infrastructure;
@ -53,7 +53,7 @@ builder.Services.AddHostedService<Trader>();
builder.Services.AddSingleton<IUpdateHandler, BotMessagesHandler>();
builder.Services.AddSingleton<BotModeSwitcher>();
builder.Services.AddSingleton<IDataBus, DataBus>();
builder.Services.AddSingleton<ITradingEventsDetector, TradingEventsDetector>();
builder.Services.AddSingleton<ITradingEventsDetector, IntervalsTradingEventsDetector>();
for (int i = 0; i < 10; i++)
{

View File

@ -11,7 +11,8 @@
"ExchangeDataRecievingEnabled": true,
"Token": "",
"ManagingAccountNamePatterns": [ "автотрейд 1" ],
"AllowedInstrumentsFigis": [ "BBG004730N88", "FUTIMOEXF000" ],
"DataRecievingInstrumentsFigis": [ "BBG004730N88", "FUTIMOEXF000", "FUTGMKN09250", "FUTBR1025000" ],
"TradingInstrumentsFigis": [ "FUTIMOEXF000" ],
"FutureComission": 0.0025,
"ShareComission": 0.0004,
"AccountCashPart": 0.05,