Tick_Forge/TF_Expr.mqh
261 lines
9.5 KiB
MQL5
//+------------------------------------------------------------------+
//| 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));
}
//+------------------------------------------------------------------+