Measuring a Strategy Properly: Quant Metrics and a C++ Monte Carlo

Published September 6, 2026 ยท 8 min read ยท By DataVoxy

Up front: this is an engineering write-up of a hobby backtesting tool, not financial advice and not a claim that the strategy makes money. The whole point of the post is that a good win rate can hide a bad strategy, and how to tell the difference.

When people show off a trading strategy, they usually lead with two numbers: win rate and total profit. Both are close to useless on their own. A strategy can win 90% of the time and still blow up, if the 10% of losses are large enough. So we built a small quant layer on top of the CryptoPulse strategy to measure it the way a quant researcher would, and a C++ engine to answer the question that actually matters: how badly could this go?

Why win rate lies

Imagine a strategy that wins 0.5% on 9 out of 10 trades and loses 6% on the tenth. That is a 90% win rate and a losing strategy: nine wins make +4.5%, one loss takes -6%, net negative. Win rate tells you how often you win, never how much. To judge a strategy you need to weigh reward against risk, and specifically against the kind of risk that hurts: volatility and drawdown.

The metrics that actually matter

MetricWhat it answers
Sharpe ratioReturn per unit of total volatility. Higher is better; above 1 is decent, above 2 is good.
Sortino ratioLike Sharpe, but only counts downside volatility. Upside swings should not be punished.
Max drawdownThe worst peak-to-trough drop. This is what you actually feel and what makes people quit.
Calmar ratioAnnual return divided by max drawdown. Reward per unit of pain.
VaR / CVaR (5%)The bad-tail loss, and the average loss once you are in that tail.
Profit factorGross gains divided by gross losses. Above 1 means the wins outweigh the losses.

These are a few lines of NumPy each. The one subtlety worth calling out: we annualize using the strategy's real trade frequency (trades per calendar year), not a fixed number of bars. Annualizing per-trade returns as if they were hourly gives you a Sharpe in the trillions, which is nonsense. Getting that scaling right is the difference between an honest number and a vanity number.

A real finding: the strict filter never fires

The first thing the backtester told us was uncomfortable. The original entry rule was "price above the 50 EMA and RSI oversold and near support." On 60 days of hourly BTC data those three conditions coincided zero times.

The reason is structural, not a bug: an oversold RSI usually means price has just fallen, so it tends to be below its moving average, not above it. "Uptrend" and "oversold dip" are close to mutually exclusive on this timeframe. That is exactly the kind of thing a proper backtest surfaces and a win-rate screenshot hides. We relaxed the trend filter to "the 50 EMA is rising" so the strategy could actually trade, and the analysis had something to chew on.

The C++ part: Monte Carlo and risk of ruin

Metrics summarize the one history that happened. But you traded one particular sequence of markets; a slightly different order of the same wins and losses could have looked very different. A bootstrap Monte Carlo answers "what range of outcomes was plausible" by resampling the trade returns thousands of times to build synthetic equity curves, then reading off the distribution: median outcome, 5th and 95th percentile, expected and worst-case drawdown, and the probability of ruin.

That loop is n_sims ร— horizon. For stable tail estimates you want a lot of paths, 100,000 or more. This is the one place in the whole project where the language genuinely matters, so the core lives in C++ and is called from Python through pybind11:

for (int s = 0; s < n_sims; ++s) {
    double equity = start_equity, peak = start_equity, mdd = 0.0;
    for (int t = 0; t < T; ++t) {
        double r = returns[pick(rng)];      // resample with replacement
        equity *= (1.0 + r);
        if (equity > peak) peak = equity;
        double dd = equity / peak - 1.0;
        if (dd < mdd) mdd = dd;             // track worst drawdown
    }
    // record final equity, drawdown, whether it hit the ruin threshold
}

Compiled with pybind11, it drops straight into Python as a normal module:

import mc_engine
res = mc_engine.run_montecarlo(returns, n_sims=200000, ruin_threshold=0.5)
print(res.p5_final_equity, res.risk_of_ruin)

What the numbers looked like

On a recent 60-day BTC sample the relaxed strategy produced 19 trades:

  • Sharpe โ‰ˆ 3.3, Sortino โ‰ˆ 5.1, profit factor โ‰ˆ 2.0
  • Max drawdown โ‰ˆ -1.4%, win rate โ‰ˆ 53%
  • Monte Carlo (200,000 paths): 5th percentile โ‰ˆ 0.99x, 95th โ‰ˆ 1.16x, risk of ruin โ‰ˆ 0%

Read that skeptically. A Sharpe above 3 is not a real edge here, it is a small sample (19 trades) over a favorable couple of months. The honest use of this tool is comparing strategy variants under identical conditions, not predicting future returns. Small samples flatter everything.

Was the C++ actually worth it?

Honestly, only partly, and it is worth being straight about that. Against a naive Python loop, C++ is dramatically faster. But the fair comparison is against a vectorized NumPy fallback, which is already compiled C under the hood. On this workload C++ came out roughly 4x faster than NumPy, not 100x.

The C++ advantage grows when the simulation is longer and when the logic is path-dependent in ways NumPy cannot vectorize cleanly (per-step stops, position sizing that reacts to running equity). For a short bootstrap, NumPy is plenty, which is why the tool ships with a NumPy fallback and only uses the compiled engine if it is built. Reaching for C++ is a decision you justify with a profiler, not a reflex.

Takeaways

  • Judge strategies on risk-adjusted return and drawdown, not win rate.
  • Get your annualization scaling right or your ratios are fiction.
  • A backtest earns its keep by telling you uncomfortable things, like a filter that never fires.
  • Monte Carlo turns "it worked once" into a distribution of what could happen.
  • Only reach for C++ where the profiler says the loop is the bottleneck. Here that was the Monte Carlo, and nowhere else.

See the live market view

CryptoPulse tracks BTC and ETH with live charts, Fear & Greed, and technical levels.

Open CryptoPulse โ†’ More from DataVoxy

Nothing here is financial advice. Markets carry risk, past results do not predict future results, and a backtest is a hypothesis, not a promise.