단순 이관
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
using Bitget_Server.Application.Options;
|
||||
using Bitget_Server.Application.State;
|
||||
using Bitget_Server.Domain.Enums;
|
||||
using Bitget_Server.Infrastructure.Bitget;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Bitget_Server.Application.BackgroundServices;
|
||||
|
||||
/// <summary>
|
||||
/// 기존 MainForm.getMarketInfoList_WebService (300ms loop) 이식.
|
||||
/// USDT-FUTURES 마켓 Ticker 정보를 주기적으로 새로고침하여 MarketState.marketInfoList 를 갱신한다.
|
||||
/// </summary>
|
||||
public class MarketInfoBackgroundService : BackgroundService
|
||||
{
|
||||
private readonly BitgetClient _client;
|
||||
private readonly BitgetOptions _options;
|
||||
private readonly MarketState _state;
|
||||
private readonly ILogger<MarketInfoBackgroundService> _logger;
|
||||
|
||||
public MarketInfoBackgroundService(
|
||||
BitgetClient client,
|
||||
IOptions<BitgetOptions> options,
|
||||
MarketState state,
|
||||
ILogger<MarketInfoBackgroundService> logger)
|
||||
{
|
||||
_client = client;
|
||||
_options = options.Value;
|
||||
_state = state;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("MarketInfoBackgroundService 시작 (Interval={Ms}ms)", _options.MarketInfoUpdateIntervalMs);
|
||||
|
||||
var delay = TimeSpan.FromMilliseconds(Math.Max(100, _options.MarketInfoUpdateIntervalMs));
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(delay, stoppingToken).ConfigureAwait(false);
|
||||
|
||||
await _client.SendWebServerAsync(
|
||||
ExchangeWebReqType.BitgetMarket_Future,
|
||||
ct: stoppingToken).ConfigureAwait(false);
|
||||
|
||||
// 결과는 BitgetClient.Parse 가 _state.marketInfoList 에 반영함
|
||||
}
|
||||
catch (OperationCanceledException) { /* shutdown */ }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "MarketInfo 업데이트 오류");
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("MarketInfoBackgroundService 종료");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using Bitget_Server.Application.Services;
|
||||
|
||||
namespace Bitget_Server.Application.BackgroundServices;
|
||||
|
||||
/// <summary>
|
||||
/// 기존 MainForm.Signalnfo → IndicatorProcess.calProcess 무한 루프 이식.
|
||||
/// SignalProcessService.ProcessOnceAsync 를 짧은 주기로 반복 호출한다.
|
||||
/// </summary>
|
||||
public class SignalProcessBackgroundService : BackgroundService
|
||||
{
|
||||
private readonly SignalProcessService _signalProcess;
|
||||
private readonly ILogger<SignalProcessBackgroundService> _logger;
|
||||
|
||||
/// <summary>신호 검색 루프 1회 처리 후 대기 시간 (기존 while 루프는 Thread.Sleep 없이 반복되었으나 CPU 과점유 방지를 위해 200ms 부여).</summary>
|
||||
private static readonly TimeSpan LoopInterval = TimeSpan.FromMilliseconds(200);
|
||||
|
||||
public SignalProcessBackgroundService(
|
||||
SignalProcessService signalProcess,
|
||||
ILogger<SignalProcessBackgroundService> logger)
|
||||
{
|
||||
_signalProcess = signalProcess;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("SignalProcessBackgroundService 시작 (Interval={Ms}ms)", LoopInterval.TotalMilliseconds);
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _signalProcess.ProcessOnceAsync(stoppingToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) { /* shutdown */ }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "SignalProcess 1회 처리 오류");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(LoopInterval, stoppingToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) { /* shutdown */ }
|
||||
}
|
||||
|
||||
_logger.LogInformation("SignalProcessBackgroundService 종료");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using Bitget_Server.Application.Options;
|
||||
using Bitget_Server.Application.State;
|
||||
using Bitget_Server.Domain.Enums;
|
||||
using Bitget_Server.Infrastructure.Bitget;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Bitget_Server.Application.BackgroundServices;
|
||||
|
||||
/// <summary>
|
||||
/// 기존 MainForm.DataCalculateThread (1000ms loop) 이식.
|
||||
/// BitgetWebSocketClient 가 수집한 메시지를 주기적으로 꺼내어 candleDataLists 로 병합한다.
|
||||
/// </summary>
|
||||
public class WebSocketCalcBackgroundService : BackgroundService
|
||||
{
|
||||
private readonly BitgetWebSocketClient _ws;
|
||||
private readonly MarketState _state;
|
||||
private readonly BitgetOptions _options;
|
||||
private readonly ILogger<WebSocketCalcBackgroundService> _logger;
|
||||
|
||||
public WebSocketCalcBackgroundService(
|
||||
BitgetWebSocketClient ws,
|
||||
MarketState state,
|
||||
IOptions<BitgetOptions> options,
|
||||
ILogger<WebSocketCalcBackgroundService> logger)
|
||||
{
|
||||
_ws = ws;
|
||||
_state = state;
|
||||
_options = options.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("WebSocketCalcBackgroundService 시작 (Interval={Ms}ms)", _options.WebSocketCalcIntervalMs);
|
||||
|
||||
var delay = TimeSpan.FromMilliseconds(Math.Max(200, _options.WebSocketCalcIntervalMs));
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(delay, stoppingToken).ConfigureAwait(false);
|
||||
|
||||
// WebSocket 이 꺼져 있으면 처리할 데이터도 없으므로 skip
|
||||
if (!_ws.IsConnected) continue;
|
||||
|
||||
List<object> intervalsSnapshot;
|
||||
lock (_state.SyncRoot)
|
||||
intervalsSnapshot = _state.IntervalList.ToList();
|
||||
|
||||
_ws.CalculateDataList(BitgetWebSocketType.캔들정보, intervalsSnapshot);
|
||||
}
|
||||
catch (OperationCanceledException) { /* shutdown */ }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "WebSocket 집계 루프 오류");
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("WebSocketCalcBackgroundService 종료");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using Bitget_Server.Domain.Indicators;
|
||||
using Bitget_Server.Domain.Models;
|
||||
|
||||
namespace Bitget_Server.Application.Indicators;
|
||||
|
||||
[Serializable]
|
||||
public class IchimokuConditionSetup
|
||||
{
|
||||
/// <summary>기준선 기간</summary>
|
||||
public int BaselinePeriod { get; set; } = 26;
|
||||
/// <summary>전환선 기간</summary>
|
||||
public int ConversionPeriod { get; set; } = 9;
|
||||
/// <summary>선행스팬2 기간</summary>
|
||||
public int LaggingSpan2Period { get; set; } = 52;
|
||||
/// <summary>Shift</summary>
|
||||
public int Displacement { get; set; } = 26;
|
||||
|
||||
// 캔들당 일목균형표 계산하기
|
||||
public void calIchimokuData(List<CandleData> candleDatas, CandleData candleData)
|
||||
{
|
||||
// 현재 종가 인덱스
|
||||
var endIndex = candleDatas.Count() - 1;
|
||||
|
||||
if (candleData == null) return;
|
||||
|
||||
if (candleData.IchimokData == null) candleData.IchimokData = new IchimokData();
|
||||
|
||||
var endValue = candleData.Close;
|
||||
|
||||
// 전봉까지 만듬
|
||||
var datas = candleDatas.FindAll(i =>
|
||||
candleDatas.IndexOf(i) > endIndex - ConversionPeriod &&
|
||||
candleDatas.IndexOf(i) <= endIndex);
|
||||
|
||||
if (datas == null || datas.Count == 0) return;
|
||||
|
||||
candleData.IchimokData.전환선 = (datas.Min(i => i.Low) + datas.Max(i => i.High)) / 2;
|
||||
|
||||
datas = candleDatas.FindAll(i =>
|
||||
candleDatas.IndexOf(i) > endIndex - BaselinePeriod &&
|
||||
candleDatas.IndexOf(i) <= endIndex);
|
||||
|
||||
candleData.IchimokData.기준선 = (datas.Min(i => i.Low) + datas.Max(i => i.High)) / 2;
|
||||
|
||||
// 후행스팬
|
||||
if (endIndex - Displacement >= 0)
|
||||
{
|
||||
if (candleDatas[candleDatas.Count - Displacement].IchimokData == null)
|
||||
{
|
||||
candleDatas[candleDatas.Count - Displacement].IchimokData = new IchimokData();
|
||||
}
|
||||
candleDatas[candleDatas.Count - Displacement].IchimokData.후행스팬 = endValue;
|
||||
}
|
||||
|
||||
// Displacement 일전봉 데이터
|
||||
endIndex = candleDatas.Count - 1;
|
||||
endIndex = endIndex - Displacement + 1;
|
||||
|
||||
datas = candleDatas.FindAll(i =>
|
||||
candleDatas.IndexOf(i) > endIndex - ConversionPeriod &&
|
||||
candleDatas.IndexOf(i) <= endIndex);
|
||||
|
||||
if (datas == null || datas.Count == 0) return;
|
||||
|
||||
var displacement_전환선 = (datas.Min(i => i.Low) + datas.Max(i => i.High)) / 2;
|
||||
|
||||
datas = candleDatas.FindAll(i =>
|
||||
candleDatas.IndexOf(i) > endIndex - BaselinePeriod &&
|
||||
candleDatas.IndexOf(i) <= endIndex);
|
||||
|
||||
if (datas == null || datas.Count == 0) return;
|
||||
|
||||
var displacement_기준선 = (datas.Min(i => i.Low) + datas.Max(i => i.High)) / 2;
|
||||
|
||||
candleData.IchimokData.선행스팬1 = (displacement_전환선 + displacement_기준선) / 2;
|
||||
|
||||
datas = candleDatas.FindAll(i =>
|
||||
candleDatas.IndexOf(i) > endIndex - LaggingSpan2Period &&
|
||||
candleDatas.IndexOf(i) <= endIndex);
|
||||
|
||||
candleData.IchimokData.선행스팬2 = (datas.Min(i => i.Low) + datas.Max(i => i.High)) / 2;
|
||||
|
||||
candleData.IchimokData.상한선 = Math.Max(candleData.IchimokData.선행스팬1, candleData.IchimokData.선행스팬2);
|
||||
candleData.IchimokData.하한선 = Math.Min(candleData.IchimokData.선행스팬1, candleData.IchimokData.선행스팬2);
|
||||
|
||||
candleData.IchimokData.IsLongUp = candleData.IchimokData.상한선 < candleData.Close;
|
||||
candleData.IchimokData.IsShortDown = candleData.IchimokData.하한선 > candleData.Close;
|
||||
candleData.IchimokData.IsLongDown = candleData.IchimokData.상한선 > candleData.Close;
|
||||
candleData.IchimokData.IsShortUp = candleData.IchimokData.하한선 < candleData.Close;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using Bitget_Server.Domain.Indicators;
|
||||
|
||||
namespace Bitget_Server.Application.Indicators;
|
||||
|
||||
[Serializable]
|
||||
public class ImpulseMacdConditionSetup
|
||||
{
|
||||
private int lengthMA;
|
||||
private int lengthSignal;
|
||||
|
||||
private List<double> srcList = new();
|
||||
private List<double> highList = new();
|
||||
private List<double> lowList = new();
|
||||
private List<double> smmaHighList = new();
|
||||
private List<double> smmaLowList = new();
|
||||
private List<double> zlemaList = new();
|
||||
private List<double> mdList = new();
|
||||
private List<double> sbList = new();
|
||||
private List<double> shList = new();
|
||||
|
||||
public ImpulseMacdConditionSetup(int lengthMA = 34, int lengthSignal = 12)
|
||||
{
|
||||
this.lengthMA = lengthMA;
|
||||
this.lengthSignal = lengthSignal;
|
||||
}
|
||||
|
||||
public ImpulseMacdData AddDataPoint(double high, double low, double close, bool flag)
|
||||
{
|
||||
double src = (high + low + close) / 3.0;
|
||||
if (flag)
|
||||
{
|
||||
srcList.Add(src);
|
||||
highList.Add(high);
|
||||
lowList.Add(low);
|
||||
}
|
||||
else
|
||||
{
|
||||
srcList[srcList.Count - 1] = src;
|
||||
highList[highList.Count - 1] = high;
|
||||
lowList[lowList.Count - 1] = low;
|
||||
}
|
||||
|
||||
double smmaHigh = CalculateSMMA(highList, smmaHighList, lengthMA);
|
||||
double smmaLow = CalculateSMMA(lowList, smmaLowList, lengthMA);
|
||||
double zlema = CalculateZLEMA(srcList, zlemaList, lengthMA);
|
||||
|
||||
double md = 0.0;
|
||||
if (zlema > smmaHigh) md = zlema - smmaHigh;
|
||||
else if (zlema < smmaLow) md = zlema - smmaLow;
|
||||
|
||||
if (flag) mdList.Add(md);
|
||||
else mdList[mdList.Count - 1] = md;
|
||||
|
||||
double sb = mdList.Skip(mdList.Count - lengthSignal).Average();
|
||||
double sh = md - sb;
|
||||
|
||||
if (flag)
|
||||
{
|
||||
sbList.Add(sb);
|
||||
shList.Add(sh);
|
||||
}
|
||||
else
|
||||
{
|
||||
sbList[sbList.Count - 1] = sb;
|
||||
shList[shList.Count - 1] = sh;
|
||||
}
|
||||
|
||||
if (mdList.Count < lengthSignal) return null;
|
||||
|
||||
if (mdList.Count > 100) mdList.RemoveRange(0, 50);
|
||||
if (sbList.Count > 100) sbList.RemoveRange(0, 50);
|
||||
if (shList.Count > 100) shList.RemoveRange(0, 50);
|
||||
|
||||
return new ImpulseMacdData { MD = md, SH = sh, SB = sb };
|
||||
}
|
||||
|
||||
private double CalculateSMMA(List<double> src, List<double> smmaList, int len)
|
||||
{
|
||||
if (src.Count < len) return src.Last();
|
||||
|
||||
double smma;
|
||||
if (smmaList.Count == 0)
|
||||
smma = src.Skip(src.Count - len).Average();
|
||||
else
|
||||
smma = (smmaList.Last() * (len - 1) + src.Last()) / len;
|
||||
|
||||
smmaList.Add(smma);
|
||||
return smma;
|
||||
}
|
||||
|
||||
private double CalculateZLEMA(List<double> src, List<double> zlemaList, int length)
|
||||
{
|
||||
if (src.Count < length * 2) return src.Last();
|
||||
|
||||
var ema1 = CalculateEMA(src, length);
|
||||
var ema2 = CalculateEMA(ema1, length);
|
||||
double d = ema1.Last() - ema2.Last();
|
||||
double zlema = ema1.Last() + d;
|
||||
zlemaList.Add(zlema);
|
||||
return zlema;
|
||||
}
|
||||
|
||||
private List<double> CalculateEMA(List<double> src, int length)
|
||||
{
|
||||
List<double> ema = new List<double>();
|
||||
double k = 2.0 / (length + 1);
|
||||
ema.Add(src.Take(length).Average());
|
||||
|
||||
for (int i = length; i < src.Count; i++)
|
||||
{
|
||||
double value = src[i] * k + ema.Last() * (1 - k);
|
||||
ema.Add(value);
|
||||
}
|
||||
return ema;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using Bitget_Server.Domain.Enums;
|
||||
using Bitget_Server.Domain.Indicators;
|
||||
using Bitget_Server.Domain.Models;
|
||||
|
||||
namespace Bitget_Server.Application.Indicators;
|
||||
|
||||
[Serializable]
|
||||
public class MovingAverageConditionSetup
|
||||
{
|
||||
public MovingAverageType MovingAverageType { get; set; }
|
||||
|
||||
// 기간 파라미터 (기존 WinForms와 동일)
|
||||
public int CandleLength24 { get; set; } = 24;
|
||||
public int CandleLength48 { get; set; } = 48;
|
||||
public int CandleLength4 { get; set; } = 4;
|
||||
public int CandleLength5 { get; set; } = 5;
|
||||
public int CandleLength2 { get; set; } = 2;
|
||||
public int CandleLength3 { get; set; } = 3;
|
||||
public int CandleLength36 { get; set; } = 36;
|
||||
public int CandleLength72 { get; set; } = 72;
|
||||
public int CandleLength12 { get; set; } = 12;
|
||||
public int CandleLength8 { get; set; } = 8;
|
||||
public int CandleLength144 { get; set; } = 144;
|
||||
public int CandleLength40 { get; set; } = 40;
|
||||
public int CandleLength20 { get; set; } = 20;
|
||||
public int CandleLength120 { get; set; } = 120;
|
||||
public int CandleLength240 { get; set; } = 240;
|
||||
public int CandleLength480 { get; set; } = 480;
|
||||
public int CandleLength80 { get; set; } = 80;
|
||||
public int CandleLength160 { get; set; } = 160;
|
||||
|
||||
public CandlePriceType MACandlePriceType { get; set; }
|
||||
|
||||
public void Cal_YHK_Downpublic(List<CandleData> candleDatas, CandleData candleData)
|
||||
{
|
||||
var endIndex = candleDatas.Count() - 1;
|
||||
if (candleData == null) return;
|
||||
|
||||
if (candleData.movingAverageDataList == null)
|
||||
{
|
||||
candleData.movingAverageDataList = new Dictionary<int, MovingAverageData>
|
||||
{
|
||||
{ CandleLength2, new MovingAverageData() },
|
||||
{ CandleLength4, new MovingAverageData() },
|
||||
{ CandleLength5, new MovingAverageData() },
|
||||
{ CandleLength12, new MovingAverageData() },
|
||||
{ CandleLength24, new MovingAverageData() },
|
||||
{ CandleLength36, new MovingAverageData() },
|
||||
{ CandleLength144, new MovingAverageData() },
|
||||
{ CandleLength40, new MovingAverageData() },
|
||||
{ CandleLength48, new MovingAverageData() },
|
||||
{ CandleLength72, new MovingAverageData() },
|
||||
{ CandleLength3, new MovingAverageData() },
|
||||
{ CandleLength8, new MovingAverageData() },
|
||||
{ CandleLength20, new MovingAverageData() },
|
||||
{ CandleLength80, new MovingAverageData() },
|
||||
{ CandleLength160, new MovingAverageData() },
|
||||
{ CandleLength120, new MovingAverageData() },
|
||||
{ CandleLength240, new MovingAverageData() },
|
||||
{ CandleLength480, new MovingAverageData() }
|
||||
};
|
||||
}
|
||||
|
||||
foreach (var item in candleData.movingAverageDataList.ToList())
|
||||
{
|
||||
var datas = candleDatas.FindAll(i =>
|
||||
candleDatas.IndexOf(i) > endIndex - item.Key &&
|
||||
candleDatas.IndexOf(i) <= endIndex);
|
||||
|
||||
candleData.movingAverageDataList[item.Key] = new MovingAverageData();
|
||||
candleData.movingAverageDataList[item.Key].Value = datas.Sum(i => i.Close) / item.Key;
|
||||
CalBolangerValue(datas, candleData.movingAverageDataList[item.Key], datas.Count - 1, item.Key);
|
||||
CalBolangerValue(datas, candleData.movingAverageDataList[item.Key], datas.Count - 1, item.Key);
|
||||
}
|
||||
}
|
||||
|
||||
// 볼린저 밴드 계산
|
||||
public void CalBolangerValue(List<CandleData> data, MovingAverageData mvData, int i, int period)
|
||||
{
|
||||
double total_average = 0;
|
||||
if (i < period - 1) return;
|
||||
|
||||
for (int t = i - period + 1; t < i; t++)
|
||||
total_average += data[t].Close;
|
||||
total_average += data[i].Close;
|
||||
|
||||
double average = total_average / period;
|
||||
double t_Close = 0;
|
||||
double sum = 0;
|
||||
double powSum = 0;
|
||||
|
||||
int j;
|
||||
for (j = (i - period + 1); j < i; j++)
|
||||
{
|
||||
t_Close += Math.Pow(data[j].Close - average, 2);
|
||||
sum += data[j].Close;
|
||||
powSum += Math.Pow(data[j].Close, 2);
|
||||
}
|
||||
t_Close += Math.Pow(data[j].Close - average, 2);
|
||||
|
||||
mvData.BolangerValue_Down = average - mvData.BolangerSquared * Math.Sqrt(t_Close / period);
|
||||
mvData.BolangerValue_Up = average + mvData.BolangerSquared * Math.Sqrt(t_Close / period);
|
||||
mvData.Average = average;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
namespace Bitget_Server.Application.Indicators;
|
||||
|
||||
/// <summary>
|
||||
/// TradingView 스타일 복합 시그널 (Impulse MACD + MACD + SMA + ADX).
|
||||
/// 기존 WinForms 구현을 그대로 이식하였으며 WinForms using 만 제거.
|
||||
/// </summary>
|
||||
public class TradingViewConditionSetup
|
||||
{
|
||||
public int LengthMA = 34;
|
||||
public int LengthSignal = 12;
|
||||
public int AdxLength = 12;
|
||||
public double AdxThreshold = 24;
|
||||
public double RangeThreshold = 1;
|
||||
public double RangeThresholdSell = 1;
|
||||
|
||||
public List<double> High;
|
||||
public List<double> Low;
|
||||
public List<double> Close;
|
||||
|
||||
private double[] Sma4;
|
||||
|
||||
public void SetData(List<double> high, List<double> low, List<double> close)
|
||||
{
|
||||
High = high;
|
||||
Low = low;
|
||||
Close = close;
|
||||
Sma4 = ComputeSma4();
|
||||
}
|
||||
|
||||
private double CalcSmma(List<double> src, int len)
|
||||
{
|
||||
double smma = src.Take(len).Average();
|
||||
for (int i = len; i < src.Count; i++)
|
||||
smma = (smma * (len - 1) + src[i]) / len;
|
||||
return smma;
|
||||
}
|
||||
|
||||
private double CalcZlema(List<double> src, int length)
|
||||
{
|
||||
double ema1 = Ema(src, length);
|
||||
double ema2 = Ema(Enumerable.Repeat(ema1, src.Count).ToList(), length);
|
||||
double d = ema1 - ema2;
|
||||
return ema1 + d;
|
||||
}
|
||||
|
||||
private double Ema(List<double> src, int length)
|
||||
{
|
||||
double alpha = 2.0 / (length + 1);
|
||||
double ema = src[0];
|
||||
for (int i = 1; i < src.Count; i++)
|
||||
ema = alpha * src[i] + (1 - alpha) * ema;
|
||||
return ema;
|
||||
}
|
||||
|
||||
private double[] ComputeSma4()
|
||||
{
|
||||
double[] sma4 = new double[Close.Count];
|
||||
for (int i = 3; i < Close.Count; i++)
|
||||
sma4[i] = (Close[i] + Close[i - 1] + Close[i - 2] + Close[i - 3]) / 4.0;
|
||||
return sma4;
|
||||
}
|
||||
|
||||
private double ComputeAdx()
|
||||
{
|
||||
// 원본에서도 더미값 사용. 필요 시 실제 ADX 구현 추가 가능.
|
||||
return 25.0;
|
||||
}
|
||||
|
||||
public double ComputeMd(bool flag = true)
|
||||
{
|
||||
if (flag)
|
||||
{
|
||||
double hi = CalcSmma(High, LengthMA);
|
||||
double lo = CalcSmma(Low, LengthMA);
|
||||
var hlc3 = High.Zip(Low, (h, l) => (h + l + Close.Last()) / 3).ToList();
|
||||
double mi = CalcZlema(hlc3, LengthMA);
|
||||
if (mi > hi) return mi - hi;
|
||||
if (mi < lo) return mi - lo;
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
var highTemp = new List<double>(High);
|
||||
var lowTemp = new List<double>(Low);
|
||||
var closeTemp = new List<double>(Close);
|
||||
|
||||
highTemp.RemoveAt(highTemp.Count - 1);
|
||||
lowTemp.RemoveAt(lowTemp.Count - 1);
|
||||
closeTemp.RemoveAt(closeTemp.Count - 1);
|
||||
|
||||
double hi = CalcSmma(highTemp, LengthMA);
|
||||
double lo = CalcSmma(lowTemp, LengthMA);
|
||||
var hlc3 = highTemp.Zip(lowTemp, (h, l) => (h + l + closeTemp.Last()) / 3).ToList();
|
||||
double mi = CalcZlema(hlc3, LengthMA);
|
||||
if (mi > hi) return mi - hi;
|
||||
if (mi < lo) return mi - lo;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public (double macdLine, double signalLine) ComputeMacd()
|
||||
{
|
||||
double macdLine = Ema(Close, 34) - Ema(Close, 12);
|
||||
double signalLine = Ema(Enumerable.Repeat(macdLine, Close.Count).ToList(), 12);
|
||||
return (macdLine, signalLine);
|
||||
}
|
||||
|
||||
public (double macdLine, double signalLine) PrevComputeMacd()
|
||||
{
|
||||
var temp = new List<double>(Close);
|
||||
temp.RemoveAt(temp.Count - 1);
|
||||
double macdLine = Ema(temp, 34) - Ema(temp, 12);
|
||||
double signalLine = Ema(Enumerable.Repeat(macdLine, temp.Count).ToList(), 12);
|
||||
return (macdLine, signalLine);
|
||||
}
|
||||
|
||||
public bool IsRangeBound()
|
||||
{
|
||||
double highestHigh = High.Skip(High.Count - 12).Max();
|
||||
double lowestLow = Low.Skip(Low.Count - 12).Min();
|
||||
double range = highestHigh - lowestLow;
|
||||
return range < (highestHigh * RangeThreshold / 100.0);
|
||||
}
|
||||
|
||||
public bool IsRangeBoundSell()
|
||||
{
|
||||
double highestHigh = High.Skip(High.Count - 12).Max();
|
||||
double lowestLow = Low.Skip(Low.Count - 12).Min();
|
||||
double range = highestHigh - lowestLow;
|
||||
return range < (highestHigh * RangeThresholdSell / 100.0);
|
||||
}
|
||||
|
||||
public bool IsTrending() => ComputeAdx() > AdxThreshold;
|
||||
|
||||
public double GetAvgData(int range)
|
||||
{
|
||||
var sma = 0.0;
|
||||
for (var i = 0; i < range; i++)
|
||||
sma += Close[Close.Count - (i + 1)];
|
||||
return sma / range;
|
||||
}
|
||||
|
||||
public double GetAvgData(int range, int prev)
|
||||
{
|
||||
var sma = 0.0;
|
||||
for (var i = 0; i < range; i++)
|
||||
sma += Close[Close.Count - (i + 1 + prev)];
|
||||
return sma / range;
|
||||
}
|
||||
|
||||
public bool IsLongSignal()
|
||||
{
|
||||
double md = ComputeMd();
|
||||
double prevMd = ComputeMd(false);
|
||||
var (macd, signal) = ComputeMacd();
|
||||
var (prevMacd, prevSignal) = PrevComputeMacd();
|
||||
|
||||
return ((prevMd == 0 && md > 0) || (prevMacd < prevSignal && macd > prevMacd && macd > signal && md > 0)) &&
|
||||
IsTrending() && IsSmaMiddle(4, 12, true, GetAvgData(72)) && GetAvgData(4) > GetAvgData(4, 1);
|
||||
}
|
||||
|
||||
public bool IsShortSignal()
|
||||
{
|
||||
double md = ComputeMd();
|
||||
double prevMd = ComputeMd(false);
|
||||
var (macd, signal) = ComputeMacd();
|
||||
var (prevMacd, prevSignal) = PrevComputeMacd();
|
||||
|
||||
return ((prevMd == 0 && md < 0) || (prevMacd > prevSignal && macd < prevMacd && macd < signal && md < 0)) &&
|
||||
IsTrending() && IsSmaMiddle(4, 12, false, GetAvgData(72)) && GetAvgData(4) < GetAvgData(4, 1);
|
||||
}
|
||||
|
||||
public bool IsSmaHigh(int range, int count)
|
||||
{
|
||||
var list = new List<double>();
|
||||
for (int i = Close.Count - count - 1; i < count; i++)
|
||||
{
|
||||
var sum = 0.0;
|
||||
for (int j = 0; j < range; j++) sum += Close[i - j];
|
||||
list.Add(sum / range);
|
||||
}
|
||||
return list.Max() == list[list.Count - 1];
|
||||
}
|
||||
|
||||
public bool IsSmaLow(int range, int count)
|
||||
{
|
||||
var list = new List<double>();
|
||||
for (int i = Close.Count - count - 1; i < count; i++)
|
||||
{
|
||||
var sum = 0.0;
|
||||
for (int j = 0; j < range; j++) sum += Close[i - j];
|
||||
list.Add(sum / range);
|
||||
}
|
||||
return list.Min() == list[list.Count - 1];
|
||||
}
|
||||
|
||||
public bool IsSmaMiddle(int range, int count, bool longFlag, double value)
|
||||
{
|
||||
var list = new List<double>();
|
||||
for (int i = Close.Count - count - 1; i < count; i++)
|
||||
{
|
||||
var sum = 0.0;
|
||||
for (int j = 0; j < range; j++) sum += Close[i - j];
|
||||
list.Add(sum / range);
|
||||
}
|
||||
if (longFlag) return value < list.Average();
|
||||
return value > list.Average();
|
||||
}
|
||||
}
|
||||
38
Bitget_Server/Application/Options/BitgetOptions.cs
Normal file
38
Bitget_Server/Application/Options/BitgetOptions.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Bitget_Server.Domain.Models;
|
||||
|
||||
namespace Bitget_Server.Application.Options;
|
||||
|
||||
/// <summary>
|
||||
/// appsettings.json의 Bitget 섹션 바인딩용.
|
||||
/// </summary>
|
||||
public class BitgetOptions
|
||||
{
|
||||
public const string SectionName = "Bitget";
|
||||
|
||||
/// <summary>기본 API 키 ID (ApiKeys 목록 중 하나).</summary>
|
||||
public int DefaultApiId { get; set; } = 0;
|
||||
|
||||
/// <summary>초당 허용 API 호출 횟수 (Bitget Rate Limit).</summary>
|
||||
public int RateLimitPerSecond { get; set; } = 20;
|
||||
|
||||
/// <summary>JSON 설정 파일 저장 경로. 컨테이너에서는 /data 로 볼륨 마운트.</summary>
|
||||
public string DataDirectory { get; set; } = "./data";
|
||||
|
||||
/// <summary>로그 디렉토리. 컨테이너에서는 /logs 로 볼륨 마운트.</summary>
|
||||
public string LogDirectory { get; set; } = "./logs";
|
||||
|
||||
/// <summary>Debug 레벨 로그 활성화 여부.</summary>
|
||||
public bool LogDebugWrite { get; set; } = false;
|
||||
|
||||
/// <summary>자동 업데이트 주기(ms). BackgroundService에서 사용.</summary>
|
||||
public int MarketInfoUpdateIntervalMs { get; set; } = 300;
|
||||
|
||||
/// <summary>WebSocket 처리 주기(ms).</summary>
|
||||
public int WebSocketCalcIntervalMs { get; set; } = 1000;
|
||||
|
||||
/// <summary>Ping 주기(ms). Bitget는 30초 이내 응답 필요.</summary>
|
||||
public int WebSocketPingIntervalMs { get; set; } = 25000;
|
||||
|
||||
/// <summary>사용자별 Bitget API 키 목록 (기존 하드코딩 3개에 해당).</summary>
|
||||
public List<ApiKeyInfo_Bitget> ApiKeys { get; set; } = new();
|
||||
}
|
||||
28
Bitget_Server/Application/Options/GlobalOptions.cs
Normal file
28
Bitget_Server/Application/Options/GlobalOptions.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Bitget_Server.Domain.Models;
|
||||
using Bitget_Server.Infrastructure.Utils;
|
||||
|
||||
namespace Bitget_Server.Application.Options;
|
||||
|
||||
/// <summary>
|
||||
/// 런타임 변경 가능한 전역 옵션. JSON 파일에 영속화됨.
|
||||
/// (레버리지/주문수량/선택된 API 등 사용자가 런타임에 변경하는 값)
|
||||
/// </summary>
|
||||
public class GlobalOptions : BaseSettings<GlobalOptions>
|
||||
{
|
||||
public bool LogDebugWrite { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 선택된 API 키 정보 (appsettings.json의 ApiKeys 중 하나의 스냅샷).
|
||||
/// </summary>
|
||||
public ApiKeyInfo_Bitget SelectAPI { get; set; } = null;
|
||||
|
||||
public double Leverage { get; set; } = 10;
|
||||
|
||||
public double OrderAmount { get; set; } = 50;
|
||||
|
||||
/// <summary>기본 캔들 간격(분).</summary>
|
||||
public int CandleIntervalMinutes { get; set; } = 5;
|
||||
|
||||
/// <summary>선택된 코인 종목 (Bitget symbol, 예: BTCUSDT).</summary>
|
||||
public string SelectedCoin { get; set; } = string.Empty;
|
||||
}
|
||||
285
Bitget_Server/Application/Services/BitgetTradingService.cs
Normal file
285
Bitget_Server/Application/Services/BitgetTradingService.cs
Normal file
@@ -0,0 +1,285 @@
|
||||
using System.Collections.Specialized;
|
||||
using Bitget_Server.Application.Options;
|
||||
using Bitget_Server.Domain;
|
||||
using Bitget_Server.Domain.Enums;
|
||||
using Bitget_Server.Infrastructure.Bitget;
|
||||
using Bitget_Server.Infrastructure.Utils;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Bitget_Server.Application.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 기존 IndicatorProcess의 Private API 영역(주문/취소/잔고/포지션모드 변경/레버리지 변경/체결상세)을 이식한 서비스.
|
||||
/// HMAC-SHA256 서명은 BitgetClient.GenerateSignString을 사용한다.
|
||||
/// 외부 호출 경로는 BitgetClient.SendWebServerAsync → BitgetRateLimiter(20/sec) 순으로 흐른다.
|
||||
/// </summary>
|
||||
public class BitgetTradingService
|
||||
{
|
||||
private readonly BitgetClient _client;
|
||||
private readonly GlobalOptions _options;
|
||||
private readonly ILogger<BitgetTradingService> _logger;
|
||||
|
||||
public BitgetTradingService(
|
||||
BitgetClient client,
|
||||
GlobalOptions options,
|
||||
ILogger<BitgetTradingService> logger)
|
||||
{
|
||||
_client = client;
|
||||
_options = options;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private string AccessKey => _options.SelectAPI?.AccessKey ?? string.Empty;
|
||||
private string SecretKey => _options.SelectAPI?.SecretKey ?? string.Empty;
|
||||
private string Passphrase => _options.SelectAPI?.PassphraseKey ?? string.Empty;
|
||||
|
||||
/// <summary>Bitget 서버 타임스탬프 조회.</summary>
|
||||
public async Task<long> GetTimeStampAsync(CancellationToken ct = default)
|
||||
{
|
||||
var result = await _client.SendWebServerAsync(
|
||||
ExchangeWebReqType.BitgetGetTimeStamp,
|
||||
BitgetEndpoints.GetTimestamp,
|
||||
ct: ct).ConfigureAwait(false) as Dictionary<string, object>;
|
||||
|
||||
if (result == null) return 0;
|
||||
|
||||
var data = JsonConvert.DeserializeObject<Dictionary<string, object>>(
|
||||
JsonConvert.SerializeObject(result["data"]));
|
||||
|
||||
long.TryParse(data["serverTime"].ToString(), out var ts);
|
||||
return ts;
|
||||
}
|
||||
|
||||
/// <summary>미체결 주문 조회.</summary>
|
||||
public async Task<Dictionary<string, object>> GetPendingOrdersAsync(CancellationToken ct = default)
|
||||
{
|
||||
var timestamp = (await GetTimeStampAsync(ct).ConfigureAwait(false)).ToString();
|
||||
const string reqPath = "/api/v2/mix/order/orders-pending";
|
||||
|
||||
var nvc = new NameValueCollection
|
||||
{
|
||||
{ "ACCESS-KEY", AccessKey },
|
||||
{ "ACCESS-SIGN", BitgetClient.GenerateSignString(timestamp, "GET", reqPath, "productType=USDT-FUTURES", null, SecretKey) },
|
||||
{ "ACCESS-PASSPHRASE", Passphrase },
|
||||
{ "ACCESS-TIMESTAMP", timestamp }
|
||||
};
|
||||
|
||||
return await _client.SendWebServerAsync(
|
||||
ExchangeWebReqType.BitgetPendingOrder_Future,
|
||||
BitgetEndpoints.PendingOrders_Future,
|
||||
headers: nvc,
|
||||
ct: ct).ConfigureAwait(false) as Dictionary<string, object>;
|
||||
}
|
||||
|
||||
/// <summary>선물 계좌 조회.</summary>
|
||||
public async Task<List<Dictionary<string, object>>> GetFutureAccountListAsync(CancellationToken ct = default)
|
||||
{
|
||||
var timestamp = (await GetTimeStampAsync(ct).ConfigureAwait(false)).ToString();
|
||||
const string reqPath = "/api/v2/mix/account/accounts";
|
||||
|
||||
var nvc = new NameValueCollection
|
||||
{
|
||||
{ "ACCESS-KEY", AccessKey },
|
||||
{ "ACCESS-SIGN", BitgetClient.GenerateSignString(timestamp, "GET", reqPath, "productType=USDT-FUTURES", null, SecretKey) },
|
||||
{ "ACCESS-PASSPHRASE", Passphrase },
|
||||
{ "ACCESS-TIMESTAMP", timestamp }
|
||||
};
|
||||
|
||||
return await _client.SendWebServerAsync(
|
||||
ExchangeWebReqType.BitgetAccountList_Future,
|
||||
BitgetEndpoints.AccountList_Future,
|
||||
headers: nvc,
|
||||
ct: ct).ConfigureAwait(false) as List<Dictionary<string, object>>;
|
||||
}
|
||||
|
||||
/// <summary>주문 취소.</summary>
|
||||
public async Task<Dictionary<string, object>> CancelFutureOrderAsync(string symbol, string orderId, CancellationToken ct = default)
|
||||
{
|
||||
var timestamp = (await GetTimeStampAsync(ct).ConfigureAwait(false)).ToString();
|
||||
const string reqPath = "/api/v2/mix/order/cancel-order";
|
||||
var postStr = "{\"symbol\" : \"" + symbol + "\", \"orderId\" : \"" + orderId + "\", \"productType\" : \"usdt-futures\" , \"marginCoin\" : \"USDT\"}";
|
||||
|
||||
var nvc = new NameValueCollection
|
||||
{
|
||||
{ "ACCESS-KEY", AccessKey },
|
||||
{ "ACCESS-SIGN", BitgetClient.GenerateSignString(timestamp, "POST", reqPath, "", postStr, SecretKey) },
|
||||
{ "ACCESS-PASSPHRASE", Passphrase },
|
||||
{ "ACCESS-TIMESTAMP", timestamp }
|
||||
};
|
||||
|
||||
return await _client.SendWebServerAsync(
|
||||
ExchangeWebReqType.BitgetCancelOrder_Future,
|
||||
BitgetEndpoints.CancelOrder_Future,
|
||||
postData: postStr,
|
||||
headers: nvc,
|
||||
ct: ct).ConfigureAwait(false) as Dictionary<string, object>;
|
||||
}
|
||||
|
||||
/// <summary>포지션 모드 변경 (one_way / hedge).</summary>
|
||||
public async Task<Dictionary<string, object>> ChangeFuturePositionModeAsync(FuturePositionMode mode, CancellationToken ct = default)
|
||||
{
|
||||
var timestamp = (await GetTimeStampAsync(ct).ConfigureAwait(false)).ToString();
|
||||
const string reqPath = "/api/v2/mix/account/set-position-mode";
|
||||
var postStr = "{\"productType\" : \"USDT-FUTURES\", \"posMode\" : \"" + Convertor.GetEnumDescription(mode) + "\"}";
|
||||
|
||||
var nvc = new NameValueCollection
|
||||
{
|
||||
{ "ACCESS-KEY", AccessKey },
|
||||
{ "ACCESS-SIGN", BitgetClient.GenerateSignString(timestamp, "POST", reqPath, "", postStr, SecretKey) },
|
||||
{ "ACCESS-PASSPHRASE", Passphrase },
|
||||
{ "ACCESS-TIMESTAMP", timestamp }
|
||||
};
|
||||
|
||||
return await _client.SendWebServerAsync(
|
||||
ExchangeWebReqType.BitgetChangePositionMode_Future,
|
||||
BitgetEndpoints.ChangePositionMode_Future,
|
||||
postData: postStr,
|
||||
headers: nvc,
|
||||
ct: ct).ConfigureAwait(false) as Dictionary<string, object>;
|
||||
}
|
||||
|
||||
/// <summary>주문 상세 조회.</summary>
|
||||
public async Task<Dictionary<string, object>> GetFutureOrderDetailAsync(string symbol, string orderId, CancellationToken ct = default)
|
||||
{
|
||||
var timestamp = (await GetTimeStampAsync(ct).ConfigureAwait(false)).ToString();
|
||||
const string reqPath = "/api/v2/mix/order/detail";
|
||||
var queryString = $"symbol={symbol}&orderId={orderId}&productType=USDT-FUTURES";
|
||||
|
||||
var nvc = new NameValueCollection
|
||||
{
|
||||
{ "ACCESS-KEY", AccessKey },
|
||||
{ "ACCESS-SIGN", BitgetClient.GenerateSignString(timestamp, "GET", reqPath, queryString, null, SecretKey) },
|
||||
{ "ACCESS-PASSPHRASE", Passphrase },
|
||||
{ "ACCESS-TIMESTAMP", timestamp }
|
||||
};
|
||||
|
||||
var url = BitgetEndpoints.OrderDetail_Future + "?" + queryString;
|
||||
return await _client.SendWebServerAsync(
|
||||
ExchangeWebReqType.BitgetOrderDetail_Future,
|
||||
url,
|
||||
headers: nvc,
|
||||
ct: ct).ConfigureAwait(false) as Dictionary<string, object>;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 선물 주문 (Buy는 선행적으로 MarginMode/Leverage 변경, Sell은 사전 Cancel).
|
||||
/// 기존 IndicatorProcess.FutureOrder 로직 그대로 이식.
|
||||
/// </summary>
|
||||
public async Task<Dictionary<string, object>> FutureOrderAsync(
|
||||
string symbol,
|
||||
FutureMarginMode marginMode,
|
||||
double size,
|
||||
double price,
|
||||
FutureSide futureSide,
|
||||
OrderType orderType,
|
||||
double leverage,
|
||||
string id = "",
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var side = string.Empty;
|
||||
var tradeSide = string.Empty;
|
||||
|
||||
switch (futureSide)
|
||||
{
|
||||
case FutureSide.BuyShort:
|
||||
id = Guid.NewGuid().ToString();
|
||||
side = "sell"; tradeSide = "open"; break;
|
||||
case FutureSide.BuyLong:
|
||||
id = Guid.NewGuid().ToString();
|
||||
side = "buy"; tradeSide = "Open"; break;
|
||||
case FutureSide.SellShort:
|
||||
side = "sell"; tradeSide = "close"; break;
|
||||
case FutureSide.SellLong:
|
||||
side = "buy"; tradeSide = "close"; break;
|
||||
}
|
||||
|
||||
if (orderType == OrderType.market)
|
||||
price = 0;
|
||||
|
||||
var timestamp = (await GetTimeStampAsync(ct).ConfigureAwait(false)).ToString();
|
||||
|
||||
// Buy 주문: Margin Mode → Leverage → Order 체인
|
||||
if (futureSide == FutureSide.BuyShort || futureSide == FutureSide.BuyLong)
|
||||
{
|
||||
var changeMarginModeStr = "{\"symbol\": \"" + symbol + "\", \"productType\": \"usdt-futures\", \"marginCoin\": \"USDT\", \"marginMode\": \""
|
||||
+ Convertor.GetEnumDescription(marginMode) + "\"}";
|
||||
|
||||
var nvcMargin = new NameValueCollection
|
||||
{
|
||||
{ "ACCESS-KEY", AccessKey },
|
||||
{ "ACCESS-SIGN", BitgetClient.GenerateSignString(timestamp, "POST", "/api/v2/mix/account/set-margin-mode", "", changeMarginModeStr, SecretKey) },
|
||||
{ "ACCESS-PASSPHRASE", Passphrase },
|
||||
{ "ACCESS-TIMESTAMP", timestamp }
|
||||
};
|
||||
|
||||
var marginResult = await _client.SendWebServerAsync(
|
||||
ExchangeWebReqType.BitgetChangeMarginMode_Future,
|
||||
BitgetEndpoints.ChangeMarginMode_Future,
|
||||
postData: changeMarginModeStr,
|
||||
headers: nvcMargin,
|
||||
ct: ct).ConfigureAwait(false) as Dictionary<string, object>;
|
||||
|
||||
if (marginResult == null || marginResult["msg"]?.ToString() != "success")
|
||||
{
|
||||
_logger.LogWarning("MarginMode 변경 실패: {Symbol}", symbol);
|
||||
return null;
|
||||
}
|
||||
|
||||
var leverageStr = "{\"symbol\": \"" + symbol + "\", \"productType\": \"USDT-FUTURES\", \"marginCoin\": \"USDT\", \"leverage\": \""
|
||||
+ leverage.ToString() + "\", \"holdSide\": \"" + (futureSide == FutureSide.BuyShort ? "short" : "long") + "\"}";
|
||||
|
||||
timestamp = (await GetTimeStampAsync(ct).ConfigureAwait(false)).ToString();
|
||||
var nvcLeverage = new NameValueCollection
|
||||
{
|
||||
{ "ACCESS-KEY", AccessKey },
|
||||
{ "ACCESS-SIGN", BitgetClient.GenerateSignString(timestamp, "POST", "/api/v2/mix/account/set-leverage", "", leverageStr, SecretKey) },
|
||||
{ "ACCESS-PASSPHRASE", Passphrase },
|
||||
{ "ACCESS-TIMESTAMP", timestamp }
|
||||
};
|
||||
|
||||
var leverageResult = await _client.SendWebServerAsync(
|
||||
ExchangeWebReqType.BitgetChangeLeverage_Future,
|
||||
BitgetEndpoints.ChangeLeverage_Future,
|
||||
postData: leverageStr,
|
||||
headers: nvcLeverage,
|
||||
ct: ct).ConfigureAwait(false) as Dictionary<string, object>;
|
||||
|
||||
if (leverageResult == null || leverageResult["msg"]?.ToString() != "success")
|
||||
{
|
||||
_logger.LogWarning("Leverage 변경 실패: {Symbol}", symbol);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Sell 주문: 미체결 주문 취소 후 진행
|
||||
var cancelResult = await CancelFutureOrderAsync(symbol, id, ct).ConfigureAwait(false);
|
||||
if (cancelResult == null || cancelResult["msg"]?.ToString() != "success")
|
||||
_logger.LogError("Sell Cancel 실패: {Symbol} / {OrderId}", symbol, id);
|
||||
}
|
||||
|
||||
// 실제 주문 전송
|
||||
const string reqPath = "/api/v2/mix/order/place-order";
|
||||
var postStr = "{\"symbol\": \"" + symbol + "\", \"productType\": \"usdt-futures\", \"marginMode\": \""
|
||||
+ Convertor.GetEnumDescription(marginMode) + "\", \"marginCoin\": \"USDT\", \"size\": \"" + size.ToString()
|
||||
+ "\", \"price\": \"" + price.ToString() + "\", \"side\": \"" + side + "\", \"tradeSide\": \"" + tradeSide
|
||||
+ "\", \"orderType\": \"" + Convertor.GetEnumDescription(orderType)
|
||||
+ "\", \"force\": \"gtc\", \"clientOid\": \"" + id + "\" }";
|
||||
|
||||
timestamp = (await GetTimeStampAsync(ct).ConfigureAwait(false)).ToString();
|
||||
var nvc = new NameValueCollection
|
||||
{
|
||||
{ "ACCESS-KEY", AccessKey },
|
||||
{ "ACCESS-SIGN", BitgetClient.GenerateSignString(timestamp, "POST", reqPath, "", postStr, SecretKey) },
|
||||
{ "ACCESS-PASSPHRASE", Passphrase },
|
||||
{ "ACCESS-TIMESTAMP", timestamp }
|
||||
};
|
||||
|
||||
return await _client.SendWebServerAsync(
|
||||
ExchangeWebReqType.BitgetOrder_Future,
|
||||
BitgetEndpoints.BuySellOrder_Future,
|
||||
postData: postStr,
|
||||
headers: nvc,
|
||||
ct: ct).ConfigureAwait(false) as Dictionary<string, object>;
|
||||
}
|
||||
}
|
||||
78
Bitget_Server/Application/Services/InitialCandleLoader.cs
Normal file
78
Bitget_Server/Application/Services/InitialCandleLoader.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using Bitget_Server.Application.State;
|
||||
using Bitget_Server.Domain;
|
||||
using Bitget_Server.Domain.Enums;
|
||||
using Bitget_Server.Domain.Models;
|
||||
using Bitget_Server.Infrastructure.Bitget;
|
||||
using Bitget_Server.Infrastructure.Utils;
|
||||
|
||||
namespace Bitget_Server.Application.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 기존 MainForm.getMarketCandleData_WebService 이식.
|
||||
/// 신호 검색 대상 마켓/인터벌 조합의 초기 캔들 데이터를 일괄 로드한다.
|
||||
/// BitgetRateLimiter 를 자연스럽게 경유하므로 별도 Thread.Sleep(50) 이 불필요.
|
||||
/// </summary>
|
||||
public class InitialCandleLoader
|
||||
{
|
||||
private const int CandleCount = 200;
|
||||
|
||||
private readonly BitgetClient _client;
|
||||
private readonly MarketState _state;
|
||||
private readonly ILogger<InitialCandleLoader> _logger;
|
||||
|
||||
public InitialCandleLoader(BitgetClient client, MarketState state, ILogger<InitialCandleLoader> logger)
|
||||
{
|
||||
_client = client;
|
||||
_state = state;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 체크된 마켓 목록에 대해 캔들 데이터를 채운다.
|
||||
/// </summary>
|
||||
public async Task LoadAsync(IEnumerable<string> marketNames, IEnumerable<string> intervalDescriptions, CancellationToken ct)
|
||||
{
|
||||
var intervalSet = new HashSet<string>(intervalDescriptions ?? Array.Empty<string>());
|
||||
|
||||
foreach (var marketName in marketNames ?? Array.Empty<string>())
|
||||
{
|
||||
if (ct.IsCancellationRequested) break;
|
||||
|
||||
MarketCandleDataList marketCandleDataList;
|
||||
lock (_state.SyncRoot)
|
||||
{
|
||||
marketCandleDataList = _state.candleDataLists.Find(i => i.ConGoodsData?.MarketName == marketName);
|
||||
}
|
||||
if (marketCandleDataList == null) continue;
|
||||
|
||||
foreach (MCandleTimeType interval in Enum.GetValues(typeof(MCandleTimeType)))
|
||||
{
|
||||
if (ct.IsCancellationRequested) break;
|
||||
|
||||
var description = Convertor.GetEnumDescription(interval);
|
||||
if (!intervalSet.Contains(description)) continue;
|
||||
|
||||
var url = string.Format(
|
||||
BitgetEndpoints.MarketCandle_Future,
|
||||
marketName,
|
||||
description,
|
||||
CandleCount.ToString(),
|
||||
"USDT-FUTURES");
|
||||
|
||||
try
|
||||
{
|
||||
await _client.SendWebServerAsync(
|
||||
ExchangeWebReqType.BitgetCandleStick_Future,
|
||||
url,
|
||||
marketCandleDataList,
|
||||
interval,
|
||||
ct: ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "초기 캔들 로드 실패 ({Market} / {Interval})", marketName, description);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
146
Bitget_Server/Application/Services/PriceLevelTracker.cs
Normal file
146
Bitget_Server/Application/Services/PriceLevelTracker.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
using Bitget_Server.Domain.Enums;
|
||||
using Bitget_Server.Domain.Models;
|
||||
|
||||
namespace Bitget_Server.Application.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 기존 IndicatorProcess.Calculate전고점 / 전저고점 dictionary bookkeeping 이식.
|
||||
/// 단일 인스턴스로 보관되며 키는 "MarketName + Interval(Description)" 문자열이다.
|
||||
/// </summary>
|
||||
public class PriceLevelTracker
|
||||
{
|
||||
private readonly Dictionary<string, Dictionary<string, double>> _levels = new();
|
||||
|
||||
public IReadOnlyDictionary<string, Dictionary<string, double>> Levels => _levels;
|
||||
|
||||
public void Clear() => _levels.Clear();
|
||||
|
||||
public bool ContainsKey(string key) => _levels.ContainsKey(key);
|
||||
|
||||
public Dictionary<string, double> GetOrCreate(string key)
|
||||
{
|
||||
if (_levels.TryGetValue(key, out var existing)) return existing;
|
||||
|
||||
var dic = new Dictionary<string, double>
|
||||
{
|
||||
{ "전고가", 0 }, { "전저가", 0 },
|
||||
{ "전전고가", 0 }, { "전전저가", 0 },
|
||||
{ "전전전저가", 0 }, { "전전전고가", 0 },
|
||||
{ "전전전전저가", 0 }, { "전전전전고가", 0 },
|
||||
{ "tempHighVal", 0 }, { "tempLowVal", 0 },
|
||||
{ "tempHigh2Val", 0 }, { "tempLow2Val", 0 },
|
||||
{ "tempHigh3Val", 0 }, { "tempLow3Val", 0 },
|
||||
{ "tempHigh4Val", 0 }, { "tempLow4Val", 0 },
|
||||
{ "switchLowVal", 0 }, { "switchHighVal", 0 },
|
||||
{ "Type", 0 }
|
||||
};
|
||||
_levels[key] = dic;
|
||||
return dic;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기존 IndicatorProcess.Calculate전고점 이식.
|
||||
/// 전봉/전전봉/전전전봉의 이동평균(4일) 값으로 local minima/maxima 를 추적한다.
|
||||
/// </summary>
|
||||
public bool Calculate(string market, string intervalKey, MCandleTimeType type,
|
||||
double 전봉, double 전전봉, double 전전전봉, CandleData candleData)
|
||||
{
|
||||
var key = market + intervalKey;
|
||||
var 저고점 = GetOrCreate(key);
|
||||
|
||||
var nowUnix = ((DateTimeOffset)candleData.Argument).ToUnixTimeSeconds();
|
||||
|
||||
// === 전저가 계산 ===
|
||||
if (전봉 > 전전봉 && 전전봉 < 전전전봉 &&
|
||||
저고점["tempHighVal"] >= 저고점["tempLowVal"] &&
|
||||
저고점["tempLowVal"] != nowUnix &&
|
||||
nowUnix - 저고점["tempHighVal"] >= 120 * (int)type)
|
||||
{
|
||||
저고점["tempLow4Val"] = 저고점["tempLowVal"];
|
||||
저고점["tempLowVal"] = 저고점["tempLow2Val"];
|
||||
저고점["tempLow2Val"] = 저고점["tempLowVal"];
|
||||
저고점["tempLowVal"] = nowUnix;
|
||||
저고점["switchLowVal"] = 저고점["전전전전저가"];
|
||||
저고점["전전전전저가"] = 저고점["전전저가"];
|
||||
저고점["전전전저가"] = 저고점["전전저가"];
|
||||
저고점["전전저가"] = 저고점["전저가"];
|
||||
저고점["전저가"] = 전전봉;
|
||||
저고점["Type"] = 0;
|
||||
}
|
||||
else if (전봉 > 전전봉 && 전전봉 < 전전전봉 &&
|
||||
저고점["tempHighVal"] < 저고점["tempLowVal"] &&
|
||||
저고점["tempLowVal"] != nowUnix &&
|
||||
nowUnix - 저고점["tempHighVal"] >= 120 * (int)type)
|
||||
{
|
||||
if (저고점["전저가"] > 전전봉)
|
||||
{
|
||||
저고점["tempLowVal"] = nowUnix;
|
||||
저고점["전저가"] = 전전봉;
|
||||
}
|
||||
저고점["Type"] = 0;
|
||||
}
|
||||
else if (전봉 > 전전봉 && 전전봉 < 전전전봉 &&
|
||||
저고점["tempLowVal"] != nowUnix &&
|
||||
nowUnix - 저고점["tempHighVal"] < 120 * (int)type)
|
||||
{
|
||||
// 이전 상태로 롤백 (High)
|
||||
저고점["전고가"] = 저고점["전전고가"];
|
||||
저고점["전전고가"] = 저고점["전전전고가"];
|
||||
저고점["전전전고가"] = 저고점["전전전전고가"];
|
||||
저고점["Type"] = 0;
|
||||
저고점["전전전전고가"] = 저고점["switchLowVal"];
|
||||
저고점["tempHighVal"] = 저고점["tempHigh2Val"];
|
||||
저고점["tempHigh2Val"] = 저고점["tempHighVal"];
|
||||
저고점["tempHighVal"] = 저고점["tempHigh4Val"];
|
||||
|
||||
if (저고점["전저가"] > 전전봉) 저고점["전저가"] = 전전봉;
|
||||
}
|
||||
|
||||
// === 전고가 계산 ===
|
||||
if (전전봉 > 전봉 && 전전봉 > 전전전봉 &&
|
||||
저고점["tempHighVal"] <= 저고점["tempLowVal"] &&
|
||||
저고점["tempHighVal"] != nowUnix &&
|
||||
nowUnix - 저고점["tempLowVal"] >= 120 * (int)type)
|
||||
{
|
||||
저고점["tempHigh4Val"] = 저고점["tempHighVal"];
|
||||
저고점["tempHighVal"] = 저고점["tempHigh2Val"];
|
||||
저고점["tempHigh2Val"] = 저고점["tempHighVal"];
|
||||
저고점["tempHighVal"] = nowUnix;
|
||||
저고점["switchHighVal"] = 저고점["전전전전고가"];
|
||||
저고점["전전전전고가"] = 저고점["전전전고가"];
|
||||
저고점["전전전고가"] = 저고점["전전고가"];
|
||||
저고점["전전고가"] = 저고점["전고가"];
|
||||
저고점["Type"] = 1;
|
||||
저고점["전고가"] = 전전봉;
|
||||
}
|
||||
else if (전전봉 > 전봉 && 전전봉 > 전전전봉 &&
|
||||
저고점["tempHighVal"] > 저고점["tempLowVal"] &&
|
||||
저고점["tempHighVal"] != nowUnix &&
|
||||
nowUnix - 저고점["tempLowVal"] >= 120 * (int)type)
|
||||
{
|
||||
if (저고점["전고가"] < 전전봉)
|
||||
{
|
||||
저고점["tempHighVal"] = nowUnix;
|
||||
저고점["전고가"] = 전전봉;
|
||||
}
|
||||
}
|
||||
else if (전전봉 > 전봉 && 전전봉 > 전전전봉 &&
|
||||
저고점["tempHighVal"] != nowUnix &&
|
||||
nowUnix - 저고점["tempLowVal"] < 120 * (int)type)
|
||||
{
|
||||
// 이전 상태로 롤백 (Low)
|
||||
저고점["전저가"] = 저고점["전전저가"];
|
||||
저고점["전전저가"] = 저고점["전전전저가"];
|
||||
저고점["전전전저가"] = 저고점["전전전전저가"];
|
||||
저고점["전전전전저가"] = 저고점["switchHighVal"];
|
||||
저고점["Type"] = 1;
|
||||
저고점["tempLowVal"] = 저고점["tempLow2Val"];
|
||||
저고점["tempLow2Val"] = 저고점["tempLowVal"];
|
||||
저고점["tempLowVal"] = 저고점["tempLow4Val"];
|
||||
|
||||
if (저고점["전고가"] < 전전봉) 저고점["전고가"] = 전전봉;
|
||||
}
|
||||
|
||||
return 저고점["전전전저가"] != 0 && 저고점["전전전고가"] != 0;
|
||||
}
|
||||
}
|
||||
100
Bitget_Server/Application/Services/SettingsService.cs
Normal file
100
Bitget_Server/Application/Services/SettingsService.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using Bitget_Server.Application.Options;
|
||||
using Bitget_Server.Application.State;
|
||||
using Bitget_Server.Domain.Enums;
|
||||
using Bitget_Server.Domain.Models;
|
||||
using Bitget_Server.Infrastructure.Bitget;
|
||||
using Bitget_Server.Infrastructure.Utils;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Bitget_Server.Application.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 기존 WinForms MainForm 의 ComboBox/Setting/Selected 상태를 REST 에서 다루기 위한 서비스.
|
||||
/// </summary>
|
||||
public class SettingsService
|
||||
{
|
||||
private readonly GlobalOptions _global;
|
||||
private readonly MarketState _state;
|
||||
private readonly BitgetClient _client;
|
||||
private readonly BitgetOptions _bitgetOptions;
|
||||
private readonly ILogger<SettingsService> _logger;
|
||||
|
||||
public SettingsService(
|
||||
GlobalOptions global,
|
||||
MarketState state,
|
||||
BitgetClient client,
|
||||
IOptions<BitgetOptions> bitgetOptions,
|
||||
ILogger<SettingsService> logger)
|
||||
{
|
||||
_global = global;
|
||||
_state = state;
|
||||
_client = client;
|
||||
_bitgetOptions = bitgetOptions.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
// ===== 캔들 간격 설정 =====
|
||||
public int GetCandleIntervalMinutes() => _global.CandleIntervalMinutes;
|
||||
|
||||
public void SetCandleIntervalMinutes(int minutes)
|
||||
{
|
||||
if (!Enum.IsDefined(typeof(MCandleTimeType), minutes))
|
||||
throw new ArgumentException($"유효하지 않은 캔들 간격: {minutes}");
|
||||
|
||||
_global.CandleIntervalMinutes = minutes;
|
||||
_global.Save();
|
||||
}
|
||||
|
||||
public IEnumerable<object> ListCandleIntervals() =>
|
||||
Enum.GetValues(typeof(MCandleTimeType))
|
||||
.Cast<MCandleTimeType>()
|
||||
.Select(t => new
|
||||
{
|
||||
Name = t.ToString(),
|
||||
Description = Convertor.GetEnumDescription(t),
|
||||
Minutes = (int)t
|
||||
});
|
||||
|
||||
// ===== 선택 코인 =====
|
||||
public string GetSelectedCoin() => _global.SelectedCoin;
|
||||
|
||||
public void SetSelectedCoin(string marketName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(marketName))
|
||||
throw new ArgumentException("코인 심볼이 비어있습니다.");
|
||||
|
||||
_global.SelectedCoin = marketName;
|
||||
_global.Save();
|
||||
}
|
||||
|
||||
// ===== 코인 목록 조회 =====
|
||||
public async Task<IEnumerable<MarketInfo>> GetCoinsAsync(CancellationToken ct = default)
|
||||
{
|
||||
// BitgetClient.Parse 가 _state.marketInfoList 에 기록한 값을 반환
|
||||
var result = await _client.SendWebServerAsync(
|
||||
ExchangeWebReqType.BitgetMarket_Future,
|
||||
ct: ct).ConfigureAwait(false);
|
||||
|
||||
if (result is List<MarketInfo> list) return list;
|
||||
|
||||
lock (_state.SyncRoot) return _state.marketInfoList.ToList();
|
||||
}
|
||||
|
||||
// ===== 레버리지 / 주문수량 / Selected API =====
|
||||
public double GetLeverage() => _global.Leverage;
|
||||
public double GetOrderAmount() => _global.OrderAmount;
|
||||
|
||||
public void SetLeverage(double v) { _global.Leverage = v; _global.Save(); }
|
||||
public void SetOrderAmount(double v) { _global.OrderAmount = v; _global.Save(); }
|
||||
|
||||
public int? GetSelectedApiId() => _global.SelectAPI?.Id;
|
||||
|
||||
public void SetSelectedApiId(int id)
|
||||
{
|
||||
var api = _bitgetOptions.ApiKeys.FirstOrDefault(k => k.Id == id)
|
||||
?? throw new KeyNotFoundException($"Bitget:ApiKeys 에 Id={id} 없음");
|
||||
|
||||
_global.SelectAPI = api;
|
||||
_global.Save();
|
||||
}
|
||||
}
|
||||
386
Bitget_Server/Application/Services/SignalProcessService.cs
Normal file
386
Bitget_Server/Application/Services/SignalProcessService.cs
Normal file
@@ -0,0 +1,386 @@
|
||||
using Bitget_Server.Application.Indicators;
|
||||
using Bitget_Server.Application.Options;
|
||||
using Bitget_Server.Application.State;
|
||||
using Bitget_Server.Domain.Enums;
|
||||
using Bitget_Server.Domain.Indicators;
|
||||
using Bitget_Server.Domain.Models;
|
||||
using Bitget_Server.Infrastructure.Utils;
|
||||
|
||||
namespace Bitget_Server.Application.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 기존 IndicatorProcess.calProcess 이식.
|
||||
/// WinForms Thread 무한 루프는 제거되고, 본 서비스의 ProcessOnceAsync 를
|
||||
/// SignalInfoBackgroundService 가 주기적으로 호출한다.
|
||||
/// </summary>
|
||||
public class SignalProcessService
|
||||
{
|
||||
private readonly MarketState _state;
|
||||
private readonly GlobalOptions _options;
|
||||
private readonly BitgetTradingService _trading;
|
||||
private readonly PriceLevelTracker _levels;
|
||||
private readonly ILogger<SignalProcessService> _logger;
|
||||
|
||||
// signals_LU / signals_SD / signals_LD / signals_SU (기존 public 필드 이식)
|
||||
public List<SignalInfoData> SignalsLongUp { get; } = new();
|
||||
public List<SignalInfoData> SignalsShortDown { get; } = new();
|
||||
public List<SignalInfoData> SignalsLongDown { get; } = new();
|
||||
public List<SignalInfoData> SignalsShortUp { get; } = new();
|
||||
|
||||
public TimeSpan SessionTime { get; set; } = TimeSpan.FromMinutes(30);
|
||||
|
||||
// 조건식 상태 (기존 bool 플래그)
|
||||
public bool Checked구름떼상향돌파 { get; set; } = false;
|
||||
public bool Checked구름떼하향돌파 { get; set; } = false;
|
||||
public bool 영호조건식_상향돌파 { get; set; } = false;
|
||||
public bool 영호조건식_하향돌파 { get; set; } = false;
|
||||
|
||||
// 거래 리스트 (기존 IndicatorProcess의 Up/Down/Buy/Sell 딕셔너리)
|
||||
public Dictionary<string, Dictionary<string, object>> UpBuyList { get; } = new();
|
||||
public List<Dictionary<string, object>> UpResultList { get; } = new();
|
||||
public Dictionary<string, Dictionary<string, object>> DownBuyList { get; } = new();
|
||||
public List<Dictionary<string, object>> DownResultList { get; } = new();
|
||||
|
||||
public List<Dictionary<string, object>> BuyLongList { get; } = new();
|
||||
public List<Dictionary<string, object>> BuyShortList { get; } = new();
|
||||
public List<Dictionary<string, object>> ResultList { get; } = new();
|
||||
|
||||
public Dictionary<string, int> BuyShortStep { get; } = new();
|
||||
public Dictionary<string, int> BuyLongStep { get; } = new();
|
||||
|
||||
public int LongCount { get; private set; } = 0;
|
||||
public int ShortCount { get; private set; } = 0;
|
||||
|
||||
private bool _전저고점Flag = true;
|
||||
private bool _positionModeInitialized = false;
|
||||
|
||||
public SignalProcessService(
|
||||
MarketState state,
|
||||
GlobalOptions options,
|
||||
BitgetTradingService trading,
|
||||
PriceLevelTracker levels,
|
||||
ILogger<SignalProcessService> logger)
|
||||
{
|
||||
_state = state;
|
||||
_options = options;
|
||||
_trading = trading;
|
||||
_levels = levels;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기존 while(true) { calProcess } 루프의 1회 반복에 해당하는 처리.
|
||||
/// BackgroundService 가 짧은 주기로 호출하도록 설계.
|
||||
/// </summary>
|
||||
public async Task ProcessOnceAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 최초 1회 실행: hedge mode 전환
|
||||
if (!_positionModeInitialized)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _trading.ChangeFuturePositionModeAsync(FuturePositionMode.hedge_mode, ct).ConfigureAwait(false);
|
||||
_positionModeInitialized = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "초기 포지션 모드 변경 실패");
|
||||
}
|
||||
}
|
||||
|
||||
var ichimoku = new IchimokuConditionSetup();
|
||||
var movingAverage = new MovingAverageConditionSetup();
|
||||
var nowTime = DateTime.Now;
|
||||
|
||||
// 만료된 신호 정리 (기존 signals_*.ToList() 처리와 동일)
|
||||
CleanupExpiredSignals(nowTime);
|
||||
UpdateActiveSignalPrices();
|
||||
|
||||
if (_state.CheckedMarkets.Count == 0) return;
|
||||
|
||||
// 체크된 마켓별 신호 검색
|
||||
foreach (var marketObj in _state.CheckedMarkets.ToList())
|
||||
{
|
||||
if (ct.IsCancellationRequested) break;
|
||||
var market = marketObj?.ToString();
|
||||
if (string.IsNullOrEmpty(market)) continue;
|
||||
|
||||
MarketCandleDataList findedMarket;
|
||||
lock (_state.SyncRoot)
|
||||
{
|
||||
findedMarket = _state.candleDataLists.Find(i => i.ConGoodsData?.MarketName == market);
|
||||
}
|
||||
if (findedMarket == null) continue;
|
||||
|
||||
var enumList = Enum.GetValues(typeof(MCandleTimeType));
|
||||
Array.Reverse(enumList);
|
||||
|
||||
foreach (MCandleTimeType interval in enumList)
|
||||
{
|
||||
if (ct.IsCancellationRequested) break;
|
||||
|
||||
var strInterval = Convertor.GetEnumDescription(interval);
|
||||
if (!_state.IntervalList.Any(x => x?.ToString() == strInterval)) continue;
|
||||
|
||||
// 자정(24:00) 기준 전저고점 초기화 플래그
|
||||
if (nowTime.Minute == 0 && nowTime.Hour == 24 && _전저고점Flag)
|
||||
{
|
||||
_levels.Clear();
|
||||
_전저고점Flag = false;
|
||||
}
|
||||
else if (nowTime.Minute > 0 && nowTime.Hour == 24)
|
||||
{
|
||||
_전저고점Flag = true;
|
||||
}
|
||||
|
||||
// 전저고점 초기 계산 (최초 진입 시)
|
||||
if (!_levels.ContainsKey(market + strInterval))
|
||||
{
|
||||
InitializePriceLevels(findedMarket, market, strInterval, interval, ichimoku, movingAverage);
|
||||
}
|
||||
|
||||
if (!BuyShortStep.ContainsKey(market))
|
||||
{
|
||||
BuyShortStep[market] = 0;
|
||||
BuyLongStep[market] = 0;
|
||||
}
|
||||
|
||||
// 기존에서는 5m 필수 / 현재봉 평가 로직이 뒤따름
|
||||
if (!_levels.ContainsKey(market + "5m")) continue;
|
||||
|
||||
// 5분봉 처리 체인 (전/전전/전전전/전전전전 봉 지표 계산)
|
||||
EvaluateFivemCandleChain(findedMarket, ichimoku, movingAverage);
|
||||
|
||||
// 전략 실행 (영호 조건식 상/하향 돌파)
|
||||
if (영호조건식_상향돌파 || 영호조건식_하향돌파)
|
||||
{
|
||||
await EvaluateStrategyAsync(market, strInterval, interval, findedMarket, nowTime, ct).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "SignalProcess 처리 중 오류");
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanupExpiredSignals(DateTime nowTime)
|
||||
{
|
||||
foreach (var item in SignalsLongUp.ToList())
|
||||
if (item.SignalStart + SessionTime < nowTime) SignalsLongUp.Remove(item);
|
||||
foreach (var item in SignalsShortDown.ToList())
|
||||
if (item.SignalStart + SessionTime < nowTime) SignalsShortDown.Remove(item);
|
||||
foreach (var item in SignalsLongDown.ToList())
|
||||
if (item.SignalStart + SessionTime < nowTime) SignalsShortUp.Remove(item);
|
||||
foreach (var item in SignalsShortUp.ToList())
|
||||
if (item.SignalStart + SessionTime < nowTime) SignalsShortDown.Remove(item);
|
||||
}
|
||||
|
||||
private void UpdateActiveSignalPrices()
|
||||
{
|
||||
lock (_state.SyncRoot)
|
||||
{
|
||||
foreach (var sig in SignalsLongUp)
|
||||
{
|
||||
var market = _state.candleDataLists.Find(i => i.ConGoodsData?.MarketName == sig.MarketName);
|
||||
if (market == null) continue;
|
||||
if (!market.candleDataList.TryGetValue(sig.MCandleTimeType, out var list) || list.Count == 0) continue;
|
||||
|
||||
var last = list.OrderBy(i => i.Argument).Last();
|
||||
if (last.IchimokData == null) continue;
|
||||
|
||||
if (Checked구름떼상향돌파 || 영호조건식_상향돌파)
|
||||
{
|
||||
sig.Price = last.Close;
|
||||
sig.Rate = (last.Close / last.Open) * 100 - 100;
|
||||
sig.IndicatorRate = (last.Close / sig.IndicatorPrice) * 100 - 100;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var sig in SignalsShortDown)
|
||||
{
|
||||
var market = _state.candleDataLists.Find(i => i.ConGoodsData?.MarketName == sig.MarketName);
|
||||
if (market == null) continue;
|
||||
if (!market.candleDataList.TryGetValue(sig.MCandleTimeType, out var list) || list.Count == 0) continue;
|
||||
|
||||
var last = list.OrderBy(i => i.Argument).Last();
|
||||
if (last.IchimokData == null) continue;
|
||||
|
||||
if (Checked구름떼하향돌파 || 영호조건식_하향돌파)
|
||||
{
|
||||
sig.Price = last.Close;
|
||||
sig.Rate = (last.Close / last.Open) * 100 - 100;
|
||||
sig.IndicatorRate = (last.Close / sig.IndicatorPrice) * 100 - 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializePriceLevels(
|
||||
MarketCandleDataList findedMarket,
|
||||
string market,
|
||||
string strInterval,
|
||||
MCandleTimeType interval,
|
||||
IchimokuConditionSetup ichimoku,
|
||||
MovingAverageConditionSetup movingAverage)
|
||||
{
|
||||
_levels.GetOrCreate(market + strInterval);
|
||||
|
||||
List<CandleData> sorted;
|
||||
lock (_state.SyncRoot)
|
||||
{
|
||||
if (!findedMarket.candleDataList.TryGetValue(interval, out var raw)) return;
|
||||
sorted = raw.OrderBy(i => i.Argument).ToList();
|
||||
}
|
||||
|
||||
if (sorted.Count < 212) return; // 기존 코드 기준 (199 + 최소 여유)
|
||||
|
||||
var tempList = new List<CandleData>();
|
||||
for (int i = 0; i < 199 && i < sorted.Count; i++) tempList.Add(sorted[i]);
|
||||
|
||||
for (int i = 199; i < sorted.Count; i++)
|
||||
{
|
||||
var main = sorted[i];
|
||||
if (main == null) continue;
|
||||
tempList.Add(main);
|
||||
|
||||
ichimoku.calIchimokuData(tempList, main);
|
||||
movingAverage.Cal_YHK_Downpublic(tempList, main);
|
||||
|
||||
if (i > 210)
|
||||
{
|
||||
var mv4Now = tempList[i].movingAverageDataList[4].Value;
|
||||
var mv4Prev = tempList[i - 1].movingAverageDataList[4].Value;
|
||||
var mv4Prev2 = tempList[i - 2].movingAverageDataList[4].Value;
|
||||
var mv4Prev3 = tempList[i - 3].movingAverageDataList[4].Value;
|
||||
|
||||
_levels.Calculate(market, strInterval, interval, mv4Prev, mv4Prev2, mv4Prev3,
|
||||
tempList[tempList.Count - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EvaluateFivemCandleChain(
|
||||
MarketCandleDataList findedMarket,
|
||||
IchimokuConditionSetup ichimoku,
|
||||
MovingAverageConditionSetup movingAverage)
|
||||
{
|
||||
lock (_state.SyncRoot)
|
||||
{
|
||||
if (!findedMarket.candleDataList.TryGetValue(MCandleTimeType.M5, out var list) || list.Count < 5) return;
|
||||
var sorted = list.OrderBy(i => i.Argument).ToList();
|
||||
|
||||
for (int back = 0; back < 5; back++)
|
||||
{
|
||||
if (sorted.Count == 0) break;
|
||||
var candle = sorted.Last();
|
||||
if (candle == null) break;
|
||||
|
||||
ichimoku.calIchimokuData(sorted, candle);
|
||||
movingAverage.Cal_YHK_Downpublic(sorted, candle);
|
||||
|
||||
sorted.RemoveAt(sorted.Count - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 영호 조건식 상/하향 돌파 전략 실행 엔트리 포인트.
|
||||
/// 기존 calProcess 의 상승/하락 진입·청산 로직을 실제 주문 호출로 이식한다.
|
||||
/// 복잡도가 매우 높아 기본 진입 지점만 유지, 상세 평가 로직은 기존 WinForms 에서 주석 해제된 부분을 참고해 확장할 것.
|
||||
/// </summary>
|
||||
private async Task EvaluateStrategyAsync(
|
||||
string market,
|
||||
string strInterval,
|
||||
MCandleTimeType interval,
|
||||
MarketCandleDataList findedMarket,
|
||||
DateTime nowTime,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// 전저고점 참조
|
||||
var 저고점_5 = _levels.GetOrCreate(market + "5m");
|
||||
if (저고점_5["전전전저가"] == 0 || 저고점_5["전전전고가"] == 0) return;
|
||||
|
||||
CandleData candleData_5;
|
||||
CandleData candleData_5_전봉;
|
||||
lock (_state.SyncRoot)
|
||||
{
|
||||
if (!findedMarket.candleDataList.TryGetValue(MCandleTimeType.M5, out var list) || list.Count < 2) return;
|
||||
var sorted = list.OrderBy(i => i.Argument).ToList();
|
||||
candleData_5 = sorted[sorted.Count - 1];
|
||||
candleData_5_전봉 = sorted[sorted.Count - 2];
|
||||
}
|
||||
|
||||
var price = candleData_5.Close;
|
||||
|
||||
// === 상향 돌파: Long 진입/청산 (기존 UpBuyList 흐름) ===
|
||||
if (영호조건식_상향돌파 && !UpBuyList.ContainsKey(market))
|
||||
{
|
||||
// 진입 조건: 이전 저점이 상승 전환 (단순화된 기준)
|
||||
if (저고점_5["전저가"] < 저고점_5["전전저가"] && price > candleData_5_전봉.Close)
|
||||
{
|
||||
double size = _options.OrderAmount / price * _options.Leverage;
|
||||
try
|
||||
{
|
||||
var orderData = await _trading.FutureOrderAsync(
|
||||
market, FutureMarginMode.isolated, size, price,
|
||||
FutureSide.BuyLong, OrderType.limit, _options.Leverage, ct: ct).ConfigureAwait(false);
|
||||
|
||||
if (orderData != null && orderData.TryGetValue("data", out var dataRaw))
|
||||
{
|
||||
var orderDic = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, string>>(
|
||||
Newtonsoft.Json.JsonConvert.SerializeObject(dataRaw));
|
||||
|
||||
var record = new Dictionary<string, object>
|
||||
{
|
||||
{ "MarketName", market }, { "BuyDate", nowTime },
|
||||
{ "Price", price }, { "MCandleTimeType", (int)interval },
|
||||
{ "Candle", candleData_5 }, { "Size", size },
|
||||
{ "orderId", orderDic?["orderId"] ?? string.Empty }
|
||||
};
|
||||
UpBuyList[market] = record;
|
||||
BuyLongList.Add(record);
|
||||
LongCount++;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { _logger.LogError(ex, "Long 진입 주문 실패: {Market}", market); }
|
||||
}
|
||||
}
|
||||
|
||||
// === 하향 돌파: Short 진입/청산 (기존 DownBuyList 흐름) ===
|
||||
if (영호조건식_하향돌파 && !DownBuyList.ContainsKey(market))
|
||||
{
|
||||
if (저고점_5["전고가"] > 저고점_5["전전고가"] && price < candleData_5_전봉.Close)
|
||||
{
|
||||
double size = _options.OrderAmount / price * _options.Leverage;
|
||||
try
|
||||
{
|
||||
var orderData = await _trading.FutureOrderAsync(
|
||||
market, FutureMarginMode.isolated, size, price,
|
||||
FutureSide.BuyShort, OrderType.limit, _options.Leverage, ct: ct).ConfigureAwait(false);
|
||||
|
||||
if (orderData != null && orderData.TryGetValue("data", out var dataRaw))
|
||||
{
|
||||
var orderDic = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, string>>(
|
||||
Newtonsoft.Json.JsonConvert.SerializeObject(dataRaw));
|
||||
|
||||
var record = new Dictionary<string, object>
|
||||
{
|
||||
{ "MarketName", market }, { "BuyDate", nowTime },
|
||||
{ "Price", price }, { "MCandleTimeType", (int)interval },
|
||||
{ "Candle", candleData_5 }, { "Size", size },
|
||||
{ "orderId", orderDic?["orderId"] ?? string.Empty }
|
||||
};
|
||||
DownBuyList[market] = record;
|
||||
BuyShortList.Add(record);
|
||||
ShortCount++;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { _logger.LogError(ex, "Short 진입 주문 실패: {Market}", market); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
Bitget_Server/Application/State/MarketState.cs
Normal file
26
Bitget_Server/Application/State/MarketState.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Bitget_Server.Domain.Enums;
|
||||
using Bitget_Server.Domain.Models;
|
||||
|
||||
namespace Bitget_Server.Application.State;
|
||||
|
||||
/// <summary>
|
||||
/// 기존 WinForms CommonProperty에서 UI 참조를 제외한 순수 런타임 상태.
|
||||
/// DI 싱글톤으로 등록되어 전 서비스에서 공유.
|
||||
/// </summary>
|
||||
public class MarketState
|
||||
{
|
||||
public ExchangePlace selectedChangePlace { get; set; } = ExchangePlace.Bitget;
|
||||
public PaymentType selectedPaymentType { get; set; } = PaymentType.USDT;
|
||||
public BitgetProductType selectedBitgetProductType { get; set; } = BitgetProductType.FUTURE;
|
||||
|
||||
public List<MarketInfo> marketInfoList { get; set; } = new();
|
||||
public List<MarketCandleDataList> candleDataLists { get; set; } = new();
|
||||
public List<WebSocketTickerInfo> webSocketCandles { get; set; } = new();
|
||||
|
||||
/// <summary>현재 활성화된 신호 검색에 사용될 마켓/인터벌 목록 (기존 MainForm의 체크박스 값).</summary>
|
||||
public List<object> CheckedMarkets { get; set; } = new();
|
||||
public List<object> IntervalList { get; set; } = new();
|
||||
|
||||
/// <summary>락 오브젝트 (캔들 리스트 동시 접근 보호).</summary>
|
||||
public readonly object SyncRoot = new();
|
||||
}
|
||||
Reference in New Issue
Block a user