Sumitx369/Paper_Trading_Backtesting
2
1#include "backtest.hpp"2 3#include <cmath>4#include <limits>5 6#include "indicators.hpp"7 8namespace bt {9 10namespace {11const double NaN = std::numeric_limits<double>::quiet_NaN();12inline bool ok(double v) { return !std::isnan(v); }13inline bool truthy(double v) { return ok(v) && v != 0.0; }14 15// Compute one indicator output column from its spec.16Vec compute_indicator(const IndicatorSpec& s, const Vec& open, const Vec& high,17 const Vec& low, const Vec& close, const Vec& volume) {18 (void)open;19 (void)volume;20 const int p1 = static_cast<int>(s.p1);21 const int p2 = static_cast<int>(s.p2);22 const int p3 = static_cast<int>(s.p3);23 if (s.name == "sma") return ind::sma(close, p1);24 if (s.name == "ema") return ind::ema(close, p1);25 if (s.name == "rsi") return ind::rsi(close, p1);26 if (s.name == "atr") return ind::atr(high, low, close, p1);27 if (s.name == "bb_mid") return ind::bb_mid(close, p1);28 if (s.name == "bb_upper") return ind::bb_upper(close, p1, s.p2);29 if (s.name == "bb_lower") return ind::bb_lower(close, p1, s.p2);30 if (s.name == "macd_line" || s.name == "macd_signal" || s.name == "macd_hist") {31 Vec line, sig, hist;32 ind::macd(close, p1, p2, p3, line, sig, hist);33 if (s.name == "macd_line") return line;34 if (s.name == "macd_signal") return sig;35 return hist;36 }37 return Vec(close.size(), NaN); // unknown indicator -> all NaN (never fires)38}39 40// Evaluate a postfix bytecode program at bar `t`. Returns 1.0/0.0, or NaN when41// any input is undefined (warm-up) — NaN propagates so signals never fire on42// undefined data.43double eval(const std::vector<Instr>& prog, int t,44 const std::vector<const Vec*>& cols, std::vector<double>& stack) {45 stack.clear();46 for (const Instr& in : prog) {47 switch (in.op) {48 case OP_PUSH_CONST:49 stack.push_back(in.val);50 break;51 case OP_PUSH_COL: {52 int idx = in.a, off = in.b, tt = t - off;53 double v = NaN;54 if (idx >= 0 && idx < (int)cols.size() && tt >= 0 &&55 tt < (int)cols[idx]->size())56 v = (*cols[idx])[tt];57 stack.push_back(v);58 break;59 }60 case OP_NOT: {61 double a = stack.back();62 stack.back() = ok(a) ? (truthy(a) ? 0.0 : 1.0) : NaN;63 break;64 }65 case OP_CROSSOVER:66 case OP_CROSSUNDER: {67 int A = in.a, B = in.b;68 double r = NaN;69 if (t >= 1 && A >= 0 && A < (int)cols.size() && B >= 0 &&70 B < (int)cols.size()) {71 double at = (*cols[A])[t], ap = (*cols[A])[t - 1];72 double bt = (*cols[B])[t], bp = (*cols[B])[t - 1];73 if (ok(at) && ok(ap) && ok(bt) && ok(bp)) {74 r = (in.op == OP_CROSSOVER) ? ((ap <= bp && at > bt) ? 1.0 : 0.0)75 : ((ap >= bp && at < bt) ? 1.0 : 0.0);76 }77 }78 stack.push_back(r);79 break;80 }81 default: { // binary ops82 double b = stack.back(); stack.pop_back();83 double a = stack.back();84 double r = NaN;85 switch (in.op) {86 case OP_ADD: r = a + b; break;87 case OP_SUB: r = a - b; break;88 case OP_MUL: r = a * b; break;89 case OP_DIV: r = (b != 0.0) ? a / b : NaN; break;90 case OP_GT: r = (ok(a) && ok(b)) ? (a > b) : (double)NaN; break;91 case OP_LT: r = (ok(a) && ok(b)) ? (a < b) : (double)NaN; break;92 case OP_GE: r = (ok(a) && ok(b)) ? (a >= b) : (double)NaN; break;93 case OP_LE: r = (ok(a) && ok(b)) ? (a <= b) : (double)NaN; break;94 case OP_EQ: r = (ok(a) && ok(b)) ? (a == b) : (double)NaN; break;95 case OP_NE: r = (ok(a) && ok(b)) ? (a != b) : (double)NaN; break;96 case OP_AND: r = (ok(a) && ok(b)) ? ((truthy(a) && truthy(b)) ? 1.0 : 0.0) : (double)NaN; break;97 case OP_OR: r = (ok(a) && ok(b)) ? ((truthy(a) || truthy(b)) ? 1.0 : 0.0) : (double)NaN; break;98 }99 stack.back() = r;100 }101 }102 }103 return stack.empty() ? NaN : stack.back();104}105} // namespace106 107BacktestResult run_backtest(const Vec& open, const Vec& high, const Vec& low,108 const Vec& close, const Vec& volume,109 const std::vector<IndicatorSpec>& indicators,110 const std::vector<Instr>& buy_program,111 const std::vector<Instr>& sell_program,112 double starting_capital, double fee_bps,113 double slippage_bps, double stop_loss_pct,114 double take_profit_pct) {115 BacktestResult res;116 const int n = static_cast<int>(close.size());117 res.n_bars = n;118 res.starting_capital = starting_capital;119 res.final_value = starting_capital;120 if (n == 0) return res;121 122 // Build the column table: base OHLCV then one column per indicator.123 std::vector<Vec> ind_cols;124 ind_cols.reserve(indicators.size());125 for (const auto& spec : indicators)126 ind_cols.push_back(compute_indicator(spec, open, high, low, close, volume));127 128 std::vector<const Vec*> cols = {&open, &high, &low, &close, &volume};129 for (const auto& c : ind_cols) cols.push_back(&c);130 131 res.equity.assign(n, starting_capital);132 res.buyhold.assign(n, starting_capital);133 134 // Buy & hold benchmark (gross, from first valid close).135 int f0 = 0;136 while (f0 < n && !ok(close[f0])) ++f0;137 double bh_shares = (f0 < n && close[f0] > 0) ? starting_capital / close[f0] : 0.0;138 for (int t = 0; t < n; ++t)139 res.buyhold[t] = (t >= f0 && ok(close[t])) ? bh_shares * close[t] : starting_capital;140 141 const double fee = fee_bps / 1e4;142 const double slip = slippage_bps / 1e4;143 const double sl = stop_loss_pct / 100.0; // fraction; <=0 disables144 const double tp = take_profit_pct / 100.0;145 146 double cash = starting_capital, qty = 0.0;147 bool in_pos = false;148 double entry_price = 0.0, entry_cost = 0.0;149 int entry_index = 0;150 std::vector<double> stack;151 stack.reserve(32);152 153 for (int t = 0; t < n; ++t) {154 bool buy_sig = truthy(eval(buy_program, t, cols, stack));155 bool sell_sig = truthy(eval(sell_program, t, cols, stack));156 157 if (in_pos && t > entry_index && ok(close[t])) {158 // Manage open position. Priority: stop-loss, then take-profit159 // (both intrabar via low/high), then the sell rule (at close).160 double exit_px = NaN;161 const char* reason = nullptr;162 double sl_level = entry_price * (1.0 - sl);163 double tp_level = entry_price * (1.0 + tp);164 if (sl > 0.0 && ok(low[t]) && low[t] <= sl_level) {165 exit_px = sl_level;166 reason = "stop_loss";167 } else if (tp > 0.0 && ok(high[t]) && high[t] >= tp_level) {168 exit_px = tp_level;169 reason = "take_profit";170 } else if (sell_sig) {171 exit_px = close[t];172 reason = "signal";173 }174 if (reason) {175 double exec = exit_px * (1.0 - slip);176 double proceeds = qty * exec * (1.0 - fee);177 cash += proceeds;178 res.trades.push_back({entry_index, t, entry_price, exec, qty,179 proceeds - entry_cost,180 entry_cost > 0 ? (proceeds - entry_cost) / entry_cost * 100.0 : 0.0,181 reason});182 in_pos = false;183 qty = 0.0;184 }185 } else if (!in_pos && buy_sig && ok(close[t]) && close[t] > 0) {186 double exec = close[t] * (1.0 + slip);187 double per_share = exec * (1.0 + fee);188 double q = std::floor(cash / per_share);189 if (q >= 1.0) {190 double cost = q * exec * (1.0 + fee);191 cash -= cost;192 qty = q;193 in_pos = true;194 entry_price = exec;195 entry_cost = cost;196 entry_index = t;197 }198 }199 200 double px = ok(close[t]) ? close[t] : (t > 0 ? close[t - 1] : 0.0);201 res.equity[t] = in_pos ? cash + qty * px : cash;202 }203 204 // Force-close any open position at the last bar.205 if (in_pos) {206 int t = n - 1;207 double exec = close[t] * (1.0 - slip);208 double proceeds = qty * exec * (1.0 - fee);209 cash += proceeds;210 Trade tr{entry_index, t, entry_price, exec, qty, proceeds - entry_cost,211 entry_cost > 0 ? (proceeds - entry_cost) / entry_cost * 100.0 : 0.0, "eod"};212 res.trades.push_back(tr);213 res.equity[t] = cash;214 }215 216 res.final_value = res.equity[n - 1];217 return res;218}219 220} // namespace bt221 