134 lines
5.6 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|