algotradelib 0.0.0.9000
C++ header-only library for algorithmic trading
Loading...
Searching...
No Matches
example-backtest-broker.cpp
1// Backtest a strategy on bars fetched from a real broker.
2//
3// Two phases, deliberately separated: **read, then replay**. The session is used once, to fetch bars
4// and reference data, and then closed out of the picture — the replay itself touches nothing. That
5// separation is the point. A backtest that held a session open could accidentally reach for data
6// mid-replay, which is how a backtest ends up using information the strategy could not have had.
7//
8// It places no orders. There is no code path here that could: the engine's executor queues fills and
9// has no session at all.
10//
11// Run it as:
12//
13// example-backtest-broker kis 005930
14// example-backtest-broker tossinvest 005930 [account-sequence]
15//
16// Credentials come from the same environment variables the other examples document. What you can fetch
17// bounds what you can test: KIS returns 100 daily bars a request and one `bars()` call pages up to
18// 50 requests (about 5,000 bars, roughly twenty years of KRX trading days), but serves minute bars
19// one trading day at a time.
20
21#include "kis_credentials.hpp"
22
23#include <algotradelib/backtest>
24#include <algotradelib/kis>
25#include <algotradelib/tossinvest>
26#include <algotradelib/vanilla>
27
28#include <boost/asio/ssl/context.hpp>
29
30#include <cstddef>
31#include <cstdint>
32#include <cstdlib>
33#include <exception>
34#include <iomanip>
35#include <iostream>
36#include <optional>
37#include <sstream>
38#include <memory>
39#include <string>
40#include <utility>
41
42namespace ssl = boost::asio::ssl;
43
44namespace {
45
46namespace strategy = algotradelib::strategy;
47namespace backtest = algotradelib::backtest;
48namespace vanilla = algotradelib::strategy::vanilla;
49namespace math = algotradelib::math;
50
57std::string money_or_absent(const std::optional<algotradelib::Decimal64>& amount) {
58 return amount ? amount->str() : std::string("(absent: see the caveats)");
59}
60
61
63using algotradelib::BarQuery;
64using algotradelib::BarSeries;
65using algotradelib::BlockingSession;
67using algotradelib::Currency;
68using algotradelib::Decimal64;
69using algotradelib::Instrument;
70using algotradelib::InstrumentId;
72
74struct FetchedHistory {
75 Instrument reference_;
76 BarSeries bars_;
77};
78
85template <typename Session>
86std::optional<FetchedHistory> read_history(
87 Session& session,
88 const InstrumentId& instrument,
89 std::size_t count
90) {
91 auto reference = session.instrument(instrument);
92 if (!reference) {
93 std::cerr << "could not read reference data: " << reference.error().message_ << '\n';
94 return std::nullopt;
95 }
96
97 BarQuery query;
98 query.instrument_ = instrument;
99 query.interval_ = BarInterval::ONE_DAY;
100 query.count_ = count;
101 // Adjusted, and this matters more than it looks: unadjusted prices invent a signal at every split
102 // and every dividend, so a backtest on them measures corporate actions rather than a strategy.
103 query.adjusted_ = true;
104
105 auto bars = session.bars(query);
106 if (!bars) {
107 std::cerr << "could not read bars: " << bars.error().message_ << '\n';
108 return std::nullopt;
109 }
110 if (bars.value().bars_.size() < 2) {
111 std::cerr << "the broker returned too few bars to replay\n";
112 return std::nullopt;
113 }
114
115 FetchedHistory history;
116 history.reference_ = std::move(reference.value());
117 history.bars_ = std::move(bars.value());
118 return history;
119}
120
121#if defined(ATL_HAS_EIGEN)
123template <typename Strategy>
124void replay_and_report(const char* title, Strategy instance, const FetchedHistory& history) {
125 backtest::ReplayConfig config;
126 config.cash_ = Decimal64::parse("100000000");
127 config.instruments_ = {history.reference_};
128 config.currency_ = history.bars_.currency_;
129 config.series_capacity_ = 512;
130 config.limits_.permitted_phases_ = std::nullopt;
131
132 backtest::Backtester<Strategy> engine(
133 std::move(instance),
134 std::move(config),
135 backtest::NextBarFillModel(
136 backtest::CommissionModel::of(Decimal64::parse("0.00015"), Decimal64::parse("100")),
137 backtest::SlippageModel::of_ticks(1)
138 )
139 );
140
141 for (const auto& bar : history.bars_.bars_) {
142 engine.feed(
144 history.bars_.instrument_, history.bars_.interval_, history.bars_.stamp_, bar
145 )
146 );
147 }
148 engine.finish();
149
150 const auto periods =
151 backtest::periods_per_year(history.bars_.interval_, strategy::SessionWindow::krx(), 245.0);
152 const auto report = backtest::summarize(engine.result(), engine.fills(), periods);
153
154 const auto show = [](math::Scalar value, bool as_percent) {
155 std::ostringstream text;
156 if (!math::observed(value)) {
157 text << (math::is_undefined(value) ? "undefined" : "--");
158 return text.str();
159 }
160 text << std::fixed << std::setprecision(as_percent ? 2 : 3)
161 << (as_percent ? value * 100.0 : value) << (as_percent ? " %" : "");
162 return text.str();
163 };
164
165 std::cout << title << "\n"
166 << " bars " << report.bars_ << "\n"
167 << " executions " << report.executions_ << "\n"
168 << " total return " << show(report.total_return_, true) << "\n"
169 << " Sharpe " << show(report.sharpe_, false) << " per bar, "
170 << show(report.annualized_sharpe_, false) << " annualized\n"
171 << " Sortino " << show(report.sortino_, false) << "\n"
172 << " max drawdown " << show(report.max_drawdown_, true) << "\n"
173 << " turnover " << show(report.total_turnover_, true) << "\n"
174 << " commission " << money_or_absent(report.commission_paid_) << "\n"
175 << " slippage " << money_or_absent(report.slippage_paid_) << "\n"
176 << " not modelled volume, queue position, market impact, borrow\n";
177 if (report.caveats_.cost_totals_incomplete_) {
178 // The reason a total can be absent that is not "no trades" and not "two currencies": an exact
179 // sum was refused, so the figures above are short by an amount nobody can name.
180 std::cout << " cost totals INCOMPLETE -- an exact sum did not fit; slippage is understated"
181 << " and the fee totals are withheld\n";
182 }
183 std::cout << "\n";
184}
185#endif
186
188template <typename Session>
189int backtest_with(Session& session, const std::string& symbol) {
190 const InstrumentId instrument{Country::KR, Venue::COMBINED, symbol};
191
192 const auto history = read_history(session, instrument, 400);
193 if (!history) {
194 return 1;
195 }
196
197 std::cout << "broker: " << Session::BrokerTag::name << " instrument: " << symbol
198 << " bars: " << history->bars_.bars_.size()
199 << (history->bars_.adjusted_ ? " adjusted" : " UNADJUSTED") << "\n"
200 << "the session is now finished with: nothing below reads it\n\n";
201
202#if defined(ATL_HAS_EIGEN)
203 vanilla::BuyAndHold holding;
204 holding.params_.instrument_ = instrument;
205 holding.params_.quantity_ = Decimal64::parse("100");
206 replay_and_report("BuyAndHold -- the benchmark every strategy is measured against", holding,
207 *history);
208
209 vanilla::MovingAverageCrossover::Params crossing;
210 crossing.instrument_ = instrument;
211 crossing.fast_ = 20;
212 crossing.slow_ = 50;
213 crossing.quantity_ = Decimal64::parse("100");
214 replay_and_report("SmaCrossover(20, 50)", vanilla::SmaCrossover{crossing}, *history);
215
216 vanilla::DonchianBreakout breaking;
217 breaking.params_.instrument_ = instrument;
218 breaking.params_.lookback_ = 20;
219 breaking.params_.quantity_ = Decimal64::parse("100");
220 replay_and_report("DonchianBreakout(20)", breaking, *history);
221
222 std::cout
223 << "These are real prices and a simulated fill model. Every figure assumes the whole order was\n"
224 << "available at the price shown, which no bar can tell us -- so treat a small edge here as\n"
225 << "noise rather than as a result.\n";
226#else
227 std::cout << "The report needs a linear algebra backend; configure with -DATL_USE_EIGEN=ON.\n";
228#endif
229 return 0;
230}
231
232int backtest_kis(ssl::context& tls, const std::string& symbol) {
233 namespace kis = algotradelib::kis;
234
235 kis::KisConfig config;
236 config.from_env();
237 kis::KisAccount account;
238 account.from_env();
239
240 auto client =
241 std::make_shared<kis::KisBlockingClient>(tls, std::move(config), kis_credentials::paced_options());
242 BlockingSession<kis::KisBlockingClient> session(std::move(client), std::move(account));
243 return backtest_with(session, symbol);
244}
245
246int backtest_tossinvest(ssl::context& tls, const std::string& symbol, const char* sequence) {
247 namespace toss = algotradelib::tossinvest;
248
249 toss::TossConfig config;
250 config.from_env();
251 auto client = std::make_shared<toss::TossBlockingClient>(tls, std::move(config));
252
253 std::int64_t selected = 0;
254 if (sequence != nullptr) {
255 selected = std::strtoll(sequence, nullptr, 10);
256 } else {
257 auto accounts = algotradelib::list_accounts(*client);
258 if (!accounts || accounts.value().empty()) {
259 std::cerr << "could not discover a Toss account\n";
260 return 1;
261 }
262 selected = accounts.value().front().selector_;
263 }
264
265 BlockingSession<toss::TossBlockingClient> session(std::move(client), selected);
266 return backtest_with(session, symbol);
267}
268
269} // namespace
270
271int main(int argc, char** argv) {
272 const std::string broker = argc > 1 ? argv[1] : "";
273 if (broker != "kis" && broker != "tossinvest") {
274 std::cerr
275 << "usage: example-backtest-broker <kis|tossinvest> [symbol] [account-sequence]\n\n"
276 << "Fetches history and replays it. Places no order: the engine has no session to send to.\n";
277 return 2;
278 }
279 const std::string symbol = argc > 2 ? argv[2] : "005930";
280
281 try {
282 ssl::context tls(ssl::context::tls_client);
283 tls.set_default_verify_paths();
284 tls.set_verify_mode(ssl::verify_peer);
285
286 if (broker == "kis") {
287 return backtest_kis(tls, symbol);
288 }
289 return backtest_tossinvest(tls, symbol, argc > 3 ? argv[3] : nullptr);
290 } catch (const std::exception& exception) {
291 std::cerr << "example failed: " << exception.what() << '\n';
292 return 1;
293 }
294}
The examples' KIS credentials: one JSON file per account, or the environment.
void bars(S &strategy, Ctx &context, const BarSlice &slice)
Calls strategy.on_bars(context, slice) if the strategy declares on_bars; otherwise does nothing.
Definition concept.hpp:218
BarEvent make_bar_event(InstrumentId instrument, BarInterval interval, BarStamp stamp, Bar bar)
Makes a bar event, computing its period_end_ with bar_period_end.
Definition event.hpp:273
Result< std::vector< AccountInfo< Client > > > list_accounts(Client &client)
Lists a broker's accounts with a blocking client, before a session exists; requires BrokerCaps<Client...
Country
A country (market) the library supports.
Definition types.hpp:30
Venue
A trading venue: an exchange, or the combined view across exchanges.
Definition types.hpp:43
BarInterval
The period one bar covers.
static SessionWindow krx()
Returns the KRX regular session, 09:00-15:30 KST.
Definition event.hpp:120