1125 lines
49 KiB
C#
1125 lines
49 KiB
C#
using System.Reflection;
|
|
using Serilog;
|
|
|
|
namespace PineScriptV6
|
|
{
|
|
/// <summary>
|
|
/// Pine Script 표현식 평가기.
|
|
/// 표현식당 1회 AST 빌드 후 캐시 → 매 봉마다 트리 순회만 실행 (재파싱 없음).
|
|
/// </summary>
|
|
public static class PineEvaluator
|
|
{
|
|
// ════════════════════════════════════════════════════════════════
|
|
// AST 노드 정의
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
private abstract record Node;
|
|
private record NullNode() : Node;
|
|
private record NumberNode(double Value) : Node;
|
|
private record StringNode(string Value) : Node;
|
|
/// <summary>변수 조회 (ResolveIdent 호출)</summary>
|
|
private record IdentNode(string Name) : Node;
|
|
/// <summary>함수 인자용 — 단순 식별자를 이름 문자열로 전달 (ta.sma(close, n) 용)</summary>
|
|
private record BareIdentNode(string Name) : Node;
|
|
private record FuncCallNode(string FuncName,
|
|
List<(string? ArgName, Node ArgNode)> Args) : Node;
|
|
/// <summary>시리즈 히스토리 접근: name[offset]</summary>
|
|
private record IndexNode(string SeriesName, Node Offset) : Node;
|
|
private record UnaryMinusNode(Node Operand) : Node;
|
|
private record BinaryNode(TK Op, Node Left, Node Right) : Node;
|
|
private record TernaryNode(Node Cond, Node Then, Node Else) : Node;
|
|
private record AndNode(Node Left, Node Right) : Node;
|
|
private record OrNode(Node Left, Node Right) : Node;
|
|
private record NotNode(Node Operand) : Node;
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// 캐시 (정적 — 프로그램 수명 동안 유지)
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
/// <summary>표현식 문자열 → AST 루트 (최초 1회 빌드 후 재사용)</summary>
|
|
private static readonly Dictionary<string, Node> _astCache = new();
|
|
|
|
/// <summary>함수명 → MethodInfo (리플렉션 조회 최초 1회만)</summary>
|
|
private static readonly Dictionary<string, MethodInfo?> _funcCache = new();
|
|
|
|
/// <summary>MethodInfo → ParameterInfo[] (GetParameters 최초 1회만)</summary>
|
|
private static readonly Dictionary<MethodInfo, ParameterInfo[]> _paramCache = new();
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// 프로파일링 카운터 (실행 후 DumpStats()로 출력)
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
private static readonly Dictionary<string, long> _callCount = new();
|
|
private static long _reflectCount = 0; // InvokeReflected 호출 수
|
|
private static long _fastCount = 0; // 직접 디스패치 호출 수
|
|
private static long _indexNodeCount = 0; // IndexNode(series[n]) 접근 수
|
|
private static long _identCount = 0; // ResolveIdent 호출 수
|
|
|
|
/// <summary>카운터 초기화 (ExecuteOnCandles 시작 시 호출)</summary>
|
|
public static void ResetStats()
|
|
{
|
|
_callCount.Clear();
|
|
_reflectCount = 0;
|
|
_fastCount = 0;
|
|
_indexNodeCount = 0;
|
|
_identCount = 0;
|
|
}
|
|
|
|
/// <summary>수집된 통계를 Log.Information으로 출력</summary>
|
|
public static void DumpStats()
|
|
{
|
|
Log.Information("═══════ EvalNode 프로파일 ═══════");
|
|
Log.Information(" 직접 디스패치(fast): {Fast:N0}회", _fastCount);
|
|
Log.Information(" 리플렉션(reflect): {Ref:N0}회", _reflectCount);
|
|
Log.Information(" IndexNode(series[]): {Idx:N0}회", _indexNodeCount);
|
|
Log.Information(" ResolveIdent: {Id:N0}회", _identCount);
|
|
Log.Information("─── 함수별 호출 횟수 ────────────");
|
|
foreach (var kv in _callCount.OrderByDescending(x => x.Value))
|
|
Log.Information(" {Fn,-30} {Cnt,10:N0}회", kv.Key, kv.Value);
|
|
Log.Information("═════════════════════════════════");
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// 토크나이저
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
private enum TK
|
|
{
|
|
Num, Str, Id,
|
|
Plus, Minus, Star, Slash, Pct,
|
|
EqEq, NEq, Lt, Gt, LtEq, GtEq,
|
|
Eq, // 단일 '=' (named arg용)
|
|
LP, RP, LBr, RBr,
|
|
Comma, Dot, Colon, Question,
|
|
EOF
|
|
}
|
|
|
|
private record Tok(TK Kind, string Text);
|
|
|
|
private static List<Tok> Tokenize(string src)
|
|
{
|
|
var list = new List<Tok>();
|
|
int i = 0, n = src.Length;
|
|
|
|
while (i < n)
|
|
{
|
|
if (char.IsWhiteSpace(src[i])) { i++; continue; }
|
|
|
|
// 인라인 주석
|
|
if (i + 1 < n && src[i] == '/' && src[i + 1] == '/') break;
|
|
|
|
// 문자열 리터럴
|
|
if (src[i] == '"' || src[i] == '\'')
|
|
{
|
|
char q = src[i++];
|
|
int s = i;
|
|
while (i < n && src[i] != q) i++;
|
|
list.Add(new Tok(TK.Str, src.Substring(s, i - s)));
|
|
if (i < n) i++;
|
|
continue;
|
|
}
|
|
|
|
// 숫자
|
|
if (char.IsDigit(src[i]) || (src[i] == '.' && i + 1 < n && char.IsDigit(src[i + 1])))
|
|
{
|
|
int s = i;
|
|
while (i < n && (char.IsDigit(src[i]) || src[i] == '.')) i++;
|
|
list.Add(new Tok(TK.Num, src.Substring(s, i - s)));
|
|
continue;
|
|
}
|
|
|
|
// 식별자 / 키워드
|
|
if (char.IsLetter(src[i]) || src[i] == '_' || src[i] == '@')
|
|
{
|
|
int s = i;
|
|
while (i < n && (char.IsLetterOrDigit(src[i]) || src[i] == '_' || src[i] == '@')) i++;
|
|
list.Add(new Tok(TK.Id, src.Substring(s, i - s)));
|
|
continue;
|
|
}
|
|
|
|
// 색상 코드 (#RRGGBB) → 스킵
|
|
if (src[i] == '#')
|
|
{
|
|
i++;
|
|
while (i < n && char.IsLetterOrDigit(src[i])) i++;
|
|
continue;
|
|
}
|
|
|
|
// 2자 연산자
|
|
if (i + 1 < n)
|
|
{
|
|
string two = src.Substring(i, 2);
|
|
switch (two)
|
|
{
|
|
case "==": list.Add(new Tok(TK.EqEq, "==")); i += 2; continue;
|
|
case "!=": list.Add(new Tok(TK.NEq, "!=")); i += 2; continue;
|
|
case "<=": list.Add(new Tok(TK.LtEq, "<=")); i += 2; continue;
|
|
case ">=": list.Add(new Tok(TK.GtEq, ">=")); i += 2; continue;
|
|
}
|
|
}
|
|
|
|
// 1자 연산자 / 구두점
|
|
switch (src[i])
|
|
{
|
|
case '+': list.Add(new Tok(TK.Plus, "+")); break;
|
|
case '-': list.Add(new Tok(TK.Minus, "-")); break;
|
|
case '*': list.Add(new Tok(TK.Star, "*")); break;
|
|
case '/': list.Add(new Tok(TK.Slash, "/")); break;
|
|
case '%': list.Add(new Tok(TK.Pct, "%")); break;
|
|
case '<': list.Add(new Tok(TK.Lt, "<")); break;
|
|
case '>': list.Add(new Tok(TK.Gt, ">")); break;
|
|
case '=': list.Add(new Tok(TK.Eq, "=")); break;
|
|
case '(': list.Add(new Tok(TK.LP, "(")); break;
|
|
case ')': list.Add(new Tok(TK.RP, ")")); break;
|
|
case '[': list.Add(new Tok(TK.LBr, "[")); break;
|
|
case ']': list.Add(new Tok(TK.RBr, "]")); break;
|
|
case ',': list.Add(new Tok(TK.Comma, ",")); break;
|
|
case '.': list.Add(new Tok(TK.Dot, ".")); break;
|
|
case ':': list.Add(new Tok(TK.Colon, ":")); break;
|
|
case '?': list.Add(new Tok(TK.Question, "?")); break;
|
|
}
|
|
i++;
|
|
}
|
|
|
|
list.Add(new Tok(TK.EOF, ""));
|
|
return list;
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// 공개 진입점
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
/// <summary>
|
|
/// 표현식 평가. 최초 호출 시 AST를 빌드하여 캐시; 이후 호출은 캐시된 AST를 순회.
|
|
/// </summary>
|
|
public static object? Evaluate(string expr)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(expr)) return null;
|
|
try
|
|
{
|
|
if (!_astCache.TryGetValue(expr, out var ast))
|
|
{
|
|
var tokens = Tokenize(expr.Trim());
|
|
int pos = 0;
|
|
ast = BuildTernary(tokens, ref pos);
|
|
_astCache[expr] = ast;
|
|
}
|
|
return EvalNode(ast);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Debug("[Eval] 오류: {Expr} → {Err}", expr, ex.Message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public static bool EvaluateBool (string expr) => ToBool (Evaluate(expr));
|
|
public static double EvaluateDouble(string expr) => ToDouble(Evaluate(expr));
|
|
|
|
/// <summary>
|
|
/// 표현식에서 참조하는 변수명(식별자) 집합을 반환합니다.
|
|
/// 함수 이름(ta.sma 등)·숫자·문자열 리터럴은 제외되고,
|
|
/// 인수로 전달된 단순 식별자(close, sma4 등)는 포함됩니다.
|
|
/// DependencyAnalyzer가 Pass1/Pass2 분류에 사용합니다.
|
|
/// </summary>
|
|
public static HashSet<string> GetReferencedVars(string expr)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(expr))
|
|
return new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
var tokens = Tokenize(expr.Trim());
|
|
var refs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
int n = tokens.Count;
|
|
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
if (tokens[i].Kind != TK.Id) continue;
|
|
|
|
// 점 뒤에 붙은 식별자(ta.sma의 sma 부분)는 건너뜀
|
|
if (i > 0 && tokens[i - 1].Kind == TK.Dot) continue;
|
|
|
|
// 점으로 연결된 전체 이름 빌드 (ta.sma, math.max 등)
|
|
string name = tokens[i].Text;
|
|
int j = i;
|
|
while (j + 2 < n
|
|
&& tokens[j + 1].Kind == TK.Dot
|
|
&& tokens[j + 2].Kind == TK.Id)
|
|
{
|
|
name += "." + tokens[j + 2].Text;
|
|
j += 2;
|
|
}
|
|
|
|
// 함수 호출이면 함수명 자체는 의존성이 아님
|
|
if (j + 1 < n && tokens[j + 1].Kind == TK.LP)
|
|
{
|
|
i = j;
|
|
continue;
|
|
}
|
|
|
|
// 언어 키워드/리터럴 제외
|
|
if (!IsRefKeyword(name))
|
|
refs.Add(name);
|
|
|
|
i = j;
|
|
}
|
|
|
|
return refs;
|
|
}
|
|
|
|
private static bool IsRefKeyword(string name) =>
|
|
name is "true" or "false" or "na" or "and" or "or" or "not";
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// AST 빌더 (표현식당 최초 1회만 실행)
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
// 삼항: cond ? a : b
|
|
private static Node BuildTernary(List<Tok> t, ref int pos)
|
|
{
|
|
var cond = BuildOr(t, ref pos);
|
|
if (Peek(t, pos) == TK.Question)
|
|
{
|
|
pos++;
|
|
var then = BuildOr(t, ref pos);
|
|
if (Peek(t, pos) == TK.Colon) pos++;
|
|
var els = BuildOr(t, ref pos);
|
|
return new TernaryNode(cond, then, els);
|
|
}
|
|
return cond;
|
|
}
|
|
|
|
// or
|
|
private static Node BuildOr(List<Tok> t, ref int pos)
|
|
{
|
|
var left = BuildAnd(t, ref pos);
|
|
while (IsKeyword(t, pos, "or"))
|
|
{
|
|
pos++;
|
|
var right = BuildAnd(t, ref pos);
|
|
left = new OrNode(left, right);
|
|
}
|
|
return left;
|
|
}
|
|
|
|
// and
|
|
private static Node BuildAnd(List<Tok> t, ref int pos)
|
|
{
|
|
var left = BuildNot(t, ref pos);
|
|
while (IsKeyword(t, pos, "and"))
|
|
{
|
|
pos++;
|
|
var right = BuildNot(t, ref pos);
|
|
left = new AndNode(left, right);
|
|
}
|
|
return left;
|
|
}
|
|
|
|
// not
|
|
private static Node BuildNot(List<Tok> t, ref int pos)
|
|
{
|
|
if (IsKeyword(t, pos, "not")) { pos++; return new NotNode(BuildNot(t, ref pos)); }
|
|
return BuildCompare(t, ref pos);
|
|
}
|
|
|
|
// 비교
|
|
private static Node BuildCompare(List<Tok> t, ref int pos)
|
|
{
|
|
var left = BuildAdd(t, ref pos);
|
|
while (true)
|
|
{
|
|
var op = Peek(t, pos);
|
|
if (op != TK.EqEq && op != TK.NEq && op != TK.Lt &&
|
|
op != TK.Gt && op != TK.LtEq && op != TK.GtEq) break;
|
|
pos++;
|
|
var right = BuildAdd(t, ref pos);
|
|
left = new BinaryNode(op, left, right);
|
|
}
|
|
return left;
|
|
}
|
|
|
|
// +, -
|
|
private static Node BuildAdd(List<Tok> t, ref int pos)
|
|
{
|
|
var left = BuildMul(t, ref pos);
|
|
while (Peek(t, pos) == TK.Plus || Peek(t, pos) == TK.Minus)
|
|
{
|
|
var op = t[pos++].Kind;
|
|
var right = BuildMul(t, ref pos);
|
|
left = new BinaryNode(op, left, right);
|
|
}
|
|
return left;
|
|
}
|
|
|
|
// *, /, %
|
|
private static Node BuildMul(List<Tok> t, ref int pos)
|
|
{
|
|
var left = BuildUnary(t, ref pos);
|
|
while (Peek(t, pos) == TK.Star || Peek(t, pos) == TK.Slash || Peek(t, pos) == TK.Pct)
|
|
{
|
|
var op = t[pos++].Kind;
|
|
var right = BuildUnary(t, ref pos);
|
|
left = new BinaryNode(op, left, right);
|
|
}
|
|
return left;
|
|
}
|
|
|
|
// 단항 -
|
|
private static Node BuildUnary(List<Tok> t, ref int pos)
|
|
{
|
|
if (Peek(t, pos) == TK.Minus)
|
|
{
|
|
pos++;
|
|
return new UnaryMinusNode(BuildPostfix(t, ref pos));
|
|
}
|
|
return BuildPostfix(t, ref pos);
|
|
}
|
|
|
|
private static Node BuildPostfix(List<Tok> t, ref int pos) => BuildPrimary(t, ref pos);
|
|
|
|
// 기본 요소
|
|
private static Node BuildPrimary(List<Tok> t, ref int pos)
|
|
{
|
|
if (pos >= t.Count || t[pos].Kind == TK.EOF) return new NullNode();
|
|
var tok = t[pos];
|
|
|
|
// 괄호
|
|
if (tok.Kind == TK.LP)
|
|
{
|
|
pos++;
|
|
var v = BuildTernary(t, ref pos);
|
|
if (Peek(t, pos) == TK.RP) pos++;
|
|
return v;
|
|
}
|
|
|
|
// 숫자 리터럴
|
|
if (tok.Kind == TK.Num)
|
|
{
|
|
pos++;
|
|
return double.TryParse(tok.Text,
|
|
System.Globalization.NumberStyles.Any,
|
|
System.Globalization.CultureInfo.InvariantCulture, out double d)
|
|
? new NumberNode(d) : new NullNode();
|
|
}
|
|
|
|
// 문자열 리터럴
|
|
if (tok.Kind == TK.Str) { pos++; return new StringNode(tok.Text); }
|
|
|
|
// 식별자 또는 함수 호출
|
|
if (tok.Kind == TK.Id)
|
|
{
|
|
// 점으로 연결된 이름 빌드: ta.sma, math.max, location.belowbar 등
|
|
string name = tok.Text;
|
|
pos++;
|
|
while (Peek(t, pos) == TK.Dot && pos + 1 < t.Count && t[pos + 1].Kind == TK.Id)
|
|
{
|
|
name += "." + t[pos + 1].Text;
|
|
pos += 2;
|
|
}
|
|
|
|
// 함수 호출?
|
|
if (Peek(t, pos) == TK.LP)
|
|
{
|
|
pos++; // (
|
|
var args = new List<(string? Name, Node ArgNode)>();
|
|
|
|
while (Peek(t, pos) != TK.RP && Peek(t, pos) != TK.EOF)
|
|
{
|
|
// named arg: identifier = value (= 단일, not ==)
|
|
string? argName = null;
|
|
if (t[pos].Kind == TK.Id && pos + 1 < t.Count && t[pos + 1].Kind == TK.Eq)
|
|
{
|
|
argName = t[pos].Text;
|
|
pos += 2;
|
|
}
|
|
|
|
// ── 단순 식별자(bare identifier) 처리 ────────────────────────
|
|
// ta.sma(close, 72) 등에서 'close', 'sma4' 같은 단순 이름은
|
|
// 문자열로 전달해야 Pine.cs ta.* 함수가 시리즈 이름으로 처리함.
|
|
Node argNode;
|
|
if (argName == null
|
|
&& t[pos].Kind == TK.Id
|
|
&& (pos + 1 >= t.Count || t[pos + 1].Kind is TK.Comma or TK.RP or TK.EOF))
|
|
{
|
|
argNode = new BareIdentNode(t[pos].Text);
|
|
pos++;
|
|
}
|
|
else
|
|
{
|
|
argNode = BuildTernary(t, ref pos);
|
|
}
|
|
|
|
args.Add((argName, argNode));
|
|
if (Peek(t, pos) == TK.Comma) pos++;
|
|
}
|
|
if (Peek(t, pos) == TK.RP) pos++;
|
|
|
|
return new FuncCallNode(name, args);
|
|
}
|
|
|
|
// ── 시리즈 히스토리 접근: identifier[offset] ──────────────────
|
|
if (Peek(t, pos) == TK.LBr)
|
|
{
|
|
pos++; // [
|
|
var offsetNode = BuildTernary(t, ref pos);
|
|
if (Peek(t, pos) == TK.RBr) pos++; // ]
|
|
return new IndexNode(name, offsetNode);
|
|
}
|
|
|
|
// 변수 조회
|
|
return new IdentNode(name);
|
|
}
|
|
|
|
return new NullNode();
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// AST 평가기 (매 봉마다 실행 — 재파싱 없음)
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
private static object? EvalNode(Node node)
|
|
{
|
|
switch (node)
|
|
{
|
|
case NullNode:
|
|
return null;
|
|
|
|
case NumberNode n:
|
|
return n.Value;
|
|
|
|
case StringNode s:
|
|
return s.Value;
|
|
|
|
case BareIdentNode b:
|
|
// 이름 문자열 그대로 반환 (ta.* 함수가 시리즈 이름으로 사용)
|
|
return (object?)b.Name;
|
|
|
|
case IdentNode id:
|
|
_identCount++;
|
|
return ResolveIdent(id.Name);
|
|
|
|
case IndexNode ix:
|
|
{
|
|
_indexNodeCount++;
|
|
int offset = (int)Math.Round(ToDouble(EvalNode(ix.Offset)));
|
|
// 배열 할당 없이 직접 CircularBuffer 접근 — O(1), GC 0
|
|
var buf = PineContext.GetBuffer(ix.SeriesName);
|
|
if (buf == null) return null;
|
|
double v = buf.Get(offset);
|
|
return double.IsNaN(v) ? null : (object?)v;
|
|
}
|
|
|
|
case UnaryMinusNode u:
|
|
{
|
|
var v = EvalNode(u.Operand);
|
|
return IsNa(v) ? null : (object?)(-ToDouble(v));
|
|
}
|
|
|
|
case TernaryNode tn:
|
|
// 조건이 참인 분기만 평가 (short-circuit)
|
|
return ToBool(EvalNode(tn.Cond)) ? EvalNode(tn.Then) : EvalNode(tn.Else);
|
|
|
|
case AndNode an:
|
|
{
|
|
// 기존 동작 유지: 양쪽 모두 평가
|
|
bool l = ToBool(EvalNode(an.Left));
|
|
bool r = ToBool(EvalNode(an.Right));
|
|
return (object?)(l && r);
|
|
}
|
|
|
|
case OrNode on:
|
|
{
|
|
bool l = ToBool(EvalNode(on.Left));
|
|
bool r = ToBool(EvalNode(on.Right));
|
|
return (object?)(l || r);
|
|
}
|
|
|
|
case NotNode nn:
|
|
return (object?)(!ToBool(EvalNode(nn.Operand)));
|
|
|
|
case BinaryNode bn:
|
|
return EvalBinary(bn);
|
|
|
|
case FuncCallNode fn:
|
|
{
|
|
// 함수별 호출 횟수 카운팅
|
|
_callCount.TryGetValue(fn.FuncName, out long cnt);
|
|
_callCount[fn.FuncName] = cnt + 1;
|
|
|
|
// ── 고빈도 함수 직접 디스패치 (리플렉션·List 할당 완전 우회) ──
|
|
switch (fn.FuncName)
|
|
{
|
|
case "ta.sma" when fn.Args.Count >= 2:
|
|
{
|
|
_fastCount++;
|
|
string srcName = EvalAsString(fn.Args[0].ArgNode);
|
|
int len = EvalAsInt(fn.Args[1].ArgNode);
|
|
if (len > 0)
|
|
{
|
|
double? r = IndicatorEngine.QuerySma(srcName, len);
|
|
return r.HasValue ? (object?)r.Value : null;
|
|
}
|
|
break;
|
|
}
|
|
case "ta.highest" when fn.Args.Count >= 2:
|
|
{
|
|
_fastCount++;
|
|
string srcName = EvalAsString(fn.Args[0].ArgNode);
|
|
int len = EvalAsInt(fn.Args[1].ArgNode);
|
|
if (len > 0)
|
|
{
|
|
double? r = IndicatorEngine.QueryHighest(srcName, len);
|
|
return r.HasValue ? (object?)r.Value : null;
|
|
}
|
|
break;
|
|
}
|
|
case "ta.lowest" when fn.Args.Count >= 2:
|
|
{
|
|
_fastCount++;
|
|
string srcName = EvalAsString(fn.Args[0].ArgNode);
|
|
int len = EvalAsInt(fn.Args[1].ArgNode);
|
|
if (len > 0)
|
|
{
|
|
double? r = IndicatorEngine.QueryLowest(srcName, len);
|
|
return r.HasValue ? (object?)r.Value : null;
|
|
}
|
|
break;
|
|
}
|
|
case "math.max" when fn.Args.Count >= 2:
|
|
{
|
|
_fastCount++;
|
|
var a = EvalNode(fn.Args[0].ArgNode);
|
|
var b = EvalNode(fn.Args[1].ArgNode);
|
|
if (IsNa(a) || IsNa(b)) return null;
|
|
return (object?)Math.Max(ToDouble(a), ToDouble(b));
|
|
}
|
|
case "math.min" when fn.Args.Count >= 2:
|
|
{
|
|
_fastCount++;
|
|
var a = EvalNode(fn.Args[0].ArgNode);
|
|
var b = EvalNode(fn.Args[1].ArgNode);
|
|
if (IsNa(a) || IsNa(b)) return null;
|
|
return (object?)Math.Min(ToDouble(a), ToDouble(b));
|
|
}
|
|
case "math.abs" when fn.Args.Count >= 1:
|
|
{
|
|
_fastCount++;
|
|
var a = EvalNode(fn.Args[0].ArgNode);
|
|
if (IsNa(a)) return null;
|
|
return (object?)Math.Abs(ToDouble(a));
|
|
}
|
|
case "na" when fn.Args.Count >= 1:
|
|
{
|
|
_fastCount++;
|
|
var v = EvalNode(fn.Args[0].ArgNode);
|
|
if (v is string varName)
|
|
v = PineStateVars.Get(varName) ?? (object?)PineContext.GetVariable(varName);
|
|
return (object?)IsNa(v);
|
|
}
|
|
case "nz" when fn.Args.Count >= 1:
|
|
{
|
|
_fastCount++;
|
|
var v = EvalNode(fn.Args[0].ArgNode);
|
|
if (v is string varName)
|
|
v = PineStateVars.Get(varName) ?? (object?)PineContext.GetVariable(varName);
|
|
var def = fn.Args.Count > 1 ? EvalNode(fn.Args[1].ArgNode) : (object?)0.0;
|
|
return IsNa(v) ? def : v;
|
|
}
|
|
|
|
// ── str.tostring — 직접 구현 ────────────────────────────────
|
|
case "str.tostring":
|
|
case "tostring":
|
|
{
|
|
_fastCount++;
|
|
var v = fn.Args.Count > 0 ? EvalNode(fn.Args[0].ArgNode) : null;
|
|
var fmt = fn.Args.Count > 1 ? EvalNode(fn.Args[1].ArgNode)?.ToString() ?? "" : "";
|
|
if (v == null) return "NaN";
|
|
double dv = ToDouble(v);
|
|
try
|
|
{
|
|
return string.IsNullOrEmpty(fmt)
|
|
? dv.ToString(System.Globalization.CultureInfo.InvariantCulture)
|
|
: dv.ToString(fmt.Replace("#", "0"),
|
|
System.Globalization.CultureInfo.InvariantCulture);
|
|
}
|
|
catch { return v.ToString() ?? ""; }
|
|
}
|
|
|
|
// ── 시각화 전용 — 결과값 불필요, 즉시 null 반환 ──────────────
|
|
case "plot":
|
|
case "plotchar":
|
|
case "color.rgb":
|
|
case "color.new":
|
|
case "bgcolor":
|
|
case "fill":
|
|
case "alertcondition":
|
|
case "indicator":
|
|
case "label.delete":
|
|
case "label.set_text":
|
|
case "label.set_color":
|
|
_fastCount++;
|
|
return null;
|
|
|
|
// ── input 계열 — defval(첫 번째 인수) 반환 ──────────────────
|
|
case "input.int":
|
|
case "input.float":
|
|
case "input.bool":
|
|
case "input.string":
|
|
case "input.timeframe":
|
|
case "input_int":
|
|
case "input_float":
|
|
case "input_bool":
|
|
case "input_string":
|
|
case "input_timeframe":
|
|
{
|
|
_fastCount++;
|
|
// defval = 첫 번째 위치 인수 또는 named "defval"
|
|
foreach (var (an, aNode) in fn.Args)
|
|
if (an == null || an == "defval")
|
|
return EvalNode(aNode);
|
|
return null;
|
|
}
|
|
|
|
// ── label.new — null 반환 ────────────────────────────────────
|
|
case "label.new":
|
|
_fastCount++;
|
|
return null;
|
|
|
|
// ── plotshape — 신호 감지 직접 구현 (리플렉션 우회) ──────────
|
|
case "plotshape":
|
|
{
|
|
_fastCount++;
|
|
// 첫 번째 인수: series (bool 활성화 여부)
|
|
object? seriesVal = fn.Args.Count > 0
|
|
? EvalNode(fn.Args[0].ArgNode) : null;
|
|
bool isActive;
|
|
if (seriesVal is string sv2)
|
|
{
|
|
var resolved = PineStateVars.Get(sv2)
|
|
?? (object?)PineContext.GetVariable(sv2);
|
|
isActive = ToBool(resolved);
|
|
}
|
|
else isActive = ToBool(seriesVal);
|
|
|
|
// text 인수 찾기 (named "text" 우선, 없으면 6번째 위치 인수)
|
|
string pText = "";
|
|
int posI = 0;
|
|
foreach (var (an, aNode) in fn.Args)
|
|
{
|
|
if (an == "text")
|
|
{
|
|
pText = EvalNode(aNode)?.ToString() ?? "";
|
|
break;
|
|
}
|
|
if (an == null)
|
|
{
|
|
if (posI == 5) { pText = EvalNode(aNode)?.ToString() ?? ""; break; }
|
|
posI++;
|
|
}
|
|
}
|
|
|
|
if (isActive && TradingSignal.IsSignal(pText))
|
|
{
|
|
decimal price = 0m;
|
|
try { price = (decimal)(double)Pine.close; } catch { }
|
|
var barTime = Pine.time > 0
|
|
? DateTimeOffset.FromUnixTimeMilliseconds((long)Pine.time)
|
|
: DateTimeOffset.Now;
|
|
TradingSignal.DetectAndRaise(pText, price, barTime);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
// 그 외 함수는 기존 리플렉션 경로
|
|
return EvalFuncCall(fn);
|
|
}
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static object? EvalBinary(BinaryNode b)
|
|
{
|
|
var op = b.Op;
|
|
|
|
// 비교 연산: na 처리 포함
|
|
if (op == TK.EqEq || op == TK.NEq || op == TK.Lt ||
|
|
op == TK.Gt || op == TK.LtEq || op == TK.GtEq)
|
|
{
|
|
var lv = EvalNode(b.Left);
|
|
var rv = EvalNode(b.Right);
|
|
bool lNa = IsNa(lv), rNa = IsNa(rv);
|
|
double l = ToDouble(lv), r = ToDouble(rv);
|
|
|
|
return op switch
|
|
{
|
|
TK.EqEq => (object?)(lNa == rNa && (lNa || l == r)),
|
|
TK.NEq => (object?)(lNa != rNa || (!lNa && l != r)),
|
|
TK.Lt => (object?)(!lNa && !rNa && l < r),
|
|
TK.Gt => (object?)(!lNa && !rNa && l > r),
|
|
TK.LtEq => (object?)(!lNa && !rNa && l <= r),
|
|
TK.GtEq => (object?)(!lNa && !rNa && l >= r),
|
|
_ => (object?)false
|
|
};
|
|
}
|
|
|
|
var left = EvalNode(b.Left);
|
|
var right = EvalNode(b.Right);
|
|
|
|
// + (문자열 연결 포함)
|
|
if (op == TK.Plus)
|
|
{
|
|
if (left is string || right is string)
|
|
return (object?)((left?.ToString() ?? "") + (right?.ToString() ?? ""));
|
|
if (IsNa(left) || IsNa(right)) return null;
|
|
return (object?)(ToDouble(left) + ToDouble(right));
|
|
}
|
|
|
|
// -
|
|
if (op == TK.Minus)
|
|
{
|
|
if (IsNa(left) || IsNa(right)) return null;
|
|
return (object?)(ToDouble(left) - ToDouble(right));
|
|
}
|
|
|
|
// *, /, %
|
|
if (IsNa(left) || IsNa(right)) return null;
|
|
double lv2 = ToDouble(left), rv2 = ToDouble(right);
|
|
return op switch
|
|
{
|
|
TK.Star => (object?)(lv2 * rv2),
|
|
TK.Slash => rv2 == 0 ? null : (object?)(lv2 / rv2),
|
|
TK.Pct => rv2 == 0 ? null : (object?)(lv2 % rv2),
|
|
_ => (object?)lv2
|
|
};
|
|
}
|
|
|
|
/// <summary>함수 첫 번째 인수가 단순 식별자면 이름 문자열, 그 외엔 값의 ToString</summary>
|
|
private static string EvalAsString(Node n)
|
|
{
|
|
if (n is BareIdentNode b) return b.Name;
|
|
return EvalNode(n)?.ToString() ?? "";
|
|
}
|
|
|
|
/// <summary>함수 두 번째 인수(length)를 int로 — 숫자 리터럴이면 파싱만, 나머지는 평가</summary>
|
|
private static int EvalAsInt(Node n)
|
|
{
|
|
if (n is NumberNode num) return (int)Math.Round(num.Value);
|
|
if (n is BareIdentNode b)
|
|
{
|
|
var v = PineStateVars.Get(b.Name) ?? (object?)PineContext.GetVariable(b.Name);
|
|
return v != null ? (int)Math.Round(ToDouble(v)) : 0;
|
|
}
|
|
return (int)Math.Round(ToDouble(EvalNode(n)));
|
|
}
|
|
|
|
private static object? EvalFuncCall(FuncCallNode f)
|
|
{
|
|
var args = new List<(string? Name, object? Val)>(f.Args.Count);
|
|
foreach (var (argName, argNode) in f.Args)
|
|
args.Add((argName, EvalNode(argNode)));
|
|
return InvokeFunction(f.FuncName, args);
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// 변수 조회
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
private static object? ResolveIdent(string name)
|
|
{
|
|
// 키워드 — ToLower() 할당 없이 OrdinalIgnoreCase 비교
|
|
if (name.Equals("true", StringComparison.OrdinalIgnoreCase)) return true;
|
|
if (name.Equals("false", StringComparison.OrdinalIgnoreCase)) return false;
|
|
if (name.Equals("na", StringComparison.OrdinalIgnoreCase)) return null;
|
|
|
|
// OHLCV / 내장 변수 — switch는 O(1) 해시
|
|
switch (name)
|
|
{
|
|
case "close": return Pine.close;
|
|
case "open": return Pine.open;
|
|
case "high": return Pine.high;
|
|
case "low": return Pine.low;
|
|
case "volume": return Pine.volume;
|
|
case "time": return (double)(long)Pine.time;
|
|
case "bar_index": return (double)PineContext.CurrentBarIndex;
|
|
case "bar_index_": return (double)PineContext.CurrentBarIndex;
|
|
}
|
|
|
|
// 상태 변수 — 단일 TryGetValue (Has+Get 이중 조회 제거)
|
|
if (PineStateVars.TryGet(name, out var sv)) return sv;
|
|
|
|
var cv = PineContext.GetVariable(name);
|
|
if (cv.HasValue) return cv.Value;
|
|
|
|
// 점(.)으로 연결된 이름 → Pine 중첩 클래스 프로퍼티/필드 리플렉션
|
|
if (name.Contains('.'))
|
|
{
|
|
try
|
|
{
|
|
string[] parts = name.Split('.');
|
|
Type targetType = typeof(Pine);
|
|
for (int i = 0; i < parts.Length - 1; i++)
|
|
{
|
|
var nested = targetType.GetNestedType(parts[i],
|
|
BindingFlags.Public | BindingFlags.Static);
|
|
if (nested == null) { targetType = null!; break; }
|
|
targetType = nested;
|
|
}
|
|
if (targetType != null)
|
|
{
|
|
string member = parts[^1];
|
|
var prop = targetType.GetProperty(member,
|
|
BindingFlags.Public | BindingFlags.Static);
|
|
if (prop != null) return prop.GetValue(null);
|
|
var field = targetType.GetField(member,
|
|
BindingFlags.Public | BindingFlags.Static);
|
|
if (field != null) return field.GetValue(null);
|
|
}
|
|
}
|
|
catch { }
|
|
return name.Split('.')[^1];
|
|
}
|
|
|
|
Log.Debug("[Eval] 미정의 변수: {Name}", name);
|
|
return null;
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// 함수 호출
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
private static object? InvokeFunction(string name, List<(string? Name, object? Val)> args)
|
|
{
|
|
// ── 특수 내장 함수 ──────────────────────────────────────────
|
|
if (name == "na")
|
|
{
|
|
var v = args.Count > 0 ? args[0].Val : null;
|
|
if (v is string varName)
|
|
v = PineStateVars.Get(varName) ?? (object?)PineContext.GetVariable(varName);
|
|
return IsNa(v);
|
|
}
|
|
if (name == "nz")
|
|
{
|
|
var v = args.Count > 0 ? args[0].Val : null;
|
|
if (v is string varName)
|
|
v = PineStateVars.Get(varName) ?? (object?)PineContext.GetVariable(varName);
|
|
var def = args.Count > 1 ? args[1].Val : (object?)0.0;
|
|
return IsNa(v) ? def : v;
|
|
}
|
|
if (name == "int" || name == "float")
|
|
{
|
|
var v = args.Count > 0 ? args[0].Val : null;
|
|
return IsNa(v) ? null : (object?)ToDouble(v);
|
|
}
|
|
if (name == "bool")
|
|
{
|
|
var v = args.Count > 0 ? args[0].Val : null;
|
|
return ToBool(v);
|
|
}
|
|
if (name == "str.tostring" || name == "tostring")
|
|
{
|
|
var v = args.Count > 0 ? args[0].Val : null;
|
|
var fmt = args.Count > 1 ? args[1].Val?.ToString() ?? "" : "";
|
|
if (v == null) return "NaN";
|
|
double dv = ToDouble(v);
|
|
try
|
|
{
|
|
return string.IsNullOrEmpty(fmt)
|
|
? dv.ToString(System.Globalization.CultureInfo.InvariantCulture)
|
|
: dv.ToString(fmt.Replace("#", "0"),
|
|
System.Globalization.CultureInfo.InvariantCulture);
|
|
}
|
|
catch { return v.ToString() ?? ""; }
|
|
}
|
|
|
|
// ── 리플렉션 호출 (캐시된 MethodInfo 사용) ──────────────────
|
|
try
|
|
{
|
|
if (!_funcCache.TryGetValue(name, out var method))
|
|
{
|
|
method = ResolveMethod(name);
|
|
_funcCache[name] = method;
|
|
}
|
|
|
|
if (method != null) return InvokeReflected(method, args);
|
|
|
|
Log.Debug("[Eval] 함수 미정의: {Name}", name);
|
|
return null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Debug("[Eval] 함수 오류: {Name} → {Err}", name,
|
|
ex.InnerException?.Message ?? ex.Message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>함수명 → MethodInfo 해석 (캐시 미스 시 1회만 호출)</summary>
|
|
private static MethodInfo? ResolveMethod(string name)
|
|
{
|
|
string[] parts = name.Split('.');
|
|
Type targetType = typeof(Pine);
|
|
string methodName = name;
|
|
|
|
if (parts.Length > 1)
|
|
{
|
|
Type? nested = targetType.GetNestedType(parts[0],
|
|
BindingFlags.Public | BindingFlags.Static);
|
|
|
|
if (nested != null)
|
|
{
|
|
targetType = nested;
|
|
methodName = parts[^1];
|
|
if (methodName is "int" or "float" or "bool" or "string" or "new")
|
|
methodName = "@" + methodName;
|
|
}
|
|
else
|
|
{
|
|
string fallback = string.Join("_", parts);
|
|
return typeof(Pine).GetMethod(fallback,
|
|
BindingFlags.Public | BindingFlags.Static);
|
|
}
|
|
}
|
|
|
|
return targetType.GetMethod(methodName,
|
|
BindingFlags.Public | BindingFlags.Static);
|
|
}
|
|
|
|
private static object? InvokeReflected(MethodInfo method,
|
|
List<(string? Name, object? Val)> args)
|
|
{
|
|
_reflectCount++;
|
|
if (!_paramCache.TryGetValue(method, out var pinfos))
|
|
{
|
|
pinfos = method.GetParameters();
|
|
_paramCache[method] = pinfos;
|
|
}
|
|
int count = pinfos.Length;
|
|
object[] result = new object[count];
|
|
|
|
for (int i = 0; i < count; i++)
|
|
result[i] = pinfos[i].HasDefaultValue
|
|
? Type.Missing
|
|
: GetTypeDefault(pinfos[i].ParameterType);
|
|
|
|
bool hasParams = count > 0 &&
|
|
pinfos[count - 1].IsDefined(typeof(ParamArrayAttribute), false);
|
|
var paramsItems = new List<object?>();
|
|
int posIdx = 0;
|
|
|
|
foreach (var (argName, argVal) in args)
|
|
{
|
|
if (argName != null)
|
|
{
|
|
int mi = Array.FindIndex(pinfos, p => p.Name == argName);
|
|
if (mi >= 0)
|
|
result[mi] = ConvertTo(argVal, pinfos[mi].ParameterType);
|
|
}
|
|
else if (hasParams && posIdx >= count - 1)
|
|
paramsItems.Add(argVal);
|
|
else if (posIdx < count)
|
|
{
|
|
result[posIdx] = ConvertTo(argVal, pinfos[posIdx].ParameterType);
|
|
posIdx++;
|
|
}
|
|
}
|
|
|
|
if (hasParams)
|
|
result[count - 1] = paramsItems.Select(v => (dynamic?)v).ToArray();
|
|
|
|
return method.Invoke(null, result);
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// 유틸리티
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
public static bool IsNa(object? v) => v == null;
|
|
|
|
public static bool ToBool(object? v)
|
|
{
|
|
if (v == null) return false;
|
|
if (v is bool b) return b;
|
|
if (v is double d) return !double.IsNaN(d) && d != 0;
|
|
if (v is int i) return i != 0;
|
|
if (v is string s)
|
|
{
|
|
if (s.Equals("true", StringComparison.OrdinalIgnoreCase)) return true;
|
|
if (s.Equals("false", StringComparison.OrdinalIgnoreCase)) return false;
|
|
return !string.IsNullOrEmpty(s);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public static double ToDouble(object? v)
|
|
{
|
|
if (v == null) return double.NaN;
|
|
if (v is double d) return d;
|
|
if (v is bool b) return b ? 1.0 : 0.0;
|
|
if (v is int i) return i;
|
|
if (v is float f) return f;
|
|
if (v is string s && double.TryParse(s,
|
|
System.Globalization.NumberStyles.Any,
|
|
System.Globalization.CultureInfo.InvariantCulture, out double parsed))
|
|
return parsed;
|
|
return double.NaN;
|
|
}
|
|
|
|
private static object ConvertTo(object? val, Type targetType)
|
|
{
|
|
if (targetType == typeof(string)) return val?.ToString() ?? string.Empty;
|
|
|
|
// 단순 식별자가 문자열로 전달된 경우 → 값 타입(int/double/float/bool)에만 변수 조회
|
|
// object/dynamic 타입인 경우엔 그대로 문자열 전달 → ta.* 함수가 시리즈 이름으로 처리
|
|
if (val is string varName
|
|
&& (targetType == typeof(int) || targetType == typeof(double)
|
|
|| targetType == typeof(float) || targetType == typeof(bool)))
|
|
{
|
|
var resolved = PineStateVars.Get(varName)
|
|
?? (object?)PineContext.GetVariable(varName);
|
|
if (resolved != null) val = resolved;
|
|
}
|
|
|
|
if (targetType == typeof(bool)) return ToBool(val);
|
|
if (targetType == typeof(int))
|
|
{
|
|
double d = ToDouble(val);
|
|
if (double.IsNaN(d))
|
|
{
|
|
Log.Debug("[ConvertTo] int 변환 실패 (val={Val}) → Type.Missing", val);
|
|
return Type.Missing;
|
|
}
|
|
return (int)Math.Round(d);
|
|
}
|
|
if (targetType == typeof(double))
|
|
{
|
|
double d = ToDouble(val);
|
|
return double.IsNaN(d) ? Type.Missing : (object)d;
|
|
}
|
|
if (targetType == typeof(float))
|
|
{
|
|
double d = ToDouble(val);
|
|
return double.IsNaN(d) ? Type.Missing : (object)(float)d;
|
|
}
|
|
return val ?? Type.Missing;
|
|
}
|
|
|
|
private static object GetTypeDefault(Type t)
|
|
{
|
|
if (t == typeof(int)) return 0;
|
|
if (t == typeof(double)) return 0.0;
|
|
if (t == typeof(float)) return 0.0f;
|
|
if (t == typeof(bool)) return false;
|
|
if (t == typeof(string)) return string.Empty;
|
|
return null!;
|
|
}
|
|
|
|
private static TK Peek(List<Tok> t, int pos) =>
|
|
pos < t.Count ? t[pos].Kind : TK.EOF;
|
|
|
|
private static bool IsKeyword(List<Tok> t, int pos, string kw) =>
|
|
pos < t.Count && t[pos].Kind == TK.Id &&
|
|
t[pos].Text.Equals(kw, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
}
|