Methodology Backtesting Engine

Chapter 2

Backtesting Engine

Our backtesting engine is built in-house using an event-driven architecture. It simulates trading decisions exactly as they would occur in reality — one bar at a time, never using information from the future.

1. Event-Driven Architecture

The core design principle of our engine is that the strategy can only ever see data from the current bar and earlier — never from future bars. This is enforced architecturally, not as a policy. It is physically impossible for the strategy code to access forward data.

Processing Loop (per bar)

Data Feed

OHLCV bar arrives

Indicator Engine

Recalculate indicators

Strategy Logic

Evaluate entry/exit rules

Order Generator

Create pending orders

Order Execution

Fill on next-bar open

Portfolio Update

P&L, equity curve

Sequential Processing

Each bar is processed one at a time, in chronological order. The engine maintains a strict cursor — it can read bar T and all prior bars, but bar T+1 does not exist yet.

Deterministic Output

Given the same data file and the same strategy parameters, the engine produces identical results every time. No randomness, no floating-point variance between runs.

Multi-Asset Support

The engine can run strategies simultaneously across multiple instruments, respecting portfolio-level capital constraints and cross-asset correlation.

2. Signal Generation

Trading signals are generated by applying indicator functions to the price series. All indicator values at bar T are computed using only data available at bar T — i.e., prices from bar 0 through bar T inclusive. The signal is then acted on at bar T+1's open price.

# Simplified pseudocode — illustrates the look-ahead prohibition for T in range(warmup_period, len(bars)): current_bar = bars[T] # Only this bar's OHLCV is visible history = bars[:T+1] # All bars up to and including T # Indicators computed on [0..T] — future bars NOT accessible sma_fast = mean(history.close[-20:]) sma_slow = mean(history.close[-50:]) rsi = rsi_func(history.close, period=14) # Signal on bar T → execution on bar T+1 open (next function call) if sma_fast > sma_slow and rsi < 70: queue_order(side='BUY', qty=position_size(), exec_bar=T+1)

Supported Indicator Types

  • Moving averages (SMA, EMA, WMA, DEMA, TEMA, KAMA)
  • Momentum (RSI, MACD, Stochastic, CCI, ROC)
  • Volatility (ATR, Bollinger Bands, Keltner Channels, HV)
  • Volume (OBV, CMF, VWAP, Volume Profile)
  • Trend (ADX, Parabolic SAR, Ichimoku Cloud)
  • Oscillators (Williams %R, Ultimate Oscillator, MFI)
  • Statistical (Z-score, linear regression slope, correlation)
  • Custom composite indicators (strategy-specific)

Signal Types

  • Entry long / Entry short
  • Exit long / Exit short (explicit)
  • Stop-loss trigger (price-based or ATR multiple)
  • Take-profit trigger (fixed target or trailing)
  • Scale-in / Scale-out (pyramid entries)
  • Time-based exit (fixed holding period)
  • Regime filter (market condition gate)
  • Risk-based exit (portfolio drawdown threshold)

3. Execution Model

The execution model determines how orders are filled. We use conservative assumptions that slightly underestimate real-world performance — intentionally. It is better to be pleasantly surprised than disappointed.

Execution Parameters

Fill price
Next-bar open (T+1 open) Prevents unrealistic same-bar fills
Slippage
0.05% per trade (default) Conservative estimate for liquid names
Commission — HK
0.10% round-trip Typical retail HK broker rate
Commission — US
0.05% round-trip Typical US broker rate
Commission — JP/EU
0.15% round-trip Reflects higher regional costs
Minimum lot size
Enforced per exchange rules No fractional shares
Short selling
Allowed where historically permitted Exchange uptick rules applied
Borrow cost
0.5%–5% annualized (strategy dependent) For short positions

Why Next-Bar Open?

Most retail backtesting tools allow filling orders on the same bar that generated the signal — which is impossible in practice. If a crossover occurs at the close of bar T, you cannot trade at bar T's close because the signal was not known until the bar closed.

Our rule: Signal fires at close of bar T → order queued → filled at open of bar T+1. This is the earliest realistically executable time.
What we never do: Fill at bar T's close, high, or low. Those prices are only known after the bar closes — you cannot trade at them in real time.

4. Position Sizing Models

Position sizing determines how much capital is allocated to each trade. The sizing model used in a report is always disclosed in the report header. Different strategies use different models depending on their risk characteristics.

Fixed Fractional (% of Equity)

Most common

The most widely used model. A fixed percentage of the current account equity is risked on each trade. As the account grows, position sizes grow proportionally; as it shrinks, they shrink.

# Formula
Position size = Account equity × risk% ÷ (entry price – stop price)
# Example
Account: HK$100,000 | Risk: 2% | Entry: HK$10 | Stop: HK$9 → Size = HK$2,000 ÷ HK$1 = 2,000 shares

Fixed Dollar Amount

Simple strategies

A constant dollar amount is allocated to each trade, regardless of account size. Straightforward to understand but does not adapt to account growth.

# Formula
Position size = Fixed dollar amount ÷ entry price
# Example
Fixed: HK$10,000 | Entry: HK$10 → Size = 1,000 shares

Volatility-Adjusted (ATR-Based)

Trend strategies

Position size is adjusted inversely to the instrument's recent volatility, measured by Average True Range (ATR). High-volatility instruments get smaller positions; low-volatility get larger.

# Formula
Position size = (Account equity × risk%) ÷ (ATR(14) × ATR multiplier)
# Example
Account: HK$100,000 | Risk: 2% | ATR: HK$0.50 | Mult: 2 → Size = HK$2,000 ÷ HK$1 = 2,000 shares

Kelly Criterion (Half-Kelly)

Advanced use

The Kelly formula optimizes long-run growth based on the strategy's historical win rate and reward-to-risk ratio. We use half-Kelly (50% of the formula output) to reduce variance.

# Formula
f* = (W × R – (1–W)) ÷ R → half-Kelly = f* ÷ 2
# Example
Win rate: 55% | R:R ratio: 2:1 → f* = (0.55×2 – 0.45)/2 = 32.5% → Half-Kelly = 16.25% of equity

5. Portfolio-Level Simulation

When a strategy trades multiple instruments simultaneously, the engine enforces portfolio-level constraints. This prevents the unrealistic scenario where an unlimited number of trades can be open at once.

Capital Allocation

Each open position consumes capital. The engine tracks total deployed capital and refuses new entries if remaining cash falls below the minimum position size. No leverage unless explicitly specified in the strategy.

Maximum Open Positions

Each strategy defines a maximum number of concurrent open positions. When the limit is reached, new entry signals are queued and filled as existing positions close. This prevents over-diversification.

Portfolio Drawdown Stop

If the overall portfolio drawdown exceeds a configurable threshold (default: 25%), all positions are closed and new entries are halted for a cooldown period. This simulates risk management discipline.

Rebalancing & Drift

For multi-asset strategies, the engine periodically rebalances position weights back to target allocations. Rebalancing frequency and tolerance bands are configurable per strategy.

6. Supported Timeframes

The engine supports multiple data resolutions. The appropriate timeframe for a strategy depends on its holding period and signal frequency.

Timeframe Typical Holding Period Strategy Type Availability
Daily (EOD) 5 days – 12 months Swing trading, position trading All markets
Weekly 1–6 months Trend following, long-term All markets
Monthly 3–24 months Macro strategies, factor models All markets
4-Hour 1–5 days Short-term swing HK, US (via aggregation)
1-Hour 4–48 hours Intraday swing HK, US only
15-Minute 1–8 hours Day trading HK, US only
5-Minute 30 min – 4 hours Scalping strategies US only (limited history)