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;
}
}
}