From 067d7d138d370a8eaa92a9a26024afdb9905bb61 Mon Sep 17 00:00:00 2001 From: vlad zverzhkhovskiy Date: Sat, 13 Sep 2025 10:52:45 +0300 Subject: [PATCH] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- KLHZ.Trader.Core.Math/Declisions/Utils/FFT.cs | 29 ++++ .../Declisions/Utils/LocalTrends.cs | 162 ++++++++---------- .../KLHZ.Trader.Core.Math.csproj | 1 + KLHZ.Trader.Core.Tests/FFTTests.cs | 31 ++++ KLHZ.Trader.Core.Tests/LocalTrendsTests.cs | 132 ++++++++++++++ KLHZ.Trader.Core/Common/BotModeSwitcher.cs | 4 +- .../TG/Services/BotMessagesHandler.cs | 15 +- 7 files changed, 284 insertions(+), 90 deletions(-) create mode 100644 KLHZ.Trader.Core.Math/Declisions/Utils/FFT.cs create mode 100644 KLHZ.Trader.Core.Tests/FFTTests.cs create mode 100644 KLHZ.Trader.Core.Tests/LocalTrendsTests.cs diff --git a/KLHZ.Trader.Core.Math/Declisions/Utils/FFT.cs b/KLHZ.Trader.Core.Math/Declisions/Utils/FFT.cs new file mode 100644 index 0000000..141d90e --- /dev/null +++ b/KLHZ.Trader.Core.Math/Declisions/Utils/FFT.cs @@ -0,0 +1,29 @@ +using MathNet.Numerics; +using MathNet.Numerics.IntegralTransforms; + +namespace KLHZ.Trader.Core.Math.Declisions.Utils +{ + public static class FFT + { + public static void Test() + { + var da = new List(); + for (int i = 0; i < 1000; i++) + { + da.Add((float)System.Math.Sin(0.01 * i)); + } + + 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++) + { + var d = res[i] - start[i]; + } + } + } +} diff --git a/KLHZ.Trader.Core.Math/Declisions/Utils/LocalTrends.cs b/KLHZ.Trader.Core.Math/Declisions/Utils/LocalTrends.cs index eb632ed..733d5a3 100644 --- a/KLHZ.Trader.Core.Math/Declisions/Utils/LocalTrends.cs +++ b/KLHZ.Trader.Core.Math/Declisions/Utils/LocalTrends.cs @@ -1,6 +1,5 @@ using KLHZ.Trader.Core.Contracts.Declisions.Dtos.Enums; -using KLHZ.Trader.Core.Contracts.Declisions.Interfaces; -using KLHZ.Trader.Core.Math.Declisions.Dtos; +using MathNet.Numerics; namespace KLHZ.Trader.Core.Math.Declisions.Utils { @@ -9,108 +8,97 @@ namespace KLHZ.Trader.Core.Math.Declisions.Utils /// public static class LocalTrends { - public static TradingEvent CheckByLocalTrends(DateTime[] times, decimal[] prices, TimeSpan firstPeriod, TimeSpan secondPeriod, decimal meanfullDiff, int boundIndex) + internal static bool TryGetLocalTrends(DateTime[] times, decimal[] prices, TimeSpan firstPeriod, TimeSpan lastPeriod, + double meanfullDiff, out TradingEvent res) { - var res = TradingEvent.None; - res |= CheckUptrendStart(times, prices, firstPeriod, secondPeriod, meanfullDiff, boundIndex); - res |= CheckUptrendEnd(times, prices, firstPeriod, secondPeriod, meanfullDiff, boundIndex); + res = TradingEvent.None; + var success = false; - return res; - } + if (times.Length == 0) + { + return success; + } + var x1 = new List(); + var y1 = new List(); + var x2 = new List(); + var y2 = new List(); + var y1_approximated = new List(); + var y2_approximated = new List(); - internal static TradingEvent CheckUptrendStart(DateTime[] times, decimal[] prices, TimeSpan firstPeriod, TimeSpan secondPeriod, decimal meanfullDiff, int boundIndex) - { - var periodStat = GetTwoPeriodsProcessingData(times, prices, firstPeriod, secondPeriod, boundIndex, meanfullDiff); - var isStartOk = periodStat.Success && periodStat.DiffStart < 0.5m * meanfullDiff; - var isEndOk = periodStat.Success && periodStat.DiffEnd >= meanfullDiff; - return isStartOk && isEndOk && prices[periodStat.Start] - prices[periodStat.End] >= meanfullDiff ? TradingEvent.UptrendStart : TradingEvent.None; - } - - internal static TradingEvent CheckUptrendEnd(DateTime[] times, decimal[] prices, TimeSpan firstPeriod, TimeSpan secondPeriod, decimal meanfullDiff, int boundIndex) - { - var periodStat = GetTwoPeriodsProcessingData(times, prices, firstPeriod, secondPeriod, boundIndex, meanfullDiff); - var isStartOk = periodStat.Success && periodStat.DiffStart > 0 && periodStat.DiffStart > 1.5m * meanfullDiff; - var isEndOk = periodStat.Success && periodStat.DiffEnd < meanfullDiff; - return isStartOk && isEndOk && prices[periodStat.End] - prices[periodStat.Start] >= meanfullDiff ? TradingEvent.UptrendEnd : TradingEvent.None; ; - } - - internal static TwoLocalTrendsResultDto GetTwoPeriodsProcessingData(DateTime[] times, decimal[] prices, TimeSpan firstPeriod, TimeSpan lastPeriod, int boundIndex, decimal meanfullDiff) - { - var res = new TwoLocalTrendsResultDto(success: false, 0, 0, 0, 0, 0, TimeSpan.Zero, TimeSpan.Zero); - int count = -1; var lastTime = times[times.Length - 1]; - var bound = -1; - var start = -1; - var end = times.Length - 1; + var firstTime = times[0]; + var fullPeriod = firstPeriod + lastPeriod; - for (int i = times.Length - 1; i > -1; i--) + if (lastTime - firstTime > fullPeriod) { - if (count > 0 && bound < 0 && (count == boundIndex || lastTime - times[i] >= lastPeriod)) + for (int i = 1; i < times.Length - 1; i++) { - bound = i; - lastPeriod = lastTime - times[i]; - } - if (lastTime - times[i] >= lastPeriod + firstPeriod) - { - start = i; - - break; + var dt1 = lastTime - times[times.Length - i]; + if (dt1 <= lastPeriod) + { + x2.Add((times[times.Length - i] - firstTime).TotalSeconds); + y2.Add((double)prices[times.Length - i]); + } + else if (dt1 <= fullPeriod) + { + x1.Add((times[times.Length - i] - firstTime).TotalSeconds); + y1.Add((double)prices[times.Length - i]); + } + else + { + success = true; + break; + } } - count++; + 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; + } + success = true; } - - if (start < bound && start >= 0 && bound > 0) - { - var diff1 = prices[bound] - prices[start]; - var diff2 = prices[end] - prices[bound]; - res = new TwoLocalTrendsResultDto(true, diff1, diff2, start, bound, end, times[bound] - times[start], times[end] - times[bound]); - } - return res; - } - internal static bool CheckLongOpen(this IPriceHistoryCacheUnit unit, TimeSpan firstPeriod, TimeSpan secondPeriod, decimal meanfullDiff, int boundIndex) - { - var data = unit.GetData().Result; - var periodStat = GetTwoPeriodsProcessingData(data.timestamps, data.prices, firstPeriod, secondPeriod, boundIndex, meanfullDiff); - var isStartOk = periodStat.Success && periodStat.DiffStart < -meanfullDiff; - var isEndOk = periodStat.Success && periodStat.DiffEnd >= meanfullDiff; - return isStartOk && isEndOk && data.prices[periodStat.Start] - data.prices[periodStat.End] >= meanfullDiff; + return success; } - internal static bool CheckLongClose(this IPriceHistoryCacheUnit unit, TimeSpan firstPeriod, TimeSpan secondPeriod, decimal meanfullDiff, int boundIndex) + internal static bool TryCalcTrendDiff(DateTime[] times, decimal[] prices, out decimal diff) { - var data = unit.GetData().Result; - var periodStat = GetTwoPeriodsProcessingData(data.timestamps, data.prices, firstPeriod, secondPeriod, boundIndex, meanfullDiff); - var isStartOk = periodStat.Success && periodStat.DiffStart > 0 && periodStat.DiffStart > 1.5m * meanfullDiff; - var isEndOk = periodStat.Success && periodStat.DiffEnd < meanfullDiff; - return isStartOk && isEndOk && data.prices[periodStat.End] - data.prices[periodStat.Start] >= meanfullDiff; - } - - internal static TradingEvent Detect(IPriceHistoryCacheUnit data) - { - decimal meanfullDiff = 1m; - - var res = TradingEvent.None; - //var downtrendStarts = data.CheckDowntrendStarting(TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(7), meanfullDiff); - var uptrendStarts = data.CheckLongOpen(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(7), meanfullDiff, 3); - var uptrendStarts2 = data.CheckLongOpen(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(3), meanfullDiff, 2); - var downtrendEnds = data.CheckLongOpen(TimeSpan.FromSeconds(120), TimeSpan.FromSeconds(10), meanfullDiff, 5); - uptrendStarts |= downtrendEnds; - uptrendStarts |= uptrendStarts2; - if (uptrendStarts) + diff = 0; + if (times.Length <= 1 || times.Length != prices.Length) { - res |= TradingEvent.LongClose; + return false; } - //var downtrendEnds = data.CheckDowntrendEnding(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(15), meanfullDiff); - - var uptrendEnds = data.CheckLongClose(TimeSpan.FromSeconds(15), TimeSpan.FromSeconds(20), meanfullDiff * 1.5m, 8); - var uptrendEnds2 = data.CheckLongClose(TimeSpan.FromSeconds(120), TimeSpan.FromSeconds(30), meanfullDiff, 8); - uptrendEnds |= uptrendEnds2; - if (uptrendEnds) + else { - res |= TradingEvent.LongOpen; + var startTime = times[0]; + var x = new double[times.Length]; + for (int i = 0; i < times.Length - 1; i++) + { + x[i] = (times[i] - startTime).TotalSeconds; + } + var line = Fit.Line(x.ToArray(), prices.Select(p => (double)p).ToArray()); + + var p1 = line.A + line.B * 0; + var p2 = line.A + line.B * (times[times.Length - 1] - times[0]).TotalSeconds; + + diff = (decimal)(p2 - p1); + return true; } - return res; } } } diff --git a/KLHZ.Trader.Core.Math/KLHZ.Trader.Core.Math.csproj b/KLHZ.Trader.Core.Math/KLHZ.Trader.Core.Math.csproj index a52c9ca..94c4fe2 100644 --- a/KLHZ.Trader.Core.Math/KLHZ.Trader.Core.Math.csproj +++ b/KLHZ.Trader.Core.Math/KLHZ.Trader.Core.Math.csproj @@ -7,6 +7,7 @@ + diff --git a/KLHZ.Trader.Core.Tests/FFTTests.cs b/KLHZ.Trader.Core.Tests/FFTTests.cs new file mode 100644 index 0000000..59ac4b3 --- /dev/null +++ b/KLHZ.Trader.Core.Tests/FFTTests.cs @@ -0,0 +1,31 @@ +using MathNet.Numerics; +using MathNet.Numerics.IntegralTransforms; +using System.Security.Cryptography; + +namespace KLHZ.Trader.Core.Tests +{ + public class FFTTests + { + [Test] + public static void Test() + { + var da = new List(); + for (int i = 0; i < 100; i++) + { + da.Add((float)System.Math.Sin(0.5 * i) + (float)(RandomNumberGenerator.GetInt32(0, 100)) / 300); + } + + 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++) + { + var d = res[i] - start[i]; + } + } + } +} diff --git a/KLHZ.Trader.Core.Tests/LocalTrendsTests.cs b/KLHZ.Trader.Core.Tests/LocalTrendsTests.cs new file mode 100644 index 0000000..ed764ea --- /dev/null +++ b/KLHZ.Trader.Core.Tests/LocalTrendsTests.cs @@ -0,0 +1,132 @@ +using KLHZ.Trader.Core.Math.Declisions.Utils; + +namespace KLHZ.Trader.Core.Tests +{ + public class LocalTrendsTests + { + public static (DateTime[] timestamps, decimal[] prices) GetData(int bound, decimal sign = 1m) + { + var date1 = DateTime.UtcNow.AddSeconds(-2 * bound); + var val = 0m; + var timestamps = new List(); + var prices = new List(); + var step = 0.5m; + for (int i = 0; i < 2 * bound; i++) + { + date1 = date1.AddSeconds(1); + timestamps.Add(date1); + val += step * sign; + prices.Add(val); + if (i == bound) + { + sign = sign * -1; + } + } + + return (timestamps.ToArray(), prices.ToArray()); + } + + [Test] + public void TryGetLocalTrends1() + { + var data = GetData(50); + + if (LocalTrends.TryGetLocalTrends(data.timestamps, data.prices, TimeSpan.FromSeconds(49), TimeSpan.FromSeconds(49), 22, out var res)) + { + Assert.That(res == Contracts.Declisions.Dtos.Enums.TradingEvent.UptrendEnd); + } + else + { + Assert.Fail(); + } + } + + [Test] + public void TryGetLocalTrends2() + { + var data = GetData(50, -1); + + if (LocalTrends.TryGetLocalTrends(data.timestamps, data.prices, TimeSpan.FromSeconds(49), TimeSpan.FromSeconds(49), 22, out var res)) + { + Assert.That(res == Contracts.Declisions.Dtos.Enums.TradingEvent.UptrendStart); + } + else + { + Assert.Fail(); + } + } + + [Test] + public void TryGetLocalTrends3() + { + var data = GetData(100, -1); + + if (LocalTrends.TryGetLocalTrends(data.timestamps, data.prices, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30), 10, out var res)) + { + Assert.That(res == Contracts.Declisions.Dtos.Enums.TradingEvent.None); + } + else + { + Assert.Fail(); + } + } + + [Test] + public void TryGetLocalTrends4() + { + if (LocalTrends.TryGetLocalTrends(Array.Empty(), Array.Empty(), TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30), 10, out var res)) + { + Assert.That(res == Contracts.Declisions.Dtos.Enums.TradingEvent.None); + } + else + { + Assert.Pass(); + } + } + + [Test] + public void TryGetLocalTrends5() + { + if (LocalTrends.TryGetLocalTrends(new DateTime[1] { DateTime.UtcNow }, new decimal[] { 1m }, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30), 10, out var res)) + { + Assert.That(res == Contracts.Declisions.Dtos.Enums.TradingEvent.None); + } + else + { + Assert.Pass(); + } + } + + [Test] + public void TryCalcTrendDiff1() + { + var data = GetData(500, -1); + + if (LocalTrends.TryCalcTrendDiff(data.timestamps.Take(100).ToArray(), data.prices.Take(100).ToArray(), out var res)) + { + Assert.That(res < 0); + Assert.That(System.Math.Abs(res) <= 50 && System.Math.Abs(res) > 40); + } + else + { + Assert.Fail(); + } + } + + [Test] + public void TryCalcTrendDiff2() + { + var data = GetData(500); + + if (LocalTrends.TryCalcTrendDiff(data.timestamps.Take(100).ToArray(), data.prices.Take(100).ToArray(), out var res)) + { + Assert.That(res > 0); + Assert.That(System.Math.Abs(res) <= 50 && System.Math.Abs(res) > 40); + } + else + { + Assert.Fail(); + } + } + } +} diff --git a/KLHZ.Trader.Core/Common/BotModeSwitcher.cs b/KLHZ.Trader.Core/Common/BotModeSwitcher.cs index 438658c..5efaa4e 100644 --- a/KLHZ.Trader.Core/Common/BotModeSwitcher.cs +++ b/KLHZ.Trader.Core/Common/BotModeSwitcher.cs @@ -3,8 +3,8 @@ public static class BotModeSwitcher { private readonly static object _locker = new(); - private static bool _canSell = true; - private static bool _canPurchase = true; + private static bool _canSell = false; + private static bool _canPurchase = false; public static bool CanSell() { diff --git a/KLHZ.Trader.Core/TG/Services/BotMessagesHandler.cs b/KLHZ.Trader.Core/TG/Services/BotMessagesHandler.cs index 3a7e2b8..d4da307 100644 --- a/KLHZ.Trader.Core/TG/Services/BotMessagesHandler.cs +++ b/KLHZ.Trader.Core/TG/Services/BotMessagesHandler.cs @@ -89,7 +89,20 @@ namespace KLHZ.Trader.Core.TG.Services CommandType = TradeCommandType.MarketSell, RecomendPrice = null, Figi = asset.Figi, - Count = (long)asset.Count, + Count = System.Math.Abs((long)asset.Count), + EnableMargin = false, + }; + await _eventBus.Broadcast(command); + } + if (asset.Count < 0) + { + var command = new TradeCommand() + { + AccountId = asset.AccountId, + CommandType = TradeCommandType.MarketBuy, + RecomendPrice = null, + Figi = asset.Figi, + Count = System.Math.Abs((long)asset.Count), EnableMargin = false, }; await _eventBus.Broadcast(command);