using System.Diagnostics;
using Serilog;
using Serilog.Events;
namespace PineScriptV6
{
// ════════════════════════════════════════════════════════════════
// 문 (Statement) 타입 정의
// ════════════════════════════════════════════════════════════════
/// Pine Script 한 문(statement)의 추상 기반 클래스
public abstract record PineStmt;
/// var bool inLong = false → 최초 1회만 초기화
public record VarDeclStmt(string Name, string TypeHint, string Expr) : PineStmt;
/// sma4 = expr 또는 newLongSignal = false
public record AssignStmt(string Name, string Expr) : PineStmt;
/// inLong := true (상태 변수 재할당)
public record ReassignStmt(string Name, string Expr) : PineStmt;
/// plotshape(...), bgcolor(...) 등 반환값 없는 호출
public record ExprStmt(string Expr) : PineStmt;
/// if condition\n body
public record IfStmt(string Condition, List ThenBody, List? ElseBody) : PineStmt;
// ════════════════════════════════════════════════════════════════
// 실행기
// ════════════════════════════════════════════════════════════════
public class PineScriptExecutor
{
private readonly string _filePath;
private List? _program; // 최초 1회 파싱
private DependencyAnalyzer.ExecutionPlan? _plan; // 최초 1회 분석
// 실시간 모드용 — WarmUp 이후 ProcessCandle에서 사용
private int _realtimeBarIndex = 0;
public PineScriptExecutor(string filePath = "./example.pine")
{
_filePath = filePath;
}
// ════════════════════════════════════════════════════════════════
// 배치 모드 (CSV 히스토리 전체 실행)
// ════════════════════════════════════════════════════════════════
///
/// 봉 데이터를 순서대로 주입하며 매 봉마다 스크립트를 실행합니다.
/// Two-Pass: Pass1(지표) → Pass2(상태머신) 순으로 실행.
/// signalFrom: 이 시각 이후 봉에만 상세 추적 로그를 출력합니다.
///
public void ExecuteOnCandles(List candles,
DateTimeOffset? signalFrom = null)
{
ResetState();
PineEvaluator.ResetStats();
EnsurePlan();
if (_program!.Count == 0)
{
Log.Error("[Executor] 스크립트 로드 실패: {Path}", _filePath);
return;
}
Log.Information("[Executor] Two-Pass 실행 시작 | 총 {Count}봉 " +
"(Pass1={P1} Pass2={P2}) | 추적={From}",
candles.Count,
_plan!.Pass1Stmts.Count, _plan.Pass2Stmts.Count,
signalFrom.HasValue ? signalFrom.Value.ToString("yyyy-MM-dd") : "전체");
long swPass1Ms = 0, swPass2Ms = 0, swInjectMs = 0, swLogMs = 0;
var sw = Stopwatch.StartNew();
for (int i = 0; i < candles.Count; i++)
{
CandleData c = candles[i];
bool inSignalRange = !signalFrom.HasValue || c.Time >= signalFrom.Value;
var t0 = sw.ElapsedTicks;
InjectOhlcv(c, i);
var t1 = sw.ElapsedTicks;
// Pass 1 — 지표 계산 (O(1) 증분)
ExecuteStmts(_plan.Pass1Stmts);
var t2 = sw.ElapsedTicks;
// Pass 2 — 상태 머신 (순차)
ExecuteStmts(_plan.Pass2Stmts);
var t3 = sw.ElapsedTicks;
if (inSignalRange)
LogBarDetail(c, i + 1, candles.Count);
else
LogWarmupSnap(c, i + 1, candles.Count);
var t4 = sw.ElapsedTicks;
swInjectMs += t1 - t0;
swPass1Ms += t2 - t1;
swPass2Ms += t3 - t2;
swLogMs += t4 - t3;
}
double ticksPerMs = (double)Stopwatch.Frequency / 1000.0;
Log.Information("[Executor] 실행 완료 | {Count}봉 처리", candles.Count);
Log.Information("═══ Pass별 소요 시간 ═══════════════");
Log.Information(" Inject OHLCV : {Ms,8:F1} ms", swInjectMs / ticksPerMs);
Log.Information(" Pass1(지표) : {Ms,8:F1} ms", swPass1Ms / ticksPerMs);
Log.Information(" Pass2(상태) : {Ms,8:F1} ms", swPass2Ms / ticksPerMs);
Log.Information(" Logging : {Ms,8:F1} ms", swLogMs / ticksPerMs);
Log.Information("════════════════════════════════════");
PineEvaluator.DumpStats();
}
// ════════════════════════════════════════════════════════════════
// 실시간 모드 (스트리밍)
// ════════════════════════════════════════════════════════════════
///
/// 실시간 연결 전, 과거 봉으로 지표 상태를 초기화합니다.
/// warmupCandles: 최근 WarmupBars개 봉 (지표 최대 윈도우 이상이어야 함).
///
public void WarmUp(List warmupCandles)
{
ResetState();
EnsurePlan();
Log.Information("[Executor] WarmUp 시작 | {Count}봉", warmupCandles.Count);
for (int i = 0; i < warmupCandles.Count; i++)
{
InjectOhlcv(warmupCandles[i], i);
ExecuteStmts(_plan!.Pass1Stmts);
ExecuteStmts(_plan.Pass2Stmts);
}
_realtimeBarIndex = warmupCandles.Count;
Log.Information("[Executor] WarmUp 완료 | 다음 bar_index={Idx}", _realtimeBarIndex);
}
///
/// 실시간 단일 봉 처리.
/// WarmUp() 이후에 호출해야 합니다.
/// 반환값: 이 봉에서 발생한 신호 목록 (없으면 빈 리스트).
///
public List ProcessCandle(CandleData candle)
{
EnsurePlan();
int prevCount = TradingSignal.Results.Count;
InjectOhlcv(candle, _realtimeBarIndex++);
ExecuteStmts(_plan!.Pass1Stmts);
ExecuteStmts(_plan.Pass2Stmts);
// 이번 봉에서 새로 추가된 신호만 반환
return TradingSignal.Results.Count > prevCount
? TradingSignal.Results.GetRange(prevCount,
TradingSignal.Results.Count - prevCount)
: new List();
}
// ════════════════════════════════════════════════════════════════
// 내부 헬퍼
// ════════════════════════════════════════════════════════════════
private void ResetState()
{
PineContext.Reset();
PineStateVars.Clear();
TradingSignal.ClearResults();
IndicatorEngine.Reset();
_realtimeBarIndex = 0;
}
private void EnsurePlan()
{
_program ??= LoadScript();
if (_plan != null) return;
_plan = DependencyAnalyzer.Analyze(_program);
Log.Information("[Executor] 의존성 분석 완료 | Pass1={P1}문 Pass2={P2}문 | " +
"Pass1변수={Vars}개 StateVars={State}개",
_plan.Pass1Stmts.Count, _plan.Pass2Stmts.Count,
_plan.Pass1Vars.Count, _plan.StateVars.Count);
}
private static void InjectOhlcv(CandleData c, int barIndex)
{
Pine.open = (double)c.Open;
Pine.high = (double)c.High;
Pine.low = (double)c.Low;
Pine.close = (double)c.Close;
Pine.volume = (double)c.Volume;
Pine.time = c.Time.ToUnixTimeMilliseconds();
PineContext.CurrentBarIndex = barIndex;
}
// ════════════════════════════════════════════════════════════════
// 추적 로그
// ════════════════════════════════════════════════════════════════
/// 분석 구간 매 봉: 핵심 변수 한 줄 + 신호·NearMiss 상세
private static void LogBarDetail(CandleData c, int barNum, int total)
{
bool debugOn = Log.IsEnabled(LogEventLevel.Debug);
// ── ① 신호 발생 여부 최우선 확인 (4개 조회) ─────────────
bool sigL = PineEvaluator.ToBool(PineStateVars.Get("newLongSignal"));
bool sigLS = PineEvaluator.ToBool(PineStateVars.Get("longSellSignal"));
bool sigS = PineEvaluator.ToBool(PineStateVars.Get("newShortSignal"));
bool sigSS = PineEvaluator.ToBool(PineStateVars.Get("shortSellSignal"));
if (sigL || sigLS || sigS || sigSS)
{
Log.Information(
"★[SIGNAL] {Time} Bar={B}/{T} | " +
"NewLong={SL} LongSell={SLS} NewShort={SS} ShortSell={SSS} | " +
"Close={C} sma72={S72} cloudTop={CT}",
c.Time.ToString("yyyy-MM-dd HH:mm"), barNum, total,
sigL, sigLS, sigS, sigSS,
FV(c.Close),
FV(PineStateVars.Get("sma72")),
FV(PineStateVars.Get("cloudTopNow")));
return;
}
// ── ② rawNew* 확인 (NearMiss) ─────────────────────────────
bool rawL = PineEvaluator.ToBool(PineStateVars.Get("rawNewLong"));
bool rawS = PineEvaluator.ToBool(PineStateVars.Get("rawNewShort"));
// ── ③ NearMiss도 없고 Debug도 꺼져있으면 즉시 리턴 ───────
// → 나머지 10개 Dict 조회·포맷팅 완전 생략
if (!rawL && !rawS && !debugOn) return;
// ── ④ 나머지 변수 (필요한 경우에만 조회) ─────────────────
bool inL = PineEvaluator.ToBool(PineStateVars.Get("inLong"));
bool inS = PineEvaluator.ToBool(PineStateVars.Get("inShort"));
bool sidewaysL = PineEvaluator.ToBool(PineStateVars.Get("isSidewaysLong"));
bool sidewaysS = PineEvaluator.ToBool(PineStateVars.Get("isSidewaysShort"));
bool touchL = PineEvaluator.ToBool(PineStateVars.Get("isLongRecentTouch"));
bool touchS = PineEvaluator.ToBool(PineStateVars.Get("isShortRecentTouch"));
if (rawL || rawS)
{
Log.Information(
"△[NEAR ] {Time} | rawL={RL} rawS={RS} inL={IL} inS={IS} | " +
"sideL={SL} sideS={SS} touchL={TL} touchS={TS}",
c.Time.ToString("yyyy-MM-dd HH:mm"),
rawL, rawS, inL, inS, sidewaysL, sidewaysS, touchL, touchS);
}
// ── ⑤ 매 봉 상세: Debug 레벨 (기본 꺼짐 → 스킵) ─────────
if (!debugOn) return;
string sma72 = FV(PineStateVars.Get("sma72"));
string sma4M = FV(PineStateVars.Get("sma4Middle"));
string cloudTop = FV(PineStateVars.Get("cloudTopNow"));
string rangeTh = FV(PineStateVars.Get("rangeThresholdLongPercent"));
bool rawLS = PineEvaluator.ToBool(PineStateVars.Get("rawLongSell"));
bool rawSS = PineEvaluator.ToBool(PineStateVars.Get("rawShortSell"));
Log.Debug(
"[BAR ] {Time} | C={C} s72={S72} s4M={S4M} cTop={CT} rTh={RT} | " +
"sideL={SL} sideS={SS} tL={TL} tS={TS} | " +
"rawL={RL} rawS={RS} rawLS={RLS} rawSS={RSS} | inL={IL} inS={IS}",
c.Time.ToString("yyyy-MM-dd HH:mm"),
FV(c.Close), sma72, sma4M, cloudTop, rangeTh,
B(sidewaysL), B(sidewaysS), B(touchL), B(touchS),
B(rawL), B(rawS), B(rawLS), B(rawSS),
B(inL), B(inS));
}
/// 워밍업 구간: 50봉마다 지표 준비 상황만 출력
private static void LogWarmupSnap(CandleData c, int barNum, int total)
{
if (barNum % 50 != 0 && barNum != 1) return;
Log.Information(
"[WARM] Bar={B}/{T} {Time} | close={C} sma72={S72} cloudTop={CT} rangeTh={RT}",
barNum, total, c.Time.ToString("yyyy-MM-dd HH:mm"),
FV(c.Close),
FV(PineStateVars.Get("sma72")),
FV(PineStateVars.Get("cloudTopNow")),
FV(PineStateVars.Get("rangeThresholdLongPercent")));
}
// ── 포맷 헬퍼 ────────────────────────────────────────────────────
private static string FV(object? v)
{
if (v == null) return "na";
if (v is double d) return d.ToString("F4");
if (v is decimal m) return m.ToString("F4");
return v.ToString() ?? "na";
}
private static string FV(decimal m) => m.ToString("F4");
private static string B(bool b) => b ? "T" : "F";
// ── 스크립트 로드 & 파싱 ──────────────────────────────────────
private List LoadScript()
{
if (!File.Exists(_filePath))
{
Log.Error("[Executor] 파일 없음: {Path}", _filePath);
return new List();
}
// 들여쓰기 보존 → (indent, content) 쌍으로 수집
var lines = new List<(int Indent, string Raw)>();
foreach (string raw in File.ReadAllLines(_filePath))
{
string trimEnd = raw.TrimEnd();
if (string.IsNullOrWhiteSpace(trimEnd)) continue;
string content = trimEnd.TrimStart();
// 주석·버전 어노테이션 제외
if (content.StartsWith("//") || content.StartsWith("//@")) continue;
int indent = trimEnd.Length - content.Length;
lines.Add((indent, content));
}
Log.Information("[Executor] 스크립트 로드 | 유효 라인 {Count}개 | {Path}",
lines.Count, _filePath);
int pos = 0;
var stmts = BuildBlock(lines, ref pos, -1);
Log.Information("[Executor] 문(Statement) 파싱 완료 | {Count}개", stmts.Count);
return stmts;
}
// ── 블록 빌더 (재귀) ─────────────────────────────────────────
///
/// parentIndent 보다 큰 들여쓰기 수준의 라인들을 문 목록으로 변환합니다.
///
private List BuildBlock(List<(int Indent, string Raw)> lines,
ref int pos, int parentIndent)
{
var stmts = new List();
while (pos < lines.Count)
{
var (indent, raw) = lines[pos];
// 상위 블록으로 복귀
if (indent <= parentIndent) break;
// ── if 처리 ──
if (raw.StartsWith("if ", StringComparison.OrdinalIgnoreCase) ||
raw.StartsWith("if(", StringComparison.OrdinalIgnoreCase))
{
string condition = ExtractCondition(raw);
int myIndent = indent;
pos++;
// then 블록: 다음 라인들 중 indent > myIndent
var thenBody = pos < lines.Count && lines[pos].Indent > myIndent
? BuildBlock(lines, ref pos, myIndent)
: new List();
// else 블록
List? elseBody = null;
if (pos < lines.Count
&& lines[pos].Indent == myIndent
&& lines[pos].Raw.Equals("else", StringComparison.OrdinalIgnoreCase))
{
pos++;
elseBody = pos < lines.Count && lines[pos].Indent > myIndent
? BuildBlock(lines, ref pos, myIndent)
: new List();
}
stmts.Add(new IfStmt(condition, thenBody, elseBody));
continue; // pos는 BuildBlock 내부에서 이미 이동
}
// ── 일반 문 ──
var stmt = ParseStatement(raw);
if (stmt != null) stmts.Add(stmt);
pos++;
}
return stmts;
}
// ── 문 파싱 ───────────────────────────────────────────────────
private static PineStmt? ParseStatement(string line)
{
// 1) var 선언: var bool inLong = false
if (line.StartsWith("var ", StringComparison.OrdinalIgnoreCase))
{
string rest = line.Substring(4).Trim();
// type hint 제거
string typeHint = "";
string[] types = { "float[]", "int[]", "bool[]", "string[]",
"bool", "int", "float", "string" };
foreach (var t in types)
{
if (rest.StartsWith(t + " ", StringComparison.OrdinalIgnoreCase))
{
typeHint = t;
rest = rest.Substring(t.Length).Trim();
break;
}
}
int eqIdx = FindAssignEq(rest);
if (eqIdx > 0)
{
string name = rest.Substring(0, eqIdx).Trim();
string expr = rest.Substring(eqIdx + 1).Trim();
return new VarDeclStmt(name, typeHint, expr);
}
// var foo (초기화 없음)
string justName = rest.Split(' ')[0].Trim();
return new VarDeclStmt(justName, typeHint, "na");
}
// 2) := 재할당: inLong := true
{
int cIdx = FindReassignOp(line);
if (cIdx > 0)
{
string name = line.Substring(0, cIdx).Trim();
string expr = line.Substring(cIdx + 2).Trim();
if (IsSimpleIdent(name))
return new ReassignStmt(name, expr);
}
}
// 3) 대입: sma4 = expr
{
int eqIdx = FindAssignEq(line);
if (eqIdx > 0)
{
string lhs = line.Substring(0, eqIdx).Trim();
string rhs = line.Substring(eqIdx + 1).Trim();
if (IsSimpleIdent(lhs) && !string.IsNullOrEmpty(rhs))
return new AssignStmt(lhs, rhs);
}
}
// 4) 표현식 문: plotshape(...), plot(...) 등
if (!string.IsNullOrEmpty(line))
return new ExprStmt(line);
return null;
}
// ── 문 실행 ───────────────────────────────────────────────────
private static void ExecuteStmts(List stmts)
{
foreach (var stmt in stmts)
ExecuteOne(stmt);
}
private static void ExecuteOne(PineStmt stmt)
{
switch (stmt)
{
// ── var 선언 (최초 1회만 초기화) ──
case VarDeclStmt vd:
{
if (!PineStateVars.Has(vd.Name))
{
object? val = PineEvaluator.Evaluate(vd.Expr);
PineStateVars.Declare(vd.Name, val);
Log.Debug("[Exec] var {Name} = {Val}", vd.Name, val);
}
break;
}
// ── 일반 대입 (매 봉마다 재평가) ──
case AssignStmt a:
{
object? val = PineEvaluator.Evaluate(a.Expr);
PineStateVars.Set(a.Name, val);
// double 값은 PineContext 시리즈에도 Push (ta.sma 등이 읽음)
double dv = PineEvaluator.ToDouble(val);
if (!double.IsNaN(dv))
PineContext.SetVariable(a.Name, dv);
Log.Debug("[Exec] {Name} = {Val}", a.Name, val);
break;
}
// ── := 재할당 ──
case ReassignStmt r:
{
object? val = PineEvaluator.Evaluate(r.Expr);
PineStateVars.Set(r.Name, val);
double dv = PineEvaluator.ToDouble(val);
if (!double.IsNaN(dv))
PineContext.SetVariable(r.Name, dv);
Log.Debug("[Exec] {Name} := {Val}", r.Name, val);
break;
}
// ── 표현식 문 (side-effect 목적) ──
case ExprStmt e:
{
PineEvaluator.Evaluate(e.Expr);
break;
}
// ── if / else ──
case IfStmt ifs:
{
bool cond = PineEvaluator.EvaluateBool(ifs.Condition);
Log.Debug("[Exec] if ({Cond}) → {Result}", ifs.Condition, cond);
if (cond)
ExecuteStmts(ifs.ThenBody);
else if (ifs.ElseBody != null)
ExecuteStmts(ifs.ElseBody);
break;
}
}
}
// ════════════════════════════════════════════════════════════════
// 파싱 헬퍼
// ════════════════════════════════════════════════════════════════
/// if 라인에서 조건 문자열 추출
private static string ExtractCondition(string line)
{
// "if " or "if(" 이후 텍스트
string rest = line.Length > 3 ? line.Substring(3).Trim() : "";
// 바깥 괄호 제거: "(cond)" → "cond"
if (rest.StartsWith("(") && rest.EndsWith(")"))
{
// 매칭 괄호 확인
int depth = 0;
bool matched = false;
for (int i = 0; i < rest.Length; i++)
{
if (rest[i] == '(') depth++;
else if (rest[i] == ')') { depth--; if (depth == 0 && i == rest.Length - 1) { matched = true; break; } }
}
if (matched) rest = rest.Substring(1, rest.Length - 2).Trim();
}
return rest;
}
///
/// 순수 대입 '=' 위치 반환.
/// ==, !=, <=, >=, := 는 제외.
/// 괄호/대괄호 안은 탐색 제외.
///
private static int FindAssignEq(string line)
{
int depth = 0;
for (int i = 1; i < line.Length; i++)
{
char c = line[i];
if (c == '(' || c == '[') { depth++; continue; }
if (c == ')' || c == ']') { depth--; continue; }
if (depth > 0) continue;
if (c == '=')
{
char prev = line[i - 1];
char next = i + 1 < line.Length ? line[i + 1] : '\0';
bool compound = prev is '!' or '<' or '>' or '=' or ':';
if (!compound && next != '=') return i;
}
}
return -1;
}
/// := 위치 반환 (없으면 -1), 괄호 밖만 탐색
private static int FindReassignOp(string line)
{
int depth = 0;
for (int i = 0; i < line.Length - 1; i++)
{
char c = line[i];
if (c == '(' || c == '[') { depth++; continue; }
if (c == ')' || c == ']') { depth--; continue; }
if (depth > 0) continue;
if (c == ':' && line[i + 1] == '=') return i;
}
return -1;
}
/// 단순 변수명 여부 (영문자·숫자·밑줄만, 첫 글자는 영문자/밑줄)
private static bool IsSimpleIdent(string s)
{
if (string.IsNullOrEmpty(s)) return false;
if (!char.IsLetter(s[0]) && s[0] != '_') return false;
foreach (char c in s)
if (!char.IsLetterOrDigit(c) && c != '_') return false;
return true;
}
}
}