//+------------------------------------------------------------------+ //| TF_Expr.mqh - Tick Forge price-function parser | //| Copyright (c) 2026 Joshua Dunnink. MIT License (see LICENSE). | //| | //| A small, safe expression compiler: text -> reverse-polish program | //| -> fast evaluation. No eval, no file access, no side effects. | //| | //| Variables p (x) original price p0 first price in range | //| r p / p0 t progress in range, 0..1 | //| d days since range start | //| Operators + - * / ^ and unary minus, parentheses | //| Functions abs sqrt ln log10 exp sin cos pow(a,b) min(a,b) max(a,b)| //| Constants pi e | //+------------------------------------------------------------------+ #property strict enum ENUM_TF_OP { TFOP_NUM = 0, TFOP_P, TFOP_P0, TFOP_R, TFOP_T, TFOP_D, TFOP_ADD, TFOP_SUB, TFOP_MUL, TFOP_DIV, TFOP_POW, TFOP_NEG, TFOP_ABS, TFOP_SQRT, TFOP_LN, TFOP_LOG10, TFOP_EXP, TFOP_SIN, TFOP_COS, TFOP_MIN, TFOP_MAX }; class CTFExpr { private: string m_src; int m_pos; string m_err; int m_op[]; double m_val[]; int m_n; int m_depth, m_maxdepth; void Emit(const int op, const double v = 0.0); void SkipWs(); bool Fail(const string e) { if(m_err == "") m_err = e + " at position " + IntegerToString(m_pos + 1); return false; } bool ParseExpr(); bool ParseTerm(); bool ParseUnary(); bool ParsePower(); bool ParsePrimary(); bool Peek(const ushort c); int FuncOp(const string name, int &argc); public: CTFExpr() : m_n(0) {} bool Compile(const string text); string Error() const { return m_err; } double Eval(const double p, const double p0, const double t, const double d) const; }; void CTFExpr::Emit(const int op, const double v) { if(ArraySize(m_op) <= m_n) { ArrayResize(m_op, m_n + 32); ArrayResize(m_val, m_n + 32); } m_op[m_n] = op; m_val[m_n] = v; m_n++; // stack bookkeeping: pushes +1, binary ops -1, unary/func1 0 switch(op) { case TFOP_NUM: case TFOP_P: case TFOP_P0: case TFOP_R: case TFOP_T: case TFOP_D: m_depth++; if(m_depth > m_maxdepth) m_maxdepth = m_depth; break; case TFOP_ADD: case TFOP_SUB: case TFOP_MUL: case TFOP_DIV: case TFOP_POW: case TFOP_MIN: case TFOP_MAX: m_depth--; break; default: break; } } void CTFExpr::SkipWs() { while(m_pos < StringLen(m_src) && (m_src[m_pos] == ' ' || m_src[m_pos] == '\t')) m_pos++; } bool CTFExpr::Peek(const ushort c) { SkipWs(); return (m_pos < StringLen(m_src) && m_src[m_pos] == c); } bool CTFExpr::Compile(const string text) { m_src = text; m_pos = 0; m_err = ""; m_n = 0; m_depth = 0; m_maxdepth = 0; ArrayResize(m_op, 32); ArrayResize(m_val, 32); if(StringLen(text) == 0) { m_err = "empty expression"; return false; } if(!ParseExpr()) return false; SkipWs(); if(m_pos < StringLen(m_src)) return Fail("unexpected '" + StringSubstr(m_src, m_pos, 1) + "'"); if(m_maxdepth > 60) { m_err = "expression too deep"; return false; } return true; } bool CTFExpr::ParseExpr() { if(!ParseTerm()) return false; while(true) { SkipWs(); if(m_pos >= StringLen(m_src)) return true; ushort c = m_src[m_pos]; if(c != '+' && c != '-') return true; m_pos++; if(!ParseTerm()) return false; Emit(c == '+' ? TFOP_ADD : TFOP_SUB); } return true; } bool CTFExpr::ParseTerm() { if(!ParseUnary()) return false; while(true) { SkipWs(); if(m_pos >= StringLen(m_src)) return true; ushort c = m_src[m_pos]; if(c != '*' && c != '/') return true; m_pos++; if(!ParseUnary()) return false; Emit(c == '*' ? TFOP_MUL : TFOP_DIV); } return true; } bool CTFExpr::ParseUnary() { SkipWs(); if(m_pos < StringLen(m_src) && (m_src[m_pos] == '-' || m_src[m_pos] == '+')) { bool neg = (m_src[m_pos] == '-'); m_pos++; if(!ParseUnary()) return false; if(neg) Emit(TFOP_NEG); return true; } return ParsePower(); } bool CTFExpr::ParsePower() { if(!ParsePrimary()) return false; SkipWs(); if(m_pos < StringLen(m_src) && m_src[m_pos] == '^') { m_pos++; if(!ParseUnary()) return false; // right associative, allows 2^-1 Emit(TFOP_POW); } return true; } int CTFExpr::FuncOp(const string name, int &argc) { argc = 1; if(name == "abs") return TFOP_ABS; if(name == "sqrt") return TFOP_SQRT; if(name == "ln") return TFOP_LN; if(name == "log10") return TFOP_LOG10; if(name == "exp") return TFOP_EXP; if(name == "sin") return TFOP_SIN; if(name == "cos") return TFOP_COS; argc = 2; if(name == "pow") return TFOP_POW; if(name == "min") return TFOP_MIN; if(name == "max") return TFOP_MAX; return -1; } bool CTFExpr::ParsePrimary() { SkipWs(); if(m_pos >= StringLen(m_src)) return Fail("unexpected end"); ushort c = m_src[m_pos]; if(c == '(') { m_pos++; if(!ParseExpr()) return false; if(!Peek(')')) return Fail("missing ')'"); m_pos++; return true; } if((c >= '0' && c <= '9') || c == '.') { int s = m_pos; while(m_pos < StringLen(m_src) && ((m_src[m_pos] >= '0' && m_src[m_pos] <= '9') || m_src[m_pos] == '.')) m_pos++; // optional exponent 1e-3 if(m_pos < StringLen(m_src) && (m_src[m_pos] == 'e' || m_src[m_pos] == 'E')) { int save = m_pos; m_pos++; if(m_pos < StringLen(m_src) && (m_src[m_pos] == '+' || m_src[m_pos] == '-')) m_pos++; int ds = m_pos; while(m_pos < StringLen(m_src) && m_src[m_pos] >= '0' && m_src[m_pos] <= '9') m_pos++; if(m_pos == ds) m_pos = save; } Emit(TFOP_NUM, StringToDouble(StringSubstr(m_src, s, m_pos - s))); return true; } if((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_') { int s = m_pos; while(m_pos < StringLen(m_src) && ((m_src[m_pos] >= 'a' && m_src[m_pos] <= 'z') || (m_src[m_pos] >= 'A' && m_src[m_pos] <= 'Z') || (m_src[m_pos] >= '0' && m_src[m_pos] <= '9') || m_src[m_pos] == '_')) m_pos++; string id = StringSubstr(m_src, s, m_pos - s); StringToLower(id); if(Peek('(')) { int argc; int op = FuncOp(id, argc); if(op < 0) return Fail("unknown function '" + id + "'"); m_pos++; // ( if(!ParseExpr()) return false; if(argc == 2) { if(!Peek(',')) return Fail("function '" + id + "' needs 2 arguments"); m_pos++; if(!ParseExpr()) return false; } if(!Peek(')')) return Fail("missing ')'"); m_pos++; Emit(op); return true; } if(id == "p" || id == "x") { Emit(TFOP_P); return true; } if(id == "p0") { Emit(TFOP_P0); return true; } if(id == "r") { Emit(TFOP_R); return true; } if(id == "t") { Emit(TFOP_T); return true; } if(id == "d") { Emit(TFOP_D); return true; } if(id == "pi") { Emit(TFOP_NUM, M_PI); return true; } if(id == "e") { Emit(TFOP_NUM, M_E); return true; } m_pos = s; return Fail("unknown name '" + id + "'"); } return Fail("unexpected '" + StringSubstr(m_src, m_pos, 1) + "'"); } double CTFExpr::Eval(const double p, const double p0, const double t, const double d) const { double st[64]; int sp = 0; for(int i = 0; i < m_n; i++) { switch(m_op[i]) { case TFOP_NUM: st[sp++] = m_val[i]; break; case TFOP_P: st[sp++] = p; break; case TFOP_P0: st[sp++] = p0; break; case TFOP_R: st[sp++] = (p0 != 0.0 ? p / p0 : 0.0); break; case TFOP_T: st[sp++] = t; break; case TFOP_D: st[sp++] = d; break; case TFOP_ADD: sp--; st[sp - 1] += st[sp]; break; case TFOP_SUB: sp--; st[sp - 1] -= st[sp]; break; case TFOP_MUL: sp--; st[sp - 1] *= st[sp]; break; case TFOP_DIV: sp--; st[sp - 1] = (st[sp] != 0.0 ? st[sp - 1] / st[sp] : MathArcsin(2.0)); break; // NaN case TFOP_POW: sp--; st[sp - 1] = MathPow(st[sp - 1], st[sp]); break; case TFOP_NEG: st[sp - 1] = -st[sp - 1]; break; case TFOP_ABS: st[sp - 1] = MathAbs(st[sp - 1]); break; case TFOP_SQRT: st[sp - 1] = MathSqrt(st[sp - 1]); break; case TFOP_LN: st[sp - 1] = MathLog(st[sp - 1]); break; case TFOP_LOG10: st[sp - 1] = MathLog10(st[sp - 1]); break; case TFOP_EXP: st[sp - 1] = MathExp(st[sp - 1]); break; case TFOP_SIN: st[sp - 1] = MathSin(st[sp - 1]); break; case TFOP_COS: st[sp - 1] = MathCos(st[sp - 1]); break; case TFOP_MIN: sp--; st[sp - 1] = MathMin(st[sp - 1], st[sp]); break; case TFOP_MAX: sp--; st[sp - 1] = MathMax(st[sp - 1], st[sp]); break; } } return (sp == 1 ? st[0] : MathArcsin(2.0)); } //+------------------------------------------------------------------+