algotradelib 0.0.0.9000
C++ header-only library for algorithmic trading
Loading...
Searching...
No Matches
loop.hpp
1#pragma once
2
3#include "./clock.hpp"
4#include "./execution.hpp"
5#include "./stop.hpp"
6
7#include <algotradelib/core>
8#include <algotradelib/strategy>
9
10#include <algorithm>
11#include <array>
12#include <chrono>
13#include <cstddef>
14#include <optional>
15#include <string>
16#include <string_view>
17#include <type_traits>
18#include <utility>
19#include <vector>
20
21namespace algotradelib {
22namespace runner {
23
60
66inline constexpr std::string_view poll_kind_name(PollKind kind) noexcept {
67 switch (kind) {
69 return "calendar";
70 case PollKind::BARS:
71 return "bars";
73 return "quotes";
75 return "holdings";
77 return "working_orders";
79 return "buying_power";
81 return "price_limits";
82 }
83 return "";
84}
85
87inline constexpr PollKind all_poll_kinds[] = {
88 // First: the phase derived from it decides whether anything else in this iteration may be sent.
90 // Second, before every poll that can feed, so this iteration's orders use this iteration's bands.
92 // The listing, then holdings, before anything that feeds. A fill between the two reads then
93 // over-counts the position (harmless) rather than under-counting it (which would buy again).
96 // After holdings, so money and positions are equally fresh.
100};
101
102static_assert(
103 lists_every_value(all_poll_kinds, [](PollKind kind) { return !poll_kind_name(kind).empty(); }),
104 "every PollKind must appear in all_poll_kinds once: a kind left out is never polled, and the loop "
105 "would silently answer with whatever the previous poll left behind"
106);
107
109inline constexpr std::size_t poll_kind_count = std::size(all_poll_kinds);
110
127
129inline constexpr std::array<PollOutcome, 4> all_poll_outcomes{
134};
135
141inline constexpr std::string_view poll_outcome_name(PollOutcome outcome) noexcept {
142 switch (outcome) {
144 return "updated";
146 return "unchanged";
148 return "failed";
150 return "rate_limited";
151 }
152 return "";
153}
154
155static_assert(
156 lists_every_value(all_poll_outcomes, [](PollOutcome outcome) { return !poll_outcome_name(outcome).empty(); }),
157 "all_poll_outcomes must list every PollOutcome"
158);
159
171 std::optional<std::chrono::nanoseconds> bars_;
173 std::optional<std::chrono::nanoseconds> quotes_;
175 std::optional<std::chrono::nanoseconds> holdings_;
185 std::optional<std::chrono::nanoseconds> working_orders_;
192 std::optional<std::chrono::nanoseconds> buying_power_;
194 std::chrono::nanoseconds tick_{std::chrono::seconds(1)};
203 std::optional<std::chrono::nanoseconds> calendar_{std::chrono::hours(6)};
210 std::optional<std::chrono::nanoseconds> price_limits_;
216 std::optional<std::chrono::nanoseconds> interval_for(PollKind kind) const noexcept {
217 switch (kind) {
219 return calendar_;
220 case PollKind::BARS:
221 return bars_;
222 case PollKind::QUOTES:
223 return quotes_;
225 return holdings_;
227 return working_orders_;
229 return buying_power_;
231 return price_limits_;
232 }
233 return std::nullopt;
234 }
235};
236
245template <typename Source, typename = void>
246struct source_provides_working_orders : std::false_type {};
247
248template <typename Source>
250 Source, std::void_t<decltype(Source::provides_working_orders)>
251> : std::bool_constant<Source::provides_working_orders> {};
252
263template <typename Runner, typename = void>
264struct runner_keeps_sent_orders : std::true_type {};
265
266template <typename Runner>
268 Runner, std::void_t<decltype(Runner::keeps_sent_orders_until_listed)>
269> : std::bool_constant<Runner::keeps_sent_orders_until_listed> {};
270
275template <typename Runner, typename = void>
276struct runner_counts_kept_drops : std::false_type {};
277
278template <typename Runner>
280 Runner, std::void_t<decltype(std::declval<const Runner&>().kept_intents_dropped())>
281> : std::true_type {};
282
287template <typename Runner, typename = void>
288struct runner_counts_warmup : std::false_type {};
289
290template <typename Runner>
292 Runner, std::void_t<decltype(std::declval<const Runner&>().warmup_instructions_discarded())>
293> : std::true_type {};
294
309 struct StatedTotal {
313 std::size_t statements_{0};
314 };
315
317 std::vector<StatedTotal> commission_;
319 std::vector<StatedTotal> tax_;
321 std::size_t orders_stating_cost_{0};
326 std::size_t orders_stating_none_{0};
341 std::size_t unrepresentable_sums_{0};
342
348 std::optional<Decimal64> commission_in(const Currency& currency) const {
349 return figure_in(commission_, currency);
350 }
351
357 std::optional<Decimal64> tax_in(const Currency& currency) const {
358 return figure_in(tax_, currency);
359 }
360
366 std::size_t commission_statements_in(const Currency& currency) const {
367 return statements_in(commission_, currency);
368 }
369
375 std::size_t tax_statements_in(const Currency& currency) const {
376 return statements_in(tax_, currency);
377 }
378
386 bool add(std::vector<StatedTotal>& totals, const Money& amount) {
387 for (auto& held : totals) {
388 if (held.amount_.currency_ == amount.currency_) {
389 const auto sum = decimal_add(held.amount_.amount_, amount.amount_);
390 if (!sum) {
392 return false;
393 }
394 held.amount_.amount_ = *sum;
395 ++held.statements_;
396 return true;
397 }
398 }
399 totals.push_back(StatedTotal{amount, 1});
400 return true;
401 }
402
409 static std::optional<Decimal64> figure_in(
410 const std::vector<StatedTotal>& totals,
411 const Currency& currency
412 ) {
413 for (const auto& held : totals) {
414 if (held.amount_.currency_ == currency) {
415 return held.amount_.amount_;
416 }
417 }
418 return std::nullopt;
419 }
420
427 static std::size_t statements_in(
428 const std::vector<StatedTotal>& totals,
429 const Currency& currency
430 ) {
431 for (const auto& held : totals) {
432 if (held.amount_.currency_ == currency) {
433 return held.statements_;
434 }
435 }
436 return 0;
437 }
438
448 ChargedCosts since(const ChargedCosts& before) const {
449 ChargedCosts delta;
450 std::size_t refused = 0;
451 const auto difference = [&refused](
452 const std::vector<StatedTotal>& now,
453 const std::vector<StatedTotal>& then
454 ) {
455 std::vector<StatedTotal> out;
456 for (const auto& held : now) {
457 auto earlier = Decimal64::parse("0");
458 std::size_t before_statements = 0;
459 for (const auto& was : then) {
460 if (was.amount_.currency_ == held.amount_.currency_) {
461 earlier = was.amount_.amount_;
462 before_statements = was.statements_;
463 break;
464 }
465 }
466 // Whether anything was stated is the statement count, not the money.
467 if (held.statements_ == before_statements) {
468 continue;
469 }
470 const auto moved = decimal_subtract(held.amount_.amount_, earlier);
471 if (!moved) {
472 // Counted, not skipped: something was stated and its amount cannot be represented.
473 ++refused;
474 continue;
475 }
476 out.push_back(
477 StatedTotal{Money{*moved, held.amount_.currency_}, held.statements_ - before_statements}
478 );
479 }
480 return out;
481 };
482 delta.commission_ = difference(commission_, before.commission_);
483 delta.tax_ = difference(tax_, before.tax_);
490 (unrepresentable_sums_ - before.unrepresentable_sums_) + refused;
491 return delta;
492 }
493};
494
500template <typename Source, typename = void>
501struct source_states_charged_costs : std::false_type {};
502
503template <typename Source>
505 Source, std::void_t<decltype(std::declval<const Source&>().charged_costs())>
506> : std::true_type {};
507
517template <typename Source, typename = void>
518struct source_names_buying_power_currencies : std::false_type {};
519
520template <typename Source>
522 Source, std::void_t<decltype(std::declval<const Source&>().buying_power_currencies())>
523> : std::true_type {};
524
528 std::size_t consecutive_limit_{5};
529};
530
534 std::chrono::system_clock::time_point at_{};
536 std::vector<std::pair<PollKind, PollOutcome>> polls_;
543 std::optional<strategy::HaltReason> gate_;
545 bool fed_{false};
554 bool phase_changed_{false};
555
560 bool updated() const noexcept {
561 for (const auto& entry : polls_) {
562 if (entry.second == PollOutcome::UPDATED) {
563 return true;
564 }
565 }
566 return false;
567 }
568};
569
581 std::size_t iterations_{0};
585 std::size_t attempted_[poll_kind_count] = {};
587 std::size_t failed_[poll_kind_count] = {};
589 std::size_t blocked_{0};
591 std::optional<StopIntent> stopped_by_;
593 std::optional<strategy::HaltReason> final_gate_;
608 std::size_t price_bands_withheld_{0};
614 std::size_t orders_unbuildable_{0};
618 std::size_t orders_guard_refused_{0};
631 std::size_t orders_indeterminate_{0};
639 std::size_t plans_withheld_{0};
643 std::size_t cancels_rejected_{0};
645 std::size_t cancels_refused_{0};
647 std::size_t cancels_unresolved_{0};
651 std::optional<BrokerRefusal> last_broker_refusal_;
653 std::optional<strategy::Rejection> last_unbuildable_;
655 std::size_t phase_transitions_{0};
662 bool abandoned_{false};
664 std::optional<PollKind> abandoned_kind_;
666 bool order_list_read_{false};
668 std::size_t withdrawn_{0};
670 std::size_t flattened_{0};
672 std::size_t closing_placed_{0};
674 std::size_t closing_rejected_{0};
676 std::size_t closing_refused_{0};
681 std::size_t closing_withheld_{0};
685 std::size_t closing_unavailable_{0};
687 std::size_t closing_unresolved_{0};
693 std::size_t closing_unconfirmed_{0};
699 std::chrono::nanoseconds closing_waited_{0};
712 std::optional<Obligation> owed_stop_;
713
723 std::size_t closing_contended_{0};
724
730 std::size_t closing_unsizable_{0};
731
747
767
768 std::size_t stop_handler_refused_{0};
770 std::size_t stop_handler_sent_{0};
771
773 std::vector<std::string> notes_;
774
780 std::size_t attempted(PollKind kind) const noexcept {
781 return attempted_[static_cast<int>(kind)];
782 }
783
789 std::size_t failed(PollKind kind) const noexcept { return failed_[static_cast<int>(kind)]; }
790};
791
800template <typename Runner>
802public:
804 static constexpr bool provides_working_orders = true;
805
811 void queue(PollKind kind, PollOutcome outcome) {
812 queued_[static_cast<int>(kind)].push_back(outcome);
813 }
814
820 void set_default(PollKind kind, PollOutcome outcome) {
821 defaults_[static_cast<int>(kind)] = outcome;
822 }
823
828 void set_order_list(PollOutcome outcome) { order_list_ = outcome; }
829
834 void set_buying_power_currencies(std::vector<Currency> currencies) {
835 currencies_ = std::move(currencies);
836 }
837
844 calendar_ = std::move(calendar);
845 defaults_[static_cast<int>(PollKind::CALENDAR)] = PollOutcome::UPDATED;
846 }
847
852 const std::vector<Currency>& buying_power_currencies() const noexcept { return currencies_; }
853
858 std::size_t order_list_reads() const noexcept { return order_list_reads_; }
859
865 std::size_t polls(PollKind kind) const noexcept { return polls_[static_cast<int>(kind)]; }
866
873 void set_price_limits(InstrumentId instrument, PriceLimits limits) {
874 price_limits_.emplace_back(std::move(instrument), std::move(limits));
875 defaults_[static_cast<int>(PollKind::PRICE_LIMITS)] = PollOutcome::UPDATED;
876 }
877
885 PollOutcome poll(PollKind kind, Runner& runner_instance) {
886 const int at = static_cast<int>(kind);
887 ++polls_[at];
888 auto outcome = defaults_[at];
889 if (!queued_[at].empty()) {
890 outcome = queued_[at].front();
891 queued_[at].erase(queued_[at].begin());
892 }
893 // The one kind whose answer goes somewhere: the loop reads the calendar back out of the context,
894 // so a source that did not put it there would leave every phase assertion in this header testing
895 // an empty calendar.
896 if (kind == PollKind::CALENDAR && calendar_ && outcome == PollOutcome::UPDATED) {
897 runner_instance.context().set_calendar(*calendar_);
898 }
899 // The second kind whose answer goes somewhere -- onto the runner rather than into the context,
900 // which is where reconciliation reads a band from. Installed on every successful poll, as a real
901 // source does, so a test can age one by letting the loop's clock run.
902 if (kind == PollKind::PRICE_LIMITS && outcome == PollOutcome::UPDATED) {
903 for (const auto& band : price_limits_) {
904 runner_instance.set_price_limits(band.first, band.second);
905 }
906 }
907 return outcome;
908 }
909
915 ++order_list_reads_;
916 return order_list_;
917 }
918
919private:
920 // Every kind's default outcome, filled rather than listed.
921 //
922 // It used to be a list with one entry per `PollKind`, and `PollOutcome::UPDATED` is the zero value
923 // -- so adding an enumerator silently defaulted the new kind to "new data arrived" in every test
924 // that had not set it. Filling cannot be one short.
925 static std::array<PollOutcome, poll_kind_count> unchanged_defaults() {
926 std::array<PollOutcome, poll_kind_count> filled{};
927 filled.fill(PollOutcome::UNCHANGED);
928 return filled;
929 }
930
931 std::vector<PollOutcome> queued_[poll_kind_count];
932 std::array<PollOutcome, poll_kind_count> defaults_{unchanged_defaults()};
934 std::vector<Currency> currencies_;
935 std::optional<TradingCalendar> calendar_;
936 std::vector<std::pair<InstrumentId, PriceLimits>> price_limits_;
937 std::size_t polls_[poll_kind_count] = {};
938 std::size_t order_list_reads_{0};
939};
940
975template <typename Runner, typename Source, typename Clock = SteadyClock>
977public:
985 PollingLoop(Runner& runner, Source& source, PollSchedule schedule = {}, Clock clock = {})
986 : runner_(&runner), source_(&source), schedule_(std::move(schedule)), clock_(std::move(clock)) {}
987
992 const Clock& clock() const noexcept { return clock_; }
997 Clock& clock() noexcept { return clock_; }
998
1003 const PollSchedule& schedule() const noexcept { return schedule_; }
1004
1005private:
1006 // Every monotone instruction-outcome figure the runner holds, read at one instant.
1007 //
1008 // Twelve of them, read together rather than one call at a time, because they are differenced
1009 // against each other's snapshot: a set taken at two instants describes one interval, and twelve
1010 // separately captured values do not.
1011 struct InstructionOutcomes {
1012 std::size_t unbuildable_{0};
1013 std::size_t unconfirmed_timed_out_{0};
1014 std::size_t reconcile_withheld_{0};
1015 std::size_t guard_refused_{0};
1016 std::size_t binding_refused_{0};
1017 std::size_t broker_refused_{0};
1018 std::size_t indeterminate_{0};
1019 std::size_t cancels_rejected_{0};
1020 std::size_t cancels_refused_{0};
1021 std::size_t cancels_unresolved_{0};
1022 std::size_t cancels_indeterminate_{0};
1023 std::size_t plans_withheld_{0};
1024 std::size_t instructions_unavailable_{0};
1025 };
1026
1027 // Reads them all from the runner.
1028 InstructionOutcomes instruction_outcomes() const {
1029 InstructionOutcomes at;
1030 at.unbuildable_ = runner_->orders_unbuildable();
1031 at.unconfirmed_timed_out_ = runner_->orders_unconfirmed_timed_out();
1032 at.reconcile_withheld_ = runner_->orders_reconcile_withheld();
1033 at.guard_refused_ = runner_->orders_guard_refused();
1034 at.binding_refused_ = runner_->orders_binding_refused();
1035 at.broker_refused_ = runner_->orders_broker_refused();
1036 at.indeterminate_ = runner_->orders_indeterminate();
1037 at.cancels_rejected_ = runner_->cancels_rejected();
1038 at.cancels_refused_ = runner_->cancels_refused();
1039 at.cancels_unresolved_ = runner_->cancels_unresolved();
1040 at.cancels_indeterminate_ = runner_->cancels_indeterminate();
1041 at.plans_withheld_ = runner_->plans_withheld();
1042 at.instructions_unavailable_ = runner_->instructions_unavailable();
1043 return at;
1044 }
1045
1046 // Writes the difference between two snapshots onto `report`.
1047 static void note_instruction_outcomes(
1048 LoopReport& report,
1049 const InstructionOutcomes& before,
1050 const InstructionOutcomes& after
1051 ) {
1052 report.orders_unbuildable_ = after.unbuildable_ - before.unbuildable_;
1053 report.orders_unconfirmed_timed_out_ = after.unconfirmed_timed_out_ - before.unconfirmed_timed_out_;
1054 report.orders_reconcile_withheld_ = after.reconcile_withheld_ - before.reconcile_withheld_;
1055 report.orders_guard_refused_ = after.guard_refused_ - before.guard_refused_;
1056 report.orders_binding_refused_ = after.binding_refused_ - before.binding_refused_;
1057 report.orders_broker_refused_ = after.broker_refused_ - before.broker_refused_;
1058 report.orders_indeterminate_ = after.indeterminate_ - before.indeterminate_;
1059 report.cancels_rejected_ = after.cancels_rejected_ - before.cancels_rejected_;
1060 report.cancels_refused_ = after.cancels_refused_ - before.cancels_refused_;
1061 report.cancels_unresolved_ = after.cancels_unresolved_ - before.cancels_unresolved_;
1062 report.cancels_indeterminate_ = after.cancels_indeterminate_ - before.cancels_indeterminate_;
1063 report.plans_withheld_ = after.plans_withheld_ - before.plans_withheld_;
1064 report.instructions_unavailable_ = after.instructions_unavailable_ - before.instructions_unavailable_;
1065 }
1066
1067public:
1068
1079 void set_maximum_portfolio_age(std::chrono::nanoseconds age) {
1080 maximum_portfolio_age_ = age;
1081 // Pushed down as the **policy**, not as a verdict. The runner re-evaluates it against whatever
1082 // snapshot is in hand when it settles, which is the only version that cannot describe a
1083 // portfolio that has since been replaced -- a source callback can feed, and settle, between this
1084 // loop's own checks.
1085 runner_->set_maximum_portfolio_age(age);
1086 }
1087
1118 void set_closing_wait(std::chrono::nanoseconds limit) { closing_wait_ = limit; }
1119
1170 void set_owed_order_limit(std::chrono::nanoseconds limit) { owed_order_limit_ = limit; }
1171
1183 allow_unbounded_portfolio_age_ = true;
1184 maximum_portfolio_age_.reset();
1185 runner_->set_maximum_portfolio_age(std::nullopt);
1186 }
1187
1193 void set_maximum_price_limits_age(std::optional<std::chrono::nanoseconds> age) {
1194 runner_->set_maximum_price_limits_age(age);
1195 }
1196
1199 runner_->allow_unbounded_price_limits_age();
1200 }
1201
1210 allow_absent_portfolio_ = true;
1211 // Pushed down for the same reason the age policy is: this loop's hold is not in force at every
1212 // point an instruction can be settled, so the runner enforces the absence rule itself and an
1213 // opt-out that stopped at the hold would not actually opt out of anything.
1214 runner_->allow_absent_portfolio();
1215 }
1216
1222 session_window_ = std::move(window);
1223 }
1224
1229 const strategy::SessionWindow& session_window() const noexcept { return session_window_; }
1230
1235 void set_failure_policy(FailurePolicy policy) { failure_policy_ = std::move(policy); }
1236
1241 void set_control_file(FileStopChannel channel) { control_ = std::move(channel); }
1242
1254 LoopReport run(StopSource& stop, std::size_t max_iterations = 0) {
1255 LoopReport report;
1256 // Carried in rather than left false: the field says whether the order list has been read at
1257 // least once, and the loop remembers that across runs. A second `run` on the same loop had been
1258 // reporting it as unread, which reads as a startup gate that never opened.
1259 report.order_list_read_ = order_list_read_;
1260 // **Differenced around the run, not accumulated from the passes.** An order can leave the
1261 // process from inside a source's `poll`, which calls `Runner::feed` itself and discards the
1262 // report it gets back, so a per-pass figure would only ever see what a stop did. The runner's
1263 // counter is monotonic for exactly this reason, and reading it at both ends is how
1264 // `RiskGuard::total_sent` is already used around a callback. Taken per `run` so a second run on
1265 // the same loop reports its own sends rather than the first one's as well.
1266 const auto outside_phase_before = runner_->sends_outside_permitted_phase();
1267 // Differenced for the reason above: both are monotone on the runner, which outlives this call.
1268 const auto bands_withheld_before = runner_->price_bands_withheld();
1269 const auto unclamped_rejected_before = runner_->unclamped_orders_rejected();
1270 const auto unclamped_refused_before = runner_->unclamped_orders_refused();
1271 // **The polling path's own figures, taken before any stop is honoured.** `honour` runs the stop
1272 // handler and the closing pass through the same `Runner::settle`, so a snapshot taken at the end
1273 // of `run` would count a refused flatten here *and* in `closing_refused_` -- one failure reported
1274 // twice, under a contract that says this family describes the polling path.
1275 const auto outcomes_before = instruction_outcomes();
1276 auto outcomes_after = outcomes_before;
1277 // **The record travels with the counters, and for the same reason.** Read after `honour`, a
1278 // refusal from the *closing* pass would have replaced it while the count still described a
1279 // polling one -- a report naming an order none of its figures counted.
1280 std::optional<BrokerRefusal> refusal_at_boundary;
1281 std::optional<strategy::Rejection> unbuildable_at_boundary;
1282 // Differenced around the run for the same reason, and the reason is sharper here: the source
1283 // survives `run` returning, so an absolute figure would report the previous run's charges again.
1284 const auto charged_before = [&]() {
1286 return source_->charged_costs();
1287 } else {
1288 return ChargedCosts{};
1289 }
1290 }();
1291 for (auto& due : next_due_) {
1292 due.reset();
1293 }
1294 for (auto& count : consecutive_failures_) {
1295 count = 0;
1296 }
1297 closing_.reset();
1298 warmup_discarded_at_start_ = warmup_discarded();
1300 kept_dropped_at_start_ = runner_->kept_intents_dropped();
1301 }
1302
1303 while (max_iterations == 0 || report.iterations_ < max_iterations) {
1304 const auto iteration = step(stop, report);
1305 ++report.iterations_;
1306 if (iteration.gate_) {
1307 ++report.blocked_;
1308 }
1309 report.final_gate_ = iteration.gate_;
1310 report.final_phase_ = iteration.phase_;
1311 if (iteration.phase_changed_) {
1312 ++report.phase_transitions_;
1313 }
1314 last_ = iteration;
1315
1316 if (report.abandoned_) {
1317 if (!closing_) {
1318 outcomes_after = instruction_outcomes();
1319 refusal_at_boundary = runner_->last_broker_refusal();
1320 unbuildable_at_boundary = runner_->last_unbuildable();
1321 }
1322 // As for a stop: retained work lives for one run. A run resumed after an outage of unknown
1323 // length must not place what was decided before it.
1324 runner_->clear_retained_targets();
1325 if (const auto intent = stop.intent()) {
1326 interrupt_stop(*intent, report);
1327 } else if (owed_past_limit(clock_.now())) {
1328 // Giving up took `POLL_FAILING`, which lets a withdrawal through. The hold for an order nobody
1329 // can account for (`set_owed_order_limit`) is the stronger one and stays, with no stop
1330 // requested: no read has decided yet, and a later run on this loop takes it up again.
1332 report.final_gate_ = runner_->guard().blocking_reason();
1333 }
1334 break;
1335 }
1336 if (const auto intent = stop.intent()) {
1337 if (!closing_) {
1338 // Sampled here, before the stop runs anything, for the reason stated where this began. Once,
1339 // when the stop is first seen: what a strategy asks for while the stop waits is refused by
1340 // the hold, and those refusals describe the stop, not the polling path.
1341 outcomes_after = instruction_outcomes();
1342 refusal_at_boundary = runner_->last_broker_refusal();
1343 unbuildable_at_boundary = runner_->last_unbuildable();
1344 begin_stop(report);
1345 }
1346 // A run's last permitted iteration honours the stop rather than leave it pending.
1347 const bool last_iteration = max_iterations != 0 && report.iterations_ >= max_iterations;
1348 if (!last_iteration && closing_should_wait(*intent, report)) {
1349 clock_.wait_for(closing_until_next());
1350 continue;
1351 }
1352 finish_stop(*intent, report);
1353 report.stopped_by_ = intent;
1354 break;
1355 }
1356 clock_.wait_for(until_next(iteration.at_));
1357 }
1358
1359 // Deliberately **not** released here. A run that ended because an operator asked it to, or
1360 // because a poll kept failing, must leave the runner unable to send: releasing the halt on the
1361 // way out would reopen the gate at exactly the moment the loop stopped watching it. A caller who
1362 // genuinely wants to trade again resumes the guard themselves, which is a deliberate act.
1363 //
1364 // A healthy run that simply exhausted `max_iterations` never halted anything, so there is
1365 // nothing to leave behind in that case.
1367 runner_->sends_outside_permitted_phase() - outside_phase_before;
1368 report.price_bands_withheld_ = runner_->price_bands_withheld() - bands_withheld_before;
1370 runner_->unclamped_orders_rejected() - unclamped_rejected_before;
1372 runner_->unclamped_orders_refused() - unclamped_refused_before;
1373 // Taken at the loop's exit for a run nothing stopped, and at the stop for one something did.
1374 if (!report.stopped_by_ && !report.abandoned_) {
1375 outcomes_after = instruction_outcomes();
1376 refusal_at_boundary = runner_->last_broker_refusal();
1377 unbuildable_at_boundary = runner_->last_unbuildable();
1378 }
1379 note_instruction_outcomes(report, outcomes_before, outcomes_after);
1380 // **Carried only when this run's polling saw one**, which the count already decides: any refusal
1381 // during this run overwrote whatever the runner held before it, so a non-zero count means the
1382 // record is this run's. Comparing the records instead would need an equality `BrokerRefusal` has
1383 // no reason to have.
1384 if (report.orders_broker_refused_ != 0) {
1385 report.last_broker_refusal_ = refusal_at_boundary;
1386 }
1387 // The same rule for the reconciliation's own refusals.
1388 if (report.orders_unbuildable_ != 0) {
1389 report.last_unbuildable_ = unbuildable_at_boundary;
1390 }
1391 // **Said only when a band was actually held back**, not merely because no policy was stated. A
1392 // run that registers no band and polls none has nothing to decide, and a note telling it to
1393 // state a policy would be advice about a mechanism it is not using. Once per loop, like the
1394 // portfolio's own note, because a run that ignores it does not need it every iteration.
1395 //
1396 // **Asked of the runner rather than remembered here.** This loop borrows a mutable runner and
1397 // does not own its policy: a maximum stated directly on the runner before this loop existed
1398 // would have read as "nobody stated one", and a withdrawal made directly on the runner would
1399 // have been suppressed by this loop's memory of its own wrapper being called. One policy, one
1400 // place that answers for it.
1401 if (report.price_bands_withheld_ > 0 && !runner_->price_limits_age_policy_stated() &&
1402 !said_no_band_age_policy_) {
1403 said_no_band_age_policy_ = true;
1404 report.notes_.emplace_back(
1405 "no price band age policy: call set_maximum_price_limits_age or "
1406 "allow_unbounded_price_limits_age"
1407 );
1408 }
1410 report.charged_costs_ = source_->charged_costs().since(charged_before);
1411 }
1412 note_sellable_convention(report);
1413 note_unestablished_phase(report);
1414 note_on_fill_never_fires(report);
1415 note_listing_outpaces_holdings(report);
1416 note_warmup_discarded(report);
1417 note_kept_intents_dropped(report);
1418 return report;
1419 }
1420
1425 const IterationReport& last() const noexcept { return last_; }
1426
1427private:
1428 // Puts the sellable-convention probe's verdict in the report, when it reached one.
1429 //
1430 // The probe answers a question no broker's documentation is known to -- whether the sellable figure
1431 // already excludes resting sells -- and it answers it as a by-product of ordinary polling. An
1432 // operator should not have to write code to read that, so a run that learned something says so,
1433 // with the counts behind it because one observation is evidence rather than proof.
1434 //
1435 // Silent when nothing was observed, which is most runs: a note saying "undetermined" on every run
1436 // that never sold anything is noise, and noise is how a report stops being read.
1437 void note_sellable_convention(LoopReport& report) const {
1438 const auto evidence = runner_->sellable_convention().overall();
1439 if (evidence.total() == 0) {
1440 return;
1441 }
1442 report.notes_.push_back(
1443 "broker's sellable figure appears " +
1444 std::string(strategy::sellable_convention_name(evidence.verdict())) + " (" +
1445 std::to_string(evidence.net_) + " net, " + std::to_string(evidence.gross_) + " gross)"
1446 );
1447 }
1448
1449 // Returns how many instructions the runner has discarded as history, where it counts them.
1450 std::size_t warmup_discarded() const {
1451 if constexpr (runner_counts_warmup<Runner>::value) {
1452 return runner_->warmup_instructions_discarded();
1453 } else {
1454 return 0;
1455 }
1456 }
1457
1458 // Says, once per run, how many instructions were decided on bars fed as history and not sent.
1459 //
1460 // Only the newest bar of each instrument's read may send (`Runner::feed_warmup`), so a cold start's
1461 // history and a backlog after an outage are shown to the strategy and nothing decided on them goes
1462 // out. Worth a line because it is also what a bar cadence slower than the bars' interval costs: a
1463 // poll bringing five minute bars acts on the newest one only.
1464 void note_warmup_discarded(LoopReport& report) const {
1465 const auto discarded = warmup_discarded() - warmup_discarded_at_start_;
1466 if (discarded == 0) {
1467 return;
1468 }
1469 report.notes_.push_back(
1470 std::to_string(discarded) + " instruction(s) decided on bars fed as history were not sent: "
1471 "only the newest bar of each instrument's read may send (poll bars no slower than their interval)"
1472 );
1473 }
1474
1475 // Says, once per run, how many order intents kept for the phase were dropped at release.
1476 void note_kept_intents_dropped(LoopReport& report) const {
1477 if constexpr (runner_counts_kept_drops<Runner>::value) {
1478 const auto dropped = runner_->kept_intents_dropped() - kept_dropped_at_start_;
1479 if (dropped != 0) {
1480 report.notes_.push_back(
1481 std::to_string(dropped) + " order intent(s) kept for the market phase were not sent at the "
1482 "open: the instrument's holding had changed since they were decided, or a stop now rested"
1483 );
1484 }
1485 } else {
1486 static_cast<void>(report);
1487 }
1488 }
1489
1490 // Says, once per run, that the strategy's `on_fill` never fires on this runner.
1491 //
1492 // See `Runner::on_fill_never_fires`. On every run rather than only the first, because a report is
1493 // read on its own, and a note that appeared only in a run nobody kept would be no note at all.
1494 void note_on_fill_never_fires(LoopReport& report) const {
1495 if constexpr (Runner::on_fill_never_fires) {
1496 report.notes_.push_back(
1497 "strategy defines on_fill; it never fires on this runner (fills are not observed live: "
1498 "read positions from holdings, see writing-a-strategy.md)"
1499 );
1500 } else {
1501 static_cast<void>(report);
1502 }
1503 }
1504
1505 // Says, once per run, that working orders are polled more often than holdings.
1506 //
1507 // Not refused: it is a legitimate choice for a strategy that mostly watches its orders. But every
1508 // plan made between a listing and the next holdings read is withheld (`PollSchedule::working_orders_`
1509 // says why), and an operator reading a report full of `portfolio_superseded` should find the cause
1510 // stated beside it.
1511 void note_listing_outpaces_holdings(LoopReport& report) const {
1512 const auto& listing = schedule_.working_orders_;
1513 const auto& holdings = schedule_.holdings_;
1514 if (listing && holdings && *listing < *holdings) {
1515 report.notes_.push_back(
1516 "working orders are polled more often than holdings; plans made between a listing and the "
1517 "next holdings read are withheld as portfolio_superseded"
1518 );
1519 }
1520 }
1521
1522 // Runs one iteration: poll what is due, then let the runner act if the gate is open.
1523 IterationReport step(StopSource& stop, LoopReport& report) {
1524 IterationReport iteration;
1525 iteration.at_ = clock_.now();
1526
1527 // Read before polling, so an operator's stop is honoured without first spending requests on a
1528 // broker the loop is about to walk away from.
1529 check_control_file(stop, report);
1530 // A stop discards what was retained for a better account read: nothing decided before it is to be
1531 // replayed after it.
1532 if (stop.requested()) {
1533 runner_->clear_retained_targets();
1534 }
1535
1536 // **Installed before anything can send, and announced only after the polls.** The two halves are
1537 // separate on purpose and neither is optional.
1538 //
1539 // A source's `poll` calls `Runner::feed`, which settles, so an order can leave the process from
1540 // inside a poll -- the same fact the gate below is computed early for. If the phase were installed
1541 // after the polls, the first bar arriving after the close would be settled while the context still
1542 // said `OPEN`, and an opening order would pass a phase limit that exists to refuse it. Once per
1543 // session, on the close.
1544 //
1545 // Announcing the change is the opposite case: `on_phase` may place orders, and it should see this
1546 // iteration's polls rather than the previous one's -- fresh prices, a fresh portfolio, and a gate
1547 // re-evaluated against them. That is what the replay's step does (`backtest/engine.hpp`, "The price moves with the portfolio"), with
1548 // the same argument: a handler deciding on a mark from one moment beside a price from another is
1549 // not looking at a fresher picture, it is looking at an incoherent one.
1550 install_phase(iteration);
1551
1552 // **Before anything this loop calls can feed, because several of them can.** The runner measures
1553 // a snapshot's age against the later of this and its own decision instant, and its decision
1554 // instant is a bar's period end -- which on a daily bar is yesterday's close. Set here, a plan
1555 // settled from inside `read_order_list` or a poll is aged against the clock rather than against
1556 // the event that triggered it.
1557 runner_->set_observed_at(iteration.at_);
1558 // Stated here too, and for the same reason: the runner enforces the absence rule at the moment
1559 // an instruction is decided, which is the only place that is inside every callback this loop
1560 // makes. A run that opted out says so, and the runner then places as it did before.
1561 runner_->require_known_portfolio(!allow_absent_portfolio_);
1562
1563 // **Established before any poll runs, because a poll is what feeds the runner.** A source's
1564 // `poll` calls `Runner::feed` itself -- `live::SessionSource::poll_bars` does, per instrument --
1565 // and `feed` settles, so an order can leave the process from inside a poll. Computing the gate
1566 // afterwards would mean the startup gate and the staleness gate were installed one poll too
1567 // late, and on the very first iteration they would not be installed at all: a bar arriving
1568 // before the first order-list read could place an order against a position a predecessor may
1569 // have left, which is the exact hazard the startup gate exists for.
1570 hold(gate_for(stop, iteration.at_, report, /*may_read=*/true));
1571
1572 for (const auto kind : all_poll_kinds) {
1573 if (!is_due(kind, iteration.at_)) {
1574 continue;
1575 }
1576 // **Re-established immediately before every poll**, because a poll is a point at which an order
1577 // can leave the process and anything that ran before it may have replaced the calendar the phase
1578 // was derived from. Three things can have: a previous poll in this same loop, the calendar poll
1579 // in particular since it is polled first, and `gate_for`'s out-of-band `read_order_list` above.
1580 //
1581 // Before rather than after, which is the correction: "after every poll" closed the first two and
1582 // left the third, because the pre-loop install runs *before* `gate_for` and nothing re-derived
1583 // the phase between that call and the first poll. Placing it here makes the rule positional
1584 // instead of enumerated -- every send site is preceded by an install, and no list of which polls
1585 // can change what has to stay correct.
1586 //
1587 // The cost is a date comparison per due poll.
1588 install_phase(iteration);
1589 // **And the gate, for the same reason.** The polls before this one may have read the account: on
1590 // a cold start the hold for an account never read was installed before the polls, and without
1591 // re-evaluating it here it would still stand when the bars read after this iteration's holdings
1592 // are fed -- withholding the first decision against an account that has, by then, been read.
1593 // `may_read` is false: the order list is attempted once per iteration, above.
1594 hold(gate_for(stop, iteration.at_, report, /*may_read=*/false));
1595 const auto watch = watch_phase(iteration);
1596 // **Sampled before the poll, not after it.** A poll can send: `Runner::feed` settles, and a
1597 // working-order poll now feeds `on_order_update` as well as the bar and quote polls feeding
1598 // their handlers. An order a handler places during the working-order poll was sent *after*
1599 // the listing was fetched and cannot be in it -- so counting placements afterwards would
1600 // stamp that listing as though it covered the order, and `closing_source_verified` would
1601 // then approve a flatten against a picture missing a resting order. The count taken here is
1602 // the one that was true when the listing was read.
1603 const auto placements_before_poll = runner_->placements_at_broker();
1604 const auto outcome = source_->poll(kind, *runner_);
1605 if (kind == PollKind::WORKING_ORDERS) {
1606 working_asked_at_ = clock_.monotonic_now();
1607 } else if (kind == PollKind::HOLDINGS) {
1608 holdings_asked_at_ = clock_.monotonic_now();
1609 }
1610 note_revised_phase(std::string(poll_kind_name(kind)) + " poll", watch, report);
1611 // `UNCHANGED` counts, because a listing that is identical to the last one is still a listing
1612 // -- but only on a source that publishes them at all, which `note_working_orders_read`
1613 // enforces: a broker without the inquiry answers `UNCHANGED` without asking anybody.
1614 if (kind == PollKind::WORKING_ORDERS &&
1615 (outcome == PollOutcome::UPDATED || outcome == PollOutcome::UNCHANGED)) {
1616 note_working_orders_read(iteration.at_, placements_before_poll);
1617 }
1618 ++report.attempted_[static_cast<int>(kind)];
1619 iteration.polls_.emplace_back(kind, outcome);
1620 schedule_next(kind, iteration.at_);
1621 note_outcome(kind, outcome, report);
1622 if (outcome == PollOutcome::UPDATED) {
1623 iteration.fed_ = true;
1624 }
1625 if (report.abandoned_) {
1626 // Recorded before returning, so the report does not claim sending was permitted while the
1627 // guard `note_outcome` just halted says otherwise.
1628 iteration.gate_ = runner_->guard().blocking_reason();
1629 return iteration;
1630 }
1631 }
1632
1633 // Observed after the polls, not before: a poll can refresh the listing without a bar arriving,
1634 // and that is precisely when an order of ours begins showing as resting. Observing first would
1635 // read every iteration's portfolio one iteration late. A repeat observation of unchanged state
1636 // concludes nothing, so a quiet iteration costs a scan and teaches the probe nothing.
1637 runner_->observe_sellable_convention();
1638
1639 // Re-evaluated after the polls so the report and the next iteration reflect what those polls
1640 // changed -- a holdings refresh should lift the staleness hold now, not one iteration later.
1641 // The order list is deliberately not attempted a second time: one attempt per iteration, and
1642 // the gate simply stands until the next one.
1643 // The last send site: the announcement below settles what `on_phase` asks for, and the final poll
1644 // may have replaced the calendar after its own pre-poll install.
1645 install_phase(iteration);
1646 hold(gate_for(stop, iteration.at_, report, /*may_read=*/false));
1647
1648 // Announced last, so the handler runs on everything this iteration established. A change is
1649 // announced once, not every iteration it persists for.
1650 if (announced_ != iteration.phase_) {
1651 // **The clock moves with the phase, and only forwards.** `Runner::feed(MarketPhase)` does not
1652 // advance `Context::now_`; only a bar or a quote does, from a broker's stamp. So a daily-bar run
1653 // would hand `on_phase` an instant hours old, and `RiskGuard` files a send at
1654 // `GuardInputs::now_` and counts sends within `rate_window_` of it -- the phase order and the
1655 // next bar's order would be filed at what the guard thinks are two unrelated moments, and two
1656 // orders would pass a one-order window. The replay's step (`backtest/engine.hpp`) documents the same hazard
1657 // for the replay.
1658 //
1659 // Forwards only: a broker stamp ahead of this machine's clock must not be walked backwards,
1660 // because a handler deciding at an instant *earlier* than the bar it has already been shown is
1661 // the same incoherence with the sign flipped. The skew is the broker's and is not this loop's
1662 // to quietly correct.
1663 //
1664 // A bar fed earlier in this same iteration set `now_` to its own period end, which on a daily bar
1665 // is yesterday's close. Its order is still counted in the rate window at this iteration's
1666 // reading, beside this one: the runner counts a send at the later of the decision instant and
1667 // the reading this loop gave it.
1668 if (runner_->context().now() < iteration.at_) {
1669 runner_->context().set_now(iteration.at_);
1670 }
1671 announced_ = iteration.phase_;
1672 iteration.phase_changed_ = true;
1673 // Watched for the same reason a poll is: `Context::set_calendar` is public, so an `on_phase`
1674 // handler can revise the calendar and submit in one call, and `settle` runs before this returns.
1675 const auto watch = watch_phase(iteration);
1676 runner_->feed(iteration.phase_);
1677 note_revised_phase("on_phase handler", watch, report);
1678 }
1679
1680 // **Last of all the iteration's decisions**, so every handler that could restate the instrument --
1681 // the bars, the quotes, the listing's updates, and `on_phase` just above -- has spoken first, and
1682 // its instruction has replaced the retained target; a retry before `on_phase` could send an entry
1683 // the phase change was about to countermand. And after every account poll and the gate they
1684 // refreshed, so what is retried is sized against this iteration's holdings, listing and budget,
1685 // under this iteration's holds -- inside the holdings poll it ran before the gate lifted and before
1686 // buying power was read.
1687 runner_->retry_retained_targets();
1688
1689 // Reported from the guard rather than from what the loop just computed, so the report names the
1690 // most fundamental hold in force whoever owns it -- an operator's halt, or a journal that
1691 // failed, outranks anything the loop decided.
1692 iteration.gate_ = runner_->guard().blocking_reason();
1693 return iteration;
1694 }
1695
1696 // What the phase and the send count were before the loop handed control to somebody else.
1697 struct PhaseWatch {
1698 std::chrono::system_clock::time_point at_{};
1700 std::size_t sends_{0};
1701 };
1702
1703 // Records the phase and the send count, for comparison after a call into user code.
1704 PhaseWatch watch_phase(const IterationReport& iteration) const {
1705 return PhaseWatch{iteration.at_, iteration.phase_, runner_->guard().total_sent()};
1706 }
1707
1708 // Counts and names a call that sent an order and left the phase changed behind it.
1709 //
1710 // **The one hazard here the loop cannot prevent and can only witness.** It re-derives the phase before
1711 // every call it makes that could send, but nothing of its runs *inside* such a call: a
1712 // source that replaces the calendar and then feeds, or an `on_phase` handler that does the same before
1713 // submitting, settles an order against the phase as it stood when the call began. So the loop looks
1714 // afterwards, which turns an invisible violation into a line in the report -- the same choice
1715 // `closing_source_unverified_` makes about a listing it could not trust.
1716 //
1717 // **Both halves are required, and each one removes a wrong answer.** The phase must have moved, or an
1718 // ordinary send would be reported. Something must have been sent, or an ordinary calendar poll would be
1719 // reported on every iteration -- which is why this no longer excludes the calendar poll by kind. That
1720 // exclusion was a false-positive workaround and it hid this case: a source obeying
1721 // the letter of the contract, writing the calendar in the calendar poll and *then* feeding.
1722 //
1723 // `total_sent()` rather than the guard's rate-window deque, because that one prunes and can fall, so
1724 // it cannot be differenced across a call.
1725 void note_revised_phase(const std::string& what, const PhaseWatch& before, LoopReport& report) {
1726 if (runner_->guard().total_sent() == before.sends_) {
1727 return;
1728 }
1729 if (phase_at(before.at_) == before.phase_) {
1730 return;
1731 }
1732 ++report.sends_under_a_revised_phase_;
1733 if (report.sends_under_a_revised_phase_ == 1) {
1734 // Once per run, not once per call: a strategy doing this on every bar would otherwise bury every
1735 // other note under thousands of copies of this one.
1736 // **Worded to exactly what was observed**, which is co-occurrence and not causation. Two cases
1737 // the stronger claim would have got wrong: a call that sent *first* and revised the calendar
1738 // afterwards, where no order used the stale answer; and a call that only cancelled, since
1739 // `Runner::send_cancel` never consults the phase at all. Both still break the writer contract and
1740 // are worth reporting, so the note states the facts and names the check rather than the verdict.
1741 report.notes_.push_back(
1742 "the " + what + " changed the market phase and sent a request in the same call; check whether "
1743 "that request used the earlier phase. Only the calendar poll may write the trading calendar, "
1744 "and it must not send");
1745 }
1746 }
1747
1748 // Computes the phase for this iteration and installs it, so nothing can send under a stale one.
1749 //
1750 // **Called immediately before every point at which this loop hands control to the source**: once
1751 // before `gate_for`, which performs an out-of-band order-list read; once before each due poll; and
1752 // once before the phase announcement. So the invariant is exactly this, and no stronger:
1753 // **whenever the loop calls into the source, `Context::phase_` is derived from the calendar as it
1754 // stood at that call.**
1755 //
1756 // **What that deliberately does not promise**, because it cannot: a source is free to replace the
1757 // calendar and then feed the runner inside a single `poll`, and no loop code runs in between, so
1758 // that order would settle against the phase as it stood when the poll began. `PollKind::CALENDAR`
1759 // documents the contract that rules it out, `SessionSource` satisfies it -- it writes the calendar in
1760 // the calendar poll and nowhere else -- and `note_revised_phase` reports a source that
1761 // breaks it, because a violation nobody is told about is one nobody fixes. The stronger at-send-site
1762 // invariant does not hold.
1763 //
1764 // The instant is the iteration's own `at_` throughout, never re-read from the clock, so a poll that
1765 // took a second does not move the session boundary mid-iteration.
1766 void install_phase(IterationReport& iteration) {
1767 iteration.phase_ = phase_at(iteration.at_);
1768 // Recorded here rather than derived later, and it outlives a `run` for the same reason `announced_`
1769 // does: "this loop has never known the phase" is a claim about the loop, not about one call.
1770 if (iteration.phase_ != strategy::MarketPhase::UNKNOWN) {
1771 phase_ever_known_ = true;
1772 }
1773 runner_->context().set_phase(iteration.phase_);
1774 }
1775
1776 // Returns the phase at `at`, from the configured window and whatever calendar has arrived.
1777 //
1778 // The composition `ReplayClock` already performs, with a polled calendar in place of a replay's
1779 // per-bar answer. An uncovered date is `UNKNOWN` and never "closed": `TradingCalendar` draws that
1780 // distinction because assuming closed stops a strategy on a day that is in fact open, and assuming
1781 // open lets one order into a holiday.
1782 //
1783 // The date is the *market's*, not this process's. `SessionWindow` owns the offset and derives both
1784 // answers from it, so the day the calendar is asked about and the day the session boundaries are
1785 // computed for cannot disagree.
1786 strategy::MarketPhase phase_at(std::chrono::system_clock::time_point at) const {
1787 const auto& calendar = runner_->context().calendar();
1788 return session_window_.phase(at, calendar.is_trading_day(session_window_.local_date(at)));
1789 }
1790
1791 // The clause naming unreadable calendar rows, or nothing when there were none.
1792 //
1793 // A row whose date the broker sent unreadably is dropped, so the day it was for is uncovered and
1794 // the note above would otherwise send an operator to check a span or a broker that is fine. The
1795 // count says the broker *did* answer, and what it answered could not be read.
1796 std::string unreadable_calendar_rows() const {
1797 const auto unreadable = runner_->context().calendar().unreadable_days_;
1798 if (unreadable == 0) {
1799 return {};
1800 }
1801 return "; the broker sent " + std::to_string(unreadable) + " calendar row" +
1802 (unreadable == 1 ? "" : "s") + " whose date could not be read";
1803 }
1804
1805 // Says so when a run never established the phase, and names which of the three reasons applies.
1806 //
1807 // Silent otherwise, like `note_sellable_convention`: a line on every healthy run is how a report
1808 // stops being read. But a run that placed nothing because the phase was never known must not look
1809 // like a quiet market -- and this is **not** a hold on the guard, deliberately. A hold would also
1810 // stop cancellations, which `Runner::send_cancel` refuses on any blocking reason while never
1811 // consulting the phase at all; taking one because a reference-data poll failed would make an
1812 // unknown phase more restrictive than a closed one, and a withdrawal only ever reduces exposure.
1813 // `RiskGuard` already refuses every placement that opens or increases a position with
1814 // `PHASE_NOT_PERMITTED`, and permits one that closes a held position -- which is exactly what a
1815 // hold would wrongly stop, for the same reason it would wrongly stop a cancellation.
1816 void note_unestablished_phase(LoopReport& report) const {
1817 if (report.iterations_ == 0 || report.final_phase_ != strategy::MarketPhase::UNKNOWN) {
1818 return;
1819 }
1820 // **"Never" is a claim, so it is tracked rather than derived.** A loop can establish the phase and
1821 // then lose it -- the local date rolls past what the calendar covers, or the broker revises it --
1822 // and telling an operator it was never established would send them looking for a configuration
1823 // fault that is not there.
1824 //
1825 // This used to read `report.phase_transitions_ > 0`, which was wrong: `announced_`
1826 // outlives a `run` while the report's counter restarts at zero, so a run with nothing left to
1827 // announce reports no transitions and would claim the phase had never been known -- of a loop that
1828 // spent its first run open. The verdict was right in the case that was tested and the reasoning was
1829 // wrong, which is the worse of the two.
1830 const bool ever_known = phase_ever_known_;
1831 // Three reasons, not four. "The poll never came due" was here and is unreachable: `run` resets
1832 // `next_due_`, so a configured cadence makes the calendar due on the first iteration of every
1833 // run. A branch describing a state that cannot happen is worse than no branch, because a reader
1834 // who sees it believes it can.
1835 std::string reason;
1836 if (!schedule_.calendar_) {
1837 reason = "no calendar cadence is configured";
1838 } else if (report.failed(PollKind::CALENDAR) == report.attempted(PollKind::CALENDAR)) {
1839 reason = "every calendar poll failed";
1840 } else if (runner_->context().calendar().days_.empty()) {
1841 // What the loop can actually tell: a poll reported success and no calendar arrived. It does
1842 // **not** claim to know why, because it cannot distinguish a broker that publishes none -- where
1843 // the request is not even compiled -- from a source that fetched one and installed nothing.
1844 reason = "a calendar poll succeeded but no calendar arrived (this broker may publish none)";
1845 reason += unreadable_calendar_rows();
1846 } else {
1847 // Named, not merely called uncovered. "The calendar does not cover today" sends a reader to
1848 // check a span they cannot see; the date is the one thing that turns this note into an action.
1849 const auto date = session_window_.local_date(last_.at_);
1850 reason = "the calendar does not cover " + std::to_string(date.year_) + "-" +
1851 std::to_string(date.month_) + "-" + std::to_string(date.day_);
1852 reason += unreadable_calendar_rows();
1853 }
1854 report.notes_.push_back(
1855 std::string(ever_known ?
1856 "the market phase stopped being established (" :
1857 "the market phase was never established (") +
1858 reason + "); a guard permitting only known phases refused every placement opening a position" +
1859 (ever_known ? " after that" : ""));
1860 }
1861
1862 // Attempts the startup order-list read once, and returns whether it succeeded.
1863 //
1864 // The startup gate's read and a pending stop's both come here, so the two cannot drift apart.
1865 bool read_order_list_once(std::chrono::system_clock::time_point at, LoopReport& report) {
1866 // Watched like a poll, because it is the same kind of thing: a call into the source, from which a
1867 // source that revised the calendar and sent would reach the identical hazard through a different
1868 // door. The phase in force here is the one `step` installed immediately before calling this.
1869 const auto watch = PhaseWatch{at, phase_at(at), runner_->guard().total_sent()};
1870 // As in `step`, and here the startup read dispatches nothing at all -- but the count is taken
1871 // before the call for the same reason, so that the rule holds positionally rather than by an
1872 // argument about which reads can send.
1873 const auto placements_before_read = runner_->placements_at_broker();
1874 const auto outcome = source_->read_order_list(*runner_);
1875 // The working-order endpoint, where the source has one: a closing wait counts this as asking it.
1876 working_asked_at_ = clock_.monotonic_now();
1877 note_revised_phase("order-list read", watch, report);
1878 if (outcome != PollOutcome::UPDATED && outcome != PollOutcome::UNCHANGED) {
1879 return false;
1880 }
1881 order_list_read_ = true;
1882 report.order_list_read_ = true;
1883 order_list_at_ = at;
1884 // The startup read is a working-order read *on a source that has the inquiry*. On one that
1885 // does not, it reads order history for the journal and never populates `working_`, so
1886 // `note_working_orders_read` deliberately does nothing and a closing pass stays unverified.
1887 note_working_orders_read(at, placements_before_read);
1888 return true;
1889 }
1890
1891 // Returns which of the loop's *own* conditions blocks sending, reading the order list if due.
1892 //
1893 // Only conditions the loop computes, because a hold is released by whoever took it. `OPERATOR`
1894 // belongs to a person and `LOCAL_FAILURE` to the runner; a loop that took either would be a
1895 // second holder of someone else's hold -- the defect `HaltReason` exists to prevent,
1896 // reintroduced one layer up.
1897 //
1898 // **Always evaluated, even while someone else's hold stands.** Short-circuiting on a foreign hold
1899 // would leave the loop's own hold un-maintained underneath it, so the moment an operator resumed,
1900 // the guard would be wide open against a portfolio nobody had rechecked.
1901 //
1902 // Called twice per iteration -- before the polls, which is what actually protects anything, and
1903 // after them so the report is current. `may_read` is false on the second call so the order-list
1904 // read is attempted at most once per iteration.
1905 std::optional<strategy::HaltReason> gate_for(
1906 StopSource& stop,
1907 std::chrono::system_clock::time_point at,
1908 LoopReport& report,
1909 bool may_read
1910 ) {
1911 if (stop.requested()) {
1912 // **A withdraw or a flatten reads the broker's order list first, if it never has.** Each builds
1913 // its instructions from the picture, and a stop pending before the first iteration -- a process
1914 // started with `flatten` in its control file -- used to return here before the startup read, so
1915 // its closing pass was unverified and a flatten closed nothing, for any position. That also left
1916 // no way to flatten a position behind a resting stop: the in-process flatten withdraws the stop
1917 // and, rightly, sends nothing beside it, and the restart that should finish the job could not.
1918 // The read is the startup gate's own, under its conditions; a quiesce sends nothing and needs no
1919 // picture, so it does not spend the request.
1920 //
1921 // **`STOPPING` on every path, the failed read included.** `ORDER_LIST_UNREAD` lets reducing
1922 // orders through (`halt_blocks`), and nothing but the stop's own may go out once one is pending.
1923 // Held before the read as well, because the source is a template parameter and may feed.
1924 const auto intent = stop.intent();
1925 // Once a stop is waiting, on the wait's own floor: a read that failed is not asked again at
1926 // every tick. The iteration that takes the stop up reads as any other.
1927 const bool on_the_floor = !closing_ || !asked_within_floor(PollKind::WORKING_ORDERS);
1928 if (intent && stop_intent_closes(*intent) && !order_list_read_ && may_read && on_the_floor &&
1929 !runner_->guard().halted(strategy::HaltReason::LOCAL_FAILURE)) {
1931 read_order_list_once(at, report);
1932 }
1934 }
1935 if (!order_list_read_) {
1936 // Not attempted when the caller says not to, and not attempted at all while the runner's
1937 // journal is broken: a runner that cannot record what it does must not be sent looking for
1938 // more work to fail at. Either way the gate stands, which is the safe answer.
1939 //
1940 // `STOPPING` rather than `ORDER_LIST_UNREAD` on each path here while an order nobody can account
1941 // for is past the limit (below): the stronger hold, which also refuses a withdrawal, must not be
1942 // traded for the weaker one because a read failed. And held before the read, since the source is
1943 // a template parameter and its read may feed.
1944 const bool owed = owed_past_limit(clock_.now());
1946 if (!may_read || runner_->guard().halted(strategy::HaltReason::LOCAL_FAILURE)) {
1947 return unread;
1948 }
1949 if (owed) {
1951 }
1952 if (!read_order_list_once(at, report)) {
1953 return unread;
1954 }
1955 }
1956 // **An order nobody can account for, owed too long, stops the run's placements**
1957 // (`set_owed_order_limit`), in two steps. Once the limit has passed on the clock, this gate holds
1958 // `STOPPING`; it is evaluated before every poll, so an order-list read that begins after a check
1959 // that found the limit passed, and whatever its handlers ask for, runs under the hold (what was
1960 // already under way at the limit runs on the check before it). Once a read in an iteration that
1961 // began after the limit has succeeded and left the order unaccounted for, the stop is requested:
1962 // that iteration's every check found the limit passed, so that read ran under the hold. A read
1963 // whose sweep names the order lifts the hold instead.
1964 if (auto owed = owed_too_long()) {
1965 report.notes_.push_back(owed_stop_note(*owed));
1966 report.owed_stop_ = std::move(owed);
1967 stop.request(StopIntent::QUIESCE);
1969 }
1970 if (owed_past_limit(clock_.now())) {
1972 }
1973 // Measured from the snapshot's own `as_of_` rather than from when the poll returned, because
1974 // that is when the broker says the positions were true. A poll that succeeded at noon carrying
1975 // a snapshot the broker took at 11:00 is an hour stale, and the poll's own timestamp would call
1976 // it fresh.
1977 const auto as_of = runner_->context().portfolio().as_of_;
1978 if (as_of == std::chrono::system_clock::time_point{}) {
1979 // A default-constructed `Portfolio` carries the epoch, and no broker ever reports a snapshot
1980 // taken in 1970, so the epoch means **never populated** -- which is not the same as old, and
1981 // is the more dangerous of the two: an empty portfolio is the *permissive* answer to every
1982 // question the send path asks it, so a run that has never read the account sizes an entry as
1983 // though it were flat.
1984 //
1985 // **No longer inside the age check.** It was, and hoisting it was not a one-line change:
1986 // the hold would reach cancellations, and an order that cannot be withdrawn keeps resting and
1987 // can fill. That objection is answered rather than avoided -- `strategy::halt_blocks` lets a
1988 // named withdrawal through a hold that only means this runner is uninformed, and
1989 // `Runner::plan_withhold_reason` stops the *derived* cancellation that objection overlooked.
1990 //
1991 // The opt-out reaches the age comparison below as well, not just this hold. The epoch is 1970
1992 // and therefore older than any bound an operator would state, so an absence opt-out that did
1993 // not skip the comparison would simply move the refusal to `PORTFOLIO_STALE`.
1994 if (!allow_absent_portfolio_) {
1996 }
1997 // Opted out, so the age comparison is skipped rather than fallen through to -- and the checks
1998 // after this one still run, because an absent portfolio says nothing about buying power.
1999 }
2000
2001 // **A run must state how old a snapshot may be, or state that it accepts any age.**
2002 //
2003 // This was optional, and optional meant that a run which never thought about holdings sent
2004 // orders sized against whatever the portfolio happened to contain -- including nothing at all.
2005 // Fail-open by default was the defect. The library states no default
2006 // duration of its own, because any constant it picked would be a risk policy it has no basis
2007 // for: how stale a position may be before it must not be sized against is the operator's
2008 // judgement about their own market and cadence, not this library's.
2009 // **Checked after absence, not before it.** Both can be true at once, and "you have never read
2010 // this account" is the more useful of the two to be told: the missing policy is a configuration
2011 // fact the note below states in full, while absence is a fact about the data every ceiling and
2012 // every target subtraction is about to be measured against.
2013 if (!maximum_portfolio_age_ && !allow_unbounded_portfolio_age_) {
2014 if (!said_no_age_policy_) {
2015 said_no_age_policy_ = true;
2016 report.notes_.emplace_back(
2017 "no portfolio age policy: call set_maximum_portfolio_age or allow_unbounded_portfolio_age"
2018 );
2019 }
2021 }
2022 if (maximum_portfolio_age_ && as_of != std::chrono::system_clock::time_point{} &&
2023 at - as_of > *maximum_portfolio_age_) {
2025 }
2026 // **A runner that keeps its orders until the broker accounts for them needs the listing.** A holdings
2027 // poll refreshes positions and keeps `working_`; only a working-order poll replaces it. A runner
2028 // that sends to a broker keeps each order it sent in `working_` until that listing arrives
2029 // (`Runner::persist_reservation`), so with no listing -- none published, none polled, or one
2030 // older than the policy -- an order that expired or was refused would stay "working" for ever,
2031 // a target would stop being chased, and a filled one would be cancelled again on every pass.
2032 // Held, with the missing piece named once, whatever the portfolio age policy: opting out of a
2033 // holdings age is not opting out of a listing.
2034 if constexpr (runner_keeps_sent_orders<Runner>::value) {
2035 if (const auto missing = listing_missing(at)) {
2036 if ((said_listing_ & missing->first) == 0) {
2037 said_listing_ |= missing->first;
2038 report.notes_.emplace_back(missing->second);
2039 }
2041 }
2042 }
2043 if (runner_->guard().limits().enforce_buying_power_) {
2044 // Checked only when the caller asked for the limit. A loop with no buying-power cadence and
2045 // no check configured is an ordinary loop, and must not be held for a reading nobody wants.
2046 if (const auto unread = buying_power_unread(at)) {
2047 return *unread;
2048 }
2049 }
2050 return std::nullopt;
2051 }
2052
2053 // Returns whether some budget the run needs is missing or too old to size against.
2054 //
2055 // **Every configured currency must have its own fresh reading**, not merely one of them. A source
2056 // that names its currencies is asked, because an account trading two and answering for one is a
2057 // partial answer: the per-order guard would still refuse the unread currency, but the loop would
2058 // go on reporting an open gate while a budget it was told to watch never arrived at all.
2059 //
2060 // A source that names none falls back to "any reading at all", which is the most the loop can
2061 // check without knowing what was asked for.
2062 std::optional<strategy::HaltReason> buying_power_unread(
2063 std::chrono::system_clock::time_point at
2064 ) const {
2065 const auto& readings = runner_->context().portfolio().buying_power_;
2066 // Aged against the reading's own stamp, for the same reason the portfolio is: a figure that
2067 // arrived an hour ago is an hour old however recently the poll succeeded.
2068 const auto usable = [&](const BuyingPower& reading) {
2069 return !maximum_buying_power_age_ ||
2070 at - reading.received_at_ <= *maximum_buying_power_age_;
2071 };
2072 if constexpr (source_names_buying_power_currencies<Source>::value) {
2073 const auto& wanted = source_->buying_power_currencies();
2074 if (!wanted.empty()) {
2075 for (const auto& currency : wanted) {
2076 bool found = false;
2077 for (const auto& reading : readings) {
2078 if (reading.cash_.currency_ == currency && usable(reading)) {
2079 found = true;
2080 break;
2081 }
2082 }
2083 if (!found) {
2085 }
2086 }
2087 return std::nullopt;
2088 }
2089 }
2090 if (readings.empty()) {
2092 }
2093 for (const auto& reading : readings) {
2094 if (!usable(reading)) {
2096 }
2097 }
2098 return std::nullopt;
2099 }
2100
2101 // Sets how old a buying-power reading may be before the gate closes on it.
2102public:
2114 void set_maximum_buying_power_age(std::chrono::nanoseconds age) {
2115 maximum_buying_power_age_ = age;
2116 }
2117
2118private:
2119 // Reads the control file, escalating `stop` when it names an intent.
2120 void check_control_file(StopSource& stop, LoopReport& report) {
2121 if (!control_.enabled()) {
2122 return;
2123 }
2124 if (const auto intent = control_.poll()) {
2125 stop.request(*intent);
2126 return;
2127 }
2128 if (!control_.unrecognized().empty()) {
2129 // Reported rather than guessed at, because picking the nearest intent would decide how much of
2130 // an operator's position to close on their behalf.
2131 report.notes_.push_back(
2132 "control file names no intent: '" + control_.unrecognized() + "'");
2133 }
2134 }
2135
2136 // Counts an outcome, and abandons the run when one kind keeps failing.
2137 void note_outcome(PollKind kind, PollOutcome outcome, LoopReport& report) {
2138 const int at = static_cast<int>(kind);
2139 if (outcome == PollOutcome::FAILED) {
2140 ++report.failed_[at];
2141 ++consecutive_failures_[at];
2142 if (consecutive_failures_[at] >= failure_policy_.consecutive_limit_) {
2143 report.abandoned_ = true;
2144 report.abandoned_kind_ = kind;
2145 report.notes_.push_back(
2146 std::string("gave up after repeated failures polling ") + std::string(poll_kind_name(kind)));
2148 }
2149 return;
2150 }
2151 switch (outcome) {
2153 // Counted above.
2154 return;
2156 // The limiter working says nothing either way, so the count stands: a kind alternating
2157 // failure and refusal is still broken.
2158 return;
2161 // A poll that answered, with something new or with nothing new, ends a run of failures.
2162 consecutive_failures_[at] = 0;
2163 return;
2164 }
2165 }
2166
2167 // Enters a stop, once per run: the hold, the strategy's notice, and the end of whatever it had pending.
2168 // A stop that closes may then wait (`closing_should_wait`) before `finish_stop` builds its pass.
2169 void begin_stop(LoopReport& report) {
2170 closing_ = ClosingWait{};
2171 closing_->began_ = clock_.monotonic_now();
2172 closing_->deadline_ = closing_->began_ + std::chrono::duration_cast<std::chrono::steady_clock::duration>(
2173 closing_wait_ > std::chrono::nanoseconds::zero() ? closing_wait_ : std::chrono::nanoseconds::zero());
2175 // Whenever the stop was requested -- before the iteration or during one of its polls -- nothing
2176 // retained before it is to be replayed after it, by this run or a resumed one. A target waiting on
2177 // a resting stop is the same kind of work.
2178 runner_->clear_retained_targets();
2179 runner_->clear_deferred_targets();
2180
2181 // **The strategy is told first, and cannot trade on the way out.** `on_stop` is dispatched here,
2182 // while the hold stands, so every instruction it asks for is refused by the guard and counted.
2183 // It must not be dispatched later: from the moment the loop queues its own closing orders, a
2184 // strategy's order would be planned against the same snapshot and settled in the same window,
2185 // going through none of the sizing the closing pass does -- a second full-size close beside the
2186 // first, or a placement during a `WITHDRAW`, which promises none.
2187 // **Not watched, and it does not need to be.** `on_stop` runs with a mutable context and can revise
2188 // the calendar like any other handler, but `hold(STOPPING)` above is already in force, so every
2189 // instruction it asks for is refused before anything is sent -- which `stop_handler_sent_` asserts as
2190 // a report field precisely so that ordering cannot be broken silently. The revision it may have made
2191 // *is* answered, one call later: the closing derive below picks it up.
2192 //
2193 // For the record, the complete list of calls this loop makes out of itself is: `source_->poll`,
2194 // `source_->read_order_list`, `source_->buying_power_currencies` (a const accessor that is handed no
2195 // runner and so can neither feed nor write a calendar), `runner_->observe_sellable_convention` (which
2196 // reads the portfolio and dispatches nothing), this one, and `settle_closing`. The two that can send
2197 // under a phase are watched.
2198 const auto parting = runner_->notify_stop();
2199 // `withheld_plans_` counts here for the same reason it counts in `Report::quiet`: an `on_stop`
2200 // handler may state a target, `STOPPING` is held by then, and a plan that would have cancelled
2201 // something is withheld whole. Left out of this total, the loop would report that a parting
2202 // instruction met no resistance while the instruction never went anywhere.
2203 report.stop_handler_refused_ = parting.refused_.size() + parting.refused_cancel_.size() +
2204 parting.rejected_.size() + parting.withheld_.size() + parting.withheld_plans_.size() +
2205 parting.unavailable_.size();
2206 report.stop_handler_sent_ = parting.would_place_.size() + parting.would_cancel_.size();
2207 }
2208
2209 // Leaves the runner unable to send when a run is abandoned with a stop pending, and says the stop's
2210 // closing pass did not run. The abandonment took `POLL_FAILING`, under which a named withdrawal still
2211 // goes; a stop asked for more than that.
2212 void interrupt_stop(StopIntent intent, LoopReport& report) {
2214 // The stop may never have been taken up (`begin_stop`): a target waiting on a resting stop order is
2215 // not to go out after it either.
2216 runner_->clear_deferred_targets();
2217 report.stopped_by_ = intent;
2218 if (closing_) {
2219 report.closing_waited_ = std::chrono::duration_cast<std::chrono::nanoseconds>(
2220 clock_.monotonic_now() - closing_->began_);
2221 }
2222 if (stop_intent_closes(intent)) {
2223 report.closing_interrupted_ = true;
2224 report.notes_.push_back(
2225 std::string("the run was abandoned for failing polls with a ") + std::string(stop_intent_name(intent)) +
2226 " pending: its closing pass did not run, nothing was withdrawn or closed by it, and the runner is"
2227 " left unable to send; read the broker's order list and holdings");
2228 }
2229 }
2230
2231 // Carries out `intent`, which is where the three stops actually differ.
2232 void finish_stop(StopIntent intent, LoopReport& report) {
2233 report.closing_waited_ = std::chrono::duration_cast<std::chrono::nanoseconds>(
2234 clock_.monotonic_now() - closing_->began_);
2235 closing_->reads_wanted_ = false;
2236 if (!stop_intent_closes(intent)) {
2237 return;
2238 }
2239 // **Whatever a strategy queued without settling goes nowhere.** The closing instructions below are
2240 // settled with the loop's hold lifted, and anything else still on the queue would be settled with
2241 // them. Settled here, under the hold, it is refused like the rest of what a strategy asks during a stop.
2242 {
2243 const auto stale = runner_->settle_closing();
2244 report.stop_handler_refused_ += stale.refused_.size() + stale.refused_cancel_.size() +
2245 stale.rejected_.size() + stale.withheld_.size() + stale.withheld_plans_.size() + stale.unavailable_.size();
2246 report.stop_handler_sent_ += stale.would_place_.size() + stale.would_cancel_.size();
2247 }
2248 // Nothing decided under the hold is kept, so this clears nothing today. It stands so that a change
2249 // to that rule cannot let kept work out with the closing orders below.
2250 runner_->clear_retained_targets();
2251 runner_->clear_deferred_targets();
2252
2253 // **Both intents state their instructions outright rather than expressing them as a target.**
2254 //
2255 // A withdraw always had to: there is no target to reconcile against, so the working orders are
2256 // named one by one. A flatten used to state a zero `TargetPosition` and let reconciliation do
2257 // the cancelling, which was wrong twice over -- reconciliation keeps a resting order that points
2258 // the same way as the target, so the flatten could send nothing at all, and it skipped orders on
2259 // instruments with no position to target. Stating both halves here means one withdrawal per
2260 // resting order, no instruction derived from another, and no case where the two disagree.
2261 //
2262 // Recorded before anything is queued, because it is a statement about the picture the pass is
2263 // built from rather than about any instruction in it.
2264 report.closing_source_unverified_ = !closing_source_verified(clock_.now());
2265
2266 // Sized from what is working, which counts an order this runner sent and holdings do not show yet
2267 // only once that row is rebuilt: a sell that filled while holdings lag is otherwise sold again.
2268 runner_->refresh_unconfirmed();
2269 // Counted before the pass, so its own new orders are not: what a serialized runner owes that no
2270 // listing or holdings explain is exposure this pass cannot withdraw or size around.
2271 report.closing_unconfirmed_ = runner_->obligations_unexplained();
2272 const auto& portfolio = runner_->context().portfolio();
2273 if (!stop_intent_closes_positions(intent)) {
2274 for (const auto& working : portfolio.working_) {
2275 // Already withdrawn by this runner, and the broker took the request: asked again it refuses.
2276 if (runner_->withdrawal_accepted_for(working)) {
2277 continue;
2278 }
2279 runner_->context().cancel(working.ref_, "stop-withdraw");
2280 ++report.withdrawn_;
2281 }
2282 } else {
2283 // **A flatten withdraws everything, then closes only what it cannot double-sell.**
2284 //
2285 // Two ways to get this wrong, and the library has now had both. Expressing the flatten as a
2286 // zero `TargetPosition` lets reconciliation treat a resting same-side order as progress and
2287 // send nothing at all -- a limit sell that may never fill is not being out of the market.
2288 // Sending the *whole* holding instead ignores that order, and since a cancellation is a
2289 // request rather than an outcome, the resting sell can fill while its cancellation is still
2290 // pending: ten long becomes ten short. The second error is much worse than the first, because
2291 // it manufactures a position in the opposite direction rather than leaving the one that was
2292 // already there.
2293 //
2294 // So the close is sized to what cannot double up: the holding minus whatever is still working
2295 // on the same side. Every working order is withdrawn either way, so a cancellation that
2296 // completes leaves part of the position open rather than a resting order nobody is watching,
2297 // and a cancellation that fails because the order filled leaves the account flat. Neither
2298 // outcome can overshoot, and the pass says it could not prove which one happened.
2299 for (const auto& working : portfolio.working_) {
2300 if (runner_->withdrawal_accepted_for(working)) {
2301 continue;
2302 }
2303 runner_->context().cancel(working.ref_, "stop-flatten");
2304 ++report.withdrawn_;
2305 }
2306 // **One close per instrument, sized from the net of its rows.** This looped over rows, and a
2307 // broker reporting one instrument as several lots is ordinary -- so it went wrong twice at
2308 // once. Rows of +100 and -40 are a holding of +60, and it submitted SELL 100 *and* BUY 40:
2309 // outside the session the phase rule refuses both, so the operator's flatten sent nothing
2310 // although SELL 60 was permitted all along. And the deduction below subtracts what is working
2311 // on the closing side, which is a figure for the whole instrument -- subtracted once per row,
2312 // it was charged twice, so rows of +60 and +40 against a working SELL 20 sent 40 and 20 and
2313 // left 20 of the position behind. Neither is reachable once the instrument is the unit.
2314 std::vector<InstrumentId> closed;
2315 for (const auto& position : portfolio.positions_) {
2316 bool seen = false;
2317 for (const auto& already : closed) {
2318 if (already == position.instrument_) {
2319 seen = true;
2320 break;
2321 }
2322 }
2323 if (seen) {
2324 continue;
2325 }
2326 closed.push_back(position.instrument_);
2327
2328 // The net, not this row. A snapshot whose rows will not add up cannot say what flattening
2329 // would take, and guessing is the one thing a closing pass must not do.
2330 const auto net = portfolio.net_quantity_of(position.instrument_);
2331 if (!net) {
2332 // **Not `closing_contended_`.** That one means a competing order, and its note sends an
2333 // operator to the order list. What failed here is the holding's own arithmetic.
2334 ++report.closing_unsizable_;
2335 continue;
2336 }
2337 if (net->coefficient() == 0) {
2338 // Flat on the net, however many rows say so. A closed position can sit in a snapshot at
2339 // zero, and two rows can cancel; an order for none of it is one the broker would reject,
2340 // for nothing.
2341 continue;
2342 }
2343 if (report.closing_source_unverified_) {
2344 // **Nothing is placed against a listing that cannot be trusted.** The deduction below
2345 // subtracts what is *known* to be working on the closing side, and an unverified picture
2346 // is precisely one where that is not all of it: an order this runner sent after the last
2347 // listing, or a source that cannot enumerate orders at all. Sending a full-size close
2348 // beside an order nobody can see is the same double sell, reached by a different door --
2349 // and reporting it afterwards does not recall the instruction.
2350 //
2351 // The withdrawals above still went, because cancelling only ever reduces exposure. What
2352 // is left is the position, held and reported, which is the safe end of every branch here.
2353 ++report.closing_contended_;
2354 continue;
2355 }
2356 const bool held_long = decimal_less(Decimal64{}, *net);
2357 const auto magnitude = held_long ?
2358 std::optional<Decimal64>(*net) :
2359 decimal_subtract(Decimal64{}, *net);
2360 if (!magnitude) {
2361 // **A representable net can have no representable magnitude**: two rows summing to the
2362 // most negative `Decimal64` have no positive counterpart, so the size of the buy that
2363 // would close them cannot be stated. This used to `continue` in silence, which moved no
2364 // count -- so `closing_complete()` returned true over a position the pass had never
2365 // sized, which is the one thing that call promises it will not do.
2366 ++report.closing_unsizable_;
2367 continue;
2368 }
2369 const OrderSide closing_side = held_long ? OrderSide::SELL : OrderSide::BUY;
2370 const auto contending = portfolio.working_quantity(position.instrument_, closing_side);
2371 const auto sendable = decimal_subtract(*magnitude, contending);
2372 if (!sendable) {
2373 // The same silence for the same reason: a size that cannot be computed is not a size of
2374 // zero, and a pass that skipped it did not knowingly send nothing. **Unlike the two cases
2375 // above, this one can fail with both inputs representable** -- a held magnitude and a
2376 // working quantity whose scales cannot be aligned -- which is why the note names the
2377 // working orders as well as the holdings.
2378 ++report.closing_unsizable_;
2379 continue;
2380 }
2381 if (contending.coefficient() != 0) {
2382 // Sized down, and the reduction is a fact about what this pass could not establish rather
2383 // than about what the operator asked for. Counted so `closing_complete()` cannot claim a
2384 // flatten that may have left part of the position open.
2385 ++report.closing_contended_;
2386 }
2387 if (decimal_less(*sendable, Decimal64{}) || sendable->coefficient() == 0) {
2388 // Everything held is already working on the closing side. Sending anything at all here is
2389 // the over-close, so the withdrawal above is the whole of this instrument's flatten.
2390 continue;
2391 }
2392 runner_->context().submit(
2394 position.instrument_,
2395 closing_side,
2397 ).with_tag("stop-flatten")
2398 );
2399 ++report.flattened_;
2400 }
2401 }
2402
2403 // Lifted so the closing instructions can actually go out -- a withdraw that cannot send is not a
2404 // withdraw -- and closed again immediately afterwards. The window is exactly one `stop()` call
2405 // wide, and it is the only point at which a stop in progress can send anything.
2406 //
2407 // Only the loop's *own* hold lifts. An operator who halted by hand, or a runner whose journal
2408 // failed, still refuses every closing instruction -- placements through `RiskGuard::check` and
2409 // cancellations through the same holds in `Runner::send_cancel` -- and the closing counts say
2410 // so, so `closing_complete()` is false and the notes say the position is still held. A stop
2411 // arriving by signal or control file therefore cannot send anything through a halt somebody had
2412 // already set.
2413 hold(std::nullopt);
2414 // **Derived once more, immediately before the only call that can send during a stop.** `on_stop` ran
2415 // a few lines above with a mutable context, so it may have revised the calendar -- and unlike a
2416 // strategy's write inside `on_phase`, this one the loop *can* answer, because `settle_closing` is a
2417 // separate call it makes itself. Without this, a handler that revised the calendar to closed
2418 // saw the flatten go out under the `OPEN` it replaced.
2419 //
2420 // At `clock_.now()` rather than the iteration's instant, because a stop is honoured after the
2421 // iteration and a closing pass should be checked against the session as it is now.
2422 const auto closing_at = clock_.now();
2423 runner_->context().set_phase(phase_at(closing_at));
2424 const auto watch = PhaseWatch{closing_at, phase_at(closing_at), runner_->guard().total_sent()};
2425 const auto closing = runner_->settle_closing();
2426 note_revised_phase("closing pass", watch, report);
2428 // What the pass itself caused to be kept (a close the phase or the broker's hours refused) is not to
2429 // go out later either: a stop makes each request once.
2430 runner_->clear_retained_targets();
2431 runner_->clear_deferred_targets();
2432
2433 report.closing_placed_ = closing.would_place_.size();
2434 // **Both halves of every bucket.** A closing instruction is a placement or a cancellation, and
2435 // each can fail in the same five ways; counting only the placement half is how a stop that sent
2436 // nothing reported itself complete.
2437 report.closing_rejected_ = closing.rejected_.size() + closing.rejected_cancel_.size();
2438 report.closing_refused_ = closing.refused_.size() + closing.refused_cancel_.size();
2439 // Whole plans too: a serialized runner withholds a closing order's plan while the instrument owes one.
2440 report.closing_withheld_ = closing.withheld_.size() + closing.withheld_plans_.size();
2441 report.closing_indeterminate_ =
2442 closing.indeterminate_.size() + closing.indeterminate_cancel_.size();
2443 report.closing_unavailable_ = closing.unavailable_.size() + closing.unavailable_cancel_.size();
2444 report.closing_unresolved_ = closing.unresolved_cancel_.size();
2445
2446 // Named individually rather than summarized, because "your flatten did not go out" is the one
2447 // note in this whole report an operator must not have to infer.
2448 if (!closing.refused_.empty() || !closing.refused_cancel_.empty()) {
2449 report.notes_.push_back(
2450 "a guard refused " + std::to_string(report.closing_refused_) +
2451 " closing instruction(s); the position is still held");
2452 }
2453 if (!closing.rejected_.empty()) {
2454 // Deliberately does not name who decided. The bucket holds three things -- reconciliation
2455 // could not build the instruction, the broker refused it, or the binding refused to send it
2456 // -- and an operator reading a stop report needs the consequence, which is the same for all
2457 // three. The report's own entries carry which it was, on `RejectReason` for a placement and
2458 // on `by_broker_` for a cancellation.
2459 report.notes_.push_back(
2460 std::to_string(closing.rejected_.size()) +
2461 " closing instruction(s) were not placed; the position is still held");
2462 }
2463 if (!closing.rejected_cancel_.empty()) {
2464 report.notes_.push_back(
2465 std::to_string(closing.rejected_cancel_.size()) +
2466 " closing cancellation(s) were refused; read the broker's order list for their state");
2467 }
2468 if (report.closing_withheld_ != 0) {
2469 report.notes_.push_back(
2470 "withheld " + std::to_string(report.closing_withheld_) +
2471 " closing instruction(s) pending reconciliation, or until an earlier order is accounted for;"
2472 " read the broker's order list");
2473 }
2474 if (report.closing_unconfirmed_ != 0) {
2475 report.notes_.push_back(
2476 std::to_string(report.closing_unconfirmed_) +
2477 " order(s) this runner sent were not yet accounted for by holdings or a listing, so the closing pass"
2478 " could not withdraw or size around them; read the broker's order list and holdings");
2479 }
2480 if (report.closing_wait_expired_) {
2481 report.notes_.push_back(
2482 "the stop waited " +
2483 std::to_string(std::chrono::duration_cast<std::chrono::seconds>(report.closing_waited_).count()) +
2484 " s for holdings and the order list to account for this runner's orders, or for a marked"
2485 " instrument to be resolved, and that did not happen; the closing pass was built from what was in"
2486 " hand (set_closing_wait)");
2487 }
2488 if (report.closing_indeterminate_ != 0) {
2489 report.notes_.push_back(
2490 std::to_string(report.closing_indeterminate_) +
2491 " closing instruction(s) have an unknown outcome; read the broker's order list");
2492 }
2493 if (report.closing_unavailable_ != 0) {
2494 report.notes_.push_back(
2495 "local machinery could not send " + std::to_string(report.closing_unavailable_) +
2496 " closing instruction(s); no broker was asked and the position is still held");
2497 }
2498 if (report.stop_handler_refused_ != 0) {
2499 report.notes_.push_back(
2500 "the strategy's on_stop asked for " + std::to_string(report.stop_handler_refused_) +
2501 " instruction(s) during the stop; none were sent, because an operator's stop is not a"
2502 " moment for a strategy to trade");
2503 }
2504 if (report.closing_contended_ != 0) {
2505 report.notes_.push_back(
2506 report.closing_source_unverified_ ?
2507 "no closing order was sent for " + std::to_string(report.closing_contended_) +
2508 " position(s), because an order this loop cannot see could close them too; they are"
2509 " still held and the withdrawals above are all that went" :
2510 std::to_string(report.closing_contended_) +
2511 " position(s) were closed only for the part no working order was already selling or"
2512 " buying; if those withdrawals succeed, part of the position is still held");
2513 }
2514 if (report.closing_unsizable_ != 0) {
2515 // **Names both inputs, because the three causes do not share one.** Two of them are the
2516 // holdings rows alone -- they will not add up, or the net has no representable closing
2517 // magnitude. The third subtracts what is already working on the closing side, and that
2518 // subtraction can fail with both inputs perfectly representable: a held quantity of
2519 // 1,000,000,000,000,000,000 and a working close of 0.1 cannot be aligned. An earlier note
2520 // blamed the holdings snapshot alone and would have sent an operator to check one of two
2521 // inputs. It still does not say "contended", which means a competing order reduced the close;
2522 // here nothing could be sized at all.
2523 report.notes_.push_back(
2524 "no closing order could be sized for " + std::to_string(report.closing_unsizable_) +
2525 " position(s): the holdings quantities would not add up or fit, or could not be combined"
2526 " with what is already working on the closing side; nothing was sent for them and they are"
2527 " still held");
2528 }
2529 if (report.closing_source_unverified_) {
2530 report.notes_.push_back(
2531 std::string("the stop could not verify what was outstanding: ") + unverified_source_reason() +
2532 "; orders or positions a previous process left may remain");
2533 }
2534 if (report.closing_unresolved_ != 0) {
2535 report.notes_.push_back(
2536 std::to_string(report.closing_unresolved_) +
2537 " closing cancellation(s) named an order the portfolio does not hold; read the broker's"
2538 " order list");
2539 }
2540 }
2541
2542 // Takes `gate`'s hold on the guard and releases the one this loop took before it.
2543 //
2544 // There is no "did I halt it" flag any more, and that is the point: the loop names the hold it
2545 // takes and releases only that one, so an operator halting by hand into a loop's closed gate is
2546 // no longer invisible, and the loop reopening its gate no longer lifts their halt with it. The
2547 // hold is released and retaken rather than diffed, because taking a hold is idempotent.
2548 //
2549 // Only the loop's *own* conditions are held. `OPERATOR` belongs to a person and `LOCAL_FAILURE`
2550 // to the runner; both outlive any gate the loop computes, so they are reported and never taken or
2551 // released here.
2552 void hold(std::optional<strategy::HaltReason> gate) {
2553 if (gate && !owns(*gate)) {
2554 gate.reset();
2555 }
2556 if (held_ == gate) {
2557 return;
2558 }
2559 if (held_) {
2560 runner_->guard().resume(*held_);
2561 }
2562 if (gate) {
2563 runner_->guard().halt(*gate);
2564 }
2565 held_ = gate;
2566 }
2567
2568 // Says what is missing for a runner that keeps its sent orders until the broker accounts for them, as a
2569 // note bit and the note, or nothing when the listing is in hand.
2570 std::optional<std::pair<unsigned, std::string>> listing_missing(
2571 std::chrono::system_clock::time_point at
2572 ) const {
2573 if constexpr (!source_provides_working_orders<Source>::value) {
2574 (void)at;
2575 return std::make_pair(1u, std::string(
2576 "this runner keeps the orders it sends until a working-order listing or holdings account for "
2577 "them, and the source publishes no listing: placing holds (declare provides_working_orders and "
2578 "poll the listing)"
2579 ));
2580 } else {
2581 if (!schedule_.working_orders_) {
2582 return std::make_pair(2u, std::string(
2583 "no working-order cadence: this runner keeps the orders it sends until a working-order "
2584 "listing or holdings account for them, so placing holds until PollSchedule::working_orders_ "
2585 "is set"
2586 ));
2587 }
2588 if (maximum_portfolio_age_ && working_orders_at_ &&
2589 at - *working_orders_at_ > *maximum_portfolio_age_) {
2590 return std::make_pair(4u, std::string(
2591 "working-order listing older than the portfolio age policy: poll working orders "
2592 "(PollSchedule::working_orders_) more often than that age, by at least a tick; placing "
2593 "holds until it is read"
2594 ));
2595 }
2596 return std::nullopt;
2597 }
2598 }
2599
2600 // Returns the oldest order nobody can account for that was sent longer than the limit before `at`: one
2601 // no sweep has named, or one held for an operator (`set_owed_order_limit`). None with the limit off, or
2602 // from a runner that is not serialized.
2603 std::optional<Obligation> owed_longer_than_limit(std::chrono::system_clock::time_point at) const {
2604 std::optional<Obligation> oldest;
2605 if (owed_order_limit_ <= std::chrono::nanoseconds::zero()) {
2606 return oldest;
2607 }
2608 for (auto& owed : runner_->obligations()) {
2609 if (!obligation_is_unaccounted(owed.state_)) {
2610 continue;
2611 }
2612 if (at - owed.sent_at_ > owed_order_limit_ && (!oldest || owed.sent_at_ < oldest->sent_at_)) {
2613 oldest = std::move(owed);
2614 }
2615 }
2616 return oldest;
2617 }
2618
2619 // Whether such an order is past the limit as of `now`: placing is held from here on.
2620 bool owed_past_limit(std::chrono::system_clock::time_point now) const {
2621 return owed_longer_than_limit(now).has_value();
2622 }
2623
2624 // The order the run stops for: one still unaccounted for after the last successful order-list read,
2625 // where that read's iteration began longer after the order's send than the limit (`order_list_at_` is
2626 // the iteration's instant, which is not later than any check made in it). Measured to that read and
2627 // not to now, so that a loop reading the list less often than the limit gives a sweep its chance to
2628 // name the order first. None before the order list was read.
2629 std::optional<Obligation> owed_too_long() const {
2630 if (!order_list_read_ || !order_list_at_) {
2631 return std::nullopt;
2632 }
2633 return owed_longer_than_limit(*order_list_at_);
2634 }
2635
2636 // The note for a run the loop stopped for `owed`.
2637 static std::string owed_stop_note(const Obligation& owed) {
2638 return "stopped placing: the " + std::string(owed.side_ == OrderSide::BUY ? "buy" : "sell") + " of " +
2639 owed.quantity_.str() + " " + owed.instrument_.symbol_ + " sent " + format_iso8601_utc(owed.sent_at_) +
2640 " (journal id " + owed.journal_id_ + ") is still owed, and " +
2641 (owed.state_ == ObligationState::OPERATOR ? "is held for an operator" : "no sweep has found its order") +
2642 ". Nothing was cancelled or closed. Read the broker's order list and holdings for it, then account for "
2643 "it with Runner::release_obligations; until then every start stops here again";
2644 }
2645
2646 // Returns whether a stop that closes should let the loop read again before its pass is built.
2647 //
2648 // True while a read could still give the pass what it lacks and the wait has not reached its limit;
2649 // `STOPPING` is held throughout (`begin_stop`, `gate_for`), so the loop only reads meanwhile: what a
2650 // strategy asks for is refused. See `set_closing_wait` for what it waits for and what it does not.
2651 bool closing_should_wait(StopIntent intent, LoopReport& report) {
2652 if (!stop_intent_closes(intent) || closing_wait_ <= std::chrono::nanoseconds::zero()) {
2653 return false;
2654 }
2655 // A source that lists no working orders can never verify the pass, however long the loop reads:
2656 // waiting would only delay the same result.
2657 if constexpr (!source_provides_working_orders<Source>::value) {
2658 return false;
2659 }
2660 // Under a person's hold or the runner's own, the pass can send nothing whatever the reads show.
2661 if (runner_->guard().halted(strategy::HaltReason::OPERATOR) ||
2662 runner_->guard().halted(strategy::HaltReason::LOCAL_FAILURE)) {
2663 return false;
2664 }
2665 const auto monotonic = clock_.monotonic_now();
2666 report.closing_waited_ = std::chrono::duration_cast<std::chrono::nanoseconds>(monotonic - closing_->began_);
2667 // As the pass does before it counts: an order holdings now show is no longer owed.
2668 runner_->refresh_unconfirmed();
2669 closing_->reads_wanted_ = closing_needs_evidence(clock_.now());
2670 if (!closing_->reads_wanted_) {
2671 return false;
2672 }
2673 if (monotonic >= closing_->deadline_) {
2674 report.closing_wait_expired_ = true;
2675 closing_->reads_wanted_ = false;
2676 return false;
2677 }
2678 return true;
2679 }
2680
2681 // Whether a further read could change what the closing pass would do: the picture it is built from is
2682 // not verified, an order of this runner awaits an account, or an instrument that is held is marked.
2683 bool closing_needs_evidence(std::chrono::system_clock::time_point at) const {
2684 if (!closing_source_verified(at) || runner_->obligations_awaiting_account() != 0) {
2685 return true;
2686 }
2687 // A mark on something held, unless only an operator can lift it. Held by the instrument's net, as the
2688 // pass sizes it; rows that cannot be netted count as held.
2689 const auto& portfolio = runner_->context().portfolio();
2690 for (const auto& position : portfolio.positions_) {
2691 const auto net = portfolio.net_quantity_of(position.instrument_);
2692 if ((!net || net->coefficient() != 0) && runner_->ledger().requires_reconcile(position.instrument_) &&
2693 !obligation_needs_operator(runner_->obligation_state(position.instrument_))) {
2694 return true;
2695 }
2696 }
2697 return false;
2698 }
2699
2700 // Whether a closing wait may read `kind` (working orders or holdings) now: it wants the read, and that
2701 // endpoint was last asked at least `closing_reread_` ago, whoever asked -- the ordinary cadence, the
2702 // startup read, or this wait. The iteration in which the limit passes may still read, like the startup
2703 // read: the pass that follows is built from it, and nothing reads after it.
2704 bool closing_read_due(PollKind kind) const {
2705 if (!closing_ || !closing_->reads_wanted_) {
2706 return false;
2707 }
2708 return !asked_within_floor(kind);
2709 }
2710
2711 // Whether `kind`'s endpoint (working orders or holdings) was last asked less than `closing_reread_` ago.
2712 bool asked_within_floor(PollKind kind) const {
2713 const auto& asked = kind == PollKind::WORKING_ORDERS ? working_asked_at_ : holdings_asked_at_;
2714 return asked && clock_.monotonic_now() - *asked < closing_reread_;
2715 }
2716
2717 // Returns how long a waiting stop sleeps: until the next read it may make, its limit, an ordinary poll
2718 // of another kind, or the tick, whichever is first. The ordinary due times of the two closing reads are
2719 // left out: the wait reads them on its own floor, and an overdue one would otherwise spin the loop.
2720 std::chrono::nanoseconds closing_until_next() const {
2721 auto wait = schedule_.tick_;
2722 const auto wall = clock_.now();
2723 for (const auto kind : all_poll_kinds) {
2724 const auto& due = next_due_[static_cast<int>(kind)];
2725 if (!due || kind == PollKind::WORKING_ORDERS || kind == PollKind::HOLDINGS) {
2726 continue;
2727 }
2728 wait = std::min(wait, std::chrono::duration_cast<std::chrono::nanoseconds>(*due - wall));
2729 }
2730 const auto monotonic = clock_.monotonic_now();
2731 const auto until = [&](std::chrono::steady_clock::time_point when) {
2732 return std::chrono::duration_cast<std::chrono::nanoseconds>(when - monotonic);
2733 };
2734 wait = std::min(wait, until(closing_->deadline_));
2735 for (const auto* asked : {&working_asked_at_, &holdings_asked_at_}) {
2736 wait = std::min(wait, *asked ? until(**asked + closing_reread_) : std::chrono::nanoseconds::zero());
2737 }
2738 return wait > std::chrono::nanoseconds::zero() ? wait : std::chrono::nanoseconds::zero();
2739 }
2740
2741 // Returns whether the loop knows what a closing pass at `at` would have to send.
2742 //
2743 // Two sources, both required. The broker's own order list must have been read once, which is what
2744 // the startup gate exists for, and what a stop requested before the first iteration attempts itself
2745 // (a failed attempt leaves it unread). And the
2746 // holdings snapshot -- which is where a `WITHDRAW`'s working orders and a `FLATTEN`'s positions
2747 // actually come from -- must exist, be within the configured age, and be **no older than the
2748 // working-order listing**: a fill between the two reads leaves the listing before it reaches the
2749 // holdings, so a flatten would see neither the order nor the shares it bought.
2750 bool closing_source_verified(std::chrono::system_clock::time_point at) const {
2751 if (!order_list_read_) {
2752 return false;
2753 }
2754 // The working-order picture is its own source with its own freshness, and `as_of_` is not its
2755 // timestamp: `SessionSource::poll_holdings` refreshes positions and `as_of_` while *keeping* the
2756 // working orders it already had, and only a working-order poll replaces those. So a stop can
2757 // have a snapshot stamped a second ago whose resting orders were last read an hour ago.
2758 if (!working_orders_at_) {
2759 return false;
2760 }
2761 // And the decisive one, which no timestamp catches: an order this runner sent after that read
2762 // cannot be in it. A listing is out of date the moment the next placement is accepted, and an
2763 // indeterminate send counts too, because the order may exist.
2764 if (runner_->placements_at_broker() != placements_at_working_read_) {
2765 return false;
2766 }
2767 if (maximum_portfolio_age_ && at - *working_orders_at_ > *maximum_portfolio_age_) {
2768 return false;
2769 }
2770 const auto as_of = runner_->context().portfolio().as_of_;
2771 if (as_of == std::chrono::system_clock::time_point{}) {
2772 return false;
2773 }
2774 // On one clock: both stamps are the local arrival time of their replies, which
2775 // `working_orders_at_` above is not.
2776 if (runner_->context().portfolio().listing_newer_than_holdings()) {
2777 return false;
2778 }
2779 return !maximum_portfolio_age_ || at - as_of <= *maximum_portfolio_age_;
2780 }
2781
2782 // Records that the working-order picture was just refreshed, with what it could not contain.
2783 //
2784 // Does nothing when the source does not publish working orders, so a stop on such a broker stays
2785 // unverified rather than being told a history read or a no-op poll refreshed a listing it never
2786 // built.
2787 void note_working_orders_read(
2788 std::chrono::system_clock::time_point at, std::size_t placements_before_poll
2789 ) {
2790 if constexpr (source_provides_working_orders<Source>::value) {
2791 working_orders_at_ = at;
2792 order_list_at_ = at;
2793 // **The caller's count, taken before the poll ran.** Reading it here instead would include
2794 // an order an `on_order_update` handler placed *during* the poll -- an order sent after the
2795 // listing was fetched, which the listing therefore cannot contain.
2796 placements_at_working_read_ = placements_before_poll;
2797 } else {
2798 (void)at;
2799 (void)placements_before_poll;
2800 }
2801 }
2802
2803 // Returns which half of the closing source is missing, for the operator's note.
2804 std::string unverified_source_reason() const {
2805 if constexpr (!source_provides_working_orders<Source>::value) {
2806 return "this source does not publish the broker's outstanding orders";
2807 }
2808 if (!order_list_read_ || !working_orders_at_) {
2809 return "the broker's order list has not been read";
2810 }
2811 if (runner_->placements_at_broker() != placements_at_working_read_) {
2812 return "an order was sent after the working orders were last read";
2813 }
2814 if (runner_->context().portfolio().as_of_ == std::chrono::system_clock::time_point{}) {
2815 return "no holdings snapshot has arrived";
2816 }
2817 if (runner_->context().portfolio().listing_newer_than_holdings()) {
2818 return "the holdings were read before the working orders, so a fill between them is in neither";
2819 }
2820 if (maximum_portfolio_age_ &&
2821 clock_.now() - *working_orders_at_ > *maximum_portfolio_age_) {
2822 return "the working orders were read longer ago than the configured maximum";
2823 }
2824 return "the holdings snapshot is older than the configured maximum";
2825 }
2826
2827 // Returns whether `kind` is due at `at`.
2828 bool is_due(PollKind kind, std::chrono::system_clock::time_point at) const {
2829 // A waiting stop reads the two closing kinds on its own floor, whatever their cadence (a disabled
2830 // one included), and nothing reads them faster than that meanwhile. The iteration that takes the
2831 // stop up is not waiting yet: it reads as any other.
2832 if (closing_ && closing_->reads_wanted_ &&
2833 (kind == PollKind::WORKING_ORDERS || kind == PollKind::HOLDINGS)) {
2834 return closing_read_due(kind);
2835 }
2836 const auto interval = schedule_.interval_for(kind);
2837 if (!interval) {
2838 return false;
2839 }
2840 const auto& due = next_due_[static_cast<int>(kind)];
2841 return !due || at >= *due;
2842 }
2843
2844 // Records when `kind` is next due.
2845 void schedule_next(PollKind kind, std::chrono::system_clock::time_point at) {
2846 const auto interval = schedule_.interval_for(kind);
2847 if (!interval) {
2848 return;
2849 }
2850 next_due_[static_cast<int>(kind)] =
2851 at + std::chrono::duration_cast<std::chrono::system_clock::duration>(*interval);
2852 }
2853
2854 // Returns how long to wait before the next iteration.
2855 //
2856 // The soonest due poll, capped at the tick so a stop through the control file is noticed within
2857 // one tick however sparse the polling is. Never negative: a loop that has fallen behind catches up
2858 // rather than pausing once per overdue poll.
2859 std::chrono::nanoseconds until_next(std::chrono::system_clock::time_point at) const {
2860 auto wait = schedule_.tick_;
2861 for (const auto kind : all_poll_kinds) {
2862 const auto& due = next_due_[static_cast<int>(kind)];
2863 if (!due) {
2864 continue;
2865 }
2866 const auto remaining =
2867 std::chrono::duration_cast<std::chrono::nanoseconds>(*due - at);
2868 if (remaining < wait) {
2869 wait = remaining;
2870 }
2871 }
2872 return wait > std::chrono::nanoseconds::zero() ? wait : std::chrono::nanoseconds::zero();
2873 }
2874
2875 Runner* runner_;
2876 Source* source_;
2877 PollSchedule schedule_;
2878 Clock clock_;
2879 FileStopChannel control_;
2880 FailurePolicy failure_policy_;
2881 strategy::SessionWindow session_window_{strategy::SessionWindow::krx()};
2882 // The phase most recently announced, which is not the same as the phase in force.
2883 //
2884 // `UNKNOWN` to begin with rather than "nothing announced yet", because that is genuinely what the
2885 // loop knows before its first calendar arrives -- and it means a run that starts knowing nothing and
2886 // then learns the market is open announces that as a change, which is exactly right.
2888 // Whether this loop has ever derived a phase that was not `UNKNOWN`, across every `run`.
2889 bool phase_ever_known_{false};
2890 std::optional<std::chrono::nanoseconds> maximum_portfolio_age_;
2891 // How long a stop that closes may wait for the reads that account for this runner's orders, and how
2892 // often it repeats them.
2893 std::chrono::nanoseconds closing_wait_{std::chrono::seconds(30)};
2894 std::chrono::nanoseconds owed_order_limit_{std::chrono::minutes(10)};
2895 static constexpr std::chrono::seconds closing_reread_{5};
2896 // A stop in progress in this run: when it was entered and when its wait ends, on the monotonic reading,
2897 // and whether the wait currently wants the two closing reads.
2898 struct ClosingWait {
2899 std::chrono::steady_clock::time_point began_{};
2900 std::chrono::steady_clock::time_point deadline_{};
2901 bool reads_wanted_{false};
2902 };
2903 std::optional<ClosingWait> closing_;
2904 // When each closing read's endpoint was last asked, on the monotonic reading: by its ordinary poll, by
2905 // the startup order-list read (the working-order endpoint), or by a closing wait.
2906 std::optional<std::chrono::steady_clock::time_point> working_asked_at_;
2907 std::optional<std::chrono::steady_clock::time_point> holdings_asked_at_;
2908 // Whether the missing-band-policy note has been said; it is said once per loop.
2909 bool said_no_band_age_policy_{false};
2910 // Whether the operator stated that any snapshot age is acceptable; see the setter.
2911 bool allow_unbounded_portfolio_age_{false};
2912 // Whether the operator stated that this run may act with no snapshot at all; see the setter.
2913 bool allow_absent_portfolio_{false};
2914 // Whether the missing-age-policy note has been reported, so it is said once and not per poll.
2915 bool said_no_age_policy_{false};
2916 // Which of `listing_missing`'s notes have been given, so each is said once per loop.
2917 unsigned said_listing_{0};
2918 // The runner's discarded-history count when this run began; see `note_warmup_discarded`.
2919 std::size_t warmup_discarded_at_start_{0};
2920 // The runner's dropped-kept-intent count when this run began; see `note_kept_intents_dropped`.
2921 std::size_t kept_dropped_at_start_{0};
2922 std::optional<std::chrono::nanoseconds> maximum_buying_power_age_;
2923 std::optional<std::chrono::system_clock::time_point> next_due_[poll_kind_count];
2924 std::size_t consecutive_failures_[poll_kind_count] = {};
2925 bool order_list_read_{false};
2926 // Returns whether `reason` is a condition this loop computes, and may therefore hold.
2927 static constexpr bool owns(strategy::HaltReason reason) noexcept {
2928 switch (reason) {
2929 // A person's hold and a local failure are released by their owners, never by the loop.
2932 return false;
2939 return true;
2940 }
2941 return false;
2942 }
2943
2944 // When the working-order picture was last refreshed, or nothing while it never has been.
2945 std::optional<std::chrono::system_clock::time_point> working_orders_at_;
2946 // The instant of the iteration in which the source last read the broker's order list, of either kind:
2947 // a working-order listing, or the startup read of a source without that inquiry (which reads order
2948 // history for its journal). The sweep that can name an order runs inside that read.
2949 std::optional<std::chrono::system_clock::time_point> order_list_at_;
2950 // `Runner::placements_at_broker()` as of that read, so a later send invalidates it.
2951 std::size_t placements_at_working_read_{0};
2952
2953 // The hold this loop currently owns, and the only one it will ever release.
2954 std::optional<strategy::HaltReason> held_;
2955 IterationReport last_;
2956};
2957
2958} // namespace runner
2959} // namespace algotradelib
A three-letter currency code, such as KRW or USD (ISO 4217 style).
Definition types.hpp:234
static Decimal64 parse(std::string_view input)
Parses a decimal string exactly (no binary floating point).
Definition types.hpp:323
void set_calendar(TradingCalendar calendar)
Replaces the trading calendar (for runners and sources, not strategies).
Definition context.hpp:290
A control file an operator writes to stop a running loop with a given intent.
Definition stop.hpp:229
bool enabled() const noexcept
Tests whether a path was configured.
Definition stop.hpp:241
std::optional< StopIntent > poll()
Reads the file's first word.
Definition stop.hpp:254
const std::string & unrecognized() const noexcept
Returns the word the last poll did not recognize.
Definition stop.hpp:289
A source for tests: each poll returns a queued or default outcome, without any network.
Definition loop.hpp:801
PollOutcome poll(PollKind kind, Runner &runner_instance)
Polls: returns the next queued outcome or the default, and on UPDATED installs the calendar or bands ...
Definition loop.hpp:885
static constexpr bool provides_working_orders
true: its outcomes stand for a real working-order listing.
Definition loop.hpp:804
void queue(PollKind kind, PollOutcome outcome)
Queues the outcome of the next poll of a kind.
Definition loop.hpp:811
std::size_t polls(PollKind kind) const noexcept
Returns how many times a kind was polled.
Definition loop.hpp:865
void set_price_limits(InstrumentId instrument, PriceLimits limits)
Adds a band a successful price-limits poll installs, and makes that poll default to UPDATED.
Definition loop.hpp:873
void set_buying_power_currencies(std::vector< Currency > currencies)
Sets the buying-power currencies the source claims to ask about.
Definition loop.hpp:834
void set_order_list(PollOutcome outcome)
Sets the outcome of the order-list read (UPDATED by default).
Definition loop.hpp:828
void set_default(PollKind kind, PollOutcome outcome)
Sets the outcome of every unqueued poll of a kind (UNCHANGED by default).
Definition loop.hpp:820
PollOutcome read_order_list(Runner &)
Reads the order list: returns the configured outcome. The runner argument is ignored.
Definition loop.hpp:914
void set_calendar(TradingCalendar calendar)
Sets the calendar a successful calendar poll installs, and makes that poll default to UPDATED.
Definition loop.hpp:843
std::size_t order_list_reads() const noexcept
Returns how many times the order list was read.
Definition loop.hpp:858
const std::vector< Currency > & buying_power_currencies() const noexcept
Returns the buying-power currencies.
Definition loop.hpp:852
void set_maximum_portfolio_age(std::chrono::nanoseconds age)
Sets how old the holdings (and, for a runner that sends to a broker, the working-order listing) may b...
Definition loop.hpp:1079
void set_maximum_price_limits_age(std::optional< std::chrono::nanoseconds > age)
Sets how old a price band may be and still be applied; passed to the runner (see Runner::set_maximum_...
Definition loop.hpp:1193
void set_maximum_buying_power_age(std::chrono::nanoseconds age)
Sets how old buying-power readings may be.
Definition loop.hpp:2114
void set_failure_policy(FailurePolicy policy)
Sets how many consecutive failures of one poll kind stop the loop.
Definition loop.hpp:1235
void set_session_window(strategy::SessionWindow window)
Sets the session window the market phase is derived from (KRX by default, as in a backtest).
Definition loop.hpp:1221
void set_control_file(FileStopChannel channel)
Sets the control file an operator writes a stop intent into.
Definition loop.hpp:1241
void set_closing_wait(std::chrono::nanoseconds limit)
Sets how long a WITHDRAW or FLATTEN may wait before its closing pass (30 seconds by default).
Definition loop.hpp:1118
const Clock & clock() const noexcept
Returns the clock.
Definition loop.hpp:992
void allow_unbounded_portfolio_age()
Accepts holdings of any age, instead of stating a maximum.
Definition loop.hpp:1182
void allow_unbounded_price_limits_age()
Applies price bands of any age (see Runner::allow_unbounded_price_limits_age).
Definition loop.hpp:1198
void set_owed_order_limit(std::chrono::nanoseconds limit)
Sets how long a serialized runner's order may stay owed with nobody able to account for it before the...
Definition loop.hpp:1170
LoopReport run(StopSource &stop, std::size_t max_iterations=0)
Runs until a stop is requested, a poll kind exhausts its failure budget, or max_iterations iterations...
Definition loop.hpp:1254
Clock & clock() noexcept
Returns the clock, for a test to advance.
Definition loop.hpp:997
const IterationReport & last() const noexcept
Returns what the last iteration did.
Definition loop.hpp:1425
const PollSchedule & schedule() const noexcept
Returns the schedule.
Definition loop.hpp:1003
PollingLoop(Runner &runner, Source &source, PollSchedule schedule={}, Clock clock={})
Constructs a loop.
Definition loop.hpp:985
void allow_absent_portfolio()
Lets the run place orders before holdings have ever been read (for a broker without a holdings inquir...
Definition loop.hpp:1209
const strategy::SessionWindow & session_window() const noexcept
Returns the session window.
Definition loop.hpp:1229
Runs one strategy: feeds it events, reconciles what it asks for, checks the risk limits,...
Definition runner.hpp:297
static constexpr bool on_fill_never_fires
true when this runner sends to a broker and the strategy defines on_fill, which then never fires (no ...
Definition runner.hpp:315
const strategy::SellableConventionProbe< Tag > & sellable_convention() const noexcept
Returns the sellable-convention probe.
Definition runner.hpp:524
std::size_t warmup_instructions_discarded() const noexcept
Returns how many instructions feed_warmup discarded.
Definition runner.hpp:1635
void set_price_limits(const InstrumentId &id, PriceLimits limits)
Records an instrument's price band for today, used to clamp order prices.
Definition runner.hpp:574
const Context & context() const noexcept
Returns the context.
Definition runner.hpp:543
A stop request the loop polls; safe to set from a signal handler.
Definition stop.hpp:158
OrderIntent with_tag(std::string tag) const
Returns a copy with a tag.
Definition intent.hpp:343
static OrderIntent market(InstrumentId instrument, OrderSide side, SizeSpec size, TimeInForce time_in_force=TimeInForce::DAY)
Makes a market order, which carries no price.
Definition intent.hpp:322
SellableEvidence overall() const
Returns the evidence summed over every instrument.
static SizeSpec quantity(Decimal64 amount)
Makes a size in shares or contracts.
Definition intent.hpp:147
Scalar missing()
Returns the value that stands for "no observation" (a quiet NaN).
Definition scalar.hpp:28
Runs a strategy: the runner, the polling loop, the order journal, and stopping.
Definition clock.hpp:7
constexpr std::string_view poll_kind_name(PollKind kind) noexcept
Returns a poll kind's lowercase name, such as "working_orders".
Definition loop.hpp:66
PollKind
What a PollingLoop polls for; each has a cadence in PollSchedule.
Definition loop.hpp:30
@ WORKING_ORDERS
Working orders, which reconciliation and cancellation need.
Definition loop.hpp:38
@ 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
@ HOLDINGS
Holdings, which orders are sized against.
Definition loop.hpp:36
@ QUOTES
Current quotes, for strategies that act between bars.
Definition loop.hpp:34
@ CALENDAR
The trading calendar, from which the loop derives the market phase; polled first, and on by default.
Definition loop.hpp:49
@ BUYING_POWER
Buying power, polled beside holdings (not before each order).
Definition loop.hpp:40
constexpr bool stop_intent_closes_positions(StopIntent intent) noexcept
Tests whether a stop intent closes positions with market orders, after withdrawing the working orders...
Definition stop.hpp:131
constexpr bool obligation_is_unaccounted(ObligationState state) noexcept
Tests whether an order in this state counts toward the loop's owed-order time limit (PollingLoop::set...
PollOutcome
What became of one poll.
Definition loop.hpp:117
@ RATE_LIMITED
The rate limiter refused the poll; not counted as a failure.
Definition loop.hpp:125
@ 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 poll_outcome_name(PollOutcome outcome) noexcept
Returns an outcome's lowercase name, such as "rate_limited".
Definition loop.hpp:141
constexpr PollKind all_poll_kinds[]
Every poll kind, in the order an iteration polls them.
Definition loop.hpp:87
constexpr std::array< PollOutcome, 4 > all_poll_outcomes
Every PollOutcome, in declaration order.
Definition loop.hpp:129
StopIntent
What an operator asks for when stopping a running loop.
Definition stop.hpp:29
@ QUIESCE
Stop sending new orders; nothing is cancelled or closed. The default, and what a signal means.
Definition stop.hpp:34
constexpr std::size_t poll_kind_count
The number of PollKind values.
Definition loop.hpp:109
constexpr bool obligation_needs_operator(ObligationState state) noexcept
Tests whether only an operator can settle an obligation in this state, so no read of the broker's lis...
@ OPERATOR
An order only an operator may account for (Runner::release_obligations).
constexpr bool stop_intent_closes(StopIntent intent) noexcept
Tests whether a stop intent asks for a closing pass (cancellations, and for FLATTEN closing orders) a...
Definition stop.hpp:114
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 stop(S &strategy, Ctx &context)
Calls strategy.on_stop(context) if the strategy declares on_stop; otherwise does nothing.
Definition concept.hpp:168
MarketPhase
Where the clock stands relative to the trading session.
Definition event.hpp:30
@ UNKNOWN
The calendar does not cover this date; nothing is known about it.
Definition event.hpp:32
constexpr std::string_view sellable_convention_name(SellableConvention convention) noexcept
Returns a lowercase name for a convention, such as "net_of_resting", for logs.
HaltReason
Who or what is holding trading stopped.
Definition risk.hpp:29
@ BUYING_POWER_UNREAD
A buying-power limit is enforced, but there is no current reading to check it against.
Definition risk.hpp:66
@ LOCAL_FAILURE
Local machinery failed, such as the order journal not being writable; nothing is sent.
Definition risk.hpp:39
@ PORTFOLIO_ABSENT
No holdings snapshot has been read yet, so what the account holds is unknown.
Definition risk.hpp:54
@ ORDER_LIST_UNREAD
The broker's order list has not been read since this process started.
Definition risk.hpp:48
@ OPERATOR
A person stopped trading with RiskGuard::halt(); only RiskGuard::resume() clears it.
Definition risk.hpp:31
@ PORTFOLIO_STALE
The holdings snapshot is older than the configured maximum age.
Definition risk.hpp:60
@ POLL_FAILING
A poll kept failing and the loop gave up, as opposed to STOPPING on request.
Definition risk.hpp:68
@ STOPPING
A stop was requested and the run is shutting down.
Definition risk.hpp:41
The library: broker-neutral types (core), and the modules below.
@ HOLDINGS
On the holdings snapshot, at no extra request.
Definition types.hpp:1094
constexpr bool lists_every_value(const std::array< Enum, N > &values, IsEnumerator is_enumerator) noexcept
Tests whether a list holds every value of an enum exactly once, in any order.
std::optional< Decimal64 > decimal_add(const Decimal64 &lhs, const Decimal64 &rhs)
Adds two decimals exactly.
Definition types.hpp:509
std::optional< Decimal64 > decimal_subtract(const Decimal64 &lhs, const Decimal64 &rhs)
Subtracts two decimals exactly.
Definition types.hpp:530
bool decimal_less(const Decimal64 &lhs, const Decimal64 &rhs)
Compares two decimals numerically, whatever their scales.
Definition types.hpp:665
OrderSide
The side of an order: its economic direction, buy or sell.
Definition types.hpp:1683
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.
Definition types.hpp:2453
Identifies an instrument: its country, venue and symbol.
Definition types.hpp:78
An exact amount of money in one currency.
Definition types.hpp:895
Currency currency_
The currency; the library never adds amounts in different currencies.
Definition types.hpp:899
Decimal64 amount_
The amount.
Definition types.hpp:897
The highest and lowest price an instrument may trade at today.
The trading days a broker reported for one market, ascending by date.
Definition calendar.hpp:44
One currency's running figure and how many statements contributed to it.
Definition loop.hpp:309
std::size_t statements_
How many statements contributed.
Definition loop.hpp:313
Commission and tax the broker stated on the orders a loop happened to poll (Toss states them; KIS doe...
Definition loop.hpp:303
std::size_t unrepresentable_sums_
How many additions or subtractions did not fit a Decimal64; nonzero means the figures are short (it c...
Definition loop.hpp:341
std::size_t orders_stating_none_
How many orders were first seen stating no cost (every KIS order); never decreased,...
Definition loop.hpp:326
std::size_t tax_statements_in(const Currency &currency) const
Returns how many tax statements a currency has.
Definition loop.hpp:375
std::size_t working_list_disappearances_
How many orders left the working-order listing between two polls (filled, cancelled or expired); a di...
Definition loop.hpp:331
std::size_t conflicting_restatements_
How many times an order restated a different cost at the same filled quantity; the first figure is ke...
Definition loop.hpp:336
std::vector< StatedTotal > tax_
Tax stated, per currency, on the same terms.
Definition loop.hpp:319
bool add(std::vector< StatedTotal > &totals, const Money &amount)
Adds an amount to its currency's figure in a list of totals.
Definition loop.hpp:386
static std::size_t statements_in(const std::vector< StatedTotal > &totals, const Currency &currency)
Returns a currency's statement count from a list of totals.
Definition loop.hpp:427
std::optional< Decimal64 > tax_in(const Currency &currency) const
Returns the tax stated in a currency.
Definition loop.hpp:357
std::optional< Decimal64 > commission_in(const Currency &currency) const
Returns the commission stated in a currency.
Definition loop.hpp:348
ChargedCosts since(const ChargedCosts &before) const
Returns what was observed since an earlier copy, for one run's report.
Definition loop.hpp:448
std::size_t commission_statements_in(const Currency &currency) const
Returns how many commission statements a currency has.
Definition loop.hpp:366
static std::optional< Decimal64 > figure_in(const std::vector< StatedTotal > &totals, const Currency &currency)
Returns a currency's figure from a list of totals.
Definition loop.hpp:409
std::vector< StatedTotal > commission_
Commission stated, per currency; a currency with no statement has no entry (not zero).
Definition loop.hpp:317
std::size_t orders_stating_cost_
How many orders have stated a cost (including zero); counted once per order, never decreased.
Definition loop.hpp:321
How a PollingLoop responds to polls that keep failing.
Definition loop.hpp:526
std::size_t consecutive_limit_
Consecutive failures of one poll kind after which the loop stops (POLL_FAILING).
Definition loop.hpp:528
What one loop iteration did.
Definition loop.hpp:532
bool phase_changed_
Whether a phase change was announced to the strategy.
Definition loop.hpp:554
strategy::MarketPhase phase_
The market phase the loop computed.
Definition loop.hpp:552
bool updated() const noexcept
Tests whether any poll brought new data.
Definition loop.hpp:560
std::optional< strategy::HaltReason > gate_
The hold that blocked placements, if any.
Definition loop.hpp:543
bool fed_
Whether any poll returned UPDATED: something was fed to the runner or installed in it.
Definition loop.hpp:545
std::vector< std::pair< PollKind, PollOutcome > > polls_
What each poll returned, in order.
Definition loop.hpp:536
std::chrono::system_clock::time_point at_
When the iteration ran.
Definition loop.hpp:534
What a whole PollingLoop::run did, and why it ended.
Definition loop.hpp:579
std::size_t closing_contended_
Positions whose closing order was sized down, or not sent, because another order could close them too...
Definition loop.hpp:723
std::size_t price_bands_withheld_
Plans built while a known price band was not applied (no age policy, too old, or dated after the cloc...
Definition loop.hpp:608
std::size_t phase_transitions_
How many phase changes were announced to the strategy.
Definition loop.hpp:655
bool abandoned_
Whether the loop gave up because a poll kept failing.
Definition loop.hpp:662
std::size_t plans_withheld_
Whole plans held back (any WithheldPlanReason); see Runner::plans_withheld.
Definition loop.hpp:639
std::size_t orders_reconcile_withheld_
Orders held back for an instrument of unknown order state; see Runner::orders_reconcile_withheld.
Definition loop.hpp:616
std::size_t flattened_
Closing orders queued for a FLATTEN (asked for, not necessarily sent).
Definition loop.hpp:670
std::optional< Obligation > owed_stop_
The order for which the loop stopped placing by itself (PollingLoop::set_owed_order_limit): owed long...
Definition loop.hpp:712
std::size_t iterations_
How many iterations ran.
Definition loop.hpp:581
strategy::MarketPhase final_phase_
The phase the last iteration computed.
Definition loop.hpp:595
std::size_t orders_unconfirmed_timed_out_
Sent orders that timed out unconfirmed; see Runner::set_confirmation_timeout for what each mode then ...
Definition loop.hpp:637
bool closing_wait_expired_
Whether that wait ended at its limit, with the closing pass built from what was in hand then.
Definition loop.hpp:701
std::size_t closing_placed_
Closing orders (placements, not cancellations) accepted by the executor (for a dry run,...
Definition loop.hpp:672
std::optional< PollKind > abandoned_kind_
The poll kind that exhausted its failure budget, if one did.
Definition loop.hpp:664
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 instructions_unavailable_
Instructions not sent because local machinery was unavailable; see Runner::instructions_unavailable.
Definition loop.hpp:641
std::size_t sends_outside_permitted_phase_
Orders sent in a phase RiskLimits::permitted_phases_ does not permit, because they close toward flat;...
Definition loop.hpp:660
std::size_t closing_rejected_
Closing instructions (placements and cancellations) rejected by reconciliation, the broker or the bin...
Definition loop.hpp:674
std::size_t closing_unavailable_
Closing instructions not sent because local machinery was unavailable; fix it, resume_after_local_fai...
Definition loop.hpp:685
std::size_t stop_handler_sent_
Instructions the strategy's on_stop got out during a stop; always zero.
Definition loop.hpp:770
std::size_t sends_under_a_revised_phase_
How many calls into a source or strategy both sent something and left the market phase changed.
Definition loop.hpp:603
std::size_t failed_[poll_kind_count]
How many polls of each kind failed; see failed().
Definition loop.hpp:587
bool closing_interrupted_
Whether a WITHDRAW or FLATTEN was pending when the run was abandoned for failing polls,...
Definition loop.hpp:706
std::size_t failed(PollKind kind) const noexcept
Returns how many polls of a kind failed.
Definition loop.hpp:789
std::chrono::nanoseconds closing_waited_
The time from the loop taking up a stop to its closing pass (or to the run's abandonment),...
Definition loop.hpp:699
std::size_t stop_handler_refused_
Instructions the strategy's on_stop asked for during a stop, refused as intended (the operator's orde...
Definition loop.hpp:768
std::size_t cancels_rejected_
Cancellations refused by the broker or binding; see Runner::cancels_rejected.
Definition loop.hpp:643
std::optional< strategy::Rejection > last_unbuildable_
Why reconciliation could not build the last such instruction in this run, if any.
Definition loop.hpp:653
std::size_t closing_indeterminate_
Closing instructions (cancellations included) with an unknown outcome.
Definition loop.hpp:683
std::size_t closing_withheld_
Closing instructions held back because the instrument has an order of unknown outcome,...
Definition loop.hpp:681
std::optional< strategy::HaltReason > final_gate_
The most fundamental hold in force when the run ended, whoever took it.
Definition loop.hpp:593
std::size_t closing_unresolved_
Closing cancellations naming an order the portfolio does not list; read the broker's list rather than...
Definition loop.hpp:687
std::size_t orders_guard_refused_
Orders the risk guard refused; see Runner::orders_guard_refused.
Definition loop.hpp:618
bool order_list_read_
Whether the broker's order list was read at least once.
Definition loop.hpp:666
std::size_t withdrawn_
Cancellations queued for a WITHDRAW or FLATTEN (asked for, not necessarily sent).
Definition loop.hpp:668
bool closing_source_unverified_
Whether the closing pass could not trust its picture of the account. Makes closing_complete() false.
Definition loop.hpp:746
ChargedCosts charged_costs_
Commission and tax the broker stated on the orders this run saw; a subtotal (see ChargedCosts).
Definition loop.hpp:583
std::optional< BrokerRefusal > last_broker_refusal_
The broker's code and message for the last order it refused during this run, if any.
Definition loop.hpp:651
std::size_t unclamped_orders_refused_
Orders refused before sending while carrying such a price; see Runner::unclamped_orders_refused.
Definition loop.hpp:612
std::size_t cancels_indeterminate_
Cancellations with an unknown outcome.
Definition loop.hpp:649
std::optional< StopIntent > stopped_by_
The stop intent that ended the run, if one did.
Definition loop.hpp:591
std::size_t orders_indeterminate_
Orders with an unknown outcome (SendOutcome::INDETERMINATE, including most KIS, Toss and Kiwoom refus...
Definition loop.hpp:631
std::size_t cancels_unresolved_
Cancellations naming an order the portfolio does not list.
Definition loop.hpp:647
std::size_t attempted(PollKind kind) const noexcept
Returns how many polls of a kind were attempted.
Definition loop.hpp:780
bool closing_complete() const noexcept
Tests whether every closing instruction the loop asked for was accepted, and the loop knew what to as...
Definition loop.hpp:761
std::size_t closing_refused_
Closing instructions the risk guard refused.
Definition loop.hpp:676
std::size_t cancels_refused_
Cancellations a hold refused.
Definition loop.hpp:645
std::size_t attempted_[poll_kind_count]
How many polls of each kind were attempted, indexed by PollKind; see attempted().
Definition loop.hpp:585
std::size_t orders_unbuildable_
Orders reconciliation could not build during this run's polling; see Runner::orders_unbuildable.
Definition loop.hpp:614
std::size_t closing_unconfirmed_
Orders a serialized runner still owed when the closing pass began that neither holdings nor a listing...
Definition loop.hpp:693
std::size_t unclamped_orders_rejected_
Sent orders the broker refused while carrying a price a withheld band would have clamped; see Runner:...
Definition loop.hpp:610
std::size_t closing_unsizable_
Positions whose closing order could not be sized: the holding rows do not add up, the closing quantit...
Definition loop.hpp:730
std::vector< std::string > notes_
Notes for the operator, such as an unrecognized control-file word or a missing policy.
Definition loop.hpp:773
std::size_t orders_binding_refused_
Orders the binding refused to send; see Runner::orders_binding_refused.
Definition loop.hpp:620
std::size_t orders_broker_refused_
Orders the broker refused (SendOutcome::REJECTED); see last_broker_refusal_. At KIS,...
Definition loop.hpp:626
How often a PollingLoop polls each kind; an unset interval disables that poll.
Definition loop.hpp:169
std::optional< std::chrono::nanoseconds > interval_for(PollKind kind) const noexcept
Returns the interval for a poll kind.
Definition loop.hpp:216
std::optional< std::chrono::nanoseconds > calendar_
How often to read the trading calendar; six hours by default.
Definition loop.hpp:203
std::optional< std::chrono::nanoseconds > holdings_
How often to read holdings; without it the portfolio is never refreshed.
Definition loop.hpp:175
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
std::optional< std::chrono::nanoseconds > working_orders_
How often to read the working orders.
Definition loop.hpp:185
std::optional< std::chrono::nanoseconds > buying_power_
How often to read buying power.
Definition loop.hpp:192
std::optional< std::chrono::nanoseconds > quotes_
How often to fetch quotes.
Definition loop.hpp:173
Detects whether a runner reports kept_intents_dropped().
Definition loop.hpp:276
Detects whether a runner reports warmup_instructions_discarded().
Definition loop.hpp:288
Detects Runner::keeps_sent_orders_until_listed.
Definition loop.hpp:264
Detects whether a source provides const std::vector<Currency>& buying_power_currencies() const.
Definition loop.hpp:518
Detects whether a source declares static constexpr bool provides_working_orders = true,...
Definition loop.hpp:246
Detects whether a source provides const ChargedCosts& charged_costs() const, which LoopReport then re...
Definition loop.hpp:501
The intraday session of a market, as the operator states it.
Definition event.hpp:106
static SessionWindow krx()
Returns the KRX regular session, 09:00-15:30 KST.
Definition event.hpp:120