387 lines
16 KiB
C#
387 lines
16 KiB
C#
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); }
|
|
}
|
|
}
|
|
}
|
|
}
|