Team Ai
Apppublic

Sumitx369/Paper_Trading_Backtesting

sourceHugging Faceupdated 4mo agoView on Hugging Face
2likes
backtest.cpp221 linesDownload Raw Back to src
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