Files
PineSript/PineScript/TradingSignal.cs
2026-10-20 17:57:28 +09:00

134 lines
5.6 KiB
C#

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