Special Relativity in Financial Modeling 1.0.0
Lorentz transforms, spacetime classification, and geodesic price paths for quantitative finance
Loading...
Searching...
No Matches
event_backtester.cpp
Go to the documentation of this file.
1/// @file src/event_backtester.cpp
2/// @brief Implementation of the event-driven BacktestEngine and
3/// RelativisticStrategy.
4
6#include "srfm/manifold.hpp"
7#include "srfm/constants.hpp"
8
9#include <algorithm>
10#include <cmath>
11#include <numeric>
12
13namespace srfm::event_bt {
14
15// ─── BacktestEngine::process_event ────────────────────────────────────────────
16
17void BacktestEngine::process_event(const BacktestEvent& event) {
18 last_prices_[event.symbol] = event.price;
19
20 if (!strategy_) return;
21
22 std::optional<Order> order_opt;
23 if (event.type == EventType::Trade || event.type == EventType::Quote) {
24 order_opt = strategy_->on_trade(event);
25 } else if (event.type == EventType::Bar) {
26 order_opt = strategy_->on_bar(event);
27 }
28
29 if (!order_opt.has_value()) return;
30
31 auto fill_opt = execute_order(*order_opt, event);
32 if (!fill_opt.has_value()) return;
33
34 const Fill& fill = *fill_opt;
35 fills_.push_back(fill);
36
37 // Update cash
38 double cost = fill.fill_price * fill.fill_qty + fill.commission;
39 if (fill.order.side == OrderSide::Buy) {
40 portfolio_.cash -= cost;
41 } else {
42 portfolio_.cash += fill.fill_price * fill.fill_qty - fill.commission;
43 }
44
45 // Update position
46 double signed_qty = (fill.order.side == OrderSide::Buy)
47 ? fill.fill_qty
48 : -fill.fill_qty;
49 portfolio_.positions[fill.order.symbol] += signed_qty;
50
51 // Record equity snapshot
52 double equity = portfolio_.cash;
53 for (auto const& [sym, qty] : portfolio_.positions) {
54 auto it = last_prices_.find(sym);
55 if (it != last_prices_.end()) {
56 equity += qty * it->second;
57 }
58 }
59 portfolio_.equity_curve.push_back(equity);
60}
61
62// ─── BacktestEngine::execute_order ────────────────────────────────────────────
63
64std::optional<Fill> BacktestEngine::execute_order(const Order& order,
65 const BacktestEvent& event) {
66 if (order.quantity <= 0.0) return std::nullopt;
67
68 double fill_price = event.price;
69
70 // Limit order check
71 if (order.order_type == OrderType::Limit) {
72 if (order.side == OrderSide::Buy && event.price > order.limit_price) return std::nullopt;
73 if (order.side == OrderSide::Sell && event.price < order.limit_price) return std::nullopt;
74 fill_price = order.limit_price;
75 }
76
77 // Insufficient cash guard for buy orders
78 double notional = fill_price * order.quantity;
79 double comm = notional * commission_;
80 if (order.side == OrderSide::Buy && (notional + comm) > portfolio_.cash) {
81 // Scale down to affordable quantity
82 double affordable = portfolio_.cash / (fill_price * (1.0 + commission_));
83 if (affordable <= 0.0) return std::nullopt;
84 notional = fill_price * affordable;
85 comm = notional * commission_;
86 return Fill{order, fill_price, affordable, event.timestamp_ms, comm};
87 }
88
89 return Fill{order, fill_price, order.quantity, event.timestamp_ms, comm};
90}
91
92// ─── BacktestEngine::run ──────────────────────────────────────────────────────
93
95 if (strategy_) strategy_->on_start();
96
97 while (!queue_.empty()) {
98 BacktestEvent event = queue_.top();
99 queue_.pop();
100 process_event(event);
101 }
102
103 if (strategy_) strategy_->on_end();
104
105 return compute_result();
106}
107
108// ─── BacktestEngine::compute_result ───────────────────────────────────────────
109
110BacktestResult BacktestEngine::compute_result() const noexcept {
111 BacktestResult result;
112 result.num_trades = static_cast<int>(fills_.size());
113
114 const auto& eq = portfolio_.equity_curve;
115 if (eq.empty()) return result;
116
117 double final_equity = eq.back();
118 result.total_return = (final_equity - initial_cash_) / initial_cash_;
119
120 // Max drawdown
121 double peak = initial_cash_;
122 double max_dd = 0.0;
123 double running = initial_cash_;
124 for (double e : eq) {
125 if (e > peak) peak = e;
126 double dd = (peak - e) / peak;
127 if (dd > max_dd) max_dd = dd;
128 running = e;
129 }
130 result.max_drawdown = max_dd;
131
132 // Daily returns proxy: per-fill equity changes
133 std::vector<double> returns;
134 returns.reserve(eq.size());
135 double prev = initial_cash_;
136 for (double e : eq) {
137 if (prev != 0.0) returns.push_back((e - prev) / prev);
138 prev = e;
139 }
140
141 if (!returns.empty()) {
142 double mean_r = std::accumulate(returns.begin(), returns.end(), 0.0)
143 / static_cast<double>(returns.size());
144 double var = 0.0;
145 for (double r : returns) var += (r - mean_r) * (r - mean_r);
146 var /= static_cast<double>(returns.size());
147 double std_r = std::sqrt(var);
148 // Annualise assuming ~252 trading days
149 constexpr double ann = 252.0;
150 result.sharpe_ratio = (std_r > 0.0)
151 ? (mean_r / std_r) * std::sqrt(ann)
152 : 0.0;
153 }
154
155 // Win rate and profit factor from fills
156 if (!fills_.empty()) {
157 double gross_win = 0.0;
158 double gross_loss = 0.0;
159 int wins = 0;
160
161 for (std::size_t i = 1; i < fills_.size(); ++i) {
162 const Fill& prev_f = fills_[i - 1];
163 const Fill& curr_f = fills_[i];
164 if (curr_f.order.symbol != prev_f.order.symbol) continue;
165
166 double pnl = 0.0;
167 if (prev_f.order.side == OrderSide::Buy
168 && curr_f.order.side == OrderSide::Sell) {
169 pnl = (curr_f.fill_price - prev_f.fill_price) * curr_f.fill_qty
170 - curr_f.commission - prev_f.commission;
171 } else if (prev_f.order.side == OrderSide::Sell
172 && curr_f.order.side == OrderSide::Buy) {
173 pnl = (prev_f.fill_price - curr_f.fill_price) * curr_f.fill_qty
174 - curr_f.commission - prev_f.commission;
175 }
176
177 if (pnl > 0.0) { gross_win += pnl; ++wins; }
178 else if (pnl < 0.0) { gross_loss += std::abs(pnl); }
179 }
180
181 int round_trips = static_cast<int>(fills_.size()) / 2;
182 result.win_rate = (round_trips > 0)
183 ? static_cast<double>(wins) / round_trips
184 : 0.0;
185 result.profit_factor = (gross_loss > 0.0) ? gross_win / gross_loss : 0.0;
186 }
187
188 return result;
189}
190
191// ─── RelativisticStrategy ─────────────────────────────────────────────────────
192
193std::optional<Order> RelativisticStrategy::evaluate(const BacktestEvent& event) {
194 if (!has_prev_) {
195 prev_event_ = event;
196 has_prev_ = true;
197 return std::nullopt;
198 }
199
200 // Build SpacetimeEvents from the two consecutive market events.
201 // Map: time → timestamp_ms in seconds, price → price,
202 // volume → volume, momentum → Δprice / prev_price (rate of change)
203 double dt_s = static_cast<double>(event.timestamp_ms - prev_event_.timestamp_ms) / 1000.0;
204 double dp = event.price - prev_event_.price;
205 double momentum = (prev_event_.price > 0.0) ? dp / prev_event_.price : 0.0;
206
208 /* time */ static_cast<double>(prev_event_.timestamp_ms) / 1000.0,
209 /* price */ prev_event_.price,
210 /* volume */ prev_event_.volume,
211 /* momentum */ 0.0
212 };
214 /* time */ static_cast<double>(event.timestamp_ms) / 1000.0,
215 /* price */ event.price,
216 /* volume */ event.volume,
217 /* momentum */ momentum
218 };
219
220 auto interval_opt = srfm::manifold::SpacetimeInterval::compute(A, B);
221 if (!interval_opt.has_value()) {
222 prev_event_ = event;
223 return std::nullopt;
224 }
225
226 auto itype = srfm::manifold::SpacetimeInterval::classify(*interval_opt);
227
229 ++spacelike_count_;
230 prev_event_ = event;
231 return std::nullopt;
232 }
233
234 ++timelike_count_;
235 prev_event_ = event;
236
237 // Simple momentum signal: price increased → buy, decreased → sell.
238 if (std::abs(momentum) < threshold_) return std::nullopt;
239
240 Order order;
241 order.symbol = event.symbol;
242 order.quantity = base_qty_;
243 order.order_type = OrderType::Market;
244 order.timestamp_ms = event.timestamp_ms;
245 order.side = (momentum > 0.0) ? OrderSide::Buy : OrderSide::Sell;
246
247 return order;
248}
249
250std::optional<Order> RelativisticStrategy::on_trade(const BacktestEvent& event) {
251 return evaluate(event);
252}
253
254std::optional<Order> RelativisticStrategy::on_bar(const BacktestEvent& event) {
255 return evaluate(event);
256}
257
258} // namespace srfm::event_bt
std::optional< Order > on_trade(const BacktestEvent &event) override
std::optional< Order > on_bar(const BacktestEvent &event) override
static IntervalType classify(double interval_squared) noexcept
static std::optional< double > compute(const SpacetimeEvent &a, const SpacetimeEvent &b, double c_market=constants::SPEED_OF_INFORMATION) noexcept
Physical and financial constants for the SRFM system.
Event-Driven Backtester — Round 3 addition.
Spacetime Market Manifold — AGT-02 public API (implemented by AGT-06).
@ Market
Fill immediately at current price.
@ Limit
Fill only when price <= limit_price (buy) or >= (sell)
@ Trade
Individual trade execution tick.
@ Quote
Bid-ask quote update.
@ Bar
OHLCV bar (1-min, 5-min, daily, etc.)
@ Timelike
ds² < 0 — causal market movement (β < c)
A single market event with a timestamp.
double volume
Volume for this event (0 for quotes)
EventType type
Trade, Quote, or Bar.
long long timestamp_ms
Unix epoch milliseconds.
double price
Trade price / bar close / mid-quote.
Aggregate performance statistics from a completed backtest.
double total_return
(final_equity - initial_equity) / initial
int num_trades
Total fills executed.
double profit_factor
Gross gains / gross losses.
double max_drawdown
Peak-to-trough equity drawdown fraction.
double win_rate
Fraction of profitable trades.
double sharpe_ratio
Annualised Sharpe (assuming 252 days)
Confirmation of an executed order.
An order emitted by a Strategy in response to a market event.
double limit_price
Only used for Limit orders.
std::map< std::string, double > positions
symbol -> net quantity
std::vector< double > equity_curve
Equity snapshot after each fill.
A point in 4D spacetime (t, x, y, z).
double price
Price spatial coordinate.
Definition manifold.hpp:53