algotradelib 0.0.0.9000
C++ header-only library for algorithmic trading
Loading...
Searching...
No Matches
example-live-program.cpp
Go to the documentation of this file.
1
52
53#include "kis_credentials.hpp"
55#include "live_program.hpp"
56
57#include <algotradelib/kis>
58#include <algotradelib/kiwoom>
59#include <algotradelib/tossinvest>
60#include <algotradelib/vanilla>
61
62#include <boost/asio/ssl/context.hpp>
63
64#include <cctype>
65#include <cstdint>
66#include <cstdlib>
67#include <exception>
68#include <filesystem>
69#include <iostream>
70#include <memory>
71#include <optional>
72#include <string>
73#include <utility>
74#include <vector>
75
76namespace {
77
78namespace ssl = boost::asio::ssl;
79namespace atl = algotradelib;
81namespace vanilla = algotradelib::strategy::vanilla;
82
84struct Arguments {
85 std::string broker_;
86 std::string symbol_;
87 bool live_{false};
88 std::optional<std::string> journal_;
89 std::optional<std::string> control_;
90 std::optional<std::int64_t> toss_account_;
91 std::optional<std::filesystem::path> credentials_;
92 bool credentials_from_flag_{false};
93 std::optional<std::string> market_data_;
94 std::optional<std::string> kiwoom_account_;
95};
96
98vanilla::BuyAndHold strategy_for(const atl::InstrumentId& instrument) {
99 vanilla::BuyAndHold holding;
100 holding.params_.instrument_ = instrument;
101 holding.params_.quantity_ = atl::Decimal64::parse("1");
102 return holding;
103}
104
110live_program::Settings settings_for(const atl::InstrumentId& instrument) {
111 live_program::Settings settings;
112 settings.universe_ = {instrument};
113
116 settings.limits_.max_orders_per_window_ = 2;
117
118 // Daily bars, `SessionSource`'s default. The loop reads the order list and holdings before it feeds
119 // a bar. At start the strategy is shown the history read, but only the newest closed bar's decision
120 // can be sent: at once if the market is open; otherwise an opening order waits for the first `OPEN`
121 // iteration (which needs a calendar), and an exit goes out at once and is kept if the broker refuses
122 // it. After that a day's bar is fed once the day has ended, at the first poll after midnight KST,
123 // and the same rule applies ("kept for the next session" in running-a-strategy-live.md). Polled
124 // every minute so that poll comes promptly; a minute-bar strategy sets `bar_interval_`.
125 settings.schedule_.bars_ = std::chrono::minutes(1);
126 settings.schedule_.holdings_ = std::chrono::seconds(30);
127 settings.schedule_.working_orders_ = std::chrono::seconds(30);
128 // A band is a per-trading-day value; refresh it well inside a session.
129 settings.schedule_.price_limits_ = std::chrono::minutes(10);
130 // The calendar keeps its six-hour default.
131
132 // Every age stated. A holdings snapshot older than two minutes -- four missed polls -- stops
133 // sizing; a band older than twenty -- two missed polls -- stops clamping.
134 settings.maximum_portfolio_age_ = std::chrono::minutes(2);
135 settings.maximum_price_limits_age_ = std::chrono::minutes(20);
136 return settings;
137}
138
140std::string file_safe(std::string text) {
141 for (auto& ch : text) {
142 if (!std::isalnum(static_cast<unsigned char>(ch)) && ch != '-') {
143 ch = '_';
144 }
145 }
146 return text;
147}
148
156void name_journal(
157 live_program::Settings& settings,
158 const Arguments& arguments,
159 const std::string& broker,
160 bool demo,
161 const std::string& account
162) {
163 settings.journal_scope_ = broker + ':' + (demo ? "demo" : "live") + ':' + account;
164 const std::string stem = "atl-" + broker + '-' + (demo ? "demo" : "live") + '-' + file_safe(account);
165 settings.journal_path_ = arguments.journal_ ? *arguments.journal_ : stem + ".jsonl";
166 settings.control_file_ = arguments.control_ ? *arguments.control_ : stem + ".stop";
167}
168
174void announce(
175 const Arguments& arguments,
176 const live_program::Settings& settings,
177 bool demo,
178 const char* demo_variable
179) {
180 std::cout << "broker : " << arguments.broker_ << (demo ? " (demo)" : " (LIVE ACCOUNT)") << '\n';
181 // Demo because nothing said live: say how to say it, since live keys on the demo endpoints fail with
182 // the broker's own authentication error, which does not point here.
183 if (demo && demo_variable != nullptr && std::getenv(demo_variable) == nullptr) {
184 std::cout << " demo by default; set " << demo_variable << "=0 to trade live\n";
185 }
186 std::cout << "instrument : " << arguments.symbol_ << '\n'
187 << "mode : " << (arguments.live_ ? "TRADE -- orders will be sent" : "observe") << '\n'
188 << "limits : at most " << settings.limits_.max_position_quantity_->str()
189 << " share(s), " << settings.limits_.max_order_notional_->str() << " per order\n";
190 if (settings.default_route_ != atl::Venue::UNSPECIFIED) {
191 std::cout << "route : " << atl::venue_name(settings.default_route_) << '\n';
192 }
193 if (arguments.live_) {
194 std::cout << "journal : " << settings.journal_path_ << " (scope " << settings.journal_scope_ << ")\n";
195 }
196}
197
199template <typename Session, typename MarketData>
200int run(
201 Session& session,
202 MarketData& market_data,
203 const atl::InstrumentId& instrument,
204 const live_program::Settings& settings,
205 bool live
206) {
209 const auto exit = live ?
210 live_program::trade(session, market_data, strategy_for(instrument), settings, stop, std::cout) :
211 live_program::observe(session, market_data, strategy_for(instrument), settings, stop, std::cout);
213 return static_cast<int>(exit);
214}
215
217template <typename Session>
218int run(Session& session, const atl::InstrumentId& instrument, const live_program::Settings& settings, bool live) {
219 return run(session, session, instrument, settings, live);
220}
221
222int run_kis(ssl::context& tls, const Arguments& arguments) {
223 namespace kis = algotradelib::kis;
224
225 kis::KisConfig config;
226 kis::KisAccount account;
227 kis_credentials::load(arguments.credentials_, config, &account, std::cout);
228 const bool demo = config.is_demo_;
229
230 const atl::InstrumentId instrument{atl::Country::KR, atl::Venue::COMBINED, arguments.symbol_};
231 auto settings = settings_for(instrument);
232 name_journal(settings, arguments, "kis", demo, account.account_number_ + '-' + account.product_code_);
233 announce(arguments, settings, demo, "KIS_DEMO");
234
236 std::make_shared<kis::KisBlockingClient>(tls, std::move(config), kis_credentials::paced_options()),
237 std::move(account)
238 );
239
240 // Market data from a session with no account, so it cannot trade: swapped by mistake, every order
241 // and account read through it is refused before any I/O.
242 if (arguments.market_data_ == "toss") {
243 if (!demo) {
244 // Toss publishes the next session's band after the close, before KIS does, and when each rolls
245 // over is not settled: a KIS demo run only.
246 throw std::runtime_error("--market-data toss is for a KIS demo run only");
247 }
248 if (!arguments.credentials_) {
249 throw std::runtime_error("--market-data toss reads tossinvest/real.json; give --credentials DIR");
250 }
251 const auto path = *arguments.credentials_ / "tossinvest" / "real.json";
253 toss_config.from_json(path.string());
255 std::make_shared<algotradelib::tossinvest::TossBlockingClient>(tls, std::move(toss_config)), 0
256 );
257 std::cout << "market data: toss " << path.string() << " (no account: cannot trade)\n";
258 return run(session, market_data, instrument, settings, arguments.live_);
259 }
260 if (!demo && arguments.market_data_ == "kis") {
261 throw std::runtime_error("--market-data kis is for a demo run; a live run reads its own market data");
262 }
263 if (demo) {
264 auto market_config = kis_credentials::market_data_config(arguments.credentials_);
265 if (market_config) {
267 std::make_shared<kis::KisBlockingClient>(
268 tls, std::move(*market_config), kis_credentials::paced_options()
269 ),
270 kis::KisAccount{}
271 );
272 std::cout << "market data: kis " << (*arguments.credentials_ / "kis" / "real.json").string()
273 << " (no account: cannot trade)\n";
274 return run(session, market_data, instrument, settings, arguments.live_);
275 }
276 if (arguments.market_data_ == "kis") {
277 throw std::runtime_error(
278 arguments.credentials_ ? "--market-data kis reads kis/real.json, and there is none" :
279 "--market-data kis reads kis/real.json; give --credentials DIR"
280 );
281 }
282 std::cout << "market data: the demo account's own (no kis/real.json); a demo host refuses\n"
283 << " reference data, so this run stops there\n";
284 }
285 return run(session, instrument, settings, arguments.live_);
286}
287
288int run_kiwoom(ssl::context& tls, const Arguments& arguments) {
289 namespace kiwoom = algotradelib::kiwoom;
290
291 kiwoom::KiwoomConfig config;
292 kiwoom_credentials::load(arguments.credentials_, arguments.kiwoom_account_, config, std::cout);
293 // Kiwoom's balance inquiry names one venue and does not say whether that filters the positions or
294 // only values them, so the adapter refuses holdings until you state both. Until then the loop has
295 // no portfolio and holds: this program runs and places nothing. Measure what your account returns
296 // (run `example-kiwoom-account`, then set its `holdings_venue_` to NXT, rebuild and run it again),
297 // then assert what you measured. The sellable scope is the third thing to state: without it the
298 // guard refuses every sell, a flatten's closing sell included.
299 config.holdings_venue_ = atl::Venue::KRX;
300 // config.holdings_position_scope_ = kiwoom::KiwoomPositionScope::WHOLE_HOLDING;
301 // config.holdings_sellable_scope_ = kiwoom::KiwoomSellableScope::WHOLE_HOLDING;
302 const bool demo = config.is_demo_;
303
305 std::make_shared<kiwoom::KiwoomBlockingClient>(tls, std::move(config), kiwoom_credentials::paced_options()),
306 kiwoom::KiwoomAccount{}
307 );
308 // Kiwoom takes no account number on a request -- the application key is the account -- so the
309 // journal is named after the account the key drives, as Kiwoom reports it.
310 const auto accounts = session.accounts();
311 if (!accounts || accounts.value().empty()) {
312 std::cerr << "could not read which account this Kiwoom key drives"
313 << (accounts ? std::string() : ": " + accounts.error().message_) << '\n';
314 return static_cast<int>(live_program::Exit::ACCOUNT_UNRESOLVED);
315 }
316
317 const atl::InstrumentId instrument{atl::Country::KR, atl::Venue::COMBINED, arguments.symbol_};
318 auto settings = settings_for(instrument);
319 // A Kiwoom order must name its venue (`dmst_stex_tp` has no default), and a strategy's orders name
320 // none, so the binding states where they go. KRX here; `Venue::NXT` for Nextrade, or
321 // `Venue::COMBINED` for Kiwoom's SOR, which its order endpoints document -- whether a demo account
322 // accepts SOR, and whether the listing's `sor_yn` flag reads as documented, has not been observed,
323 // so KRX is the route with the fewest open questions.
324 settings.default_route_ = atl::Venue::KRX;
325 name_journal(settings, arguments, "kiwoom", demo, accounts.value().front().display_number_);
326 announce(arguments, settings, demo, "KIWOOM_DEMO");
327 // Two reasons nothing is placed on Kiwoom yet, said up front so nobody waits for an order that
328 // cannot come. Both are statements you can make:
329 // - Holdings refuse until the scopes above are asserted, so the loop has no portfolio and holds.
330 // - Kiwoom publishes no trading calendar, so the market phase stays unknown, and the default
331 // limits open a position only while the market is open.
332 std::cout << "note : on Kiwoom this program places nothing yet -- holdings need a stated\n"
333 << " scope, and the market phase is unknown without a calendar\n";
334 return run(session, instrument, settings, arguments.live_);
335}
336
337int run_toss(ssl::context& tls, const Arguments& arguments) {
338 namespace toss = algotradelib::tossinvest;
339
340 toss::TossConfig config;
341 config.from_env();
342 std::cout << "credentials: environment variables (Toss reads no credentials directory)\n";
343 auto client = std::make_shared<toss::TossBlockingClient>(tls, std::move(config));
344
345 // Toss selects an account by sequence number and publishes the list. With one account that is the
346 // one; with several, the program will not guess which should trade, because a sequence is a request
347 // selector rather than an identity.
348 const int unresolved = static_cast<int>(live_program::Exit::ACCOUNT_UNRESOLVED);
349 auto accounts = atl::list_accounts(*client);
350 if (!accounts || accounts.value().empty()) {
351 std::cerr << "could not list the accounts these Toss credentials reach"
352 << (accounts ? std::string() : ": " + accounts.error().message_) << '\n';
353 return unresolved;
354 }
355 if (!arguments.toss_account_ && accounts.value().size() > 1) {
356 std::cerr << "these Toss credentials reach " << accounts.value().size()
357 << " accounts; choose one with --toss-account:\n";
358 for (const auto& account : accounts.value()) {
359 std::cerr << " --toss-account " << account.selector_ << " (" << account.type_ << ")\n";
360 }
361 return unresolved;
362 }
363 auto chosen = accounts.value().front();
364 if (arguments.toss_account_) {
365 bool found = false;
366 for (const auto& account : accounts.value()) {
367 if (account.selector_ == *arguments.toss_account_) {
368 chosen = account;
369 found = true;
370 }
371 }
372 if (!found) {
373 std::cerr << "no Toss account has sequence " << *arguments.toss_account_ << '\n';
374 return unresolved;
375 }
376 }
377 if (chosen.display_number_.empty()) {
378 std::cerr << "Toss reported no account number for sequence " << chosen.selector_
379 << ", so the journal could not be named after the account\n";
380 return unresolved;
381 }
382
383 const atl::InstrumentId instrument{atl::Country::KR, atl::Venue::COMBINED, arguments.symbol_};
384 auto settings = settings_for(instrument);
385 // Named after the account number, not the sequence: two credential sets can both have a sequence 1,
386 // and a journal that one account's run could continue would read its orders as the other's.
387 name_journal(settings, arguments, "toss", false, chosen.display_number_);
388 announce(arguments, settings, false, nullptr);
389 std::cout << "note : Toss has no paper-trading environment; --live trades real money\n";
390
391 atl::BlockingSession<toss::TossBlockingClient> session(std::move(client), chosen.selector_);
392 return run(session, instrument, settings, arguments.live_);
393}
394
396std::optional<Arguments> parse(int argc, char** argv) {
397 Arguments arguments;
398 const int given = argc;
399 try {
400 arguments.credentials_ = kis_credentials::take_directory(argc, argv);
401 arguments.credentials_from_flag_ = argc != given;
402 arguments.kiwoom_account_ = kiwoom_credentials::take_label(argc, argv);
403 } catch (const std::runtime_error& error) {
404 std::cerr << "example-live-program: " << error.what() << '\n';
405 return std::nullopt;
406 }
407 std::vector<std::string> positional;
408 for (int i = 1; i < argc; ++i) {
409 const std::string argument = argv[i];
410 const bool has_value = i + 1 < argc;
411 if (argument == "--live") {
412 arguments.live_ = true;
413 } else if (argument == "--journal" && has_value) {
414 arguments.journal_ = argv[++i];
415 } else if (argument == "--control" && has_value) {
416 arguments.control_ = argv[++i];
417 } else if (argument == "--toss-account" && has_value) {
418 arguments.toss_account_ = std::strtoll(argv[++i], nullptr, 10);
419 } else if (argument == "--market-data" && has_value) {
420 arguments.market_data_ = argv[++i];
421 if (*arguments.market_data_ != "kis" && *arguments.market_data_ != "toss") {
422 return std::nullopt;
423 }
424 } else if (argument.rfind("--", 0) == 0) {
425 return std::nullopt;
426 } else {
427 positional.push_back(argument);
428 }
429 }
430 if (positional.size() != 2) {
431 return std::nullopt;
432 }
433 arguments.broker_ = positional[0];
434 arguments.symbol_ = positional[1];
435 if (arguments.broker_ != "kis" && arguments.broker_ != "kiwoom" && arguments.broker_ != "toss") {
436 return std::nullopt;
437 }
438 // A setting the chosen broker would ignore is refused: ignored, it would trade whichever account
439 // the environment names. Market data is KIS's; Toss reads only its environment variables; the
440 // label selects a Kiwoom file.
441 if (arguments.market_data_ && arguments.broker_ != "kis") {
442 return std::nullopt;
443 }
444 if (arguments.credentials_from_flag_ && arguments.broker_ == "toss") {
445 return std::nullopt;
446 }
447 if (arguments.kiwoom_account_ && arguments.broker_ != "kiwoom") {
448 return std::nullopt;
449 }
450 return arguments;
451}
452
453} // namespace
454
462int main(int argc, char** argv) {
463 const auto arguments = parse(argc, argv);
464 if (!arguments) {
465 std::cerr
466 << "usage: example-live-program <kis|kiwoom|toss> <symbol> [--live] [--journal PATH]\n"
467 << " [--control PATH] [--toss-account SEQUENCE]\n"
468 << " [--credentials DIR] [--market-data kis|toss]\n"
469 << " [--kiwoom-account LABEL]\n\n"
470 << "Without --live it observes: nothing can be sent. KIS and Kiwoom are demo unless\n"
471 << "KIS_DEMO=0 / KIWOOM_DEMO=0; Toss has no demo, so toss --live trades real money.\n";
472 return 64;
473 }
474
475 try {
476 ssl::context tls(ssl::context::tls_client);
477 tls.set_default_verify_paths();
478 tls.set_verify_mode(ssl::verify_peer);
479
480 if (arguments->broker_ == "kis") {
481 return run_kis(tls, *arguments);
482 }
483 if (arguments->broker_ == "kiwoom") {
484 return run_kiwoom(tls, *arguments);
485 }
486 return run_toss(tls, *arguments);
487 } catch (const std::exception& exception) {
488 std::cerr << "example-live-program: " << exception.what() << '\n';
489 return 70;
490 }
491}
A blocking broker client bound to one account, with the same broker operations as BrokerSession,...
Result< std::vector< AccountInfo< Client > > > accounts()
Lists the broker's accounts; requires Caps::has_account_discovery.
static Decimal64 parse(std::string_view input)
Parses a decimal string exactly (no binary floating point).
Definition types.hpp:323
void from_json(const std::string &filename)
Loads the credentials from a JSON file, in the broker's format.
Definition oauth.hpp:54
static void install(StopSource &source) noexcept
Installs the handlers: SIGINT and SIGTERM request a quiesce on source; SIGHUP, where it exists,...
static void uninstall() noexcept
Restores the default disposition of SIGINT, SIGTERM and (POSIX) SIGHUP, and forgets the source.
A stop request the loop polls; safe to set from a signal handler.
Definition stop.hpp:158
The examples' KIS credentials: one JSON file per account, or the environment.
algotradelib::network::HttpClientOptions paced_options()
Returns transport options that pace a KIS client at one request a second.
std::optional< algotradelib::kis::KisConfig > market_data_config(const std::optional< std::filesystem::path > &directory)
Returns a configuration for market data alone, from DIR/kis/real.json.
void load(const std::optional< std::filesystem::path > &directory, algotradelib::kis::KisConfig &config, algotradelib::kis::KisAccount *account, std::ostream &out)
Loads the credentials (and account) KIS_DEMO chooses, and prints where from (a path or "environment v...
std::optional< std::filesystem::path > take_directory(int &argc, char **argv)
Removes --credentials DIR from the command line and returns the directory, else ATL_CREDENTIALS_DIR.
The examples' Kiwoom credentials: one JSON file per key, or the environment.
algotradelib::network::HttpClientOptions paced_options()
Returns transport options that pace a Kiwoom client at one request a second.
std::optional< std::string > take_label(int &argc, char **argv)
Removes --kiwoom-account LABEL from the command line and returns the label.
void load(const std::optional< std::filesystem::path > &directory, const std::optional< std::string > &label, algotradelib::kiwoom::KiwoomConfig &config, std::ostream &out)
Loads the credentials from DIR/kiwoom/{demo,real}-LABEL.json (as KIWOOM_DEMO chooses; label kr by def...
The production wiring of a live run, with no broker named: the part of example-live-program a consume...
Exit observe(Session &session, MarketData &market_data, Strategy strategy_instance, const Settings &settings, runner::StopSource &stop, std::ostream &out, Clock clock={})
Runs a strategy against a session's account and another's market data, with nothing to send through.
@ ACCOUNT_UNRESOLVED
The program could not settle which account to trade (used by example-live-program).
Exit trade(Session &session, MarketData &market_data, Strategy strategy_instance, const Settings &settings, runner::StopSource &stop, std::ostream &out, Clock clock={})
Trades a strategy through a session, reading market data and reference data from another.
The Korea Investment & Securities (KIS) Open API client.
Definition auth.hpp:9
The Kiwoom Securities REST API client.
Definition auth.hpp:12
Runs a strategy: the runner, the polling loop, the order journal, and stopping.
Definition clock.hpp:7
void stop(S &strategy, Ctx &context)
Calls strategy.on_stop(context) if the strategy declares on_stop; otherwise does nothing.
Definition concept.hpp:168
The strategies this library ships (buy and hold, crossovers, mean reversion, sizing,...
The Toss Securities Open API client.
Definition auth.hpp:9
The library: broker-neutral types (core), and the modules below.
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...
constexpr std::string_view venue_name(Venue venue) noexcept
Returns a display name for a venue, such as "KRX".
Definition types.hpp:58
Identifies an instrument: its country, venue and symbol.
Definition types.hpp:78
std::optional< std::chrono::nanoseconds > holdings_
How often to read holdings; without it the portfolio is never refreshed.
Definition loop.hpp:175
std::optional< std::chrono::nanoseconds > price_limits_
How often to read today's price bands.
Definition loop.hpp:210
std::optional< std::chrono::nanoseconds > bars_
How often to fetch bars.
Definition loop.hpp:171
std::optional< std::chrono::nanoseconds > working_orders_
How often to read the working orders.
Definition loop.hpp:185
std::optional< Decimal64 > max_position_quantity_
The largest absolute position, in shares, any instrument may reach.
Definition risk.hpp:277
std::optional< std::size_t > max_orders_per_window_
The most orders that may be sent within rate_window_.
Definition risk.hpp:293
std::optional< Decimal64 > max_order_notional_
The largest value one order may have, in its own currency.
Definition risk.hpp:270
Toss Securities credentials (client id and secret). Toss has no demo environment.
Definition config.hpp:24
What a run is told, gathered so main states it once and a test can state it differently.
std::vector< atl::InstrumentId > universe_
The instruments the source polls.
std::string journal_path_
Where the order journal lives.
std::chrono::nanoseconds maximum_portfolio_age_
How old a holdings snapshot – and the order listing – may be before the loop stops sizing against it.
std::chrono::nanoseconds maximum_price_limits_age_
How old a price band may be before the loop stops clamping to it.
std::string control_file_
The file an operator writes quiesce, withdraw or flatten into to stop the run.
runner::PollSchedule schedule_
How often each kind is polled. trade needs working_orders_ set, shorter than maximum_portfolio_age_: ...
atl::Venue default_route_
The venue an order is sent to, since a strategy's orders name none; UNSPECIFIED takes the broker's ow...
std::string journal_scope_
Which account the journal belongs to; a journal naming another refuses to open.
strategy::RiskLimits limits_
The limits the guard enforces.