38#include <algotradelib/live>
39#include <algotradelib/runner>
49namespace live_program {
51namespace atl = algotradelib;
52namespace live = algotradelib::live;
53namespace runner = algotradelib::runner;
54namespace strategy = algotradelib::strategy;
143template <
typename Session,
typename Runner>
145 for (
const auto& instrument : settings.
universe_) {
146 auto reference = session.instrument(instrument);
148 out <<
"could not read reference data for " << instrument.symbol_ <<
": "
149 << reference.error().message_ <<
'\n';
152 bound.context().set_instrument(std::move(reference.value()));
163 if (recovery.
clean()) {
164 out <<
"recovery: clean -- the journal owes nothing\n";
167 out <<
"recovery: NOT clean\n";
168 for (
const auto& instrument : recovery.
marked_) {
169 out <<
" withheld until the broker's lists resolve it: " << instrument.symbol_ <<
'\n';
172 out <<
" the last line was torn: the previous process died while writing it\n";
175 out <<
" " << recovery.
unreadable_lines_ <<
" line(s) this library did not write\n";
178 out <<
" the journal exists and could not be read\n";
181 out <<
" an unresolved instruction names no instrument\n";
183 if (!recovery.
detail_.empty()) {
184 out <<
" detail: " << recovery.
detail_ <<
'\n';
194 out <<
"to stop: Ctrl-C or SIGTERM (quiesce), or\n"
195 <<
" echo quiesce > " << settings.
control_file_ <<
" # stop placing, leave orders resting\n"
196 <<
" echo withdraw > " << settings.
control_file_ <<
" # cancel the account's working orders\n"
197 <<
" echo flatten > " << settings.
control_file_ <<
" # withdraw, then close the account's positions\n"
198 <<
"the file is not deleted: a word left in it is carried out at the next start; remove it\n"
199 <<
"before trading again\n";
200 if (settings.
closing_wait_ > std::chrono::nanoseconds::zero()) {
201 out <<
"a withdraw or a flatten may first wait up to "
202 << std::chrono::duration_cast<std::chrono::seconds>(settings.
closing_wait_).count()
203 <<
" s for the order list and holdings to account for this\n"
204 <<
"program's own orders. ";
206 out <<
"A supervisor's grace period must cover that stop: the wait, the calls in\n"
207 <<
"flight (a paginated read, a paced request) and the closing orders themselves, with margin; a\n"
208 <<
"SIGTERM does not turn a pending flatten into a quiesce\n";
217 out <<
"\niterations : " << report.
iterations_ <<
'\n'
224 <<
"blocked : " << report.
blocked_ <<
" iteration(s)\n"
234 <<
"plans withheld : " << report.
plans_withheld_ <<
" (account unread, too old or older than the order listing, or a hold while it would cancel)\n"
242 <<
"cancels : " << report.
cancels_rejected_ <<
" rejected (by the broker or the adapter), "
244 <<
" refused by a hold, " << report.
cancels_unresolved_ <<
" for an order not in the picture\n"
246 <<
" (sent, and not confirmed in time: its instrument waits for holdings or its history, or for you if "
247 "it was never named or the runner overlaps orders)\n";
258 out <<
"ABANDONED : after repeated failures polling "
262 for (
const auto& note : report.
notes_) {
263 out <<
"note : " << note <<
'\n';
275 <<
" -- whether every closing instruction was taken, not whether the account is flat;\n"
276 <<
" read the broker's order list and holdings to learn that\n";
294template <
typename Loop>
324template <
typename Strategy,
typename Session,
typename MarketData,
typename Clock = runner::SteadyClock>
327 MarketData& market_data,
328 Strategy strategy_instance,
335 if (!journal.open()) {
336 out <<
"journal refused: " << journal.rejection() <<
'\n'
337 <<
"a runner that cannot record what it sends does not send\n";
338 return Exit::JOURNAL_REFUSED;
351 if (
const auto failure = bound.local_failure()) {
355 out <<
"HALTED: " << *failure <<
'\n'
356 <<
"nothing was started. This program's only way on gives up the whole journal. That is safe\n"
357 <<
"only once no order the journal may track can execute any more:\n"
358 <<
" 1. read the broker's order list, order history and holdings for this account yourself.\n"
359 <<
" A damaged journal may not name every order it tracks: take every order sent on this\n"
360 <<
" account since the journal began, on every day and every page of the order history, and\n"
361 <<
" every order still working;\n"
362 <<
" 2. each must have ended: filled in full, or cancelled with its history row saying so. A\n"
363 <<
" cancellation only requested, an order list that no longer shows it, and the close of the\n"
364 <<
" session do not show that an order has ended;\n"
365 <<
" 3. holdings must show the final fills of each;\n"
366 <<
" 4. only then move the journal aside, keeping it: mv " << settings.
journal_path_ <<
' '
368 <<
"the next start then begins a new journal and sizes from the holdings and working orders the\n"
369 <<
"broker reports. If steps 2 and 3 cannot be shown for every order, leave the journal where it\n"
370 <<
"is. Do NOT move it aside while an order it may track is left working: a new journal does not\n"
371 <<
"know the order, and when it fills before holdings show it, the order is sent again.\n";
379 if constexpr (
decltype(bound)::on_fill_never_fires) {
380 out <<
"note: this strategy defines on_fill, which never fires on a live runner; anything it\n"
381 <<
" does there (a protective stop, say) will not happen. See writing-a-strategy.md.\n";
387 source.set_journal(journal);
391 bound, source, settings.
schedule_, shared_clock
402 if (
const auto failure = bound.local_failure()) {
403 out <<
"LOCAL FAILURE : " << *failure <<
'\n'
404 <<
" read the broker's order list before starting again\n";
407 if (report.abandoned_) {
410 if (
stop_closes(report) && !report.closing_complete()) {
413 if (report.owed_stop_) {
414 out <<
"OWED ORDER : the run stopped placing by itself (see the note above). A supervisor must not\n"
415 <<
" restart it: the order is restored from the journal and the run stops again.\n"
416 <<
"this journal still owes:\n";
417 for (
const auto& owed : bound.obligations()) {
418 out <<
" " << owed.instrument_.symbol_ <<
' ' << (owed.side_ == atl::OrderSide::BUY ?
"buy " :
"sell ")
422 "order " + owed.order_id_ +
", not yet in holdings")
425 out <<
"this program has no command to release one order, and its only way on gives up the whole\n"
426 <<
"journal. That is safe only once none of the orders above can execute any more:\n"
427 <<
" 1. read the broker's order list, order history and holdings for every order above;\n"
428 <<
" 2. each must have ended: filled in full, or cancelled with its history row saying so. A\n"
429 <<
" cancellation only requested, an order list that no longer shows it, and the close of the\n"
430 <<
" session do not show that an order has ended;\n"
431 <<
" 3. holdings must show the final fills of each;\n"
432 <<
" 4. only then move the journal aside, keeping it: mv " << settings.
journal_path_ <<
' '
434 <<
"the next start then begins a new journal and sizes from the account as it is.\n"
435 <<
"Do NOT move the journal aside while an order it tracks is left working: a new journal does not\n"
436 <<
"know the order, and when it fills before holdings show it, the order is sent again. A program\n"
437 <<
"of your own releases that instrument's obligations instead (Runner::release_obligations; see\n"
438 <<
"set_owed_order_limit) and keeps the rest of the journal.\n";
461template <
typename Strategy,
typename Session,
typename Clock = runner::SteadyClock>
464 Strategy strategy_instance,
470 return trade(session, session, std::move(strategy_instance), settings, stop, out, std::move(clock));
494template <
typename Strategy,
typename Session,
typename MarketData,
typename Clock = runner::SteadyClock>
497 MarketData& market_data,
498 Strategy strategy_instance,
504 auto bound = live::bind_observer(session, std::move(strategy_instance), settings.
limits_);
505 if (!load_reference_data(market_data, bound, settings, out)) {
506 return Exit::REFERENCE_UNREADABLE;
514 bound, source, settings.
schedule_, std::move(clock)
518 out <<
"observing: nothing can be sent (pass --live to trade)\n";
525 out <<
"closing : simulated -- the observer sent nothing, so nothing at the broker was\n"
526 <<
" placed or withdrawn by this run\n";
543template <
typename Strategy,
typename Session,
typename Clock = runner::SteadyClock>
546 Strategy strategy_instance,
552 return observe(session, session, std::move(strategy_instance), settings, stop, out, std::move(clock));
The source a runner::PollingLoop polls: reads a broker session and feeds bars, quotes,...
A clock that reads another one, so that two holders (a loop and its journal) read one time.
The order journal on disk: one JSON object per line, appended and flushed to disk (fsync) before appe...
A control file an operator writes to stop a running loop with a given intent.
Runs a runner for a live session: polls a source on a schedule, gates placements, derives the market ...
A stop request the loop polls; safe to set from a signal handler.
void print_how_to_stop(const Settings &settings, std::ostream &out)
Prints how to stop the run, before it starts, so nobody reaches for kill -9.
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.
bool stop_closes(const runner::LoopReport &report) noexcept
Tests whether a run ended with a stop that sends closing instructions.
Exit
What the program exits with, so a supervisor can tell its endings apart.
@ JOURNAL_REFUSED
The journal would not open, so nothing was bound.
@ LOOP_ABANDONED
The loop gave up after repeated poll failures: it was not trading when it returned.
@ LOCAL_FAILURE
Local machinery failed during the run – usually a journal write – and the runner halted itself....
@ CLOSING_INCOMPLETE
A stop that closes ran and closing_complete() is false: read the broker's order list.
@ ACCOUNT_UNRESOLVED
The program could not settle which account to trade (used by example-live-program).
@ REFERENCE_UNREADABLE
Reference data could not be read, so no order could be priced; nothing was started.
@ OWED_ORDER
The run stopped placing by itself: an order has been owed too long with nobody able to account for it...
@ RECOVERY_HALTED
Recovery halted the runner; nothing was started.
@ CLEAN
The run ended, and any closing pass it made got every instruction out.
bool load_reference_data(Session &session, Runner &bound, const Settings &settings, std::ostream &out)
Reads each instrument's reference data (lot size, price grid) onto the runner, once; the loop does no...
void configure(Loop &loop, const Settings &settings)
Configures the loop the same way for both modes: every age stated, the control file installed.
void print_loop(const runner::LoopReport &report, std::ostream &out)
Prints the loop's own figures and notes, which read the same in both modes.
void print_report(const runner::LoopReport &report, std::ostream &out)
Prints the loop report and what closing_complete() does and does not mean.
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.
void print_recovery(const runner::RecoveryReport &recovery, std::ostream &out)
Prints what recovery found, in the words an operator acts on.
Connects a runner to a broker session for live trading.
constexpr std::size_t default_series_capacity
The number of trailing bars per instrument a bound runner keeps by default.
JournaledRunner< Strategy, Session, Journal, Clock > bind_journaled(Session &session, Journal &journal, Strategy strategy, strategy::RiskLimits limits={}, std::size_t series_capacity=default_series_capacity, Venue default_route=Venue::UNSPECIFIED, Clock clock={})
Binds a strategy to a broker session, recording every instruction in a journal before it is sent.
Runs a strategy: the runner, the polling loop, the order journal, and stopping.
constexpr std::string_view poll_kind_name(PollKind kind) noexcept
Returns a poll kind's lowercase name, such as "working_orders".
@ WORKING_ORDERS
Working orders, which reconciliation and cancellation need.
@ HOLDINGS
Holdings, which orders are sized against.
RecoveryReport recover_runner(Runner &runner_instance, const Journal &journal)
Recovers a journal into a runner at startup: marks each instrument left owed, restores a serialized r...
@ QUIESCE
Stop sending new orders; nothing is cancelled or closed. The default, and what a signal means.
@ OPERATOR
An order only an operator may account for (Runner::release_obligations).
@ UNNAMED
An order whose existence is unknown (its reply was lost and no sweep named it).
constexpr std::string_view stop_intent_name(StopIntent intent) noexcept
Returns a lowercase name for an intent, as written in the control file ("withdraw").
What a strategy sees and emits: events, intents, the portfolio, reconciliation and risk limits.
constexpr std::string_view market_phase_name(MarketPhase phase) noexcept
Returns a lowercase name for a phase, such as "pre_open", for logs and display.
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 halt_reason_name(HaltReason reason) noexcept
Returns a lowercase name for a hold, such as "portfolio_stale", for logs.
The library: broker-neutral types (core), and the modules below.
Venue
A trading venue: an exchange, or the combined view across exchanges.
BarInterval
The period one bar covers.
std::string format_iso8601_utc(std::chrono::system_clock::time_point value)
Formats an instant as an ISO 8601 timestamp in UTC, such as 2026-10-06T00:30:00Z.
What a whole PollingLoop::run did, and why it ended.
bool abandoned_
Whether the loop gave up because a poll kept failing.
std::size_t plans_withheld_
Whole plans held back (any WithheldPlanReason); see Runner::plans_withheld.
std::size_t orders_reconcile_withheld_
Orders held back for an instrument of unknown order state; see Runner::orders_reconcile_withheld.
std::size_t iterations_
How many iterations ran.
strategy::MarketPhase final_phase_
The phase the last iteration computed.
std::size_t orders_unconfirmed_timed_out_
Sent orders that timed out unconfirmed; see Runner::set_confirmation_timeout for what each mode then ...
std::optional< PollKind > abandoned_kind_
The poll kind that exhausted its failure budget, if one did.
std::size_t blocked_
How many iterations were blocked from placing (a named cancellation may still have gone).
std::size_t failed(PollKind kind) const noexcept
Returns how many polls of a kind failed.
std::size_t cancels_rejected_
Cancellations refused by the broker or binding; see Runner::cancels_rejected.
std::optional< strategy::Rejection > last_unbuildable_
Why reconciliation could not build the last such instruction in this run, if any.
std::optional< strategy::HaltReason > final_gate_
The most fundamental hold in force when the run ended, whoever took it.
std::size_t orders_guard_refused_
Orders the risk guard refused; see Runner::orders_guard_refused.
bool order_list_read_
Whether the broker's order list was read at least once.
std::optional< BrokerRefusal > last_broker_refusal_
The broker's code and message for the last order it refused during this run, if any.
std::size_t cancels_indeterminate_
Cancellations with an unknown outcome.
std::optional< StopIntent > stopped_by_
The stop intent that ended the run, if one did.
std::size_t orders_indeterminate_
Orders with an unknown outcome (SendOutcome::INDETERMINATE, including most KIS, Toss and Kiwoom refus...
std::size_t cancels_unresolved_
Cancellations naming an order the portfolio does not list.
std::size_t attempted(PollKind kind) const noexcept
Returns how many polls of a kind were attempted.
bool closing_complete() const noexcept
Tests whether every closing instruction the loop asked for was accepted, and the loop knew what to as...
std::size_t cancels_refused_
Cancellations a hold refused.
std::size_t orders_unbuildable_
Orders reconciliation could not build during this run's polling; see Runner::orders_unbuildable.
std::vector< std::string > notes_
Notes for the operator, such as an unrecognized control-file word or a missing policy.
std::size_t orders_binding_refused_
Orders the binding refused to send; see Runner::orders_binding_refused.
std::size_t orders_broker_refused_
Orders the broker refused (SendOutcome::REJECTED); see last_broker_refusal_. At KIS,...
How often a PollingLoop polls each kind; an unset interval disables that poll.
What a journal showed was left owed by the process that wrote it.
bool unattributable_
Whether something was in flight that names no instrument (a torn line too short to name one); recover...
std::vector< InstrumentId > marked_
Instruments marked in the ledger, withheld until the order list resolves them.
bool truncated_
Whether the last line was torn.
std::string detail_
What was found, for the operator.
std::size_t unreadable_lines_
Complete lines that did not parse.
bool clean() const noexcept
Tests whether nothing was owed or damaged.
bool unreadable_file_
Whether the journal exists and could not be read; it may hold orders left in flight.
The limits the operator sets for RiskGuard.
What a run is told, gathered so main states it once and a test can state it differently.
atl::BarInterval bar_interval_
Which bars the source asks for, and how many: the history read at start is this many.
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 closing_wait_
How long a withdraw or flatten may wait for the order list and holdings to account for this runner's ...
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.
int bar_count_
How many bars each bar poll asks for.
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.
std::chrono::nanoseconds owed_order_limit_
How long an order may stay owed with nobody able to account for it (its outcome unknown and no sweep ...
std::size_t max_iterations_
Iterations before returning; 0 runs until a stop.
strategy::RiskLimits limits_
The limits the guard enforces.