algotradelib 0.0.0.9000
C++ header-only library for algorithmic trading
Loading...
Searching...
No Matches
example-strategy-live.cpp
1// One strategy, compiled for KIS and Toss from this one source, bound at a single line in `main`.
2//
3// The point, demonstrated: `trade()` below names no broker. It takes a session
4// by template parameter, and everything broker-specific is either behind a capability gate or
5// inside the adapter. Swapping KIS for Toss changes which overload `main` calls, nothing else.
6//
7// **It can place real orders.** Three modes, and only the last one trades:
8//
9// --dry-run no session at all, bars built in code, no credentials needed
10// (default) a real session for market data, but the send step goes to an executor that holds
11// no session, so there is nothing for an order to be sent through
12// --live the send step goes to the bound session
13//
14// Run it as:
15//
16// example-strategy-live --dry-run
17// example-strategy-live kis 005930 [--live]
18// example-strategy-live tossinvest 005930 [account-sequence] [--live]
19//
20// Credentials come from the same environment variables the other examples document. Without
21// `--live` a bound session still only *reads*: it fetches bars and prints what it would do.
22//
23// **On interrupting it.** A `Ctrl+C` stops this program, and that is all it does. An order the broker
24// already accepted stays live on the exchange, which knows nothing about a process that died -- and an
25// order *in flight* at that moment never reaches the classification that would have recorded its
26// outcome as unknown, because the default signal handler terminates immediately. So after
27// interrupting a `--live` run, read the broker's order list before running it again. This example
28// places at most one order per run, which bounds what can be in doubt to that one.
29//
30// **On running it again.** Each run reads the open orders before the holdings, so a buy still waiting
31// to fill counts toward the one-share limit. A buy that has filled but does not show in the holdings
32// yet is in neither, and is bought again: wait until the holdings show it. Never run two at once.
33
34#include "kis_credentials.hpp"
35
36#include <algotradelib/kis>
37#include <algotradelib/live>
38#include <algotradelib/vanilla>
39#include <algotradelib/tossinvest>
40
41#include <boost/asio/ssl/context.hpp>
42
43#include <chrono>
44#include <cstddef>
45#include <cstdlib>
46#include <exception>
47#include <iostream>
48#include <memory>
49#include <string>
50#include <string_view>
51#include <utility>
52#include <vector>
53
54namespace ssl = boost::asio::ssl;
55
56namespace {
57
58namespace strategy = algotradelib::strategy;
59namespace runner = algotradelib::runner;
60namespace live = algotradelib::live;
61namespace vanilla = algotradelib::strategy::vanilla;
62
64using algotradelib::BarQuery;
65using algotradelib::BlockingSession;
67using algotradelib::Decimal64;
68using algotradelib::InstrumentId;
71
73enum class Mode {
79 OBSERVE,
81 TRADE,
82};
83
85vanilla::BuyAndHold strategy_for(const InstrumentId& instrument) {
86 vanilla::BuyAndHold holding;
87 holding.params_.instrument_ = instrument;
88 holding.params_.quantity_ = Decimal64::parse("1");
89 return holding;
90}
91
93strategy::RiskLimits example_limits() {
94 strategy::RiskLimits limits;
95 // One share of a Korean blue chip is well under this, so a configuration mistake that asked for
96 // a hundred is refused rather than filled.
97 limits.max_order_notional_ = Decimal64::parse("500000");
98 limits.max_position_quantity_ = Decimal64::parse("1");
99 limits.max_orders_per_window_ = 2;
100 return limits;
101}
102
111struct MarketState {
112 algotradelib::Instrument reference_;
113 algotradelib::BarSeries bars_;
114 algotradelib::HoldingsSnapshot holdings_;
115};
116
124template <typename Tag, typename R>
125int drive(
126 const InstrumentId& instrument, const MarketState& state,
127 const algotradelib::WorkingOrders<Tag>& open_orders, R live_runner
128) {
129 live_runner.context().set_instrument(state.reference_);
130 // A single pass driven by hand, so the phase is this program's to state: only
131 // `runner::PollingLoop` computes one, from a session window and a polled calendar. A program that
132 // runs continuously uses the loop and does not set this at all -- see `example-live-loop.cpp`.
133 live_runner.context().set_phase(strategy::MarketPhase::OPEN);
134
135 strategy::Portfolio<Tag> portfolio;
136 portfolio.positions_ = state.holdings_.positions_;
137 // **Stamped with when the broker's answer arrived, and that is not optional.** A runner bound to a
138 // session refuses to size an order against a portfolio it cannot date: an unstamped one reads as
139 // an account nobody has read, and an empty account is the permissive answer to every ceiling.
140 // Copying the rows without the timestamp is the mistake this line exists to prevent.
141 portfolio.as_of_ = state.holdings_.received_at_;
142 // **And the orders still open, with when they were listed.** Without them a buy that is still
143 // waiting to fill is invisible, and the runner buys again past `max_position_quantity_`.
144 portfolio.working_ = open_orders.orders_;
145 portfolio.working_as_of_ = open_orders.received_at_;
146 // **And with what the broker's sellable figures are evidence of, for the same reason.** A guard
147 // enforcing `enforce_sellable_quantity_` refuses a figure whose coverage nobody stated, so a hand-
148 // built portfolio that copies the rows without this refuses every sell -- and one that invented an
149 // enforceable basis would measure a sale against a number it had not characterized. Copy it; do not
150 // construct it. The limit is on by default, so without this line every sell here would be refused.
151 portfolio.sellable_basis_ = state.holdings_.sellable_basis_;
152 live_runner.context().set_portfolio(std::move(portfolio));
153
154 const auto& last = state.bars_.bars_.back();
155 live_runner.context().set_last_price(instrument, last.close_);
156 const auto report = live_runner.feed(
157 strategy::make_bar_event(instrument, state.bars_.interval_, state.bars_.stamp_, last)
158 );
159
160 for (const auto& order : report.would_place_) {
161 std::cout << "order: " << (order.side_ == OrderSide::BUY ? "BUY " : "SELL") << ' '
162 << order.quantity_.str() << " at "
163 << (order.price_ ? order.price_->str() : std::string("market")) << '\n';
164 }
165 for (const auto& refusal : report.refused_) {
166 std::cout << "refused by a guard: " << strategy::guard_reason_name(refusal.reason_) << '\n';
167 }
168 for (const auto& rejection : report.rejected_) {
169 // Not always "could not build": the bucket also holds a broker's refusal and one this binding
170 // made without asking a broker, and the reason on the entry is what says which.
171 std::cout << "not placed: " << strategy::reject_reason_name(rejection.reason_) << '\n';
172 }
173 for (const auto& held : report.withheld_) {
174 std::cout << "withheld pending reconciliation: " << held.detail_ << '\n';
175 }
176 for (const auto& order : report.indeterminate_) {
177 std::cout << "UNKNOWN OUTCOME for " << order.instrument_.symbol_
178 << ": read the broker's order list before trading it again\n";
179 }
180 if (report.quiet()) {
181 std::cout << "the strategy asked for nothing: the target is already met\n";
182 }
183 if (!live_runner.ledger().empty()) {
184 std::cout << "\nreconciliation required before this instrument may be traded again\n";
185 return 1;
186 }
187 return 0;
188}
189
191void note_interruption_hazard() {
192 std::cout
193 << "\nnote: interrupting a live run does not withdraw an accepted order, and an order in\n"
194 << " flight at that moment leaves no record that its outcome is unknown. Read the\n"
195 << " broker's order list before running again, and wait until the holdings show any\n"
196 << " fill: a filled buy they do not show yet is bought again.\n";
197}
198
212template <typename Session>
213int trade(Session& session, const InstrumentId& instrument, Mode mode) {
214 using Caps = typename Session::Caps;
215 using Tag = typename Session::BrokerTag;
216
217 std::cout << "broker: " << Tag::name << " instrument: " << instrument.symbol_
218 << " mode: " << (mode == Mode::TRADE ? "TRADE" : "observe") << "\n\n";
219
220 if constexpr (Caps::has_client_order_id) {
221 std::cout << "this broker accepts an idempotency key, so a retry could be made safe\n";
222 } else {
223 std::cout << "this broker accepts no idempotency key, so no order is ever replayed\n";
224 }
225
226 MarketState state;
227
228 // Reference data first: without a tick grid and a lot size there is nothing reconciliation can
229 // build, and it would refuse every order by name.
230 auto reference = session.instrument(instrument);
231 if (!reference) {
232 std::cerr << "could not read reference data: " << reference.error().message_ << '\n';
233 return 1;
234 }
235 state.reference_ = std::move(reference.value());
236
237 BarQuery query;
238 query.instrument_ = instrument;
239 query.interval_ = BarInterval::ONE_DAY;
240 query.count_ = 30;
241 auto bars = session.bars(query);
242 if (!bars) {
243 std::cerr << "could not read bars: " << bars.error().message_ << '\n';
244 return 1;
245 }
246 if (bars.value().bars_.empty()) {
247 std::cerr << "the broker returned no bars\n";
248 return 1;
249 }
250 state.bars_ = std::move(bars.value());
251
252 // **Open orders before holdings.** An order that fills between the two reads then shows in the
253 // holdings. Read the other way round, it would be in neither.
254 algotradelib::WorkingOrders<Tag> open_orders;
255 if constexpr (Caps::has_working_orders) {
256 auto listed = session.working_orders();
257 if (!listed) {
258 std::cerr << "could not read open orders: " << listed.error().message_ << '\n';
259 return 1;
260 }
261 open_orders = std::move(listed.value());
262 } else {
263 if (mode == Mode::TRADE) {
264 std::cerr << "this broker lists no open orders, so a live run could not see an earlier order "
265 "still waiting to fill; use example-live-program\n";
266 return 1;
267 }
268 }
269
270 auto holdings = session.holdings();
271 if (!holdings) {
272 std::cerr << "could not read holdings: " << holdings.error().message_ << '\n';
273 return 1;
274 }
275 state.holdings_ = std::move(holdings.value());
276
277 // One line per mode. The factories read the capabilities and the tag off the session, so the two
278 // runners differ in exactly one thing -- where the send goes -- and cannot differ in any other.
279 if (mode == Mode::TRADE) {
280 const auto status = drive<Tag>(
281 instrument, state, open_orders, live::bind(session, strategy_for(instrument), example_limits())
282 );
283 note_interruption_hazard();
284 return status;
285 }
286 return drive<Tag>(
287 instrument, state, open_orders, live::bind_observer(session, strategy_for(instrument), example_limits())
288 );
289}
290
291int trade_kis(ssl::context& tls, const std::string& symbol, Mode mode) {
292 namespace kis = algotradelib::kis;
293
294 kis::KisConfig config;
295 kis::KisAccount account;
296 kis_credentials::load(kis_credentials::environment_directory(), config, &account, std::cout);
297
298 auto client =
299 std::make_shared<kis::KisBlockingClient>(tls, std::move(config), kis_credentials::paced_options());
300 BlockingSession<kis::KisBlockingClient> session(std::move(client), std::move(account));
301 return trade(session, InstrumentId{Country::KR, Venue::COMBINED, symbol}, mode);
302}
303
304int trade_tossinvest(
305 ssl::context& tls,
306 const std::string& symbol,
307 const char* sequence,
308 Mode mode
309) {
310 namespace toss = algotradelib::tossinvest;
311
312 toss::TossConfig config;
313 config.from_env();
314 auto client = std::make_shared<toss::TossBlockingClient>(tls, std::move(config));
315
316 // Toss selects an account by sequence number and publishes the list, so an unstated one is
317 // discovered rather than guessed. KIS takes the account from the environment instead, which is
318 // exactly the kind of difference the port leaves to the adapter.
319 std::int64_t selected = 0;
320 if (sequence != nullptr) {
321 selected = std::strtoll(sequence, nullptr, 10);
322 } else {
323 auto accounts = algotradelib::list_accounts(*client);
324 if (!accounts) {
325 std::cerr << "account discovery failed: " << accounts.error().message_ << '\n';
326 return 1;
327 }
328 if (accounts.value().empty()) {
329 std::cerr << "no Toss accounts are available for these credentials\n";
330 return 1;
331 }
332 selected = accounts.value().front().selector_;
333 }
334
335 BlockingSession<toss::TossBlockingClient> session(std::move(client), selected);
336 return trade(session, InstrumentId{Country::KR, Venue::COMBINED, symbol}, mode);
337}
338
340int trade_dry(const std::string& symbol) {
341 const InstrumentId instrument{Country::KR, Venue::COMBINED, symbol};
342 std::cout << "no broker bound: nothing in this path can reach a market\n\n";
343
344 runner::DryRunner<vanilla::BuyAndHold> dry(strategy_for(instrument), example_limits());
345 algotradelib::Instrument reference;
346 reference.id_ = instrument;
347 reference.type_ = algotradelib::SecurityType::STOCK;
348 reference.currency_ = algotradelib::Currency{"KRW"};
349 reference.lot_size_ = Decimal64::parse("1");
351 dry.context().set_instrument(reference);
352 dry.context().set_phase(strategy::MarketPhase::OPEN);
353 dry.context().set_last_price(instrument, Decimal64::parse("70000"));
354
355 algotradelib::Bar bar;
356 bar.timestamp_ = std::chrono::system_clock::now();
357 bar.high_ = Decimal64::parse("70500");
358 bar.low_ = Decimal64::parse("69400");
359 bar.close_ = Decimal64::parse("70000");
360 bar.volume_ = Decimal64::parse("1200000");
361
362 const auto report = dry.feed(
364 instrument, BarInterval::ONE_DAY, algotradelib::BarStamp::CLOSE, bar
365 )
366 );
367 for (const auto& order : report.would_place_) {
368 std::cout << "would send: " << (order.side_ == OrderSide::BUY ? "BUY " : "SELL") << ' '
369 << order.quantity_.str() << " at market\n";
370 }
371 return 0;
372}
373
374} // namespace
375
376int main(int argc, char** argv) {
377 std::vector<std::string> arguments(argv + 1, argv + argc);
378 Mode mode = Mode::OBSERVE;
379 bool dry = false;
380 std::vector<std::string> positional;
381 for (const auto& argument : arguments) {
382 if (argument == "--live") {
383 mode = Mode::TRADE;
384 } else if (argument == "--dry-run") {
385 dry = true;
386 } else if (argument.rfind("--", 0) == 0) {
387 // An option this program does not know is refused, never read as a positional argument: a
388 // mistyped or unsupported flag beside `--live` must not leave the run trading as if it were absent.
389 std::cerr << "example-strategy-live: unknown option " << argument
390 << " (credentials come from the environment or ATL_CREDENTIALS_DIR)\n";
391 return 2;
392 } else {
393 positional.push_back(argument);
394 }
395 }
396
397 const std::string symbol = positional.size() > 1 ? positional[1] : "005930";
398 if (dry) {
399 return trade_dry(symbol);
400 }
401
402 const std::string broker = positional.empty() ? "" : positional[0];
403 if (broker != "kis" && broker != "tossinvest") {
404 std::cerr
405 << "usage: example-strategy-live --dry-run\n"
406 << " example-strategy-live <kis|tossinvest> [symbol] [account-sequence] [--live]\n\n"
407 << "Without --live the session is used for market data only: the send step goes to an\n"
408 << "executor that holds no session, so there is nothing for an order to be sent through.\n";
409 return 2;
410 }
411
412 try {
413 ssl::context tls(ssl::context::tls_client);
414 tls.set_default_verify_paths();
415 tls.set_verify_mode(ssl::verify_peer);
416
417 if (broker == "kis") {
418 return trade_kis(tls, symbol, mode);
419 }
420 return trade_tossinvest(
421 tls, symbol, positional.size() > 2 ? positional[2].c_str() : nullptr, mode
422 );
423 } catch (const std::exception& exception) {
424 std::cerr << "example failed: " << exception.what() << '\n';
425 return 1;
426 }
427}
The examples' KIS credentials: one JSON file per account, or the environment.
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...
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.
BrokerError refusal()
Returns the error every account-side member of DataOnlyTraits answers with.
Runner< Strategy, CapsType, DryTag, RecordingExecutor< DryTag > > DryRunner
A runner that sends nothing: handlers fire and instructions are recorded by a RecordingExecutor.
Definition runner.hpp:4524
void bar(S &strategy, Ctx &context, const BarEvent &event)
Calls strategy.on_bar(context, event) if the strategy declares on_bar; otherwise does nothing.
Definition concept.hpp:191
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
@ OPEN
Inside the session.
Definition event.hpp:38
constexpr std::string_view reject_reason_name(RejectReason reason) noexcept
Returns a lowercase name for a reason, such as "no_reference_data", for logs.
constexpr std::string_view guard_reason_name(GuardReason reason) noexcept
Returns a lowercase name for a refusal reason, such as "order_rate_exceeded", for logs.
Definition risk.hpp:216
@ CLOSE
The timestamp is the instant the period ended.
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.
OrderSide
The side of an order: its economic direction, buy or sell.
Definition types.hpp:1683
BarInterval interval_
The period each bar covers.
BarStamp stamp_
Which edge of its period each bar's timestamp marks.
std::vector< Bar > bars_
The bars, oldest first.
std::chrono::system_clock::time_point received_at_
When this process received the snapshot (brokers do not timestamp holdings).
Definition types.hpp:1081
SellableBasis sellable_basis_
What the rows' sellable_quantity_ figures cover (unless a row carries its own).
Definition types.hpp:1088
std::vector< HoldingPosition > positions_
Every position row; one instrument may appear in more than one.
Definition types.hpp:1077
Currency currency_
The currency it trades in.
InstrumentId id_
The instrument.
PriceRule price_rule_
The price grid; kind UNKNOWN where the library has no table.
std::optional< Decimal64 > lot_size_
The order quantity increment, if the broker publishes one.
SecurityType type_
The kind of security, which selects the price grid.
static PriceRule krx_equity()
Returns the KRX cash-equity tick table (January 2023 revision).
std::vector< WorkingOrder< Broker > > orders_
The open orders.
Definition types.hpp:2314
std::chrono::system_clock::time_point received_at_
When the listing was received, on this machine's clock.
Definition types.hpp:2316
std::vector< WorkingOrder< Broker > > working_
Orders still outstanding at the broker.
Definition portfolio.hpp:77
SellableBasis sellable_basis_
What the sellable_quantity_ figures on positions_ cover.
std::chrono::system_clock::time_point working_as_of_
When the working-order listing was received, on the same clock as as_of_.
Definition portfolio.hpp:90
std::chrono::system_clock::time_point as_of_
When the holdings were received, on this machine's clock.
Definition portfolio.hpp:84
std::vector< HoldingPosition > positions_
Every position row the broker reported.
Definition portfolio.hpp:56
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