algotradelib 0.0.0.9000
C++ header-only library for algorithmic trading
Loading...
Searching...
No Matches
example-live-loop.cpp
Go to the documentation of this file.
1
18
19#include <algotradelib/runner>
20#include <algotradelib/vanilla>
21
22#include <chrono>
23#include <iostream>
24#include <string>
25#include <vector>
26
27using namespace algotradelib;
28
31
32namespace {
33
34using Clock = std::chrono::system_clock;
35
36InstrumentId samsung() { return InstrumentId{Country::KR, Venue::COMBINED, "005930"}; }
37
39Instrument samsung_reference() {
40 Instrument reference;
41 reference.id_ = samsung();
42 reference.type_ = SecurityType::STOCK;
43 reference.currency_ = Currency{"KRW"};
44 reference.lot_size_ = Decimal64::parse("1");
46 return reference;
47}
48
50struct CountBars {
51 std::size_t bars_{0};
52
53 template <class Ctx>
54 void on_bar(Ctx& ctx, const strategy::BarEvent&) {
55 ++bars_;
56 ctx.log().note("saw bar " + std::to_string(bars_));
57 }
58};
59
60using LoopRunner = runner::DryRunner<CountBars>;
61
84class ScriptedSource {
85public:
86 explicit ScriptedSource(int bars) : remaining_(bars) {}
87
88 runner::PollOutcome poll(runner::PollKind kind, LoopRunner& live_runner) {
90 // Today's permitted band, which reconciliation clamps a limit price into. A real source calls
91 // `session.price_limits(instrument)` per instrument here; what matters is that it lands on the
92 // runner rather than being fed as an event, and that it arrives **on a cadence** -- the band
93 // is a per-trading-day value, so one read at startup describes the day the process started on.
94 PriceLimits band;
95 band.currency_ = Currency{"KRW"};
96 band.upper_ = Decimal64::parse("91000");
97 band.lower_ = Decimal64::parse("49000");
98 live_runner.set_price_limits(samsung(), band);
100 }
101 if (kind != runner::PollKind::BARS) {
103 }
104 if (remaining_ <= 0) {
105 // Nothing new is the normal case between closes, and is not a failure.
107 }
108 --remaining_;
109
110 Bar bar;
111 bar.timestamp_ = Clock::time_point{} + std::chrono::hours(24 * (200 + fed_));
112 bar.close_ = Decimal64::parse(std::to_string(70000 + fed_ * 500));
113 bar.high_ = bar.close_;
114 bar.low_ = bar.close_;
115 bar.volume_ = Decimal64::parse("1000000");
116 ++fed_;
117
118 live_runner.context().set_last_price(samsung(), bar.close_);
119 live_runner.feed(
120 strategy::make_bar_event(samsung(), BarInterval::ONE_DAY, BarStamp::CLOSE, bar)
121 );
123 }
124
125 runner::PollOutcome read_order_list(LoopRunner&) {
126 ++reads_;
127 // A real source calls working_orders() or order_history() here. Returning a failure until the
128 // second attempt shows the gate doing its job.
130 }
131
132 int reads() const noexcept { return reads_; }
133
134private:
135 int remaining_;
136 int fed_{0};
137 int reads_{0};
138};
139
141void describe(const runner::LoopReport& report) {
142 std::cout << "iterations : " << report.iterations_ << '\n'
143 << "bar polls : " << report.attempted(runner::PollKind::BARS)
144 << " (" << report.failed(runner::PollKind::BARS) << " failed)\n"
145 << "blocked : " << report.blocked_ << " iteration(s)\n"
146 << "order list read : " << (report.order_list_read_ ? "yes" : "no") << '\n'
147 << "stopped by : "
148 << (report.stopped_by_ ?
150 std::string_view("nothing"))
151 << '\n'
152 << "final gate : "
153 << (report.final_gate_ ?
155 std::string_view("none"))
156 << '\n'
157 // The phase the loop derived for itself, and how many changes it announced. Reported
158 // because it decides most of what could be sent: `unknown` beside a default
159 // `permitted_phases_` means every placement that would open a position was refused, and
160 // an empty placement count on its own does not say that.
161 << "market phase : " << strategy::market_phase_name(report.final_phase_)
162 << " (" << report.phase_transitions_ << " transition(s))\n"
163 // And how many orders went out under a phase the limits do not permit. An order closing
164 // a held position is permitted in any phase -- which is what lets a flatten after the
165 // close reach the broker instead of being refused by the operator's own limit -- so this
166 // is the line that says it happened. Non-zero is not a fault; a nightly flatten is one.
167 << "sent off-phase : " << report.sends_outside_permitted_phase_ << '\n'
168 // And what the price-band policy decided. A band is a per-trading-day value, so a run
169 // that outlives the day it read one has to decide what to do with it: this library does
170 // not apply a band whose age nobody has bounded, and says so here rather than repricing
171 // an order against yesterday's ceiling in silence. Both stay zero in this example
172 // because `CountBars` places nothing -- the first counts plans, the second counts sent
173 // orders a broker refused after a band was withheld.
174 << "bands withheld : " << report.price_bands_withheld_ << '\n'
175 << "refused unclamped: " << report.unclamped_orders_rejected_ << '\n';
176 for (const auto& note : report.notes_) {
177 std::cout << "note : " << note << '\n';
178 }
179}
180
187Clock::time_point start_time() {
188 return Clock::time_point{} + std::chrono::hours(24 * 200);
189}
190
191} // namespace
192
197int main() {
198 std::cout << "a live loop with no broker attached\n\n";
199
200 LoopRunner live_runner;
201 live_runner.context().set_instrument(samsung_reference());
202
203 // **The phase is the loop's, not this program's.** The loop derives it from its session window and
204 // the trading calendar, and overwrites anything set here; a fixed `set_phase(MarketPhase::OPEN)`
205 // would trade as though the market never closed, and `on_phase` would never fire.
206 //
207 // A real program gets that calendar from `PollKind::CALENDAR`, which is on by default. This example
208 // has no broker, so its source answers no calendar; the date is therefore uncovered, the phase is
209 // `UNKNOWN`, and the report says so in as many words. Seeding one here is what a run with a broker
210 // gets from its first calendar poll.
212 calendar.country_ = algotradelib::Country::KR;
213 calendar.received_at_ = start_time();
214 calendar.days_.push_back(algotradelib::TradingDay{
215 strategy::SessionWindow::krx().local_date(start_time()), true});
216 live_runner.context().set_calendar(calendar);
217
218 ScriptedSource source(4);
219
220 runner::PollSchedule schedule;
221 schedule.bars_ = std::chrono::minutes(1);
222 schedule.tick_ = std::chrono::minutes(1);
223 // No broker here, so nothing could answer a calendar request; a real program leaves this at its
224 // six-hour default.
225 schedule.calendar_.reset();
226 // **The band, on a cadence rather than once.** A band registered once at startup would clamp every
227 // limit order for as long as the process ran. Ten minutes here is arbitrary; the real constraint is
228 // that it be shorter than the session, because the value changes with the previous close.
229 schedule.price_limits_ = std::chrono::minutes(10);
230
232 live_runner, source, schedule, runner::ManualClock{start_time()});
233 // **Two statements every placement-enabled run owes, and this source cannot satisfy either.**
234 //
235 // A run states how old a holdings snapshot may be before it stops sizing against it, and a run
236 // that has never read one holds rather than treating an empty account as a current one -- an
237 // empty portfolio is the *permissive* answer to every ceiling and to every target subtraction, so a
238 // run that never thought about holdings would trade as though it held nothing.
239 //
240 // This example has no broker, so it reads no holdings ever. Saying so explicitly is what an
241 // operator in the same position does; leaving them out would hold this loop for the whole run.
243 loop.allow_absent_portfolio();
244 // **The third statement, and the same shape as the first.** A run states how old a band may be, or
245 // that it accepts any age; with neither, no band is applied. Twenty
246 // minutes against a ten-minute cadence, so one failed poll is tolerated and a run of failures is
247 // not -- which is what the policy is actually bounding, since a failed poll deliberately leaves
248 // the last good band in place.
249 loop.set_maximum_price_limits_age(std::chrono::minutes(20));
250
252 const auto report = loop.run(stop, 6);
253
254 describe(report);
255
256 std::cout << "\nthe strategy saw " << live_runner.strategy().bars_ << " bar(s)\n";
257
258 // The gate refused the first iteration because the order list had not been read. That is the
259 // cheapest half of reconciliation and it needs no persistence: a process that has just started
260 // cannot know whether a predecessor died mid-POST, and an empty ledger does not mean nothing is
261 // owed -- it means nothing is remembered.
262 std::cout << "the order list was read " << source.reads() << " time(s) before the gate opened\n";
263
264 std::cout << "\nhow a real operator would stop this:\n"
265 << " runner::SignalStop::install(stop); // SIGINT and SIGTERM quiesce; SIGHUP ignored\n"
266 << " loop.set_control_file(runner::FileStopChannel(\"/var/run/atl/stop\"));\n"
267 << " echo withdraw > /var/run/atl/stop // or quiesce, or flatten\n\n"
268 << "and the trap worth knowing: a supervisor's grace period must exceed one loop\n"
269 << "iteration plus one request timeout, or its SIGKILL turns the cleanest stop into\n"
270 << "the worst one -- a process killed mid-request, with an order whose fate is unknown.\n";
271
272 return 0;
273}
static Decimal64 parse(std::string_view input)
Parses a decimal string exactly (no binary floating point).
Definition types.hpp:323
A clock for tests: wait_for advances it instead of sleeping.
Definition clock.hpp:96
Runs a runner for a live session: polls a source on a schedule, gates placements, derives the market ...
Definition loop.hpp:976
void allow_unbounded_portfolio_age()
Accepts holdings of any age, instead of stating a maximum.
Definition loop.hpp:1182
A stop request the loop polls; safe to set from a signal handler.
Definition stop.hpp:158
Runs a strategy: the runner, the polling loop, the order journal, and stopping.
Definition clock.hpp:7
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
PollKind
What a PollingLoop polls for; each has a cadence in PollSchedule.
Definition loop.hpp:30
@ BARS
Completed bars.
Definition loop.hpp:32
@ PRICE_LIMITS
Today's price bands, which an aggressive limit price is clamped to; polled second,...
Definition loop.hpp:58
PollOutcome
What became of one poll.
Definition loop.hpp:117
@ UNCHANGED
The poll succeeded and there was nothing new.
Definition loop.hpp:121
@ FAILED
The poll failed; counted toward the failure budget.
Definition loop.hpp:123
@ UPDATED
New data arrived and was fed to the runner, or installed in it (a calendar, price bands).
Definition loop.hpp:119
constexpr std::string_view stop_intent_name(StopIntent intent) noexcept
Returns a lowercase name for an intent, as written in the control file ("withdraw").
Definition stop.hpp:61
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
What a strategy sees and emits: events, intents, the portfolio, reconciliation and risk limits.
Definition concept.hpp:16
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
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.
Definition event.hpp:57
constexpr std::string_view halt_reason_name(HaltReason reason) noexcept
Returns a lowercase name for a hold, such as "portfolio_stale", for logs.
Definition risk.hpp:144
The library: broker-neutral types (core), and the modules below.
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.
std::optional< Decimal64 > lower_
The lowest price allowed today; empty means the same as for upper_.
std::optional< Decimal64 > upper_
The highest price allowed today.
Currency currency_
The currency of the bounds.
static PriceRule krx_equity()
Returns the KRX cash-equity tick table (January 2023 revision).
The trading days a broker reported for one market, ascending by date.
Definition calendar.hpp:44
std::chrono::system_clock::time_point received_at_
When the calendar was received, on this machine's clock.
Definition calendar.hpp:50
std::vector< TradingDay > days_
The days the broker reported, ascending. A date outside them is uncovered, not closed.
Definition calendar.hpp:48
Country country_
The market this calendar is for.
Definition calendar.hpp:46
One calendar date and whether the market opens on it.
Definition calendar.hpp:19
std::size_t iterations_
How many iterations ran.
Definition loop.hpp:581
std::size_t blocked_
How many iterations were blocked from placing (a named cancellation may still have gone).
Definition loop.hpp:589
std::size_t failed(PollKind kind) const noexcept
Returns how many polls of a kind failed.
Definition loop.hpp:789
std::optional< strategy::HaltReason > final_gate_
The most fundamental hold in force when the run ended, whoever took it.
Definition loop.hpp:593
bool order_list_read_
Whether the broker's order list was read at least once.
Definition loop.hpp:666
std::optional< StopIntent > stopped_by_
The stop intent that ended the run, if one did.
Definition loop.hpp:591
std::size_t attempted(PollKind kind) const noexcept
Returns how many polls of a kind were attempted.
Definition loop.hpp:780
std::vector< std::string > notes_
Notes for the operator, such as an unrecognized control-file word or a missing policy.
Definition loop.hpp:773
How often a PollingLoop polls each kind; an unset interval disables that poll.
Definition loop.hpp:169
std::optional< std::chrono::nanoseconds > calendar_
How often to read the trading calendar; six hours by default.
Definition loop.hpp:203
std::chrono::nanoseconds tick_
The loop's step, which bounds how quickly a stop is noticed; not a poll.
Definition loop.hpp:194
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
Date local_date(std::chrono::system_clock::time_point now) const
Returns the market's local date at an instant.
Definition event.hpp:162
static SessionWindow krx()
Returns the KRX regular session, 09:00-15:30 KST.
Definition event.hpp:120