diff --git a/KLHZ.Trader.Core.Contracts/Declisions/Dtos/Enums/TradingEvent.cs b/KLHZ.Trader.Core.Contracts/Declisions/Dtos/Enums/TradingEvent.cs index 4f8d20e..16a3066 100644 --- a/KLHZ.Trader.Core.Contracts/Declisions/Dtos/Enums/TradingEvent.cs +++ b/KLHZ.Trader.Core.Contracts/Declisions/Dtos/Enums/TradingEvent.cs @@ -11,6 +11,7 @@ ShortOpen = 16, UptrendEnd = 32, UptrendStart = 64, - HorisontTrend = 128, + DowntrendEnd = 128, + DowntrendStart = 256, } } diff --git a/KLHZ.Trader.Core.Math/Declisions/Dtos/FFT/Enums/ValueAmplitudePosition.cs b/KLHZ.Trader.Core.Math/Declisions/Dtos/FFT/Enums/ValueAmplitudePosition.cs new file mode 100644 index 0000000..5bd2ac3 --- /dev/null +++ b/KLHZ.Trader.Core.Math/Declisions/Dtos/FFT/Enums/ValueAmplitudePosition.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KLHZ.Trader.Core.Math.Declisions.Dtos.FFT.Enums +{ + public enum ValueAmplitudePosition + { + None = 0, + Middle = 1, + UpperThen20Decil = 10, + LowerThenMediana = -50, + } +} diff --git a/KLHZ.Trader.Core.Math/Declisions/Dtos/FFT/FFTAnalyzeResult.cs b/KLHZ.Trader.Core.Math/Declisions/Dtos/FFT/FFTAnalyzeResult.cs new file mode 100644 index 0000000..ef715f1 --- /dev/null +++ b/KLHZ.Trader.Core.Math/Declisions/Dtos/FFT/FFTAnalyzeResult.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KLHZ.Trader.Core.Math.Declisions.Dtos.FFT +{ + public class FFTAnalyzeResult + { + public required string Key { get; init; } + public Harmonic[] Harmonics { get; init; } = []; + public decimal Max { get; init; } + public decimal Min { get; init; } + public decimal Mediana { get; init; } + public decimal Upper20Decil { get; init; } + public decimal Lower20Decil { get; init; } + public DateTime LastTime { get; init; } + public DateTime StartTime { get; init; } + public bool IsEmpty => this == Empty; + + public static FFTAnalyzeResult Empty = new() { Key = "empty" }; + } +} diff --git a/KLHZ.Trader.Core.Math/Declisions/Dtos/FFT/Harmonic.cs b/KLHZ.Trader.Core.Math/Declisions/Dtos/FFT/Harmonic.cs new file mode 100644 index 0000000..adae133 --- /dev/null +++ b/KLHZ.Trader.Core.Math/Declisions/Dtos/FFT/Harmonic.cs @@ -0,0 +1,11 @@ +namespace KLHZ.Trader.Core.Math.Declisions.Dtos.FFT +{ + public class Harmonic + { + public TimeSpan Period { get; init; } + public float Magnitude { get; init; } + public float Real { get; init; } + public float Imaginary { get; init; } + public float Phase { get; init; } + } +} diff --git a/KLHZ.Trader.Core.Math/Declisions/Dtos/Harmonic.cs b/KLHZ.Trader.Core.Math/Declisions/Dtos/Harmonic.cs deleted file mode 100644 index e60f333..0000000 --- a/KLHZ.Trader.Core.Math/Declisions/Dtos/Harmonic.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace KLHZ.Trader.Core.Math.Declisions.Dtos -{ - public class Harmonic - { - public TimeSpan Period { get; set; } - public float Magnitude { get; set; } - public float Real { get; set; } - public float Imaginary { get; set; } - public float Phase { get; set; } - } -} diff --git a/KLHZ.Trader.Core.Math/Declisions/Services/Cache/PriceHistoryCacheUnit2.cs b/KLHZ.Trader.Core.Math/Declisions/Services/Cache/PriceHistoryCacheUnit2.cs index 70c006d..3fe3372 100644 --- a/KLHZ.Trader.Core.Math/Declisions/Services/Cache/PriceHistoryCacheUnit2.cs +++ b/KLHZ.Trader.Core.Math/Declisions/Services/Cache/PriceHistoryCacheUnit2.cs @@ -9,8 +9,8 @@ namespace KLHZ.Trader.Core.Math.Declisions.Services.Cache { public class PriceHistoryCacheUnit2 : IPriceHistoryCacheUnit { - public const int CacheMaxLength = 1500; - private const int _arrayMaxLength = 5000; + public const int CacheMaxLength = 30000; + private const int _arrayMaxLength = 60000; public string Figi { get; init; } diff --git a/KLHZ.Trader.Core.Math/Declisions/Utils/FFT.cs b/KLHZ.Trader.Core.Math/Declisions/Utils/FFT.cs index 30ece8c..2566a2f 100644 --- a/KLHZ.Trader.Core.Math/Declisions/Utils/FFT.cs +++ b/KLHZ.Trader.Core.Math/Declisions/Utils/FFT.cs @@ -1,4 +1,5 @@ -using KLHZ.Trader.Core.Math.Declisions.Dtos; +using KLHZ.Trader.Core.Math.Declisions.Dtos.FFT; +using KLHZ.Trader.Core.Math.Declisions.Dtos.FFT.Enums; using MathNet.Numerics; using MathNet.Numerics.IntegralTransforms; @@ -6,28 +7,102 @@ namespace KLHZ.Trader.Core.Math.Declisions.Utils { public static class FFT { - public static void Test() + public static ValueAmplitudePosition Check(FFTAnalyzeResult fftData, DateTime timestamp) { - var da = new List(); - for (int i = 0; i < 1000; i++) + var value = (decimal)CalcAmplitude(fftData.Harmonics, fftData.StartTime, timestamp); + var value2 = (decimal)CalcExtremum(fftData.Harmonics, fftData.StartTime, timestamp); + if (value > fftData.Upper20Decil) { - da.Add((float)System.Math.Sin(0.01 * i) + (float)System.Math.Cos(0.01 * i)); + return ValueAmplitudePosition.UpperThen20Decil; } - - var start = da.ToArray(); - var arrv = da.Select(d => new Complex32(d, 0)).ToArray(); - Fourier.Forward(arrv); - - Fourier.Inverse(arrv); - var res = arrv.Select(a => a.Real).ToArray(); - - for (int i = 0; i < 1000; i++) + else if (value < fftData.Mediana) { - var d = res[i] - start[i]; + return ValueAmplitudePosition.LowerThenMediana; + } + else + { + return ValueAmplitudePosition.Middle; } } - public static TimeSpan GetMainHarmonictPeriod(double[] data, TimeSpan period) + public static FFTAnalyzeResult Analyze(DateTime[] timestamps, decimal[] values, string key, TimeSpan minPeriod, TimeSpan maxPeriod) + { + var harmonics = GetHarmonics(values, timestamps[timestamps.Length - 1] - timestamps[0], minPeriod, maxPeriod); + var newValues = new decimal[timestamps.Length]; + var newValues2 = new decimal[timestamps.Length]; + var startTime = timestamps[0]; + for (int i = 0; i < timestamps.Length; i++) + { + newValues[i] = (decimal)CalcAmplitude(harmonics, startTime, timestamps[i]); + newValues2[i] = (decimal)CalcExtremum(harmonics, startTime, timestamps[i]); + } + + newValues = newValues.Order().ToArray(); + var ma = newValues2.Max(); + var mi = newValues2.Min(); + return new FFTAnalyzeResult() + { + Key = key, + Harmonics = harmonics, + LastTime = timestamps[timestamps.Length - 1], + StartTime = startTime, + Mediana = newValues[newValues.Length / 2], + Upper20Decil = newValues[(int)(newValues.Length * 0.8)], + Lower20Decil = newValues[(int)(newValues.Length * 0.2)], + Max = newValues.Max(), + Min = newValues.Min(), + }; + } + + public static Harmonic[] GetHarmonics(decimal[] data, TimeSpan period, TimeSpan minPeriod, TimeSpan maxPeriod) + { + var arrv = data.Select(d => new Complex32((float)d, 0)).ToArray(); + Fourier.Forward(arrv); + var res = new List(); + for (int i = 1; i < arrv.Length; i++) + { + var per = CaclHarmonycPeriod(period, data.Length, i); + if (per >= minPeriod && per <= maxPeriod) + { + res.Add(new Harmonic() + { + Imaginary = arrv[i].Imaginary, + Real = arrv[i].Real, + Magnitude = arrv[i].Magnitude, + Period = per, + Phase = arrv[i].Phase, + }); + } + } + + return res.ToArray(); + } + + public static double CalcAmplitude(Harmonic[] harmonics, DateTime startTime, DateTime currentTime) + { + var sum = 0d; + var timeSpan = currentTime - startTime; + for (int i = 0; i < harmonics.Length; i++) + { + var value = harmonics[i].Magnitude * System.Math.Cos(2 * System.Math.PI * timeSpan.TotalSeconds / harmonics[i].Period.TotalSeconds + harmonics[i].Phase); + sum += value; + } + return sum; + } + + public static double CalcExtremum(Harmonic[] harmonics, DateTime startTime, DateTime currentTime) + { + var sum = 0d; + var timeSpan = currentTime - startTime; + for (int i = 0; i < harmonics.Length; i++) + { + var value = - harmonics[i].Magnitude * System.Math.Sin(2 * System.Math.PI * timeSpan.TotalSeconds / harmonics[i].Period.TotalSeconds + harmonics[i].Phase); + sum += value; + } + return sum; + } + + internal static TimeSpan GetMainHarmonictPeriod(double[] data, TimeSpan period) { var arrv = data.Select(d => new Complex32((float)d, 0)).ToArray(); Fourier.Forward(arrv); @@ -38,7 +113,7 @@ namespace KLHZ.Trader.Core.Math.Declisions.Utils var max = 0f; var kmax = 1; var sum = 0f; - for (int i=0;i< arrv.Length / 2; i++) + for (int i = 0; i < arrv.Length / 2; i++) { if (i == 0) { @@ -70,64 +145,30 @@ namespace KLHZ.Trader.Core.Math.Declisions.Utils return CaclHarmonycPeriod(period, data.Length, kmax); } - public static Harmonic[] GetHarmonics(double[] data, TimeSpan period, int count) - { - var arrv = data.Select(d => new Complex32((float)d, 0)).ToArray(); - Fourier.Forward(arrv); - var res = new List(); - - for (int i = 1; i < count; i++) - { - res.Add(new Harmonic() - { - Imaginary = arrv[i].Imaginary, - Real = arrv[i].Real, - Magnitude = arrv[i].Magnitude, - Period = CaclHarmonycPeriod(period, data.Length, i), - Phase = arrv[i].Phase, - }); - } - - return res.ToArray(); - } - - public static double Calc(Harmonic[] harmonics, DateTime startTime, DateTime endTime, DateTime currentTime) - { - var sum = 0d; - var timeSpan = currentTime - startTime; - for (int i = 0; i < harmonics.Length; i++) - { - var currentPhase = System.Math.PI * 2 * timeSpan.TotalSeconds * (endTime - startTime).TotalSeconds / harmonics[i].Period.TotalSeconds / harmonics[i].Period.TotalSeconds + harmonics[i].Phase; - var value = harmonics[i].Real * System.Math.Cos(currentPhase) + harmonics[i].Imaginary * System.Math.Sign(currentPhase); - sum += value; - } - return sum; - } - - public static TimeSpan CaclHarmonycPeriod(TimeSpan signalLength, int signalLengthItems, int harmonyNumber) + internal static TimeSpan CaclHarmonycPeriod(TimeSpan signalLength, int signalLengthItems, int harmonyNumber) { var fdiscretisation = signalLengthItems / signalLength.TotalSeconds; var fharm = harmonyNumber * fdiscretisation / signalLengthItems; return TimeSpan.FromSeconds(1 / fharm); } - public static double CalcCurrentPhase(Complex32[] spectr, int harmonyNumber) + internal static double CalcCurrentPhase(Complex32[] spectr, int harmonyNumber) { var item = spectr[harmonyNumber]; return System.Math.Atan(item.Imaginary / item.Real); } - public static (double min, double max) CalcPhaseRangeFoxMax(double aSin, double aCos, double initPhase, double level) + internal static (double min, double max) CalcPhaseRangeFoxMax(double aSin, double aCos, double initPhase, double level) { - return CalcPhaseRange(aSin, aCos,initPhase, level, CheckMaxValue); + return CalcPhaseRange(aSin, aCos, initPhase, level, CheckMaxValue); } - public static (double min, double max) CalcPhaseRangeFoxMin(double aSin, double aCos, double initPhase, double level) + internal static (double min, double max) CalcPhaseRangeFoxMin(double aSin, double aCos, double initPhase, double level) { return CalcPhaseRange(aSin, aCos, initPhase, level, CheckMinValue); } - internal static (double min, double max) CalcPhaseRange(double aSin, double aCos, double initPhase, double level, Func comparer) + internal static (double min, double max) CalcPhaseRange(double aSin, double aCos, double initPhase, double level, Func comparer) { var x = new List(); var xIndexes = new List(); @@ -164,7 +205,7 @@ namespace KLHZ.Trader.Core.Math.Declisions.Utils var maxPhase = 0d; for (int i = 0; i < y.Count; i++) { - if (comparer(y[i],min,max,level)) + if (comparer(y[i], min, max, level)) { if (start < 0) { diff --git a/KLHZ.Trader.Core.Math/Declisions/Utils/LocalTrends.cs b/KLHZ.Trader.Core.Math/Declisions/Utils/LocalTrends.cs index 733d5a3..bc5ec5f 100644 --- a/KLHZ.Trader.Core.Math/Declisions/Utils/LocalTrends.cs +++ b/KLHZ.Trader.Core.Math/Declisions/Utils/LocalTrends.cs @@ -8,7 +8,7 @@ namespace KLHZ.Trader.Core.Math.Declisions.Utils /// public static class LocalTrends { - internal static bool TryGetLocalTrends(DateTime[] times, decimal[] prices, TimeSpan firstPeriod, TimeSpan lastPeriod, + public static bool TryGetLocalTrends(DateTime[] times, decimal[] prices, TimeSpan firstPeriod, TimeSpan lastPeriod, double meanfullDiff, out TradingEvent res) { res = TradingEvent.None; @@ -51,32 +51,44 @@ namespace KLHZ.Trader.Core.Math.Declisions.Utils } } - var line1 = Fit.Line(x1.ToArray(), y1.ToArray()); - var line2 = Fit.Line(x2.ToArray(), y2.ToArray()); - foreach (var x in x1) + if (x1.Count>1 && x2.Count > 1) { - y1_approximated.Add(line1.A + x * line1.B); + var line1 = Fit.Line(x1.ToArray(), y1.ToArray()); + var line2 = Fit.Line(x2.ToArray(), y2.ToArray()); + foreach (var x in x1) + { + y1_approximated.Add(line1.A + x * line1.B); + } + foreach (var x in x2) + { + y2_approximated.Add(line2.A + x * line2.B); + } + var diff1 = y1_approximated[0] - y1_approximated[y1_approximated.Count - 1]; + var diff2 = y2_approximated[0] - y2_approximated[y2_approximated.Count - 1]; + if (diff1 <= -meanfullDiff && diff2 >= meanfullDiff) + { + res |= TradingEvent.UptrendStart; + } + else if (diff1 >= meanfullDiff && diff2 <= 0) + { + res |= TradingEvent.UptrendEnd; + } + else if (diff1 <= -meanfullDiff && diff2 >= 0) + { + res |= TradingEvent.DowntrendEnd; + } + else if (diff1 >= 0 && diff2 <= -meanfullDiff) + { + res |= TradingEvent.DowntrendStart; + } + success = true; } - foreach (var x in x2) - { - y2_approximated.Add(line2.A + x * line2.B); - } - var diff1 = y1_approximated[0] - y1_approximated[y1_approximated.Count - 1]; - var diff2 = y2_approximated[0] - y2_approximated[y2_approximated.Count - 1]; - if (diff1 <= -meanfullDiff && diff2 >= meanfullDiff) - { - res |= TradingEvent.UptrendStart; - } - else if (diff1 >= meanfullDiff && diff2 <= 0) - { - res |= TradingEvent.UptrendEnd; - } - success = true; + } return success; } - internal static bool TryCalcTrendDiff(DateTime[] times, decimal[] prices, out decimal diff) + public static bool TryCalcTrendDiff(DateTime[] times, decimal[] prices, out decimal diff) { diff = 0; if (times.Length <= 1 || times.Length != prices.Length) diff --git a/KLHZ.Trader.Core.Math/Declisions/Utils/SignalProcessing.cs b/KLHZ.Trader.Core.Math/Declisions/Utils/SignalProcessing.cs index a2a81fa..bc55d0c 100644 --- a/KLHZ.Trader.Core.Math/Declisions/Utils/SignalProcessing.cs +++ b/KLHZ.Trader.Core.Math/Declisions/Utils/SignalProcessing.cs @@ -2,9 +2,9 @@ { public static class SignalProcessing { - public static (DateTime[], double[]) InterpolateData(DateTime[] timestamps, double[] values, TimeSpan timeStep) + public static (DateTime[] timestamps, decimal[] values) InterpolateData(DateTime[] timestamps, decimal[] values, TimeSpan timeStep) { - var res = new List(); + var res = new List(); var res2 = new List(); var startTime = new DateTime(timestamps[0].Year, timestamps[0].Month, timestamps[0].Day, 0, 0, 0, DateTimeKind.Utc); @@ -12,7 +12,7 @@ var totalSteps = System.Math.Ceiling((timestamps[timestamps.Length - 1] - timestamps[0]).TotalSeconds / timeStep.TotalSeconds); var deltaSeconds = System.Math.Floor(dt.TotalSeconds / timeStep.TotalSeconds); - startTime = startTime.AddSeconds(deltaSeconds * timeStep.TotalSeconds) ; + startTime = startTime.AddSeconds(deltaSeconds * timeStep.TotalSeconds); var firstBound = startTime; var secondBound = startTime + timeStep; @@ -20,11 +20,11 @@ for (int i = 0; i < totalSteps; i++) { var count = 0; - var sum = 0d; + var sum = 0m; for (int i1 = bound; i1 < timestamps.Length; i1++) { - if (timestamps[i1]> firstBound && timestamps[i1] <= secondBound) + if (timestamps[i1] > firstBound && timestamps[i1] <= secondBound) { count++; sum += values[i1]; @@ -40,9 +40,9 @@ res.Add(sum / count); res2.Add(secondBound); } - else if (bound< timestamps.Length-2) + else if (bound < timestamps.Length - 2) { - res.Add(res.Last()); + res.Add(values[bound]); res2.Add(secondBound); } diff --git a/KLHZ.Trader.Core.Tests/FFTTests.cs b/KLHZ.Trader.Core.Tests/FFTTests.cs index d3f2a48..f0e77e9 100644 --- a/KLHZ.Trader.Core.Tests/FFTTests.cs +++ b/KLHZ.Trader.Core.Tests/FFTTests.cs @@ -1,12 +1,82 @@ using KLHZ.Trader.Core.Math.Declisions.Utils; using MathNet.Numerics; using MathNet.Numerics.IntegralTransforms; -using System.Security.Cryptography; namespace KLHZ.Trader.Core.Tests { public class FFTTests { + [Test] + public static void Test1() + { + // Размер сигнала + int N = 1024; + + // Генерируем случайный сигнал + var signal = new float[N]; + Random random = new Random(); + for (int i = 0; i < N; i++) + { + signal[i] = (float)System.Math.Cos(0.01 * i);// (float)random.NextDouble() * 2 - 1; + } + // нормированный случайный сигнал [-1, 1] + + // Выполняем прямое преобразование Фурье + Complex32[] fftResult = signal.Select(s => new Complex32(s, 0)).ToArray(); + Fourier.Forward(fftResult); + + // Выделяем первые 10 гармоник (включая нулевую) + var firstTenHarmonics = fftResult.AsSpan(0, 10).ToArray(); + + //Копируем первые 11 элементов (нулевая и первые 10) + + // Вычисляем амплитуды и фазы гармоник + double[] amplitudes = new double[firstTenHarmonics.Length]; + double[] phases = new double[firstTenHarmonics.Length]; + for (int k = 0; k < firstTenHarmonics.Length; k++) + { + amplitudes[k] = firstTenHarmonics[k].Magnitude / N; + phases[k] = firstTenHarmonics[k].Phase; + } + + // Последний индекс сигнала + int lastPointIndex = N - 1; + + // Реконструкция последней точки сигнала + double reconstructedLastPoint = 0; + double reconstructedFirstPoint = 0; + for (int k = 1; k < firstTenHarmonics.Length; k++) // начинаем с первой гармоники + { + reconstructedLastPoint += amplitudes[k] * + System.Math.Cos((2 * System.Math.PI * k * lastPointIndex) / N + phases[k]); + } + for (int k = 1; k < firstTenHarmonics.Length; k++) // начинаем с первой гармоники + { + reconstructedFirstPoint += amplitudes[k] * + System.Math.Cos((2 * System.Math.PI * k * 1) / N + phases[k]); + } + Console.WriteLine($"Реконструированное значение последней точки: {reconstructedLastPoint}"); + } + public static Complex32[] InverseFourierManual(Complex32[] spectrum) + { + int N = spectrum.Length; + Complex32[] result = new Complex32[N]; + + for (int t = 0; t < N; t++) + { + Complex32 sum = Complex32.Zero; + + for (int k = 0; k < N; k++) + { + double angle = 2 * System.Math.PI * k * t / N; + sum += spectrum[k] * Complex32.Exp(new Complex32(0, (float)angle)); + } + + result[t] = sum / N; + } + + return result; + } [Test] public static void Test() { @@ -14,32 +84,60 @@ namespace KLHZ.Trader.Core.Tests var da2 = new List(); var dates = new List(); var dt = DateTime.UtcNow; - for (int i = 0; i < 1000; i++) + var dt1 = dt; + var dt2 = dt.AddSeconds(3600); + var T1 = TimeSpan.FromMinutes(10); + var T2 = TimeSpan.FromMinutes(7); + var T3 = TimeSpan.FromMinutes(30); + while (dt < dt2) { dt = dt.AddSeconds(1); - da.Add((float)System.Math.Sin(0.01 * i) + (float)System.Math.Cos(0.02 * i)); + var phase = (dt - dt1).TotalSeconds / T1.TotalSeconds * 2 * System.Math.PI; + var phase2 = (dt - dt1).TotalSeconds / T2.TotalSeconds * 2 * System.Math.PI; + var phase3 = (dt - dt1).TotalSeconds / T3.TotalSeconds * 2 * System.Math.PI; + da.Add((float)System.Math.Cos(phase) + (float)System.Math.Sin(phase2) + (float)System.Math.Sin(phase3)); dates.Add(dt); } var start = da.ToArray(); - var arrv = da.Select(d => new Complex32(d, 0)).ToArray(); - Fourier.Forward(arrv); - var harms = FFT.GetHarmonics(da.Select(d => (double)d).ToArray(), dates.Last() - dates.First(), 20); + var harms = FFT.GetHarmonics(da.Select(d => (decimal)d).ToArray(), dates.Last() - dates.First(), TimeSpan.FromSeconds(7), TimeSpan.FromSeconds(70)); - foreach(var d in dates) + var damax = da.Max(); + var damin = da.Min(); + + for (int i = 0; i < da.Count; i++) { - da2.Add((float)FFT.Calc(harms, dates.First(), dates.Last(), d)); + da[i] = da[i] / (damax - damin); } - //var tem = arrv.Select(a => Complex32.Abs(a)).ToArray(); + + foreach (var d in dates) + { + da2.Add((float)FFT.CalcAmplitude(harms, dates.First(), d)); + } + + var da2max = da2.Max(); + var da2min = da2.Min(); + + for (int i = 0; i < da2.Count; i++) + { + da2[i] = da2[i] / (da2max - da2min); + } + + damax = da.Max(); + damin = da.Min(); + da2max = da2.Max(); + da2min = da2.Min(); + //var tem = arrv.Select(a => Complex32.Abs(a)).ToArray(); //var s = tem.Sum(); //Fourier.Inverse(arrv); //var res = arrv.Select(a => a.Real).ToArray(); - - //for (int i = 0; i < 1000; i++) - //{ - // var d = res[i] - start[i]; - //} + var diffs = new List(); + for (int i = 0; i < da2.Count; i++) + { + var diff = (da2[i] - da[i]) / (damax - damin); + diffs.Add(diff); + } } [Test] @@ -66,20 +164,20 @@ namespace KLHZ.Trader.Core.Tests public static void CalcPhaseRangeForMin_Sin() { var res = FFT.CalcPhaseRangeFoxMin(1, 0, 0, 0.001); - Assert.IsTrue(res.min < 3*System.Math.PI / 2); - Assert.IsTrue(res.min > 3*System.Math.PI / 2 * 0.9); - Assert.IsTrue(res.max > 3*System.Math.PI / 2); - Assert.IsTrue(res.max < 3*System.Math.PI / 2 * 1.1); + Assert.IsTrue(res.min < 3 * System.Math.PI / 2); + Assert.IsTrue(res.min > 3 * System.Math.PI / 2 * 0.9); + Assert.IsTrue(res.max > 3 * System.Math.PI / 2); + Assert.IsTrue(res.max < 3 * System.Math.PI / 2 * 1.1); } [Test] public static void CalcPhaseRangeForMax_MinusSin() { var res = FFT.CalcPhaseRangeFoxMax(-1, 0, 0, 0.001); - Assert.IsTrue(res.min < 3*System.Math.PI / 2); - Assert.IsTrue(res.min > 3*System.Math.PI / 2 * 0.9); - Assert.IsTrue(res.max > 3*System.Math.PI / 2); - Assert.IsTrue(res.max < 3*System.Math.PI / 2 * 1.1); + Assert.IsTrue(res.min < 3 * System.Math.PI / 2); + Assert.IsTrue(res.min > 3 * System.Math.PI / 2 * 0.9); + Assert.IsTrue(res.max > 3 * System.Math.PI / 2); + Assert.IsTrue(res.max < 3 * System.Math.PI / 2 * 1.1); } [Test] @@ -89,7 +187,7 @@ namespace KLHZ.Trader.Core.Tests Assert.IsTrue(res.min < System.Math.PI * 2); Assert.IsTrue(res.min > System.Math.PI * 2 * 0.9); Assert.IsTrue(res.max > 0); - Assert.IsTrue(res.max < System.Math.PI * 2*0.1); + Assert.IsTrue(res.max < System.Math.PI * 2 * 0.1); } [Test] @@ -115,7 +213,7 @@ namespace KLHZ.Trader.Core.Tests [Test] public static void CalcPhaseRangeForMax_CosWithShift2() { - var res = FFT.CalcPhaseRangeFoxMax(0, 1, - System.Math.PI / 2, 0.001); + var res = FFT.CalcPhaseRangeFoxMax(0, 1, -System.Math.PI / 2, 0.001); Assert.IsTrue(res.min < System.Math.PI / 2); Assert.IsTrue(res.min > System.Math.PI / 2 * 0.9); Assert.IsTrue(res.max > System.Math.PI / 2); diff --git a/KLHZ.Trader.Core.Tests/SignalProcessingTests.cs b/KLHZ.Trader.Core.Tests/SignalProcessingTests.cs index 378f811..d099057 100644 --- a/KLHZ.Trader.Core.Tests/SignalProcessingTests.cs +++ b/KLHZ.Trader.Core.Tests/SignalProcessingTests.cs @@ -1,10 +1,5 @@ using KLHZ.Trader.Core.Math.Declisions.Utils; -using System; -using System.Collections.Generic; -using System.Linq; using System.Security.Cryptography; -using System.Text; -using System.Threading.Tasks; namespace KLHZ.Trader.Core.Tests { @@ -13,14 +8,14 @@ namespace KLHZ.Trader.Core.Tests [Test] public static void Test() { - var da = new List(); + var da = new List(); var times = new List(); var startDt = DateTime.UtcNow; for (int i = 0; i < 100; i++) { - startDt = startDt.AddSeconds(((double)(RandomNumberGenerator.GetInt32(1, 100)))/100); + startDt = startDt.AddSeconds(((double)(RandomNumberGenerator.GetInt32(1, 100))) / 100); times.Add(startDt); - da.Add((float)System.Math.Sin(0.01 * i) + (float)System.Math.Cos(0.01 * i)); + da.Add((decimal)System.Math.Sin(0.01 * i) + (decimal)System.Math.Cos(0.01 * i)); } var res = SignalProcessing.InterpolateData(times.ToArray(), da.ToArray(), TimeSpan.FromSeconds(5)); diff --git a/KLHZ.Trader.Core/DataLayer/Entities/Declisions/Enums/DeclisionTradeAction.cs b/KLHZ.Trader.Core/DataLayer/Entities/Declisions/Enums/DeclisionTradeAction.cs index b09e574..b5bc676 100644 --- a/KLHZ.Trader.Core/DataLayer/Entities/Declisions/Enums/DeclisionTradeAction.cs +++ b/KLHZ.Trader.Core/DataLayer/Entities/Declisions/Enums/DeclisionTradeAction.cs @@ -10,6 +10,8 @@ CloseLong = 200, CloseLongReal = 201, OpenShort = 300, + OpenShortReal = 301, CloseShort = 400, + CloseShortReal = 401, } } diff --git a/KLHZ.Trader.Core/Exchange/Services/Trader.cs b/KLHZ.Trader.Core/Exchange/Services/Trader.cs index 910770d..44102f1 100644 --- a/KLHZ.Trader.Core/Exchange/Services/Trader.cs +++ b/KLHZ.Trader.Core/Exchange/Services/Trader.cs @@ -1,4 +1,5 @@ -using KLHZ.Trader.Core.Common; +using Google.Protobuf.WellKnownTypes; +using KLHZ.Trader.Core.Common; using KLHZ.Trader.Core.Contracts.Declisions.Dtos.Enums; using KLHZ.Trader.Core.Contracts.Messaging.Dtos; using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces; @@ -10,6 +11,7 @@ using KLHZ.Trader.Core.Exchange.Models.AssetsAccounting; using KLHZ.Trader.Core.Exchange.Models.Configs; using KLHZ.Trader.Core.Exchange.Models.Trading; using KLHZ.Trader.Core.Exchange.Utils; +using KLHZ.Trader.Core.Math.Declisions.Dtos.FFT.Enums; using KLHZ.Trader.Core.Math.Declisions.Utils; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Hosting; @@ -18,8 +20,8 @@ using Microsoft.Extensions.Options; using System.Collections.Concurrent; using System.Security.Cryptography; using System.Threading.Channels; +using Telegram.Bot.Types; using Tinkoff.InvestApi; -using static Microsoft.EntityFrameworkCore.DbLoggerCategory.Database; using AssetType = KLHZ.Trader.Core.Exchange.Models.AssetsAccounting.AssetType; namespace KLHZ.Trader.Core.Exchange.Services @@ -29,8 +31,8 @@ namespace KLHZ.Trader.Core.Exchange.Services private readonly IDataBus _dataBus; private readonly TraderDataProvider _tradeDataProvider; private readonly ILogger _logger; - private readonly ConcurrentDictionary OpeningStops = new(); - private readonly ConcurrentDictionary ClosingStops = new(); + private readonly ConcurrentDictionary LongOpeningStops = new(); + private readonly ConcurrentDictionary ShortClosingStops = new(); private readonly ConcurrentDictionary Leverages = new(); private readonly decimal _futureComission; @@ -72,14 +74,86 @@ namespace KLHZ.Trader.Core.Exchange.Services _ = ProcessPrices(); } + public async ValueTask<(DateTime[] timestamps, decimal[] prices, bool isFullIntervalExists)> GetData(INewPrice message) + { + var data2 = await _tradeDataProvider.GetData(message.Figi, TimeSpan.FromHours(1.5)); + if (!data2.isFullIntervalExists) + { + data2 = await _tradeDataProvider.GetData(message.Figi, TimeSpan.FromHours(1)); + } + if (!data2.isFullIntervalExists) + { + data2 = await _tradeDataProvider.GetData(message.Figi, TimeSpan.FromHours(0.75)); + } + return data2; + } + + private async ValueTask CheckPosition((DateTime[] timestamps, decimal[] prices, bool isFullIntervalExists) data, INewPrice message) + { + var currentTime = message.IsHistoricalData ? message.Time : DateTime.UtcNow; + var position = ValueAmplitudePosition.None; + var fft = await _tradeDataProvider.GetFFtResult(message.Figi); + if (fft.IsEmpty || (currentTime - fft.LastTime).TotalSeconds > 90) + { + if (data.isFullIntervalExists) + { + var interpolatedData = SignalProcessing.InterpolateData(data.timestamps, data.prices, TimeSpan.FromSeconds(5)); + fft = FFT.Analyze(interpolatedData.timestamps, interpolatedData.values, message.Figi, TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(40)); + await _tradeDataProvider.SetFFtResult(fft); + } + } + else + { + position = FFT.Check(fft, message.Time); + if (position == Math.Declisions.Dtos.FFT.Enums.ValueAmplitudePosition.UpperThen20Decil) + { + await LogPrice(message, "upper10percent", message.Value); + await LogPrice(message, "upper30percent", message.Value); + } + if (position == Math.Declisions.Dtos.FFT.Enums.ValueAmplitudePosition.LowerThenMediana) + { + await LogPrice(message, "lower30percent", message.Value); + } + } + + return position; + } + private async Task ProcessPrices() { + var pricesCache = new Dictionary>(); while (await _pricesChannel.Reader.WaitToReadAsync()) { var message = await _pricesChannel.Reader.ReadAsync(); if (message.IsHistoricalData) { await _tradeDataProvider.AddData(message, TimeSpan.FromHours(6)); + if (!pricesCache.TryGetValue(message.Figi, out var list)) + { + list = new List(); + pricesCache[message.Figi] = list; + } + list.Add(message); + + if ((list.Last().Time - list.First().Time).TotalSeconds < 0.5) + { + list.Add(message); + continue; + } + else + { + message = new PriceChange() + { + Figi = message.Figi, + Ticker = message.Ticker, + Count = message.Count, + Direction = message.Direction, + IsHistoricalData = message.IsHistoricalData, + Time = message.Time, + Value = list.Sum(l => l.Value) / list.Count + }; + list.Clear(); + } } if (_tradingInstrumentsFigis.Contains(message.Figi)) { @@ -90,14 +164,13 @@ namespace KLHZ.Trader.Core.Exchange.Services if (message.Figi == "FUTIMOEXF000") { ProcessStops(message, currentTime); - var windowMaxSize = 1000; + var windowMaxSize = 2000; await SellAssetsIfNeed(message); var data = await _tradeDataProvider.GetData(message.Figi, windowMaxSize); var state = ExchangeScheduler.GetCurrentState(message.Time); await ProcessClearing(data, state, message); - - await ProcessNewPriceIMOEXF(data, state, message, windowMaxSize); + await ProcessNewPriceIMOEXF2(data, state, message, windowMaxSize); } } catch (Exception ex) @@ -112,6 +185,7 @@ namespace KLHZ.Trader.Core.Exchange.Services { if (!BotModeSwitcher.CanSell()) { + _logger.LogWarning("Сброс активов недоступен, т.к. отключены продажи."); return; } var accounts = _tradeDataProvider.Accounts.Values.ToArray(); @@ -124,7 +198,7 @@ namespace KLHZ.Trader.Core.Exchange.Services var profit = TradingCalculator.CaclProfit(asset.BoughtPrice, message.Value, GetComission(assetType), GetLeverage(message.Figi, asset.Count < 0), asset.Count < 0); var stoppingKey = message.Figi + asset.AccountId; - if (message.Time - asset.BoughtAt > TimeSpan.FromMinutes(4) && profit < -66m && !ClosingStops.ContainsKey(stoppingKey)) + if (message.Time - asset.BoughtAt > TimeSpan.FromMinutes(4) && profit < -66m) { var command = new TradeCommand() { @@ -139,28 +213,6 @@ namespace KLHZ.Trader.Core.Exchange.Services await _dataBus.Broadcast(command); _logger.LogWarning("Сброс актива {figi}! id команды {commandId} Направление сделки: {dir}; Количество активов: {count}; Разрешена ли маржиналка: {margin}", message.Figi, command.CommandId, command.CommandType, command.Count, command.EnableMargin); - OpeningStops[message.Figi] = DateTime.UtcNow.AddMinutes(10); - ClosingStops[stoppingKey] = DateTime.UtcNow.AddSeconds(30); - await LogDeclision(DeclisionTradeAction.CloseLong, message, profit); - await LogDeclision(DeclisionTradeAction.CloseLongReal, message, profit); - } - - if (message.Time - asset.BoughtAt > TimeSpan.FromHours(4) && profit > 100 && !ClosingStops.ContainsKey(stoppingKey)) - { - var command = new TradeCommand() - { - AccountId = asset.AccountId, - Figi = message.Figi, - CommandType = asset.Count < 0 ? Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketBuy - : Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketSell, - Count = (long)asset.Count, - RecomendPrice = null, - EnableMargin = false, - }; - await _dataBus.Broadcast(command); - _logger.LogWarning("Сброс актива {figi}! id команды {commandId} Направление сделки: {dir}; Количество активов: {count}; Разрешена ли маржиналка: {margin}", - message.Figi, command.CommandId, command.CommandType, command.Count, command.EnableMargin); - ClosingStops[stoppingKey] = DateTime.UtcNow.AddSeconds(30); await LogDeclision(DeclisionTradeAction.CloseLong, message, profit); await LogDeclision(DeclisionTradeAction.CloseLongReal, message, profit); } @@ -168,22 +220,57 @@ namespace KLHZ.Trader.Core.Exchange.Services } } - private async Task ProcessNewPriceIMOEXF((DateTime[] timestamps, decimal[] prices) data, - ExchangeState state, + private async Task CheckByWindowAverageMean((DateTime[] timestamps, decimal[] prices) data, INewPrice message, int windowMaxSize) { - var res = TradingEvent.None; var resultMoveAvFull = MovingAverage.CheckByWindowAverageMean(data.timestamps, data.prices, - windowMaxSize, 30, 180, TimeSpan.FromSeconds(20), -1m, 2m); - - res |= resultMoveAvFull.events; - + windowMaxSize, 30, 180, TimeSpan.FromSeconds(20), -1m, 2m); if (resultMoveAvFull.bigWindowAv != 0) { await LogPrice(message, Constants.BigWindowCrossingAverageProcessor, resultMoveAvFull.bigWindowAv); await LogPrice(message, Constants.SmallWindowCrossingAverageProcessor, resultMoveAvFull.smallWindowAv); } + return resultMoveAvFull.events; + } + private async Task CheckByWindowAverageMeanForShotrs((DateTime[] timestamps, decimal[] prices) data, + INewPrice message, int windowMaxSize) + { + var resultMoveAvFull = MovingAverage.CheckByWindowAverageMean(data.timestamps, data.prices, + windowMaxSize, 30, 240, TimeSpan.FromSeconds(20), -1m, 1m); + if (resultMoveAvFull.bigWindowAv != 0) + { + //await LogPrice(message, Constants.BigWindowCrossingAverageProcessor, resultMoveAvFull.bigWindowAv); + //await LogPrice(message, Constants.SmallWindowCrossingAverageProcessor, resultMoveAvFull.smallWindowAv); + } + return resultMoveAvFull.events; + } + + private Task CheckByLocalTrends((DateTime[] timestamps, decimal[] prices) data, + INewPrice message, int windowMaxSize) + { + var res = TradingEvent.None; + if (LocalTrends.TryGetLocalTrends(data.timestamps, data.prices, TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(20), 1, out var resLocalTrends)) + { + res |= (resLocalTrends & TradingEvent.UptrendStart); + if ((resLocalTrends & TradingEvent.UptrendStart) == TradingEvent.UptrendStart) + { + res |= TradingEvent.DowntrendEnd; + } + } + //if (LocalTrends.TryGetLocalTrends(data.timestamps, data.prices, TimeSpan.FromSeconds(90), TimeSpan.FromSeconds(30), 2, out var resLocalTrends2)) + //{ + // res |= (resLocalTrends & TradingEvent.DowntrendEnd); + //} + //if (LocalTrends.TryGetLocalTrends(data.timestamps, data.prices, TimeSpan.FromSeconds(90), TimeSpan.FromSeconds(20), 2.5, out var resLocalTrends3)) + //{ + // res |= (resLocalTrends & TradingEvent.DowntrendStart); + //} + return Task.FromResult(res); + } + + private async Task GetAreasRelation((DateTime[] timestamps, decimal[] prices) data, INewPrice message) + { var areasRel = -1m; if (ShapeAreaCalculator.TryGetAreasRelation(data.timestamps, data.prices, message.Value, Constants.AreasRelationWindow, out var rel)) { @@ -196,59 +283,99 @@ namespace KLHZ.Trader.Core.Exchange.Services areasRel = (decimal)areas.Sum(a => a.Value) / areas.Length; await LogPrice(message, Constants.AreasRelationProcessor, areasRel); + return areasRel > 0 ? areasRel : null; } - if ((res & TradingEvent.UptrendStart) == TradingEvent.UptrendStart - && !OpeningStops.TryGetValue(message.Figi, out _) + return null; + } + + private async Task CheckPosition(INewPrice message) + { + var data2 = await GetData(message); + var position = await CheckPosition(data2, message); + return position; + } + + private async Task CalcTrendDiff(INewPrice message) + { + var data = await _tradeDataProvider.GetData(message.Figi, TimeSpan.FromHours(1)); + if (data.isFullIntervalExists && LocalTrends.TryCalcTrendDiff(data.timestamps,data.prices, out var res)) + { + return res; + } + return null; + } + + private async Task ProcessNewPriceIMOEXF2((DateTime[] timestamps, decimal[] prices) data, + ExchangeState state, + INewPrice message, int windowMaxSize) + { + if (data.timestamps.Length <= 4) + { + return; + } + + var mavTask = CheckByWindowAverageMean(data, message, windowMaxSize); + var mavTaskShorts = CheckByWindowAverageMeanForShotrs(data, message, windowMaxSize); + var ltTask = CheckByLocalTrends(data, message, windowMaxSize); + var areasTask = GetAreasRelation(data, message); + var positionTask = CheckPosition(message); + var trendTask = CalcTrendDiff(message); + + await Task.WhenAll(mavTask, ltTask, areasTask, positionTask, trendTask, mavTaskShorts); + var assetType = _tradeDataProvider.GetAssetTypeByFigi(message.Figi); + var res = mavTask.Result | ltTask.Result; + + if ((res & TradingEvent.UptrendStart) == TradingEvent.UptrendStart + && !LongOpeningStops.ContainsKey(message.Figi) + && trendTask.Result.HasValue + && System.Math.Abs(trendTask.Result.Value)<6 && state == ExchangeState.Open - && data.timestamps.Length > 1 - && (data.timestamps[data.timestamps.Length - 1] - data.timestamps[data.timestamps.Length - 2] < TimeSpan.FromMinutes(1)) + && areasTask.Result.HasValue + && (areasTask.Result.Value >= 20 && areasTask.Result.Value < 75) + && (positionTask.Result == ValueAmplitudePosition.LowerThenMediana) ) { - if (areasRel >= 20 && areasRel < 75) + if (!message.IsHistoricalData && BotModeSwitcher.CanPurchase()) { - if (!message.IsHistoricalData && BotModeSwitcher.CanPurchase()) + var accounts = _tradeDataProvider.Accounts + .Where(a => !a.Value.Assets.ContainsKey(message.Figi)) + .ToArray(); + var loggedDeclisions = 0; + foreach (var acc in accounts) { - var accounts = _tradeDataProvider.Accounts - .Where(a => !a.Value.Assets.ContainsKey(message.Figi)) - .ToArray(); - var loggedDeclisions = 0; - foreach (var acc in accounts) + if (IsBuyAllowed(acc.Value, message.Value, 1, _accountCashPartFutures, _accountCashPart)) { - if (IsBuyAllowed(acc.Value, message.Value, 1, _accountCashPartFutures, _accountCashPart)) + if (RandomNumberGenerator.GetInt32(100) > 50) { - if (RandomNumberGenerator.GetInt32(100) > 50) + var command = new TradeCommand() { - var command = new TradeCommand() - { - AccountId = acc.Value.AccountId, - Figi = message.Figi, - CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketBuy, - Count = 1, - RecomendPrice = null, - }; - await _dataBus.Broadcast(command); - _logger.LogWarning("Покупка актива {figi}! id команды {commandId}. Направление сделки: {dir}; Количество активов: {count}; Разрешена ли маржиналка: {margin}", - message.Figi, command.CommandId, command.CommandType, command.Count, command.EnableMargin); - if (loggedDeclisions == 0) - { - await LogDeclision(DeclisionTradeAction.OpenLongReal, message); - OpeningStops[message.Figi] = DateTime.UtcNow.AddMinutes(1); - loggedDeclisions++; - } + AccountId = acc.Value.AccountId, + Figi = message.Figi, + CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketBuy, + Count = 1, + RecomendPrice = null, + }; + await _dataBus.Broadcast(command); + _logger.LogWarning("Покупка актива {figi}! id команды {commandId}. Направление сделки: {dir}; Количество активов: {count}; Разрешена ли маржиналка: {margin}", + message.Figi, command.CommandId, command.CommandType, command.Count, command.EnableMargin); + if (loggedDeclisions == 0) + { + await LogDeclision(DeclisionTradeAction.OpenLongReal, message); + LongOpeningStops[message.Figi] = message.Time.AddMinutes(1); + loggedDeclisions++; } } } } - await LogDeclision(DeclisionTradeAction.OpenLong, message); } - } - if ((res & TradingEvent.UptrendEnd) == TradingEvent.UptrendEnd) + await LogDeclision(DeclisionTradeAction.OpenLong, message); + } + if ((res & TradingEvent.UptrendEnd) == TradingEvent.UptrendEnd && positionTask.Result != ValueAmplitudePosition.LowerThenMediana) { - var assetType = _tradeDataProvider.GetAssetTypeByFigi(message.Figi); - var loggedDeclisions = 0; if (!message.IsHistoricalData && BotModeSwitcher.CanSell()) { + var loggedDeclisions = 0; var assetsForClose = _tradeDataProvider.Accounts .SelectMany(a => a.Value.Assets.Values) .Where(a => a.Figi == message.Figi && a.Count > 0) @@ -268,9 +395,9 @@ namespace KLHZ.Trader.Core.Exchange.Services GetComission(assetType), GetLeverage(message.Figi, asset.Count < 0), asset.Count < 0); } var stoppingKey = message.Figi + asset.AccountId; - if (profit > 0 && !ClosingStops.ContainsKey(stoppingKey)) + if (profit > 0) { - ClosingStops[stoppingKey] = DateTime.UtcNow.AddSeconds(30); + //ClosingStops[stoppingKey] = DateTime.UtcNow.AddSeconds(30); var command = new TradeCommand() { AccountId = asset.AccountId, @@ -293,6 +420,98 @@ namespace KLHZ.Trader.Core.Exchange.Services } await LogDeclision(DeclisionTradeAction.CloseLong, message); } + + if ((mavTaskShorts.Result & TradingEvent.UptrendEnd) == TradingEvent.UptrendEnd) + { + if (trendTask.Result.HasValue && trendTask.Result.Value < -3) + { + if (!message.IsHistoricalData) + { + var accounts = _tradeDataProvider.Accounts + .Where(a => !a.Value.Assets.ContainsKey(message.Figi)) + .ToArray(); + var loggedDeclisions = 0; + foreach (var acc in accounts) + { + if (BotModeSwitcher.CanSell()) + { + if (RandomNumberGenerator.GetInt32(100) > 50) + { + var command = new TradeCommand() + { + AccountId = acc.Value.AccountId, + Figi = message.Figi, + CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketSell, + Count = 1, + RecomendPrice = null, + EnableMargin = true + }; + await _dataBus.Broadcast(command); + _logger.LogWarning("Открытие шорта {figi}! id команды {commandId}. Направление сделки: {dir}; Количество активов: {count}; Разрешена ли маржиналка: {margin}", + message.Figi, command.CommandId, command.CommandType, command.Count, command.EnableMargin); + if (loggedDeclisions == 0) + { + await LogDeclision(DeclisionTradeAction.OpenShortReal, message); + loggedDeclisions++; + } + } + } + } + } + await LogDeclision(DeclisionTradeAction.OpenShort, message); + } + } + + if ((res & TradingEvent.DowntrendEnd) == TradingEvent.DowntrendEnd) + { + if (!ShortClosingStops.ContainsKey(message.Figi)) + { + if (!message.IsHistoricalData && BotModeSwitcher.CanPurchase()) + { + var loggedDeclisions = 0; + var assetsForClose = _tradeDataProvider.Accounts + .SelectMany(a => a.Value.Assets.Values) + .Where(a => a.Figi == message.Figi && a.Count < 0) + .ToArray(); + foreach (var asset in assetsForClose) + { + var profit = 0m; + + if (assetType == AssetType.Futures) + { + profit = TradingCalculator.CaclProfit(asset.BoughtPrice, message.Value, + GetComission(assetType), GetLeverage(message.Figi, asset.Count < 0), asset.Count < 0); + } + if (profit > 0) + { + var command = new TradeCommand() + { + AccountId = asset.AccountId, + Figi = message.Figi, + CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketBuy, + Count = System.Math.Abs((long)asset.Count), + RecomendPrice = null, + EnableMargin = false, + }; + await _dataBus.Broadcast(command); + _logger.LogWarning("Продажа актива {figi}! id команды {commandId}. Направление сделки: {dir}; Количество активов: {count}; Разрешена ли маржиналка: {margin}", + message.Figi, command.CommandId, command.CommandType, command.Count, command.EnableMargin); + if (loggedDeclisions == 0) + { + loggedDeclisions++; + await LogDeclision(DeclisionTradeAction.CloseShortReal, message, profit); + } + } + } + } + + if (message.IsHistoricalData) + { + ShortClosingStops[message.Figi] = message.Time.AddSeconds(30); + } + await LogDeclision(DeclisionTradeAction.CloseShort, message); + } + } } private async Task ProcessClearing((DateTime[] timestamps, decimal[] prices) data, ExchangeState state, INewPrice message) @@ -308,18 +527,18 @@ namespace KLHZ.Trader.Core.Exchange.Services private void ProcessStops(INewPrice message, DateTime currentTime) { - if (OpeningStops.TryGetValue(message.Figi, out var dt)) + if (LongOpeningStops.TryGetValue(message.Figi, out var dt)) { if (dt < currentTime) { - OpeningStops.TryRemove(message.Figi, out _); + LongOpeningStops.TryRemove(message.Figi, out _); } } - if (ClosingStops.TryGetValue(message.Figi, out var dt2)) + if (ShortClosingStops.TryGetValue(message.Figi, out var dt2)) { if (dt2 < currentTime) { - ClosingStops.TryRemove(message.Figi, out _); + ShortClosingStops.TryRemove(message.Figi, out _); } } } diff --git a/KLHZ.Trader.Core/Exchange/Services/TraderDataProvider.cs b/KLHZ.Trader.Core/Exchange/Services/TraderDataProvider.cs index 14d9a00..87a56db 100644 --- a/KLHZ.Trader.Core/Exchange/Services/TraderDataProvider.cs +++ b/KLHZ.Trader.Core/Exchange/Services/TraderDataProvider.cs @@ -9,6 +9,7 @@ using KLHZ.Trader.Core.DataLayer.Entities.Prices; using KLHZ.Trader.Core.Exchange.Extentions; using KLHZ.Trader.Core.Exchange.Models.AssetsAccounting; using KLHZ.Trader.Core.Exchange.Models.Configs; +using KLHZ.Trader.Core.Math.Declisions.Dtos.FFT; using KLHZ.Trader.Core.Math.Declisions.Services.Cache; using KLHZ.Trader.Core.Math.Declisions.Utils; using Microsoft.EntityFrameworkCore; @@ -34,6 +35,7 @@ namespace KLHZ.Trader.Core.Exchange.Services private readonly string[] _managedAccountsNamePatterns = []; private readonly string[] _instrumentsFigis = []; + private readonly ConcurrentDictionary _fftResults = new(); private readonly ConcurrentDictionary _instrumentsSettings = new(); private readonly ConcurrentDictionary _tickersCache = new(); private readonly ConcurrentDictionary _assetTypesCache = new(); @@ -57,6 +59,21 @@ namespace KLHZ.Trader.Core.Exchange.Services } } + public ValueTask GetFFtResult(string figi) + { + if (_fftResults.TryGetValue(figi, out var res)) + { + return ValueTask.FromResult(res); + } + return ValueTask.FromResult(FFTAnalyzeResult.Empty); + } + + public ValueTask SetFFtResult(FFTAnalyzeResult result) + { + _fftResults[result.Key] = result; + return ValueTask.CompletedTask; + } + public async ValueTask<(DateTime[] timestamps, decimal[] prices, bool isFullIntervalExists)> GetData(string figi, TimeSpan timeSpan) { if (_historyCash.TryGetValue(figi, out var unit)) @@ -221,8 +238,10 @@ namespace KLHZ.Trader.Core.Exchange.Services internal async Task SyncPortfolio(ManagedAccount account) { + try { + await _initSemaphore.WaitAsync(TimeSpan.FromSeconds(5)); var portfolio = await _investApiClient.Operations.GetPortfolioAsync(new PortfolioRequest() { AccountId = account.AccountId, @@ -252,6 +271,7 @@ namespace KLHZ.Trader.Core.Exchange.Services else { price = position.AveragePositionPrice; + } #pragma warning disable CS0612 // Тип или член устарел @@ -291,6 +311,7 @@ namespace KLHZ.Trader.Core.Exchange.Services { _logger.LogError(ex, "Ошибка при синхранизации портфеля счёта {accountId}", account.AccountId); } + _initSemaphore.Release(); } internal async Task UpdateFuturesPrice(INewPrice newPrice, decimal newPriceValue) diff --git a/KLHZ.Trader.Core/Exchange/Services/TradingCommandsExecutor.cs b/KLHZ.Trader.Core/Exchange/Services/TradingCommandsExecutor.cs index cc34d6a..8c61e0a 100644 --- a/KLHZ.Trader.Core/Exchange/Services/TradingCommandsExecutor.cs +++ b/KLHZ.Trader.Core/Exchange/Services/TradingCommandsExecutor.cs @@ -3,10 +3,8 @@ using KLHZ.Trader.Core.Contracts.Messaging.Interfaces; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Threading.Channels; -using Telegram.Bot.Types; using Tinkoff.InvestApi; using Tinkoff.InvestApi.V1; -using static Microsoft.EntityFrameworkCore.DbLoggerCategory.Database; namespace KLHZ.Trader.Core.Exchange.Services { diff --git a/KLHZ.Trader.Service/Controllers/PlayController.cs b/KLHZ.Trader.Service/Controllers/PlayController.cs index 4414838..0645238 100644 --- a/KLHZ.Trader.Service/Controllers/PlayController.cs +++ b/KLHZ.Trader.Service/Controllers/PlayController.cs @@ -7,7 +7,6 @@ using KLHZ.Trader.Core.Exchange.Services; using KLHZ.Trader.Core.Math.Declisions.Utils; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using System; namespace KLHZ.Trader.Service.Controllers { @@ -31,12 +30,12 @@ namespace KLHZ.Trader.Service.Controllers { try { - var time1 = DateTime.UtcNow.AddDays(-30); - var time2 = DateTime.UtcNow.AddMinutes(18); + var time1 = DateTime.UtcNow.AddDays(-16.5); + //var time2 = DateTime.UtcNow.AddMinutes(18); using var context1 = await _dbContextFactory.CreateDbContextAsync(); context1.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; var data = await context1.PriceChanges - .Where(c => c.Figi == figi && c.Time >= time1 && c.Time <= time2) + .Where(c => c.Figi == figi && c.Time >= time1) .OrderBy(c => c.Time) .Select(c => new NewPriceMessage() { @@ -220,11 +219,11 @@ namespace KLHZ.Trader.Service.Controllers .ToArrayAsync(); var buffer = new List(); - var res = SignalProcessing.InterpolateData(data.Select(d => d.Time).ToArray(), data.Select(d => (double)d.Value).ToArray(), + var res = SignalProcessing.InterpolateData(data.Select(d => d.Time).ToArray(), data.Select(d => d.Value).ToArray(), TimeSpan.FromSeconds(1)); - for (int i=0;i< res.Item1.Length; i++) + for (int i = 0; i < res.Item1.Length; i++) { buffer.Add(new ProcessedPrice() { @@ -234,7 +233,7 @@ namespace KLHZ.Trader.Service.Controllers Count = 1, Direction = 0, Time = res.Item1[i], - Value =(decimal) res.Item2[i] + Value = (decimal)res.Item2[i] }); } await context1.ProcessedPrices.AddRangeAsync(buffer); @@ -262,12 +261,12 @@ namespace KLHZ.Trader.Service.Controllers .ToArrayAsync(); var buffer = new List(); - var values = data.Select(d => (double)d.Value).ToArray(); + var values = data.Select(d => d.Value).ToArray(); var times = data.Select(d => d.Time).ToArray(); var res = SignalProcessing.InterpolateData(times, values, TimeSpan.FromSeconds(10)); - FFT.GetMainHarmonictPeriod(res.Item2, res.Item1.Last() - res.Item1.First()); + //FFT.GetMainHarmonictPeriod(res.Item2, res.Item1.Last() - res.Item1.First()); } catch (Exception ex)