| //+------------------------------------------------------------------+
|
| //| 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));
|
| }
|
| //+------------------------------------------------------------------+
|