using PineScriptV6; using Serilog; namespace Buyer { /// /// 실시간 트레이더. /// 1) REST API로 과거 봉을 받아 WarmUp (지표 초기화) /// 2) CandlePoller로 봉 완성 시각마다 REST 폴링 /// 3) 새 완성 봉 → ProcessCandle → 신호 발생 시 로그 출력 /// public class RealtimeTrader : IDisposable { private readonly BitgetConfig _cfg; private readonly PineScriptExecutor _executor; private readonly BitgetRestClient _rest; private readonly CandlePoller _poller; public RealtimeTrader(BitgetConfig cfg) { _cfg = cfg; _executor = new PineScriptExecutor(cfg.PineScriptPath); _rest = new BitgetRestClient(cfg); _poller = new CandlePoller(cfg, _rest); } public async Task RunAsync(CancellationToken ct = default) { // ── Step 1: 과거 봉으로 WarmUp ─────────────────────────── Log.Information("[Trader] ═══ WarmUp 시작 ═══"); Log.Information("[Trader] 심볼={Symbol} 타임프레임={Gran} 봉수={N}", _cfg.Symbol, _cfg.Granularity, _cfg.WarmUpBars); var warmupCandles = await _rest.GetCandlesAsync(_cfg.WarmUpBars); if (warmupCandles.Count == 0) { Log.Error("[Trader] WarmUp 데이터 없음 — 종료"); return; } _executor.WarmUp(warmupCandles); // WarmUp 마지막 봉 시각을 폴러에 전달 → 중복 처리 방지 var lastWarmupTime = warmupCandles[^1].Time; _poller.SetLastProcessedTime(lastWarmupTime); Log.Information("[Trader] ═══ WarmUp 완료 ═══"); Log.Information("[Trader] 마지막 WarmUp 봉: {Time}", lastWarmupTime.ToLocalTime().ToString("yyyy-MM-dd HH:mm")); Log.Information("[Trader] 실시간 폴링 모드 진입 (Ctrl+C로 종료)"); // ── Step 2: 신호 델리게이트 등록 ──────────────────────── TradingSignal.OnBuy = price => LogOrder("BUY 롱 진입", price); TradingSignal.OnSell = price => LogOrder("SELL 롱 청산", price); TradingSignal.OnShort = price => LogOrder("SHORT 숏 진입", price); TradingSignal.OnCover = price => LogOrder("COVER 숏 청산", price); // ── Step 3: 폴러 콜백 등록 ────────────────────────────── _poller.OnCandle += candle => { try { var signals = _executor.ProcessCandle(candle); Log.Information("[Trader] ProcessCandle 완료: {Time} 신호={N}개", candle.Time.ToLocalTime().ToString("yyyy-MM-dd HH:mm"), signals.Count); // 신호는 TradingSignal 델리게이트를 통해 자동으로 LogOrder 호출됨 } catch (Exception ex) { Log.Error(ex, "[Trader] ProcessCandle 오류: {Time}", candle.Time.ToString("yyyy-MM-dd HH:mm")); } }; // ── Step 4: 폴링 루프 실행 (블로킹) ───────────────────── await _poller.RunAsync(ct); Log.Information("[Trader] 종료"); } // ── 주문 로그 ──────────────────────────────────────────────── private void LogOrder(string action, decimal price) { string bar = new string('═', 42); Log.Information(bar); Log.Information(" ▶ {Action} | {Symbol} | {Price:F5}", action, _cfg.Symbol, price); Log.Information(" 시각: {Time}", DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss")); Log.Information(bar); // TODO: 실제 주문 코드는 여기에 추가 // await _orderClient.PlaceMarketOrderAsync(_cfg.Symbol, side, qty); } public void Dispose() => _rest.Dispose(); } }