23#include <algotradelib/backtest>
24#include <algotradelib/kis>
25#include <algotradelib/tossinvest>
26#include <algotradelib/vanilla>
28#include <boost/asio/ssl/context.hpp>
42namespace ssl = boost::asio::ssl;
46namespace strategy = algotradelib::strategy;
47namespace backtest = algotradelib::backtest;
48namespace vanilla = algotradelib::strategy::vanilla;
49namespace math = algotradelib::math;
57std::string money_or_absent(
const std::optional<algotradelib::Decimal64>& amount) {
58 return amount ? amount->str() : std::string(
"(absent: see the caveats)");
63using algotradelib::BarQuery;
64using algotradelib::BarSeries;
65using algotradelib::BlockingSession;
67using algotradelib::Currency;
68using algotradelib::Decimal64;
69using algotradelib::Instrument;
70using algotradelib::InstrumentId;
74struct FetchedHistory {
75 Instrument reference_;
85template <
typename Session>
86std::optional<FetchedHistory> read_history(
88 const InstrumentId& instrument,
91 auto reference = session.instrument(instrument);
93 std::cerr <<
"could not read reference data: " << reference.error().message_ <<
'\n';
98 query.instrument_ = instrument;
99 query.interval_ = BarInterval::ONE_DAY;
100 query.count_ = count;
103 query.adjusted_ =
true;
105 auto bars = session.bars(query);
107 std::cerr <<
"could not read bars: " <<
bars.error().message_ <<
'\n';
110 if (
bars.value().bars_.size() < 2) {
111 std::cerr <<
"the broker returned too few bars to replay\n";
115 FetchedHistory history;
116 history.reference_ = std::move(reference.value());
117 history.bars_ = std::move(
bars.value());
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;
132 backtest::Backtester<Strategy> engine(
135 backtest::NextBarFillModel(
136 backtest::CommissionModel::of(Decimal64::parse(
"0.00015"), Decimal64::parse(
"100")),
137 backtest::SlippageModel::of_ticks(1)
141 for (
const auto& bar : history.bars_.bars_) {
144 history.bars_.instrument_, history.bars_.interval_, history.bars_.stamp_, bar
152 const auto report = backtest::summarize(engine.result(), engine.fills(), periods);
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" :
"--");
160 text << std::fixed << std::setprecision(as_percent ? 2 : 3)
161 << (as_percent ? value * 100.0 : value) << (as_percent ?
" %" :
"");
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_) {
180 std::cout <<
" cost totals INCOMPLETE -- an exact sum did not fit; slippage is understated"
181 <<
" and the fee totals are withheld\n";
188template <
typename Session>
189int backtest_with(Session& session,
const std::string& symbol) {
190 const InstrumentId instrument{Country::KR, Venue::COMBINED, symbol};
192 const auto history = read_history(session, instrument, 400);
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";
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,
209 vanilla::MovingAverageCrossover::Params crossing;
210 crossing.instrument_ = instrument;
213 crossing.quantity_ = Decimal64::parse(
"100");
214 replay_and_report(
"SmaCrossover(20, 50)", vanilla::SmaCrossover{crossing}, *history);
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);
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";
227 std::cout <<
"The report needs a linear algebra backend; configure with -DATL_USE_EIGEN=ON.\n";
232int backtest_kis(ssl::context& tls,
const std::string& symbol) {
233 namespace kis = algotradelib::kis;
235 kis::KisConfig config;
237 kis::KisAccount account;
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);
246int backtest_tossinvest(ssl::context& tls,
const std::string& symbol,
const char* sequence) {
247 namespace toss = algotradelib::tossinvest;
249 toss::TossConfig config;
251 auto client = std::make_shared<toss::TossBlockingClient>(tls, std::move(config));
253 std::int64_t selected = 0;
254 if (sequence !=
nullptr) {
255 selected = std::strtoll(sequence,
nullptr, 10);
258 if (!accounts || accounts.value().empty()) {
259 std::cerr <<
"could not discover a Toss account\n";
262 selected = accounts.value().front().selector_;
265 BlockingSession<toss::TossBlockingClient> session(std::move(client), selected);
266 return backtest_with(session, symbol);
271int main(
int argc,
char** argv) {
272 const std::string broker = argc > 1 ? argv[1] :
"";
273 if (broker !=
"kis" && broker !=
"tossinvest") {
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";
279 const std::string symbol = argc > 2 ? argv[2] :
"005930";
282 ssl::context tls(ssl::context::tls_client);
283 tls.set_default_verify_paths();
284 tls.set_verify_mode(ssl::verify_peer);
286 if (broker ==
"kis") {
287 return backtest_kis(tls, symbol);
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';
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.
BarEvent make_bar_event(InstrumentId instrument, BarInterval interval, BarStamp stamp, Bar bar)
Makes a bar event, computing its period_end_ with bar_period_end.
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.
Venue
A trading venue: an exchange, or the combined view across exchanges.
BarInterval
The period one bar covers.
static SessionWindow krx()
Returns the KRX regular session, 09:00-15:30 KST.