16#include <algotradelib/backtest>
17#include <algotradelib/vanilla>
41std::string money_or_absent(
const std::optional<algotradelib::Decimal64>& amount) {
42 return amount ? amount->str() : std::string(
"(absent: see the caveats)");
58InstrumentId samsung() {
return InstrumentId{Country::KR, Venue::COMBINED,
"005930"}; }
60Instrument samsung_reference() {
62 reference.id_ = samsung();
63 reference.name_ =
"Samsung Electronics";
64 reference.type_ = SecurityType::STOCK;
65 reference.currency_ = Currency{
"KRW"};
66 reference.lot_size_ = Decimal64::parse(
"1");
67 reference.price_rule_ = PriceRule::krx_equity();
72const std::vector<int>& closes() {
73 static const std::vector<int> path = {
74 60000, 60500, 61200, 61000, 62000, 63000, 64500, 65000, 66200, 67000,
75 68500, 69000, 70200, 71000, 72500, 73000, 72800, 72000, 71000, 69500,
76 68000, 66500, 65000, 63500, 62000, 61000, 60000, 59000, 58500, 58000,
77 58500, 59500, 61000, 62500, 64000, 65500, 67000, 68500, 70000, 71500,
83backtest::NextBarFillModel realistic_costs() {
84 return backtest::NextBarFillModel(
85 backtest::CommissionModel::of(Decimal64::parse(
"0.00015"), Decimal64::parse(
"100")),
86 backtest::SlippageModel::of_ticks(1)
90backtest::ReplayConfig config_for() {
91 backtest::ReplayConfig config;
92 config.cash_ = Decimal64::parse(
"100000000");
93 config.instruments_ = {samsung_reference()};
94 config.series_capacity_ = 64;
97 config.limits_.permitted_phases_ = std::nullopt;
102template <
typename Strategy>
103backtest::Backtester<Strategy> replay(Strategy instance, BarInterval interval) {
104 backtest::Backtester<Strategy> engine(std::move(instance), config_for(), realistic_costs());
105 const auto start = std::chrono::system_clock::time_point{} + std::chrono::hours(24 * 20000);
106 for (std::size_t at = 0; at < closes().size(); ++at) {
107 const auto step = interval == BarInterval::ONE_MINUTE ?
108 std::chrono::minutes(
static_cast<int>(at)) :
109 std::chrono::duration_cast<std::chrono::minutes>(
110 std::chrono::hours(24 * static_cast<int>(at))
114 bar.open_ = Decimal64::parse(std::to_string(closes()[at]));
118 bar.volume_ = Decimal64::parse(
"1200000");
126std::string percent(math::Scalar value) {
127 if (!math::observed(value)) {
128 return math::is_undefined(value) ?
"undefined" :
" --";
130 std::ostringstream text;
131 text << std::fixed << std::setprecision(2) << std::setw(7) << value * 100.0 <<
" %";
136std::string plain(math::Scalar value) {
137 if (!math::observed(value)) {
138 return math::is_undefined(value) ?
"undefined" :
" --";
140 std::ostringstream text;
141 text << std::fixed << std::setprecision(3) << std::setw(9) << value;
145#if defined(ATL_HAS_EIGEN)
147void print(
const std::string& title,
const backtest::Report& report) {
148 std::cout << title <<
"\n"
149 <<
" bars " << report.bars_ <<
"\n"
150 <<
" executions " << report.executions_ <<
"\n"
151 <<
" total return " << percent(report.total_return_) <<
"\n"
152 <<
" Sharpe " << plain(report.sharpe_) <<
" (per bar)\n"
153 <<
" annualized " << plain(report.annualized_sharpe_) <<
"\n"
154 <<
" Sortino " << plain(report.sortino_) <<
"\n"
155 <<
" max drawdown " << percent(report.max_drawdown_) <<
"\n"
156 <<
" turnover " << percent(report.total_turnover_) <<
"\n"
157 <<
" commission " << money_or_absent(report.commission_paid_) <<
" KRW\n"
158 <<
" slippage " << money_or_absent(report.slippage_paid_) <<
" KRW\n";
159 std::cout <<
" assumed limit fills "
160 << backtest::limit_fill_price_name(report.caveats_.limit_fill_) <<
", slippage "
161 << report.caveats_.slippage_ticks_ <<
" tick\n";
164 for (
const auto& applied : report.caveats_.applied_rates_) {
165 std::cout <<
" commission rate " << applied.rate_.str() <<
" with a "
166 << applied.minimum_.str() <<
" floor, "
168 << applied.executions_ <<
" execution(s)\n";
170 if (report.caveats_.cost_totals_incomplete_) {
174 std::cout <<
" cost totals INCOMPLETE -- an exact sum did not fit; slippage is understated"
175 <<
" and the fee totals are withheld\n";
177 if (report.caveats_.inclusive_commission_executions_ > 0) {
178 std::cout <<
" commission is inclusive of any institution charge on "
179 << report.caveats_.inclusive_commission_executions_ <<
" execution(s)\n";
181 std::cout <<
" not modelled volume, queue position, market impact, borrow\n";
182 if (report.caveats_.gaps_ > 0) {
183 std::cout <<
" gaps " << report.caveats_.gaps_
184 <<
" (not interpolated across)\n";
186 if (report.caveats_.unfilled_ > 0) {
187 std::cout <<
" unfilled " << report.caveats_.unfilled_
188 <<
" (no next bar to fill against)\n";
190 if (report.caveats_.expired_day_orders_ > 0) {
191 std::cout <<
" expired " << report.caveats_.expired_day_orders_
192 <<
" DAY order(s) unfilled at their session's close, as at a broker\n";
194 if (report.caveats_.multiple_instruments_) {
195 std::cout <<
" CAVEAT more than one instrument: this result may carry survivorship\n"
196 <<
" bias, because reference data lists what exists now\n";
205 std::cout <<
"005930, " << closes().size()
206 <<
" bars built in code -- no credentials, no network\n\n";
208#if defined(ATL_HAS_EIGEN)
209 const auto daily_periods =
211 const auto minute_periods =
213 std::cout <<
"periods per year: " << *daily_periods <<
" daily, " << *minute_periods
214 <<
" minute -- which is why annualizing takes the count rather than assuming it\n\n";
216 vanilla::BuyAndHold holding;
217 holding.params_.instrument_ = samsung();
218 holding.params_.quantity_ = Decimal64::parse(
"1000");
220 const auto engine = replay(holding, BarInterval::ONE_DAY);
221 print(
"BuyAndHold, daily", backtest::summarize(engine.result(), engine.fills(), *daily_periods));
224 vanilla::MovingAverageCrossover::Params crossing;
225 crossing.instrument_ = samsung();
228 crossing.quantity_ = Decimal64::parse(
"1000");
230 const auto engine = replay(vanilla::SmaCrossover{crossing}, BarInterval::ONE_DAY);
232 "SmaCrossover(3, 8), daily",
233 backtest::summarize(engine.result(), engine.fills(), *daily_periods)
241 const auto engine = replay(vanilla::SmaCrossover{crossing}, BarInterval::ONE_MINUTE);
243 "SmaCrossover(3, 8), minute bars -- identical decisions, different annualizing",
244 backtest::summarize(engine.result(), engine.fills(), *minute_periods)
247 <<
" ^ Note the annualized Sharpe against the daily run above. The per-bar figure is\n"
248 <<
" identical -- the decisions were identical -- and only the constant differs, because\n"
249 <<
" sqrt(95550) is about twenty times sqrt(245). The larger number is arithmetically\n"
250 <<
" right and practically meaningless here: this path moves like daily data, so calling\n"
251 <<
" its bars minutes claims a frequency the returns do not have. Two lessons. Mislabel\n"
252 <<
" your bar frequency and the annualized figure is wrong by the square root of the\n"
253 <<
" ratio. And annualizing assumes returns are serially uncorrelated, which a trending\n"
254 <<
" strategy violates by construction.\n\n";
257 vanilla::DonchianBreakout breaking;
258 breaking.params_.instrument_ = samsung();
259 breaking.params_.lookback_ = 5;
260 breaking.params_.quantity_ = Decimal64::parse(
"1000");
262 const auto engine = replay(breaking, BarInterval::ONE_DAY);
264 "DonchianBreakout(5), daily",
265 backtest::summarize(engine.result(), engine.fills(), *daily_periods)
271 backtest::Backtester<vanilla::SmaCrossover> engine(
272 vanilla::SmaCrossover{crossing}, config_for(), backtest::NextBarFillModel{}
274 const auto start = std::chrono::system_clock::time_point{} + std::chrono::hours(24 * 20000);
275 for (std::size_t at = 0; at < closes().size(); ++at) {
277 bar.timestamp_ =
start + std::chrono::hours(24 *
static_cast<int>(at));
278 bar.open_ = Decimal64::parse(std::to_string(closes()[at]));
282 bar.volume_ = Decimal64::parse(
"1200000");
287 "SmaCrossover(3, 8), daily, at zero cost -- the difference is what costs cost",
288 backtest::summarize(engine.result(), engine.fills(), *daily_periods)
293 <<
"Read the caveats, not only the figures. A backtest is the easiest place to produce a\n"
294 <<
"confident wrong number, and the ones above assume every fill was available in full at\n"
295 <<
"the price shown, which no bar can actually tell us.\n";
298 <<
"The report needs a linear algebra backend and this build has none.\n"
299 <<
"Configure with -DATL_USE_EIGEN=ON to see the metrics. The engine itself runs either way.\n";
A three-letter currency code, such as KRW or USD (ISO 4217 style).
An exact signed decimal: an int64 coefficient times 10^-scale.
Replays a strategy over stored bars: fill and cost models, the replay ledger, and the report.
The numeric vocabulary: Scalar, missing(), and the matrix and vector types.
void start(S &strategy, Ctx &context)
Calls strategy.on_start(context) if the strategy declares on_start; otherwise does nothing.
void bar(S &strategy, Ctx &context, const BarEvent &event)
Calls strategy.on_bar(context, event) if the strategy declares on_bar; otherwise does nothing.
The strategies this library ships (buy and hold, crossovers, mean reversion, sizing,...
What a strategy sees and emits: events, intents, the portfolio, reconciliation and risk limits.
BarEvent make_bar_event(InstrumentId instrument, BarInterval interval, BarStamp stamp, Bar bar)
Makes a bar event, computing its period_end_ with bar_period_end.
BarStamp
Which edge of its period a bar's timestamp marks.
SecurityType
What kind of security an instrument is.
constexpr std::string_view commission_origin_name(CommissionOrigin origin) noexcept
Returns a lowercase name for an origin, such as "caller_stated", for reports.
Country
A country (market) the library supports.
Venue
A trading venue: an exchange, or the combined view across exchanges.
BarInterval
The period one bar covers.
One bar: the prices and volume of one period, as exact decimals.
Identifies an instrument: its country, venue and symbol.
Reference data for one instrument.
The price grid (tick sizes) an instrument's order prices must sit on.
static SessionWindow krx()
Returns the KRX regular session, 09:00-15:30 KST.