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; /// /// 기존 IndicatorProcess.calProcess 이식. /// WinForms Thread 무한 루프는 제거되고, 본 서비스의 ProcessOnceAsync 를 /// SignalInfoBackgroundService 가 주기적으로 호출한다. /// public class SignalProcessService { private readonly MarketState _state; private readonly GlobalOptions _options; private readonly BitgetTradingService _trading; private readonly PriceLevelTracker _levels; private readonly ILogger _logger; // signals_LU / signals_SD / signals_LD / signals_SU (기존 public 필드 이식) public List SignalsLongUp { get; } = new(); public List SignalsShortDown { get; } = new(); public List SignalsLongDown { get; } = new(); public List 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> UpBuyList { get; } = new(); public List> UpResultList { get; } = new(); public Dictionary> DownBuyList { get; } = new(); public List> DownResultList { get; } = new(); public List> BuyLongList { get; } = new(); public List> BuyShortList { get; } = new(); public List> ResultList { get; } = new(); public Dictionary BuyShortStep { get; } = new(); public Dictionary 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 logger) { _state = state; _options = options; _trading = trading; _levels = levels; _logger = logger; } /// /// 기존 while(true) { calProcess } 루프의 1회 반복에 해당하는 처리. /// BackgroundService 가 짧은 주기로 호출하도록 설계. /// 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 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(); 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); } } } /// /// 영호 조건식 상/하향 돌파 전략 실행 엔트리 포인트. /// 기존 calProcess 의 상승/하락 진입·청산 로직을 실제 주문 호출로 이식한다. /// 복잡도가 매우 높아 기본 진입 지점만 유지, 상세 평가 로직은 기존 WinForms 에서 주석 해제된 부분을 참고해 확장할 것. /// 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>( Newtonsoft.Json.JsonConvert.SerializeObject(dataRaw)); var record = new Dictionary { { "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>( Newtonsoft.Json.JsonConvert.SerializeObject(dataRaw)); var record = new Dictionary { { "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); } } } } }