22#include <algotradelib/backtest>
23#include <algotradelib/vanilla>
53InstrumentId traded() {
return InstrumentId{Country::KR, Venue::COMBINED,
"005930"}; }
73 std::size_t lookback_{5};
75 Decimal64 quantity_{Decimal64::parse(
"1000")};
77 std::vector<Decimal64> history_;
80 void on_bar(Ctx& ctx,
const strategy::BarEvent& bar) {
81 history_.push_back(
bar.bar_.close_);
82 if (history_.size() < lookback_) {
102 std::optional<Decimal64> sum = Decimal64::parse(
"0");
103 for (std::size_t at = history_.size() - lookback_; sum && at < history_.size(); ++at) {
107 history_.back(), Decimal64::parse(std::to_string(lookback_))
109 if (!sum || !scaled) {
117 strategy::TargetPosition target;
129Instrument reference() {
130 Instrument instrument;
131 instrument.id_ = traded();
132 instrument.name_ =
"Samsung Electronics";
133 instrument.type_ = SecurityType::STOCK;
134 instrument.currency_ = Currency{
"KRW"};
135 instrument.lot_size_ = Decimal64::parse(
"1");
139 instrument.price_rule_ = PriceRule::krx_equity();
144const std::vector<int>& closes() {
145 static const std::vector<int> path = {
146 60000, 60500, 61200, 61000, 62000, 63000, 64500, 65000, 66200, 67000,
147 68500, 69000, 70200, 71000, 72500, 73000, 72800, 72000, 71000, 69500,
148 68000, 66500, 65000, 63500, 62000, 61000, 60000, 59000, 58500, 58000,
149 58500, 59500, 61000, 62500, 64000, 65500, 67000, 68500, 70000, 71500,
154backtest::ReplayConfig config_for() {
155 backtest::ReplayConfig config;
156 config.cash_ = Decimal64::parse(
"100000000");
157 config.instruments_ = {reference()};
158 config.series_capacity_ = 64;
160 config.limits_.permitted_phases_ = std::nullopt;
165backtest::NextBarFillModel costs() {
166 return backtest::NextBarFillModel(
167 backtest::CommissionModel::of(Decimal64::parse(
"0.00015"), Decimal64::parse(
"100")),
168 backtest::SlippageModel::of_ticks(1)
180template <
typename Strategy>
181backtest::Backtester<Strategy> replay(Strategy instance) {
182 backtest::Backtester<Strategy> engine(std::move(instance), config_for(), costs());
183 const auto start = std::chrono::system_clock::time_point{} + std::chrono::hours(24 * 20000);
184 for (std::size_t at = 0; at < closes().size(); ++at) {
186 bar.timestamp_ =
start + std::chrono::hours(24 *
static_cast<int>(at));
187 bar.open_ = Decimal64::parse(std::to_string(closes()[at]));
191 bar.volume_ = Decimal64::parse(
"1200000");
198#if defined(ATL_HAS_EIGEN)
201std::string percent(math::Scalar value) {
202 if (!math::observed(value)) {
203 return math::is_undefined(value) ?
"undefined" :
" --";
205 std::ostringstream text;
206 text << std::fixed << std::setprecision(2) << std::setw(7) << value * 100.0 <<
" %";
211std::string plain(math::Scalar value) {
212 if (!math::observed(value)) {
213 return math::is_undefined(value) ?
"undefined" :
" --";
215 std::ostringstream text;
216 text << std::fixed << std::setprecision(3) << std::setw(9) << value;
221template <
typename Strategy>
222void compare(
const std::string& name, Strategy instance, math::Scalar periods) {
223 const auto engine = replay(std::move(instance));
224 const auto report = backtest::summarize(engine.result(), engine.fills(), periods);
225 std::cout <<
" " << std::left << std::setw(21) << name << std::right
226 << percent(report.total_return_) <<
" " << plain(report.sharpe_) <<
" "
227 << percent(report.max_drawdown_) <<
" " << std::setw(5) << report.executions_ <<
"\n";
240 std::cout <<
"005930, " << closes().size()
241 <<
" bars built in code -- no credentials, no network\n\n";
243#if defined(ATL_HAS_EIGEN)
249 std::cout <<
" " << std::left << std::setw(21) <<
"strategy" << std::right
250 << std::setw(9) <<
"return" <<
" " << std::setw(9) <<
"Sharpe" <<
" "
251 << std::setw(9) <<
"max dd" <<
" " << std::setw(5) <<
"fills" <<
"\n"
252 <<
" " << std::string(59,
'-') <<
"\n";
254 compare(
"MyStrategy(5)", MyStrategy{}, *periods);
256 vanilla::BuyAndHold holding;
257 holding.params_.instrument_ = traded();
258 holding.params_.quantity_ = Decimal64::parse(
"1000");
259 compare(
"BuyAndHold", holding, *periods);
261 vanilla::MovingAverageCrossover::Params crossing;
262 crossing.instrument_ = traded();
265 crossing.quantity_ = Decimal64::parse(
"1000");
266 compare(
"SmaCrossover(3,8)", vanilla::SmaCrossover{crossing}, *periods);
268 vanilla::DonchianBreakout breaking;
269 breaking.params_.instrument_ = traded();
270 breaking.params_.lookback_ = 5;
271 breaking.params_.quantity_ = Decimal64::parse(
"1000");
272 compare(
"DonchianBreakout(5)", breaking, *periods);
275 <<
"\nForty invented bars rank nothing. What this shows is that the comparison is honest: every\n"
276 <<
"strategy above saw identical bars, paid identical costs, and filled against the bar after the\n"
277 <<
"one it decided on -- which is structural here, not a rule anybody has to remember. Read\n"
278 <<
"`report.caveats_` before you believe a number, and the reading-a-backtest vignette before you\n"
279 <<
"believe a ranking.\n";
285 const auto engine = replay(MyStrategy{});
286 std::cout <<
"MyStrategy replayed " << engine.result().bars_ <<
" bars and filled "
287 << engine.result().executions_.size() <<
" orders.\n\n"
288 <<
"Scoring those fills needs a linear algebra backend and this build has none.\n"
289 <<
"Configure with -DATL_USE_EIGEN=ON for the metrics and the vanilla comparison.\n";
A three-letter currency code, such as KRW or USD (ISO 4217 style).
An exact signed decimal: an int64 coefficient times 10^-scale.
#define ATL_ASSERT_STRATEGY(S, Ctx)
Asserts at compile time that S is a strategy a runner with context Ctx can drive.
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.
ProbeContextT< NoCaps > MinimalContext
A probe context whose broker implements nothing optional.
ProbeContextT< AllCaps > FullContext
A probe context whose broker implements everything optional.
BarStamp
Which edge of its period a bar's timestamp marks.
SecurityType
What kind of security an instrument is.
std::optional< Decimal64 > decimal_add(const Decimal64 &lhs, const Decimal64 &rhs)
Adds two decimals exactly.
Country
A country (market) the library supports.
Venue
A trading venue: an exchange, or the combined view across exchanges.
bool decimal_less(const Decimal64 &lhs, const Decimal64 &rhs)
Compares two decimals numerically, whatever their scales.
std::optional< Decimal64 > decimal_multiply(const Decimal64 &lhs, const Decimal64 &rhs)
Multiplies two decimals exactly; the scales add.
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.
InstrumentId instrument_
The instrument.
Decimal64 target_quantity_
The position to hold, signed: negative is short, and zero means flat.