algotradelib 0.0.0.9000
C++ header-only library for algorithmic trading
Loading...
Searching...
No Matches
example-order-journal.cpp
1// A runner killed with an order in flight, and the restart that finds out what happened.
2//
3// No broker, no credentials, no network, and no signal raised. The "crash" is a scope that ends
4// without the terminal line ever being written, which is exactly what a `SIGKILL` leaves on disk,
5// and the "restart" is a fresh ledger reading the file its predecessor left behind.
6//
7// The point it demonstrates is the one sentence the whole feature rests on: **an empty ledger does
8// not mean nothing is owed -- it means nothing is remembered.**
9//
10// Run it as:
11//
12// example-order-journal
13
14#include <algotradelib/runner>
15
16#include <chrono>
17#include <cstdio>
18#include <iostream>
19#include <string>
20#include <vector>
21
22namespace {
23
26
35using Clock = std::chrono::system_clock;
36
37InstrumentId samsung() { return InstrumentId{Country::KR, Venue::COMBINED, "005930"}; }
38
40Clock::time_point sent_at() { return Clock::time_point{} + std::chrono::hours(24 * 20000); }
41
42strategy::PlannedOrder buy_ten() {
44 order.instrument_ = samsung();
45 order.side_ = OrderSide::BUY;
46 order.type_ = OrderType::LIMIT;
47 order.quantity_ = Decimal64::parse("10");
48 order.price_ = Decimal64::parse("70000");
49 return order;
50}
51
60template <typename Tag>
61class KilledExecutor {
62public:
63 runner::SendOutcome place(const strategy::PlannedOrder&) {
64 throw std::runtime_error("the process died with the request on the wire");
65 }
66 runner::SendOutcome cancel(const algotradelib::WorkingOrder<Tag>&) {
68 }
69};
70
72void show_journal(const std::string& path) {
73 std::FILE* handle = std::fopen(path.c_str(), "rb");
74 if (handle == nullptr) {
75 std::cout << " (no journal)\n";
76 return;
77 }
78 std::string text;
79 char buffer[1024];
80 while (const auto read = std::fread(buffer, 1, sizeof(buffer), handle)) {
81 text.append(buffer, read);
82 }
83 std::fclose(handle);
84
85 std::size_t begin = 0;
86 while (begin < text.size()) {
87 const auto end = text.find('\n', begin);
88 const auto stop = end == std::string::npos ? text.size() : end;
89 std::cout << " " << text.substr(begin, stop - begin);
90 if (end == std::string::npos) {
91 std::cout << " <- no newline: this write never finished";
92 }
93 std::cout << "\n";
94 if (end == std::string::npos) {
95 break;
96 }
97 begin = end + 1;
98 }
99}
100
102OrderSnapshot listed_order() {
103 OrderSnapshot snapshot;
104 snapshot.order_id_ = "KRX-4412";
105 snapshot.instrument_ = samsung();
106 snapshot.side_ = OrderSide::BUY;
107 snapshot.type_ = OrderType::LIMIT;
108 snapshot.status_ = OrderStatus::PENDING;
109 snapshot.quantity_ = Decimal64::parse("10");
110 snapshot.price_ = Decimal64::parse("70000");
111 snapshot.ordered_at_ = sent_at();
112 return snapshot;
113}
114
115} // namespace
116
117int main() {
118 const std::string path = "example-order-journal.jsonl";
119 std::remove(path.c_str());
120
121 std::cout << "1. A runner sends one order, and is killed before it learns the outcome.\n\n";
122 {
123 runner::FileJournal journal(path, "example:demo-account");
124 if (!journal.open()) {
125 std::cout << "could not open " << path << " for appending: " << journal.rejection() << "\n";
126 return 1;
127 }
130 executor{KilledExecutor<runner::DryTag>{}, journal, runner::ManualClock{sent_at()}};
131 executor.set_id_prefix("run-a");
132 try {
133 executor.place(buy_ten());
134 } catch (const std::exception& error) {
135 std::cout << " " << error.what() << "\n\n";
136 }
137 }
138
139 std::cout << " What the file holds:\n";
140 show_journal(path);
141 std::cout
142 << "\n One line, and it says `pending`. The order may be resting on the exchange or may\n"
143 << " never have arrived. Nothing in this process knows which, and the process is gone.\n\n";
144
145 std::cout << "2. A new process starts. Its ledger is empty.\n\n";
147 std::cout << " Before recovery, may it trade " << samsung().symbol_
148 << "? " << (ledger.requires_reconcile(samsung()) ? "no" : "yes")
149 << " <- and that is the trap\n";
150
151 const auto recovery = runner::recover_into(runner::read_journal_file(path), ledger);
152 std::cout << " After recovery: "
153 << (ledger.requires_reconcile(samsung()) ? "no" : "yes") << "\n";
154 std::cout << " Instruments marked: " << recovery.marked_.size()
155 << ", instructions owed: " << recovery.open_.size() << "\n\n";
156
157 std::cout << "3. The broker's own order list is read, and it is the only thing that resolves.\n\n";
158
159 // Too soon. An accepted write does not appear in a listing instantly, so an absent row this early
160 // is not evidence that the order does not exist.
161 runner::FileJournal journal(path, "example:demo-account");
162 const auto early = runner::reconcile_against(
163 recovery.open_, {}, sent_at() + std::chrono::seconds(5), ledger, journal
164 );
165 std::cout << " Five seconds later, an empty listing: "
166 << runner::match_verdict_name(early.matches_.front().verdict_)
167 << " -- still withheld\n";
168
169 // Later, the row is there. The order existed after all.
170 const auto settled = runner::reconcile_against(
171 recovery.open_, {listed_order()}, sent_at() + std::chrono::minutes(10), ledger, journal
172 );
173 std::cout << " Ten minutes later, the order is listed: "
174 << runner::match_verdict_name(settled.matches_.front().verdict_) << " as "
175 << settled.matches_.front().order_id_ << "\n";
176 std::cout << " May it trade " << samsung().symbol_ << " now? "
177 << (ledger.requires_reconcile(samsung()) ? "no" : "yes") << "\n\n";
178
179 std::cout << "4. The resolution is written back, so a second restart finds nothing owed.\n\n";
180 std::cout << " What the file holds now:\n";
181 show_journal(path);
182
184 const auto again = runner::recover_into(runner::read_journal_file(path), later);
185 std::cout << "\n A third process recovers: instructions owed: " << again.open_.size()
186 << ", may it trade " << samsung().symbol_ << "? "
187 << (later.requires_reconcile(samsung()) ? "no" : "yes") << "\n\n";
188
189 std::cout
190 << "Had the listing stayed empty past the window, the verdict would have been `not_placed`:\n"
191 << "no order found, and the ledger would have cleared just the same. A serialized runner goes\n"
192 << "on owing such an order all the same, since a list can lag an order it accepted. Had two\n"
193 << "rows matched, it would have been `ambiguous`, nothing would clear, and an operator would\n"
194 << "decide. The one thing that never happens is a guess.\n";
195
196 std::remove(path.c_str());
197 return 0;
198}
An exact signed decimal: an int64 coefficient times 10^-scale.
Definition types.hpp:297
The order journal on disk: one JSON object per line, appended and flushed to disk (fsync) before appe...
The instruments with an order of unknown outcome, where nothing may be placed until the broker's orde...
bool requires_reconcile(const InstrumentId &id) const
Tests whether an instrument is waiting for reconciliation.
An executor wrapper that records each instruction in a journal before sending it.
Definition journal.hpp:695
void set_id_prefix(std::string prefix)
Sets the prefix of line ids.
Definition journal.hpp:721
SendOutcome place(const strategy::PlannedOrder &order)
Records and places one order.
Definition journal.hpp:761
A clock for tests: wait_for advances it instead of sleeping.
Definition clock.hpp:96
Runs a strategy: the runner, the polling loop, the order journal, and stopping.
Definition clock.hpp:7
JournalReplay read_journal_file(const std::string &path)
Reads and parses a journal file.
SendOutcome
What became of one instruction (a placement or a cancellation) an executor was asked to send.
Definition execution.hpp:30
@ ACCEPTED
The broker accepted the instruction (for a placement, the order exists).
Definition execution.hpp:32
RecoveryReport recover_into(const JournalReplay &replay, IndeterminacyLedger &ledger)
Marks the ledger for every instrument a journal left an instruction of unknown outcome for.
ReconcileReport reconcile_against(const std::vector< OpenInstruction > &open, const std::vector< OrderSnapshot > &rows, const JournalReplay &replay, std::chrono::system_clock::time_point now, IndeterminacyLedger &ledger, Journal &journal, const MatchWindow &window={}, std::string_view unplaced_note={})
Resolves what a broker listing settles, records it in the journal, and clears the ledger for each ins...
constexpr std::string_view match_verdict_name(MatchVerdict verdict) noexcept
Returns a verdict's lowercase name, such as "not_placed", for logs.
void stop(S &strategy, Ctx &context)
Calls strategy.on_stop(context) if the strategy declares on_stop; otherwise does nothing.
Definition concept.hpp:168
What a strategy sees and emits: events, intents, the portfolio, reconciliation and risk limits.
Definition concept.hpp:16
OrderStatus
Where an order stands in its lifecycle.
Definition types.hpp:1879
OrderType
How an order's price is decided (its price basis).
Definition types.hpp:1707
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
OrderSide
The side of an order: its economic direction, buy or sell.
Definition types.hpp:1683
Identifies an instrument: its country, venue and symbol.
Definition types.hpp:78
The state of one order, as a broker listing reported it.
Definition types.hpp:2008
One order the runner is to place.
OrderType type_
The order type.
InstrumentId instrument_
The instrument to trade.
std::optional< Decimal64 > price_
The limit price, after clamping and rounding; empty for a market order.
Decimal64 quantity_
The quantity, after rounding onto the lot.