рефакторинг + запись сделок по инструментам
test / deploy_trader_prod (push) Successful in 3m16s
Details
test / deploy_trader_prod (push) Successful in 3m16s
Details
parent
2d6bc10ed8
commit
cee02fe665
|
@ -5,7 +5,8 @@ namespace KLHZ.Trader.Core.Contracts.Declisions.Interfaces
|
||||||
public interface IPriceHistoryCacheUnit
|
public interface IPriceHistoryCacheUnit
|
||||||
{
|
{
|
||||||
public string Figi { get; }
|
public string Figi { get; }
|
||||||
public ValueTask AddData(INewPriceMessage priceChange);
|
public int Length { get; }
|
||||||
|
public ValueTask AddData(INewPrice priceChange);
|
||||||
|
|
||||||
public ValueTask<(DateTime[] timestamps, float[] prices)> GetData();
|
public ValueTask<(DateTime[] timestamps, float[] prices)> GetData();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces
|
namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces
|
||||||
{
|
{
|
||||||
public interface INewPriceMessage
|
public interface INewPrice
|
||||||
{
|
{
|
||||||
public bool IsHistoricalData { get; set; }
|
public bool IsHistoricalData { get; set; }
|
||||||
public decimal Value { get; set; }
|
public decimal Value { get; set; }
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces
|
||||||
|
{
|
||||||
|
public interface IProcessedPrice : INewPrice
|
||||||
|
{
|
||||||
|
public string Processor { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos
|
namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos
|
||||||
{
|
{
|
||||||
public class NewPriceMessage : INewPriceMessage
|
public class NewPriceMessage : INewPrice
|
||||||
{
|
{
|
||||||
public decimal Value { get; set; }
|
public decimal Value { get; set; }
|
||||||
public required string Figi { get; set; }
|
public required string Figi { get; set; }
|
||||||
|
|
|
@ -6,12 +6,14 @@ namespace KLHZ.Trader.Core.Contracts.Messaging.Interfaces
|
||||||
{
|
{
|
||||||
public interface IDataBus
|
public interface IDataBus
|
||||||
{
|
{
|
||||||
public bool AddChannel(string key, Channel<INewPriceMessage> 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<TradeCommand> channel);
|
||||||
public bool AddChannel(string key, Channel<IMessage> channel);
|
public bool AddChannel(string key, Channel<IMessage> channel);
|
||||||
public bool AddChannel(string key, Channel<INewCandle> channel);
|
public bool AddChannel(string key, Channel<INewCandle> channel);
|
||||||
public Task BroadcastNewPrice(INewPriceMessage newPriceMessage);
|
public Task BroadcastNewPrice(INewPrice newPriceMessage);
|
||||||
public Task BroadcastCommand(TradeCommand command);
|
public Task BroadcastCommand(TradeCommand command);
|
||||||
public Task BroadcastNewCandle(INewCandle command);
|
public Task BroadcastNewCandle(INewCandle command);
|
||||||
|
public Task BroadcastProcessedPrice(IProcessedPrice command);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces;
|
||||||
|
|
||||||
|
namespace KLHZ.Trader.Core.Math.Declisions.Dtos
|
||||||
|
{
|
||||||
|
public class ProcessedPrice : IProcessedPrice
|
||||||
|
{
|
||||||
|
public required string Processor { get; set; }
|
||||||
|
public bool IsHistoricalData { get; set; }
|
||||||
|
public decimal Value { get; set; }
|
||||||
|
public required string Figi { get; set; }
|
||||||
|
public required string Ticker { get; set; }
|
||||||
|
public DateTime Time { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,191 @@
|
||||||
|
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces;
|
||||||
|
using KLHZ.Trader.Core.Contracts.Messaging.Interfaces;
|
||||||
|
using MathNet.Filtering.Kalman;
|
||||||
|
using MathNet.Numerics.LinearAlgebra;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
|
||||||
|
namespace KLHZ.Trader.Core.Math.Declisions.Dtos.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 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
|
||||||
|
{
|
||||||
|
//await ProcessCalman(message);
|
||||||
|
await ProcessMovAv(message, 3);
|
||||||
|
await ProcessTimeWindow(message, 5);
|
||||||
|
await ProcessTimeWindow(message, 15);
|
||||||
|
await ProcessTimeWindow(message, 120);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
using KLHZ.Trader.Core.Contracts.Declisions.Interfaces;
|
using KLHZ.Trader.Core.Contracts.Declisions.Interfaces;
|
||||||
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces;
|
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces;
|
||||||
|
|
||||||
namespace KLHZ.Trader.Core.Declisions.Services
|
namespace KLHZ.Trader.Core.Math.Declisions.Dtos.Services
|
||||||
{
|
{
|
||||||
public class PriceHistoryCacheUnit : IPriceHistoryCacheUnit
|
public class PriceHistoryCacheUnit : IPriceHistoryCacheUnit
|
||||||
{
|
{
|
||||||
|
@ -9,13 +9,24 @@ namespace KLHZ.Trader.Core.Declisions.Services
|
||||||
|
|
||||||
public string Figi { get; init; }
|
public string Figi { get; init; }
|
||||||
|
|
||||||
|
public int Length
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_locker)
|
||||||
|
{
|
||||||
|
return _length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private readonly object _locker = new();
|
private readonly object _locker = new();
|
||||||
private readonly float[] Prices = new float[ArrayMaxLength];
|
private readonly float[] Prices = new float[ArrayMaxLength];
|
||||||
private readonly DateTime[] Timestamps = new DateTime[ArrayMaxLength];
|
private readonly DateTime[] Timestamps = new DateTime[ArrayMaxLength];
|
||||||
|
|
||||||
private int Length = 0;
|
private int _length = 0;
|
||||||
|
|
||||||
public ValueTask AddData(INewPriceMessage priceChange)
|
public ValueTask AddData(INewPrice priceChange)
|
||||||
{
|
{
|
||||||
lock (_locker)
|
lock (_locker)
|
||||||
{
|
{
|
||||||
|
@ -25,9 +36,9 @@ namespace KLHZ.Trader.Core.Declisions.Services
|
||||||
Prices[Prices.Length - 1] = (float)priceChange.Value;
|
Prices[Prices.Length - 1] = (float)priceChange.Value;
|
||||||
Timestamps[Timestamps.Length - 1] = priceChange.Time;
|
Timestamps[Timestamps.Length - 1] = priceChange.Time;
|
||||||
|
|
||||||
if (Length < ArrayMaxLength)
|
if (_length < ArrayMaxLength)
|
||||||
{
|
{
|
||||||
Length++;
|
_length++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ValueTask.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
|
@ -35,17 +46,17 @@ namespace KLHZ.Trader.Core.Declisions.Services
|
||||||
|
|
||||||
public ValueTask<(DateTime[] timestamps, float[] prices)> GetData()
|
public ValueTask<(DateTime[] timestamps, float[] prices)> GetData()
|
||||||
{
|
{
|
||||||
var prices = new float[Length];
|
|
||||||
var timestamps = new DateTime[Length];
|
|
||||||
lock (_locker)
|
lock (_locker)
|
||||||
{
|
{
|
||||||
Array.Copy(Prices, Prices.Length - Length, prices, 0, prices.Length);
|
var prices = new float[_length];
|
||||||
Array.Copy(Timestamps, Prices.Length - Length, timestamps, 0, timestamps.Length);
|
var timestamps = new DateTime[_length];
|
||||||
|
Array.Copy(Prices, Prices.Length - _length, prices, 0, prices.Length);
|
||||||
|
Array.Copy(Timestamps, Prices.Length - _length, timestamps, 0, timestamps.Length);
|
||||||
return ValueTask.FromResult((timestamps, prices));
|
return ValueTask.FromResult((timestamps, prices));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public PriceHistoryCacheUnit(string figi, params INewPriceMessage[] priceChanges)
|
public PriceHistoryCacheUnit(string figi, params INewPrice[] priceChanges)
|
||||||
{
|
{
|
||||||
Figi = figi;
|
Figi = figi;
|
||||||
|
|
||||||
|
@ -69,7 +80,7 @@ namespace KLHZ.Trader.Core.Declisions.Services
|
||||||
Array.Copy(prices, 0, Prices, Prices.Length - prices.Length, prices.Length);
|
Array.Copy(prices, 0, Prices, Prices.Length - prices.Length, prices.Length);
|
||||||
Array.Copy(times, 0, Timestamps, Timestamps.Length - times.Length, times.Length);
|
Array.Copy(times, 0, Timestamps, Timestamps.Length - times.Length, times.Length);
|
||||||
|
|
||||||
Length = times.Length > ArrayMaxLength ? ArrayMaxLength : times.Length;
|
_length = times.Length > ArrayMaxLength ? ArrayMaxLength : times.Length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="MathNet.Filtering.Kalman" Version="0.7.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.8" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\KLHZ.Trader.Core.Contracts\KLHZ.Trader.Core.Contracts.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
|
@ -1,5 +1,4 @@
|
||||||
using KLHZ.Trader.Core.DataLayer.Entities.Prices;
|
using KLHZ.Trader.Core.DataLayer.Entities.Prices;
|
||||||
using KLHZ.Trader.Core.Declisions.Services;
|
|
||||||
|
|
||||||
namespace KLHZ.Trader.Core.Tests
|
namespace KLHZ.Trader.Core.Tests
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
using KLHZ.Trader.Core.DataLayer.Entities.Prices;
|
using KLHZ.Trader.Core.DataLayer.Entities.Prices;
|
||||||
using KLHZ.Trader.Core.Declisions.Services;
|
|
||||||
using KLHZ.Trader.Core.Declisions.Utils;
|
using KLHZ.Trader.Core.Declisions.Utils;
|
||||||
|
|
||||||
namespace KLHZ.Trader.Core.Tests
|
namespace KLHZ.Trader.Core.Tests
|
||||||
|
|
|
@ -10,15 +10,21 @@ namespace KLHZ.Trader.Core.Common.Messaging.Services
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, Channel<IMessage>> _messagesChannels = new();
|
private readonly ConcurrentDictionary<string, Channel<IMessage>> _messagesChannels = new();
|
||||||
private readonly ConcurrentDictionary<string, Channel<INewCandle>> _candlesChannels = new();
|
private readonly ConcurrentDictionary<string, Channel<INewCandle>> _candlesChannels = new();
|
||||||
private readonly ConcurrentDictionary<string, Channel<INewPriceMessage>> _priceChannels = new();
|
private readonly ConcurrentDictionary<string, Channel<INewPrice>> _priceChannels = new();
|
||||||
|
private readonly ConcurrentDictionary<string, Channel<IProcessedPrice>> _processedPricesChannels = new();
|
||||||
private readonly ConcurrentDictionary<string, Channel<TradeCommand>> _commandChannels = new();
|
private readonly ConcurrentDictionary<string, Channel<TradeCommand>> _commandChannels = new();
|
||||||
|
|
||||||
|
public bool AddChannel(string key, Channel<IProcessedPrice> channel)
|
||||||
|
{
|
||||||
|
return _processedPricesChannels.TryAdd(key, channel);
|
||||||
|
}
|
||||||
|
|
||||||
public bool AddChannel(string key, Channel<IMessage> channel)
|
public bool AddChannel(string key, Channel<IMessage> channel)
|
||||||
{
|
{
|
||||||
return _messagesChannels.TryAdd(key, channel);
|
return _messagesChannels.TryAdd(key, channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool AddChannel(string key, Channel<INewPriceMessage> channel)
|
public bool AddChannel(string key, Channel<INewPrice> channel)
|
||||||
{
|
{
|
||||||
return _priceChannels.TryAdd(key, channel);
|
return _priceChannels.TryAdd(key, channel);
|
||||||
}
|
}
|
||||||
|
@ -33,7 +39,7 @@ namespace KLHZ.Trader.Core.Common.Messaging.Services
|
||||||
return _commandChannels.TryAdd(key, channel);
|
return _commandChannels.TryAdd(key, channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task BroadcastNewPrice(INewPriceMessage newPriceMessage)
|
public async Task BroadcastNewPrice(INewPrice newPriceMessage)
|
||||||
{
|
{
|
||||||
foreach (var channel in _priceChannels.Values)
|
foreach (var channel in _priceChannels.Values)
|
||||||
{
|
{
|
||||||
|
@ -41,6 +47,14 @@ namespace KLHZ.Trader.Core.Common.Messaging.Services
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task BroadcastProcessedPrice(IProcessedPrice mess)
|
||||||
|
{
|
||||||
|
foreach (var channel in _processedPricesChannels.Values)
|
||||||
|
{
|
||||||
|
await channel.Writer.WriteAsync(mess);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task BroadcastNewCandle(INewCandle newPriceMessage)
|
public async Task BroadcastNewCandle(INewCandle newPriceMessage)
|
||||||
{
|
{
|
||||||
foreach (var channel in _candlesChannels.Values)
|
foreach (var channel in _candlesChannels.Values)
|
||||||
|
|
|
@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations.Schema;
|
||||||
namespace KLHZ.Trader.Core.DataLayer.Entities.Prices
|
namespace KLHZ.Trader.Core.DataLayer.Entities.Prices
|
||||||
{
|
{
|
||||||
[Table("price_changes")]
|
[Table("price_changes")]
|
||||||
public class PriceChange : INewPriceMessage
|
public class PriceChange : INewPrice
|
||||||
{
|
{
|
||||||
[Column("id")]
|
[Column("id")]
|
||||||
public long Id { get; set; }
|
public long Id { get; set; }
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace KLHZ.Trader.Core.DataLayer.Entities.Prices
|
||||||
|
{
|
||||||
|
[Table("processed_prices")]
|
||||||
|
public class ProcessedPrice : IProcessedPrice
|
||||||
|
{
|
||||||
|
[Column("id")]
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
[Column("time")]
|
||||||
|
public DateTime Time { get; set; }
|
||||||
|
|
||||||
|
[Column("value")]
|
||||||
|
public decimal Value { get; set; }
|
||||||
|
|
||||||
|
[Column("figi")]
|
||||||
|
public required string Figi { get; set; }
|
||||||
|
|
||||||
|
[Column("ticker")]
|
||||||
|
public required string Ticker { get; set; }
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
public bool IsHistoricalData { get; set; }
|
||||||
|
|
||||||
|
[Column("processor")]
|
||||||
|
public required string Processor { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace KLHZ.Trader.Core.DataLayer.Entities.Trades
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Обезличенная сделка с биржи над инструментом.
|
||||||
|
/// </summary>
|
||||||
|
[Table("instrument_trades")]
|
||||||
|
public class InstrumentTrade
|
||||||
|
{
|
||||||
|
[Column("trade_id")]
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
[Column("bought_at")]
|
||||||
|
public DateTime BoughtAt { get; set; }
|
||||||
|
|
||||||
|
[Column("figi")]
|
||||||
|
public required string Figi { get; set; }
|
||||||
|
|
||||||
|
[Column("ticker")]
|
||||||
|
public required string Ticker { get; set; }
|
||||||
|
|
||||||
|
[Column("price")]
|
||||||
|
|
||||||
|
public decimal Price { get; set; }
|
||||||
|
|
||||||
|
[Column("count")]
|
||||||
|
public decimal Count { get; set; }
|
||||||
|
|
||||||
|
[Column("direction")]
|
||||||
|
public TradeDirection Direction { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
namespace KLHZ.Trader.Core.DataLayer.Entities.Trades
|
namespace KLHZ.Trader.Core.DataLayer.Entities.Trades
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Сделка, совершенная ботом.
|
||||||
|
/// </summary>
|
||||||
[Table("trades")]
|
[Table("trades")]
|
||||||
public class Trade
|
public class Trade
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
public enum TradeDirection
|
public enum TradeDirection
|
||||||
{
|
{
|
||||||
Unknown = 0,
|
Unknown = 0,
|
||||||
Income = 1,
|
Buy = 1,
|
||||||
Outcome = 2
|
Sell = 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,10 @@ namespace KLHZ.Trader.Core.DataLayer
|
||||||
public class TraderDbContext : DbContext
|
public class TraderDbContext : DbContext
|
||||||
{
|
{
|
||||||
public DbSet<Trade> Trades { get; set; }
|
public DbSet<Trade> Trades { get; set; }
|
||||||
|
public DbSet<InstrumentTrade> InstrumentTrades { get; set; }
|
||||||
public DbSet<Declision> Declisions { get; set; }
|
public DbSet<Declision> Declisions { get; set; }
|
||||||
public DbSet<PriceChange> PriceChanges { get; set; }
|
public DbSet<PriceChange> PriceChanges { get; set; }
|
||||||
|
public DbSet<ProcessedPrice> ProcessedPrices { get; set; }
|
||||||
public DbSet<Candle> Candles { get; set; }
|
public DbSet<Candle> Candles { get; set; }
|
||||||
public TraderDbContext(DbContextOptions<TraderDbContext> options)
|
public TraderDbContext(DbContextOptions<TraderDbContext> options)
|
||||||
: base(options)
|
: base(options)
|
||||||
|
@ -30,6 +32,15 @@ namespace KLHZ.Trader.Core.DataLayer
|
||||||
v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
|
v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<InstrumentTrade>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e1 => e1.Id);
|
||||||
|
entity.Property(e => e.BoughtAt)
|
||||||
|
.HasConversion(
|
||||||
|
v => v.ToUniversalTime(),
|
||||||
|
v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<Declision>(entity =>
|
modelBuilder.Entity<Declision>(entity =>
|
||||||
{
|
{
|
||||||
entity.HasKey(e1 => e1.Id);
|
entity.HasKey(e1 => e1.Id);
|
||||||
|
@ -49,6 +60,16 @@ namespace KLHZ.Trader.Core.DataLayer
|
||||||
v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
|
v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<ProcessedPrice>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e1 => e1.Id);
|
||||||
|
entity.Ignore(e1 => e1.IsHistoricalData);
|
||||||
|
entity.Property(e => e.Time)
|
||||||
|
.HasConversion(
|
||||||
|
v => v.ToUniversalTime(),
|
||||||
|
v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<Candle>(entity =>
|
modelBuilder.Entity<Candle>(entity =>
|
||||||
{
|
{
|
||||||
entity.HasKey(e1 => new { e1.Figi, e1.Time });
|
entity.HasKey(e1 => new { e1.Figi, e1.Time });
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces;
|
||||||
|
using KLHZ.Trader.Core.Contracts.Messaging.Interfaces;
|
||||||
|
using KLHZ.Trader.Core.DataLayer;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
|
||||||
|
namespace KLHZ.Trader.Core.Declisions.Services
|
||||||
|
{
|
||||||
|
public class ProcessedPricesLogger : IHostedService
|
||||||
|
{
|
||||||
|
private readonly ILogger<ProcessedPricesLogger> _logger;
|
||||||
|
private readonly IDataBus _dataBus;
|
||||||
|
private readonly IDbContextFactory<TraderDbContext> _dbContextFactory;
|
||||||
|
|
||||||
|
private readonly Channel<IProcessedPrice> _channel = Channel.CreateUnbounded<IProcessedPrice>();
|
||||||
|
public ProcessedPricesLogger(IDataBus dataBus, IDbContextFactory<TraderDbContext> dbContextFactory, ILogger<ProcessedPricesLogger> logger)
|
||||||
|
{
|
||||||
|
_dataBus = dataBus;
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
_logger = logger;
|
||||||
|
_dataBus.AddChannel(nameof(ProcessedPricesLogger), _channel);
|
||||||
|
_ = ProcessMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessMessages()
|
||||||
|
{
|
||||||
|
using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||||
|
var buffer = new List<DataLayer.Entities.Prices.ProcessedPrice>();
|
||||||
|
var lastWrite = DateTime.UtcNow;
|
||||||
|
while (await _channel.Reader.WaitToReadAsync())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var message = await _channel.Reader.ReadAsync();
|
||||||
|
|
||||||
|
buffer.Add(new DataLayer.Entities.Prices.ProcessedPrice()
|
||||||
|
{
|
||||||
|
Figi = message.Figi,
|
||||||
|
Processor = message.Processor,
|
||||||
|
Ticker = message.Ticker,
|
||||||
|
IsHistoricalData = message.IsHistoricalData,
|
||||||
|
Time = message.Time,
|
||||||
|
Value = message.Value,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (buffer.Count > 10000 || (DateTime.UtcNow - lastWrite) > TimeSpan.FromSeconds(5) || _channel.Reader.Count == 0)
|
||||||
|
{
|
||||||
|
await context.AddRangeAsync(buffer);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
buffer.Clear();
|
||||||
|
lastWrite = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,11 +5,11 @@ using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces;
|
||||||
using KLHZ.Trader.Core.Contracts.Messaging.Interfaces;
|
using KLHZ.Trader.Core.Contracts.Messaging.Interfaces;
|
||||||
using KLHZ.Trader.Core.DataLayer;
|
using KLHZ.Trader.Core.DataLayer;
|
||||||
using KLHZ.Trader.Core.DataLayer.Entities.Declisions;
|
using KLHZ.Trader.Core.DataLayer.Entities.Declisions;
|
||||||
using KLHZ.Trader.Core.Declisions.Utils;
|
|
||||||
using KLHZ.Trader.Core.Exchange;
|
using KLHZ.Trader.Core.Exchange;
|
||||||
using KLHZ.Trader.Core.Exchange.Extentions;
|
using KLHZ.Trader.Core.Exchange.Extentions;
|
||||||
using KLHZ.Trader.Core.Exchange.Models;
|
using KLHZ.Trader.Core.Exchange.Models;
|
||||||
using KLHZ.Trader.Core.Exchange.Services;
|
using KLHZ.Trader.Core.Exchange.Services;
|
||||||
|
using KLHZ.Trader.Core.Math.Declisions.Dtos.Services;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
@ -29,7 +29,7 @@ namespace KLHZ.Trader.Core.Declisions.Services
|
||||||
private readonly BotModeSwitcher _botModeSwitcher;
|
private readonly BotModeSwitcher _botModeSwitcher;
|
||||||
private readonly IDbContextFactory<TraderDbContext> _dbContextFactory;
|
private readonly IDbContextFactory<TraderDbContext> _dbContextFactory;
|
||||||
private readonly ConcurrentDictionary<string, ManagedAccount> Accounts = new();
|
private readonly ConcurrentDictionary<string, ManagedAccount> Accounts = new();
|
||||||
private readonly ConcurrentDictionary<string, PriceHistoryCacheUnit> _historyCash = new();
|
private readonly ConcurrentDictionary<string, IPriceHistoryCacheUnit> _historyCash = new();
|
||||||
private readonly ITradingEventsDetector _tradingEventsDetector;
|
private readonly ITradingEventsDetector _tradingEventsDetector;
|
||||||
|
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ namespace KLHZ.Trader.Core.Declisions.Services
|
||||||
private readonly decimal _defaultBuyPartOfAccount;
|
private readonly decimal _defaultBuyPartOfAccount;
|
||||||
private readonly string[] _managedAccountsNamePatterns = [];
|
private readonly string[] _managedAccountsNamePatterns = [];
|
||||||
|
|
||||||
private readonly Channel<INewPriceMessage> _pricesChannel = Channel.CreateUnbounded<INewPriceMessage>();
|
private readonly Channel<INewPrice> _pricesChannel = Channel.CreateUnbounded<INewPrice>();
|
||||||
|
|
||||||
public Trader(
|
public Trader(
|
||||||
ITradingEventsDetector tradingEventsDetector,
|
ITradingEventsDetector tradingEventsDetector,
|
||||||
|
@ -89,7 +89,6 @@ namespace KLHZ.Trader.Core.Declisions.Services
|
||||||
_ = ProcessMessages();
|
_ = ProcessMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task ProcessMessages()
|
private async Task ProcessMessages()
|
||||||
{
|
{
|
||||||
while (await _pricesChannel.Reader.WaitToReadAsync())
|
while (await _pricesChannel.Reader.WaitToReadAsync())
|
||||||
|
@ -97,7 +96,7 @@ namespace KLHZ.Trader.Core.Declisions.Services
|
||||||
var message = await _pricesChannel.Reader.ReadAsync();
|
var message = await _pricesChannel.Reader.ReadAsync();
|
||||||
if (_historyCash.TryGetValue(message.Figi, out var data))
|
if (_historyCash.TryGetValue(message.Figi, out var data))
|
||||||
{
|
{
|
||||||
data.AddData(message);
|
await data.AddData(message);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -146,24 +145,6 @@ namespace KLHZ.Trader.Core.Declisions.Services
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Preprocess(string figi)
|
|
||||||
{
|
|
||||||
if (_historyCash.TryGetValue(figi, out var unit))
|
|
||||||
{
|
|
||||||
var periodData1 = unit.GetPriceDiffForTimeSpan(TimeSpan.Zero, TimeSpan.FromSeconds(10));
|
|
||||||
var periodData2 = unit.GetPriceDiffForTimeSpan(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30));
|
|
||||||
if (Math.Abs(periodData1.PeriodDiff) <= 1 && periodData2.PeriodDiff > 2)
|
|
||||||
{
|
|
||||||
//можно покупать.
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Math.Abs(periodData1.PeriodDiff) <= 1 && periodData2.PeriodDiff < -2)
|
|
||||||
{
|
|
||||||
//можно продавать.
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken)
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using KLHZ.Trader.Core.Contracts.Declisions.Dtos;
|
using KLHZ.Trader.Core.Contracts.Declisions.Dtos;
|
||||||
using KLHZ.Trader.Core.Contracts.Declisions.Interfaces;
|
using KLHZ.Trader.Core.Contracts.Declisions.Interfaces;
|
||||||
using KLHZ.Trader.Core.DataLayer.Entities.Declisions;
|
|
||||||
using KLHZ.Trader.Core.Declisions.Utils;
|
using KLHZ.Trader.Core.Declisions.Utils;
|
||||||
|
|
||||||
namespace KLHZ.Trader.Core.Declisions.Services
|
namespace KLHZ.Trader.Core.Declisions.Services
|
||||||
|
@ -36,28 +35,6 @@ namespace KLHZ.Trader.Core.Declisions.Services
|
||||||
var uptrendEnds2 = data.CheckLongClose(TimeSpan.FromSeconds(120), TimeSpan.FromSeconds(30), meanfullDiff, 15, 8);
|
var uptrendEnds2 = data.CheckLongClose(TimeSpan.FromSeconds(120), TimeSpan.FromSeconds(30), meanfullDiff, 15, 8);
|
||||||
uptrendEnds |= uptrendEnds2;
|
uptrendEnds |= uptrendEnds2;
|
||||||
|
|
||||||
//var uptrendEnds2 = data.CheckUptrendEnding(TimeSpan.FromSeconds(20), TimeSpan.FromSeconds(20), meanfullDiff);
|
|
||||||
|
|
||||||
//var uptrendEnds = uptrendEnds1 || uptrendEnds2;
|
|
||||||
|
|
||||||
var declisionAction = DeclisionTradeAction.Unknown;
|
|
||||||
|
|
||||||
//if (downtrendStarts)
|
|
||||||
//{
|
|
||||||
// //declisionAction = DeclisionTradeAction.OpenShort;
|
|
||||||
//}
|
|
||||||
if (uptrendStarts)
|
|
||||||
{
|
|
||||||
declisionAction = DeclisionTradeAction.OpenLong;
|
|
||||||
}
|
|
||||||
//else if (downtrendEnds)
|
|
||||||
//{
|
|
||||||
// //declisionAction = DeclisionTradeAction.CloseShort;
|
|
||||||
//}
|
|
||||||
else if (uptrendEnds)
|
|
||||||
{
|
|
||||||
declisionAction = DeclisionTradeAction.CloseLong;
|
|
||||||
}
|
|
||||||
return new TradingEventsDto(uptrendEnds, uptrendStarts);
|
return new TradingEventsDto(uptrendEnds, uptrendStarts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
using KLHZ.Trader.Core.Contracts.Declisions.Interfaces;
|
using KLHZ.Trader.Core.Contracts.Declisions.Interfaces;
|
||||||
using KLHZ.Trader.Core.Declisions.Dtos;
|
using KLHZ.Trader.Core.Declisions.Dtos;
|
||||||
using KLHZ.Trader.Core.Declisions.Services;
|
using KLHZ.Trader.Core.Math.Declisions.Dtos.Services;
|
||||||
|
|
||||||
namespace KLHZ.Trader.Core.Declisions.Utils
|
namespace KLHZ.Trader.Core.Declisions.Utils
|
||||||
{
|
{
|
||||||
|
@ -66,32 +66,32 @@ namespace KLHZ.Trader.Core.Declisions.Utils
|
||||||
|
|
||||||
internal static bool CheckStable(this PeriodPricesInfoDto data, float meanfullDiff)
|
internal static bool CheckStable(this PeriodPricesInfoDto data, float meanfullDiff)
|
||||||
{
|
{
|
||||||
meanfullDiff = Math.Abs(meanfullDiff);
|
meanfullDiff = System.Math.Abs(meanfullDiff);
|
||||||
return data.Success && Math.Abs(data.PeriodDiff) < 1.5 * meanfullDiff && Math.Abs(data.PeriodMax - data.PeriodMin) < 2 * meanfullDiff;
|
return data.Success && System.Math.Abs(data.PeriodDiff) < 1.5 * meanfullDiff && System.Math.Abs(data.PeriodMax - data.PeriodMin) < 2 * meanfullDiff;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool CheckGrowing(this PeriodPricesInfoDto data, float meanfullDiff)
|
internal static bool CheckGrowing(this PeriodPricesInfoDto data, float meanfullDiff)
|
||||||
{
|
{
|
||||||
return meanfullDiff > 0 && data.Success && data.PeriodDiff > meanfullDiff && Math.Abs(data.PeriodMax - data.PeriodMin) < 3 * Math.Abs(data.PeriodDiff);
|
return meanfullDiff > 0 && data.Success && data.PeriodDiff > meanfullDiff && System.Math.Abs(data.PeriodMax - data.PeriodMin) < 3 * System.Math.Abs(data.PeriodDiff);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool CheckFalling(this PeriodPricesInfoDto data, float meanfullDiff)
|
internal static bool CheckFalling(this PeriodPricesInfoDto data, float meanfullDiff)
|
||||||
{
|
{
|
||||||
meanfullDiff = -meanfullDiff;
|
meanfullDiff = -meanfullDiff;
|
||||||
return meanfullDiff < 0 && data.Success && data.PeriodDiff < meanfullDiff && Math.Abs(data.PeriodMax - data.PeriodMin) < 3 * Math.Abs(data.PeriodDiff);
|
return meanfullDiff < 0 && data.Success && data.PeriodDiff < meanfullDiff && System.Math.Abs(data.PeriodMax - data.PeriodMin) < 3 * System.Math.Abs(data.PeriodDiff);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static float CalcTrendRelationAbs(PeriodPricesInfoDto first, PeriodPricesInfoDto second)
|
internal static float CalcTrendRelationAbs(PeriodPricesInfoDto first, PeriodPricesInfoDto second)
|
||||||
{
|
{
|
||||||
var k1 = Math.Abs(first.PeriodDiff) / Math.Abs(first.Period.TotalSeconds);
|
var k1 = System.Math.Abs(first.PeriodDiff) / System.Math.Abs(first.Period.TotalSeconds);
|
||||||
var k2 = Math.Abs(second.PeriodDiff) / Math.Abs(second.Period.TotalSeconds);
|
var k2 = System.Math.Abs(second.PeriodDiff) / System.Math.Abs(second.Period.TotalSeconds);
|
||||||
if (k2 == 0 && k1 != 0) return 1000;
|
if (k2 == 0 && k1 != 0) return 1000;
|
||||||
return (float)(k1 / k2);
|
return (float)(k1 / k2);
|
||||||
}
|
}
|
||||||
internal static float CalcTrendRelationAbs(TwoPeriodsProcessingDto data)
|
internal static float CalcTrendRelationAbs(TwoPeriodsProcessingDto data)
|
||||||
{
|
{
|
||||||
var k1 = Math.Abs(data.DiffStart) / Math.Abs(data.PeriodStart.TotalSeconds);
|
var k1 = System.Math.Abs(data.DiffStart) / System.Math.Abs(data.PeriodStart.TotalSeconds);
|
||||||
var k2 = Math.Abs(data.DiffEnd) / Math.Abs(data.PeriodEnd.TotalSeconds);
|
var k2 = System.Math.Abs(data.DiffEnd) / System.Math.Abs(data.PeriodEnd.TotalSeconds);
|
||||||
if (k2 == 0 && k1 != 0) return 1000;
|
if (k2 == 0 && k1 != 0) return 1000;
|
||||||
return (float)(k1 / k2);
|
return (float)(k1 / k2);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
using KLHZ.Trader.Core.Contracts.Messaging.Interfaces;
|
using KLHZ.Trader.Core.Contracts.Messaging.Interfaces;
|
||||||
using KLHZ.Trader.Core.DataLayer;
|
using KLHZ.Trader.Core.DataLayer;
|
||||||
using KLHZ.Trader.Core.DataLayer.Entities.Prices;
|
using KLHZ.Trader.Core.DataLayer.Entities.Prices;
|
||||||
|
using KLHZ.Trader.Core.DataLayer.Entities.Trades;
|
||||||
using KLHZ.Trader.Core.Exchange.Extentions;
|
using KLHZ.Trader.Core.Exchange.Extentions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
@ -10,7 +11,6 @@ using Microsoft.Extensions.Options;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using Tinkoff.InvestApi;
|
using Tinkoff.InvestApi;
|
||||||
using Tinkoff.InvestApi.V1;
|
using Tinkoff.InvestApi.V1;
|
||||||
using Candle = KLHZ.Trader.Core.DataLayer.Entities.Prices.Candle;
|
|
||||||
|
|
||||||
namespace KLHZ.Trader.Core.Exchange.Services
|
namespace KLHZ.Trader.Core.Exchange.Services
|
||||||
{
|
{
|
||||||
|
@ -95,6 +95,12 @@ namespace KLHZ.Trader.Core.Exchange.Services
|
||||||
SubscriptionAction = SubscriptionAction.Subscribe
|
SubscriptionAction = SubscriptionAction.Subscribe
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var tradesRequest = new SubscribeTradesRequest
|
||||||
|
{
|
||||||
|
SubscriptionAction = SubscriptionAction.Subscribe
|
||||||
|
};
|
||||||
|
|
||||||
foreach (var f in _instrumentsFigis)
|
foreach (var f in _instrumentsFigis)
|
||||||
{
|
{
|
||||||
request.Instruments.Add(
|
request.Instruments.Add(
|
||||||
|
@ -102,6 +108,12 @@ namespace KLHZ.Trader.Core.Exchange.Services
|
||||||
{
|
{
|
||||||
InstrumentId = f
|
InstrumentId = f
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tradesRequest.Instruments.Add(
|
||||||
|
new TradeInstrument()
|
||||||
|
{
|
||||||
|
InstrumentId = f
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await stream.RequestStream.WriteAsync(new MarketDataRequest
|
await stream.RequestStream.WriteAsync(new MarketDataRequest
|
||||||
|
@ -109,6 +121,17 @@ namespace KLHZ.Trader.Core.Exchange.Services
|
||||||
SubscribeLastPriceRequest = request,
|
SubscribeLastPriceRequest = request,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await stream.RequestStream.WriteAsync(new MarketDataRequest
|
||||||
|
{
|
||||||
|
SubscribeTradesRequest = tradesRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||||
|
var pricesBuffer = new List<PriceChange>();
|
||||||
|
var tradesBuffer = new List<InstrumentTrade>();
|
||||||
|
var lastWritePrices = DateTime.UtcNow;
|
||||||
|
var lastWriteTrades = DateTime.UtcNow;
|
||||||
await foreach (var response in stream.ResponseStream.ReadAllAsync())
|
await foreach (var response in stream.ResponseStream.ReadAllAsync())
|
||||||
{
|
{
|
||||||
if (response.LastPrice != null)
|
if (response.LastPrice != null)
|
||||||
|
@ -122,63 +145,41 @@ namespace KLHZ.Trader.Core.Exchange.Services
|
||||||
IsHistoricalData = false,
|
IsHistoricalData = false,
|
||||||
};
|
};
|
||||||
await _eventBus.BroadcastNewPrice(message);
|
await _eventBus.BroadcastNewPrice(message);
|
||||||
using var context = await _dbContextFactory.CreateDbContextAsync();
|
|
||||||
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
pricesBuffer.Add(message);
|
||||||
await context.PriceChanges.AddAsync(message);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
}
|
if (response.Trade != null)
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SubscribeCandles()
|
|
||||||
{
|
|
||||||
using var stream = _investApiClient.MarketDataStream.MarketDataStream();
|
|
||||||
|
|
||||||
var request = new SubscribeCandlesRequest
|
|
||||||
{
|
|
||||||
SubscriptionAction = SubscriptionAction.Subscribe,
|
|
||||||
CandleSourceType = GetCandlesRequest.Types.CandleSource.Exchange
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var f in _instrumentsFigis)
|
|
||||||
{
|
|
||||||
request.Instruments.Add(
|
|
||||||
new CandleInstrument()
|
|
||||||
{
|
|
||||||
InstrumentId = f,
|
|
||||||
Interval = SubscriptionInterval.OneMinute
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await stream.RequestStream.WriteAsync(new MarketDataRequest
|
|
||||||
{
|
|
||||||
SubscribeCandlesRequest = request,
|
|
||||||
});
|
|
||||||
|
|
||||||
await foreach (var response in stream.ResponseStream.ReadAllAsync())
|
|
||||||
{
|
|
||||||
if (response.Candle != null)
|
|
||||||
{
|
{
|
||||||
var message = new Candle()
|
var trade = new KLHZ.Trader.Core.DataLayer.Entities.Trades.InstrumentTrade()
|
||||||
{
|
{
|
||||||
Figi = response.Candle.Figi,
|
Figi = response.Trade.Figi,
|
||||||
Ticker = GetTickerByFigi(response.LastPrice.Figi),
|
BoughtAt = response.Trade.Time.ToDateTime().ToUniversalTime(),
|
||||||
Time = response.Candle.Time.ToDateTime().ToUniversalTime(),
|
Ticker = GetTickerByFigi(response.Trade.Figi),
|
||||||
Close = response.Candle.Close,
|
Price = response.Trade.Price,
|
||||||
Open = response.Candle.Open,
|
Count = response.Trade.Quantity,
|
||||||
Low = response.Candle.Low,
|
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,
|
||||||
High = response.Candle.High,
|
|
||||||
Volume = response.Candle.Volume,
|
|
||||||
IsHistoricalData = false,
|
|
||||||
};
|
};
|
||||||
await _eventBus.BroadcastNewCandle(message);
|
tradesBuffer.Add(trade);
|
||||||
using var context = await _dbContextFactory.CreateDbContextAsync();
|
}
|
||||||
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
|
||||||
await context.Candles.AddAsync(message);
|
|
||||||
|
//if (pricesBuffer.Count > 200 || (DateTime.UtcNow - lastWritePrices).TotalSeconds > 10)
|
||||||
|
{
|
||||||
|
lastWritePrices = DateTime.UtcNow;
|
||||||
|
await context.PriceChanges.AddRangeAsync(pricesBuffer);
|
||||||
|
pricesBuffer.Clear();
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
//if (tradesBuffer.Count > 200 || (DateTime.UtcNow - lastWriteTrades).TotalSeconds > 10)
|
||||||
|
{
|
||||||
|
lastWriteTrades = DateTime.UtcNow;
|
||||||
|
await context.InstrumentTrades.AddRangeAsync(tradesBuffer);
|
||||||
|
tradesBuffer.Clear();
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetTickerByFigi(string figi)
|
private string GetTickerByFigi(string figi)
|
||||||
{
|
{
|
||||||
return _tickersCache.TryGetValue(figi, out var ticker) ? ticker : string.Empty;
|
return _tickersCache.TryGetValue(figi, out var ticker) ? ticker : string.Empty;
|
||||||
|
|
|
@ -237,7 +237,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
|
||||||
Count = res.LotsExecuted,
|
Count = res.LotsExecuted,
|
||||||
Price = res.ExecutedOrderPrice,
|
Price = res.ExecutedOrderPrice,
|
||||||
Position = DataLayer.Entities.Trades.PositionType.Long,
|
Position = DataLayer.Entities.Trades.PositionType.Long,
|
||||||
Direction = DataLayer.Entities.Trades.TradeDirection.Income,
|
Direction = DataLayer.Entities.Trades.TradeDirection.Buy,
|
||||||
Asset = DataLayer.Entities.Trades.AssetType.Common,
|
Asset = DataLayer.Entities.Trades.AssetType.Common,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -12,10 +12,12 @@
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||||
<PackageReference Include="Telegram.Bot" Version="22.6.0" />
|
<PackageReference Include="Telegram.Bot" Version="22.6.0" />
|
||||||
<PackageReference Include="Tinkoff.InvestApi" Version="0.6.17" />
|
<PackageReference Include="Tinkoff.InvestApi" Version="0.6.17" />
|
||||||
|
<PackageReference Include="MathNet.Filtering.Kalman" Version="0.7.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\KLHZ.Trader.Core.Contracts\KLHZ.Trader.Core.Contracts.csproj" />
|
<ProjectReference Include="..\KLHZ.Trader.Core.Contracts\KLHZ.Trader.Core.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\KLHZ.Trader.Core.Math\KLHZ.Trader.Core.Math.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -22,24 +22,31 @@ namespace KLHZ.Trader.Service.Controllers
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task Run(string figi)
|
public async Task Run(string figi)
|
||||||
{
|
{
|
||||||
using var context1 = await _dbContextFactory.CreateDbContextAsync();
|
try
|
||||||
context1.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
|
||||||
var data = await context1.PriceChanges
|
|
||||||
.Where(c => c.Figi == figi)
|
|
||||||
.OrderBy(c => c.Time)
|
|
||||||
.Select(c => new NewPriceMessage()
|
|
||||||
{
|
|
||||||
Figi = figi,
|
|
||||||
Ticker = c.Ticker,
|
|
||||||
Time = c.Time,
|
|
||||||
Value = c.Value,
|
|
||||||
IsHistoricalData = true
|
|
||||||
})
|
|
||||||
.ToArrayAsync();
|
|
||||||
|
|
||||||
foreach (var mess in data)
|
|
||||||
{
|
{
|
||||||
await _dataBus.BroadcastNewPrice(mess);
|
using var context1 = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
context1.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||||
|
var data = await context1.PriceChanges
|
||||||
|
.Where(c => c.Figi == figi)
|
||||||
|
.OrderBy(c => c.Time)
|
||||||
|
.Select(c => new NewPriceMessage()
|
||||||
|
{
|
||||||
|
Figi = figi,
|
||||||
|
Ticker = c.Ticker,
|
||||||
|
Time = c.Time,
|
||||||
|
Value = c.Value,
|
||||||
|
IsHistoricalData = true
|
||||||
|
})
|
||||||
|
.ToArrayAsync();
|
||||||
|
|
||||||
|
foreach (var mess in data)
|
||||||
|
{
|
||||||
|
await _dataBus.BroadcastNewPrice(mess);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,9 @@ builder.Services.AddDbContextFactory<TraderDbContext>(options =>
|
||||||
builder.Services.AddHostedService<BotStarter>();
|
builder.Services.AddHostedService<BotStarter>();
|
||||||
builder.Services.AddHostedService<ExchangeDataReader>();
|
builder.Services.AddHostedService<ExchangeDataReader>();
|
||||||
builder.Services.AddHostedService<Trader>();
|
builder.Services.AddHostedService<Trader>();
|
||||||
|
//builder.Services.AddHostedService<ProcessedPricesLogger>();
|
||||||
|
|
||||||
|
//builder.Services.AddHostedService<KalmanPredictor>();
|
||||||
|
|
||||||
builder.Services.AddSingleton<IUpdateHandler, BotMessagesHandler>();
|
builder.Services.AddSingleton<IUpdateHandler, BotMessagesHandler>();
|
||||||
builder.Services.AddSingleton<BotModeSwitcher>();
|
builder.Services.AddSingleton<BotModeSwitcher>();
|
||||||
|
|
|
@ -26,6 +26,7 @@ EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "postrgres", "postrgres", "{174A800A-6040-40CF-B331-8603E097CBAC}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "postrgres", "postrgres", "{174A800A-6040-40CF-B331-8603E097CBAC}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
KLHZ.Trader.Infrastructure\postgres\init.sql = KLHZ.Trader.Infrastructure\postgres\init.sql
|
KLHZ.Trader.Infrastructure\postgres\init.sql = KLHZ.Trader.Infrastructure\postgres\init.sql
|
||||||
|
migration1.sql = migration1.sql
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "loki", "loki", "{63D21DAF-FDF0-4F2D-A671-E9E59BB0CA5B}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "loki", "loki", "{63D21DAF-FDF0-4F2D-A671-E9E59BB0CA5B}"
|
||||||
|
@ -42,6 +43,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "deploy", "deploy", "{9DE95D
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KLHZ.Trader.Core.Contracts", "KLHZ.Trader.Core.Contracts\KLHZ.Trader.Core.Contracts.csproj", "{C1ADC79B-ADDB-435D-A453-9D1623D144C4}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KLHZ.Trader.Core.Contracts", "KLHZ.Trader.Core.Contracts\KLHZ.Trader.Core.Contracts.csproj", "{C1ADC79B-ADDB-435D-A453-9D1623D144C4}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KLHZ.Trader.Core.Math", "KLHZ.Trader.Core.Math\KLHZ.Trader.Core.Math.csproj", "{4C224F89-2C33-41FB-94CB-87368C86C2C3}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
@ -72,6 +75,10 @@ Global
|
||||||
{C1ADC79B-ADDB-435D-A453-9D1623D144C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{C1ADC79B-ADDB-435D-A453-9D1623D144C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{C1ADC79B-ADDB-435D-A453-9D1623D144C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{C1ADC79B-ADDB-435D-A453-9D1623D144C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{C1ADC79B-ADDB-435D-A453-9D1623D144C4}.Release|Any CPU.Build.0 = Release|Any CPU
|
{C1ADC79B-ADDB-435D-A453-9D1623D144C4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{4C224F89-2C33-41FB-94CB-87368C86C2C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{4C224F89-2C33-41FB-94CB-87368C86C2C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{4C224F89-2C33-41FB-94CB-87368C86C2C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{4C224F89-2C33-41FB-94CB-87368C86C2C3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
create table processed_prices
|
||||||
|
(
|
||||||
|
id bigserial,
|
||||||
|
time timestamp default current_timestamp,
|
||||||
|
figi text not null,
|
||||||
|
processor text not null,
|
||||||
|
ticker text not null,
|
||||||
|
value decimal not null,
|
||||||
|
primary key (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX processed_prices_index ON processed_prices USING btree(figi,processor, time);
|
Loading…
Reference in New Issue