using Serilog; namespace PineScriptV6 { /// plotshape의 text 값으로 감지된 매매 신호 종류 public enum SignalType { Buy, // Long 진입 Sell, // Long 청산 Short, // Short 진입 Cover // Short 청산 } /// /// 신호 발생 시점의 데이터를 담는 레코드. /// TradingView 결과와 비교하는 데 사용합니다. /// public record TradeSignalEvent { public DateTimeOffset Time { get; init; } public SignalType Signal { get; init; } public decimal Price { get; init; } /// 신호 발생 봉의 rangeThresholdLongPercent 값 (없으면 null) public double? RangeThresholdLong { get; init; } /// 신호 발생 봉의 rangeThresholdShortPercent 값 (없으면 null) public double? RangeThresholdShort { get; init; } /// 신호 발생 봉의 구름대 상단/하단, sma72/sma24 (진입 상태 표기용) public double? CloudTop { get; init; } public double? CloudBottom { get; init; } public double? Sma72 { get; init; } public double? Sma24 { get; init; } /// CSV 한 줄 포맷 public string ToCsvLine() => $"{Time:yyyy-MM-dd HH:mm},{Signal},{Price:F4}"; /// 콘솔 출력용 포맷 public override string ToString() => $" {Time:yyyy-MM-dd HH:mm} | {Signal,-10} | {Price,10:F4}"; } /// /// Pine Script의 plotshape → C# 매매 함수를 연결하는 신호 허브. /// 발생한 신호는 Results 컬렉션에 누적되어 TradingView 결과와 비교할 수 있습니다. /// public static class TradingSignal { // ── 매매 델리게이트 ────────────────────────────────────── public static Action? OnBuy { get; set; } public static Action? OnSell { get; set; } public static Action? OnShort { get; set; } public static Action? OnCover { get; set; } // ── 결과 수집 ──────────────────────────────────────────── /// 발생한 모든 신호를 시간 순으로 누적합니다. public static List Results { get; } = new(); /// Results를 초기화합니다. (재실행 시 사용) public static void ClearResults() => Results.Clear(); // ── text → SignalType 매핑 ─────────────────────────────── private static readonly Dictionary TextMap = new(StringComparer.OrdinalIgnoreCase) { { "Long", SignalType.Buy }, { "Long Sell", SignalType.Sell }, { "Short", SignalType.Short }, { "Short Sell", SignalType.Cover }, }; /// /// plotshape의 text 값으로 신호를 감지합니다. /// 감지된 신호는 Results에 기록되고 등록된 델리게이트를 호출합니다. /// public static void DetectAndRaise(string text, decimal price, DateTimeOffset time = default) { string key = text.Trim(); if (!TextMap.TryGetValue(key, out SignalType signal)) return; // ── 신호 발생 봉의 상태값 캡처 (없으면 null) ── double? thLong = ReadStateDouble("rangeThresholdLongPercent"); double? thShort = ReadStateDouble("rangeThresholdShortPercent"); // ── 결과 수집 ── var evt = new TradeSignalEvent { Time = time == default ? DateTimeOffset.Now : time, Signal = signal, Price = price, RangeThresholdLong = thLong, RangeThresholdShort = thShort, CloudTop = ReadStateDouble("cloudTopNow"), CloudBottom = ReadStateDouble("cloudBottomNow"), Sma72 = ReadStateDouble("sma72"), Sma24 = ReadStateDouble("sma24"), }; Results.Add(evt); Log.Information("[Signal] {Signal,-10} | {Time:yyyy-MM-dd HH:mm} | {Price:F4}", signal, evt.Time, price); // ── 델리게이트 호출 ── switch (signal) { case SignalType.Buy: OnBuy?.Invoke(price); break; case SignalType.Sell: OnSell?.Invoke(price); break; case SignalType.Short: OnShort?.Invoke(price); break; case SignalType.Cover: OnCover?.Invoke(price); break; } } public static bool IsSignal(string text) => TextMap.ContainsKey(text.Trim()); /// PineStateVars의 숫자 변수를 double?로 읽습니다. (없거나 비숫자면 null) private static double? ReadStateDouble(string name) { if (!PineStateVars.TryGet(name, out var v) || v == null) return null; double d = PineEvaluator.ToDouble(v); return double.IsNaN(d) ? (double?)null : d; } } }