132 lines
5.7 KiB
C#
132 lines
5.7 KiB
C#
namespace PineScriptV6
|
|
{
|
|
/// <summary>
|
|
/// Pine Script 문(Statement) 목록을 정적 분석하여
|
|
/// Pass1(지표 사전계산) / Pass2(상태머신)로 자동 분류합니다.
|
|
///
|
|
/// Pass1 조건: 순수 AssignStmt이며, 참조 변수가 모두
|
|
/// OHLCV 또는 이전에 분류된 다른 Pass1 변수여야 합니다.
|
|
/// Pass2 조건: var 선언, := 재할당, if 블록, ExprStmt(side-effect),
|
|
/// 또는 상태 변수를 참조하는 AssignStmt.
|
|
///
|
|
/// Pine Script 문은 이미 위상 정렬된 순서로 작성되므로
|
|
/// 별도의 위상 정렬 없이 선형 스캔으로 분류할 수 있습니다.
|
|
/// </summary>
|
|
public static class DependencyAnalyzer
|
|
{
|
|
// ── OHLCV 및 내장 변수 (항상 Pass1에서 접근 가능) ─────────────
|
|
private static readonly HashSet<string> _builtins =
|
|
new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"close", "open", "high", "low", "volume",
|
|
"bar_index", "time"
|
|
};
|
|
|
|
// ── 표현식 내 무시할 키워드/리터럴 ───────────────────────────
|
|
private static readonly HashSet<string> _keywords =
|
|
new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"true", "false", "na", "and", "or", "not"
|
|
};
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// 공개 API
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
/// <summary>
|
|
/// 분석 결과를 담는 레코드.
|
|
/// Pass1Stmts → 증분 지표 계산 (매 봉, 순서 유지)
|
|
/// Pass2Stmts → 상태 머신 (매 봉, 순서 유지)
|
|
/// </summary>
|
|
public record ExecutionPlan(
|
|
List<PineStmt> Pass1Stmts,
|
|
List<PineStmt> Pass2Stmts,
|
|
HashSet<string> Pass1Vars,
|
|
HashSet<string> StateVars
|
|
);
|
|
|
|
public static ExecutionPlan Analyze(List<PineStmt> stmts)
|
|
{
|
|
// ── Step 1: 상태 변수 수집 ──────────────────────────────────
|
|
var stateVars = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
CollectStateVars(stmts, stateVars);
|
|
|
|
// ── Step 2: 순서대로 분류 ─────────────────────────────────
|
|
var pass1Vars = new HashSet<string>(_builtins, StringComparer.OrdinalIgnoreCase);
|
|
var pass1 = new List<PineStmt>();
|
|
var pass2 = new List<PineStmt>();
|
|
|
|
foreach (var stmt in stmts)
|
|
{
|
|
if (TryPass1(stmt, stateVars, pass1Vars, out string? varName))
|
|
{
|
|
pass1.Add(stmt);
|
|
if (varName != null) pass1Vars.Add(varName);
|
|
}
|
|
else
|
|
{
|
|
pass2.Add(stmt);
|
|
}
|
|
}
|
|
|
|
return new ExecutionPlan(pass1, pass2, pass1Vars, stateVars);
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// 내부 구현
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
/// <summary>모든 var 선언과 := 재할당 변수를 재귀적으로 수집합니다.</summary>
|
|
private static void CollectStateVars(List<PineStmt> stmts, HashSet<string> result)
|
|
{
|
|
foreach (var stmt in stmts)
|
|
{
|
|
switch (stmt)
|
|
{
|
|
case VarDeclStmt vd:
|
|
result.Add(vd.Name);
|
|
break;
|
|
case ReassignStmt r:
|
|
result.Add(r.Name);
|
|
break;
|
|
case IfStmt ifs:
|
|
CollectStateVars(ifs.ThenBody, result);
|
|
if (ifs.ElseBody != null)
|
|
CollectStateVars(ifs.ElseBody, result);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// stmt가 Pass1 조건을 만족하면 true 반환.
|
|
/// varName에는 이 문이 생성하는 변수명이 들어옵니다 (없으면 null).
|
|
/// </summary>
|
|
private static bool TryPass1(
|
|
PineStmt stmt,
|
|
HashSet<string> stateVars,
|
|
HashSet<string> pass1Vars,
|
|
out string? varName)
|
|
{
|
|
varName = null;
|
|
|
|
// Pass1 대상은 순수 대입문(AssignStmt)만
|
|
if (stmt is not AssignStmt a) return false;
|
|
|
|
// 변수 자체가 상태 변수이면 Pass2
|
|
if (stateVars.Contains(a.Name)) return false;
|
|
|
|
// 표현식 내 참조 변수가 모두 Pass1 범위 안이어야 함
|
|
var refs = PineEvaluator.GetReferencedVars(a.Expr);
|
|
foreach (var r in refs)
|
|
{
|
|
if (_keywords.Contains(r)) continue;
|
|
if (!pass1Vars.Contains(r)) return false; // 상태변수 or 미분류 → Pass2
|
|
}
|
|
|
|
varName = a.Name;
|
|
return true;
|
|
}
|
|
}
|
|
}
|