algotradelib 0.0.0.9000
C++ header-only library for algorithmic trading
Loading...
Searching...
No Matches
runner.hpp
1#pragma once
2
3#include "./context.hpp"
4#include "./dispatch.hpp"
5#include "./execution.hpp"
6#include "./journal.hpp"
7
9#include "../strategy/risk.hpp"
10#include "../strategy/sellable_convention.hpp"
11
12#include <algotradelib/core>
13
14#include <algorithm>
15#include <cstddef>
16#include <iterator>
17#include <map>
18#include <optional>
19#include <stdexcept>
20#include <string>
21#include <unordered_map>
22#include <variant>
23#include <utility>
24#include <vector>
25
26namespace algotradelib {
27namespace runner {
28
29namespace runner_detail {
30
31// Whether an executor can ask the broker what may be sold of one instrument.
32//
33// Detected rather than required, for the same reason the strategy's handlers are: a dry runner, a
34// replay's queueing executor and a test double have no broker to ask, and none of them should have
35// to grow a method that returns nothing. An executor bound to a broker whose holdings already carry
36// the figure declares it too and answers nothing, so the runner's own condition stays one test.
37template <typename Executor, typename = void>
38struct fills_sellable : std::false_type {};
39
40template <typename Executor>
41struct fills_sellable<
42 Executor,
43 std::void_t<
44 decltype(std::declval<Executor&>().sellable_quantity(std::declval<const InstrumentId&>()))
45 >
46> : std::true_type {};
47
48// Whether `Executor` states what its sellable *inquiry* answer is evidence of.
49//
50// Detected rather than required, and the default is what matters: an executor that answers a
51// sellable quantity and says nothing about its basis is treated as having established nothing, so
52// the guard refuses rather than measuring a sell against a figure nobody characterized. A consumer's
53// own executor gets that default without being told it exists, which is the point -- the alternative
54// default would silently enforce against whatever its author happened to return.
55template <typename Executor, typename = void>
56struct states_sellable_basis : std::false_type {};
57
58template <typename Executor>
59struct states_sellable_basis<
60 Executor,
61 std::void_t<decltype(Executor::sellable_inquiry_basis())>
62> : std::true_type {};
63
64// Returns what `Executor`'s inquiry answer is evidence of, or an unestablished basis.
65template <typename Executor>
66constexpr SellableBasis sellable_inquiry_basis_of() {
67 if constexpr (states_sellable_basis<Executor>::value) {
68 return Executor::sellable_inquiry_basis();
69 } else {
70 return SellableBasis{};
71 }
72}
73
74// Whether `Executor` can say which reference the broker issued for the order it just accepted.
75//
76// Detected rather than required, for the same reason as above: a dry runner and a replay's
77// queueing executor have no broker to have been answered by. A reservation written without one is
78// still worth writing -- a limit that ignored the order would be the worse failure -- but it cannot
79// be cancelled by reference until a listing supplies one.
80template <typename Executor, typename = void>
81struct reports_last_ref : std::false_type {};
82
83template <typename Executor>
84struct reports_last_ref<
85 Executor, std::void_t<decltype(std::declval<const Executor&>().last_ref())>
86> : std::true_type {};
87
88// Whether `Executor` decides which venue an order that names none is sent to.
89//
90// Detected rather than required: only a broker binding routes, and a dry runner, an observer and a
91// replay's queueing executor send the planned venue as it stands. `live::SessionExecutor` answers
92// with its `default_route()`, and a `JournalingExecutor` forwards its inner executor's answer.
93template <typename Executor, typename = void>
94struct resolves_route : std::false_type {};
95
96template <typename Executor>
97struct resolves_route<
98 Executor, std::void_t<decltype(std::declval<const Executor&>().requested_route(Venue::UNSPECIFIED))>
99> : std::true_type {};
100
101// Whether `Executor` can say **why** the last instruction failed.
102//
103// Detected rather than required, on the same terms as the two above: a dry runner refuses nothing
104// and a replay's queueing executor has no broker to be refused by, so neither should have to grow a
105// method that answers nothing. An executor that binds a session declares
106// `const std::optional<BrokerError>& last_error() const` and its refusals then reach
107// `Runner::last_broker_refusal`, where a report can carry the broker's own code and message instead
108// of a count with nothing to chase.
109//
110// **The *shape* is detected, not the name.** An earlier version asked only whether `last_error()`
111// was a valid expression, which would have selected this branch for any executor that happened to
112// have a method of that name -- one returning `std::optional<std::string>`, say -- and then failed
113// to compile on `broker_code_`. A name is not a contract; the return type is.
114template <typename Executor, typename = void>
115struct states_last_error : std::false_type {};
116
117template <typename Executor>
118struct states_last_error<
119 Executor,
120 std::enable_if_t<
121 std::is_same_v<
122 std::decay_t<decltype(std::declval<const Executor&>().last_error())>,
123 std::optional<BrokerError>
124 >
125 >
126> : std::true_type {};
127
128// Whether `Executor` journals what it sends (`JournalingExecutor`); a runner sending through one to a
129// broker is serialized unless told otherwise (`Runner::allow_overlapping_orders`).
130template <typename Executor, typename = void>
131struct journals : std::false_type {};
132
133template <typename Executor>
134struct journals<Executor, std::void_t<decltype(std::declval<const Executor&>().journal_failures())>>
135 : std::true_type {};
136
137// Whether `Executor` writes what serialization needs: the holding an order was decided against, the
138// line it wrote for the order, and a line the runner builds (a discharge or a hold).
139template <typename Executor, typename = void>
140struct serializes : std::false_type {};
141
142template <typename Executor>
143struct serializes<
144 Executor,
145 std::void_t<
146 decltype(std::declval<Executor&>().place(
147 std::declval<const strategy::PlannedOrder&>(), std::optional<Decimal64>{}
148 )),
149 decltype(std::declval<const Executor&>().last_entry()),
150 decltype(std::declval<Executor&>().record(std::declval<JournalEntry&>()))
151 >
152> : std::true_type {};
153
154} // namespace runner_detail
155
160template <typename BrokerTag>
167
172template <typename BrokerTag>
182
191template <typename BrokerTag>
194 std::vector<strategy::PlannedOrder> would_place_;
201 std::vector<OrderRef<BrokerTag>> would_cancel_;
208 std::vector<strategy::Rejection> rejected_;
210 std::vector<strategy::GuardRejection> refused_;
217 std::vector<WithheldOrder> withheld_;
222 std::vector<WithheldPlan<BrokerTag>> withheld_plans_;
227 std::vector<strategy::PlannedOrder> indeterminate_;
233 std::vector<RejectedCancel<BrokerTag>> rejected_cancel_;
235 std::vector<OrderRef<BrokerTag>> indeterminate_cancel_;
237 std::vector<OrderRef<BrokerTag>> unresolved_cancel_;
242 std::vector<WithheldOrder> unavailable_;
244 std::vector<OrderRef<BrokerTag>> unavailable_cancel_;
252 std::vector<RefusedCancel<BrokerTag>> refused_cancel_;
254 std::vector<std::string> log_;
255
260 bool quiet() const noexcept {
261 // Every outcome bucket belongs here; one left out would report as silence.
262 return would_place_.empty() && would_cancel_.empty() && rejected_.empty() &&
263 refused_.empty() && withheld_.empty() && withheld_plans_.empty() && indeterminate_.empty() &&
264 unresolved_cancel_.empty() && unavailable_.empty() && unavailable_cancel_.empty() &&
265 refused_cancel_.empty() && indeterminate_cancel_.empty() && rejected_cancel_.empty();
266 }
267};
268
270struct DryTag {
272 static constexpr std::string_view name = "dryrun";
273};
274
291template <
292 typename Strategy,
293 typename CapsType = strategy::NoCaps,
294 typename BrokerTag = DryTag,
295 typename Executor = RecordingExecutor<DryTag>
296>
297class Runner {
298public:
300 using Tag = BrokerTag;
305
306 ATL_ASSERT_STRATEGY(Strategy, Context);
307
315 static constexpr bool on_fill_never_fires =
316 runner_detail::sends_to_a_broker<Executor>::value &&
317 strategy::strategy_detail::declares_on_fill<Strategy>;
318
324 static constexpr bool takes_bar_slices = strategy::strategy_detail::declares_on_bars<Strategy>;
325
333 static constexpr bool keeps_sent_orders_until_listed =
334 runner_detail::sends_to_a_broker<Executor>::value && CapsType::has_working_orders;
335
340 static constexpr bool journals_to_a_broker =
341 runner_detail::sends_to_a_broker<Executor>::value && runner_detail::journals<Executor>::value;
342
351 explicit Runner(
352 Strategy strategy = {},
353 strategy::RiskLimits limits = {},
354 std::size_t series_capacity = 512,
355 Executor executor = {}
356 )
357 : strategy_(std::move(strategy)),
358 guard_(std::move(limits)),
359 context_(series_capacity),
360 executor_(std::move(executor)) {}
361
366 const Executor& executor() const noexcept { return executor_; }
371 Executor& executor() noexcept { return executor_; }
372
380 const IndeterminacyLedger& ledger() const noexcept { return ledger_; }
385 IndeterminacyLedger& ledger() noexcept { return ledger_; }
386
395 std::optional<std::string> local_failure() const {
396 if (!guard_.halted(strategy::HaltReason::LOCAL_FAILURE)) {
397 return std::nullopt;
398 }
399 return guard_.hold_detail(strategy::HaltReason::LOCAL_FAILURE);
400 }
401
410 acknowledged_journal_failures_ = executor_journal_failures();
411 }
412
423 void set_maximum_portfolio_age(std::optional<std::chrono::nanoseconds> age) {
424 maximum_portfolio_age_ = age;
425 }
426
436 void set_observed_at(std::chrono::system_clock::time_point at) { observed_at_ = at; }
437
442 std::chrono::system_clock::time_point observed_at() const noexcept { return observed_at_; }
443
450 void allow_absent_portfolio() { allow_absent_portfolio_ = true; }
451
462 void require_known_portfolio(bool required) { require_known_portfolio_ = required; }
463
472 void halt_locally(std::string detail) { note_local_failure(std::move(detail)); }
473
478 std::size_t placements_at_broker() const noexcept { return placements_at_broker_; }
479
490 std::size_t sends_outside_permitted_phase() const noexcept {
491 return sends_outside_permitted_phase_;
492 }
493
503 void probe_sellable_convention(bool enabled = true) { probing_sellable_ = enabled; }
504
512 // Skipped, not forgotten, while the listing is newer than the holdings: the resting quantity
513 // and the sellable figure would come from two moments, and a sell starting to rest between them
514 // reads as a figure that did not move. The baseline stays the last paired reading.
515 if (probing_sellable_ && !context_.portfolio().listing_newer_than_holdings()) {
516 sellable_probe_.observe(context_.portfolio());
517 }
518 }
519
525 return sellable_probe_;
526 }
527
532 const Strategy& strategy() const noexcept { return strategy_; }
537 Strategy& strategy() noexcept { return strategy_; }
538
543 const Context& context() const noexcept { return context_; }
548 Context& context() noexcept { return context_; }
549
554 const strategy::RiskGuard& guard() const noexcept { return guard_; }
559 strategy::RiskGuard& guard() noexcept { return guard_; }
560
574 void set_price_limits(const InstrumentId& id, PriceLimits limits) {
575 DatedBand dated;
576 dated.received_at_ = observed_at_;
577 dated.limits_ = std::move(limits);
578 price_limits_.insert_or_assign(id, std::move(dated));
579 }
580
597 void set_maximum_price_limits_age(std::optional<std::chrono::nanoseconds> age) {
598 maximum_price_limits_age_ = age;
599 // This call is now the whole policy, so it also withdraws "any age".
600 allow_unbounded_price_limits_age_ = false;
601 }
602
605 allow_unbounded_price_limits_age_ = true;
606 maximum_price_limits_age_.reset();
607 }
608
613 std::size_t price_bands_withheld() const noexcept { return price_bands_withheld_; }
614
619 bool price_limits_age_policy_stated() const noexcept {
620 return maximum_price_limits_age_.has_value() || allow_unbounded_price_limits_age_;
621 }
622
632 std::size_t unclamped_orders_rejected() const noexcept { return unclamped_orders_rejected_; }
633
639 std::size_t unclamped_orders_refused() const noexcept { return unclamped_orders_refused_; }
640
654 std::size_t orders_unbuildable() const noexcept { return orders_unbuildable_; }
660 std::size_t orders_reconcile_withheld() const noexcept { return orders_reconcile_withheld_; }
665 std::size_t orders_guard_refused() const noexcept { return orders_guard_refused_; }
670 std::size_t orders_binding_refused() const noexcept { return orders_binding_refused_; }
677 std::size_t orders_broker_refused() const noexcept { return orders_broker_refused_; }
683 std::size_t orders_indeterminate() const noexcept { return orders_indeterminate_; }
684
691 std::size_t plans_withheld() const noexcept { return plans_withheld_; }
692
711 std::vector<strategy::TargetPosition> deferred_targets() const {
712 std::vector<strategy::TargetPosition> targets;
713 targets.reserve(deferred_targets_.size());
714 for (const auto& deferred : deferred_targets_) {
715 targets.push_back(deferred.target_);
716 }
717 return targets;
718 }
719
724 std::size_t targets_deferred() const noexcept { return targets_deferred_; }
725
730 std::size_t deferred_dropped_after_fill() const noexcept { return deferred_dropped_after_fill_; }
731
737 std::optional<std::chrono::system_clock::time_point> deferred_awaiting_evidence() const {
738 std::optional<std::chrono::system_clock::time_point> earliest;
739 for (const auto& deferred : deferred_targets_) {
740 if (stop_listed(deferred.target_.instrument_)) {
741 continue;
742 }
743 if (!earliest || deferred.deferred_at_ < *earliest) {
744 earliest = deferred.deferred_at_;
745 }
746 }
747 return earliest;
748 }
749
759 Report retry_deferred(const std::vector<OrderSnapshot>& evidence) {
760 return release_deferred(&evidence);
761 }
762
784 std::size_t unconfirmed_orders() const noexcept { return unconfirmed_.size(); }
785
791 void refresh_unconfirmed() { confirm_reservations(); }
792
812 [[maybe_unused]] const InstrumentId& instrument, [[maybe_unused]] OrderSide side,
813 [[maybe_unused]] const std::optional<std::string>& order_id, [[maybe_unused]] const std::string& journal_id = {}
814 ) {
815 if constexpr (keeps_sent_orders_until_listed) {
816 if (order_id && order_id->empty()) {
817 return;
818 }
819 const auto found = serialized() ? unnamed_by_journal_id(journal_id) : newest_unnamed(instrument, side);
820 // Serialized, a sweep only names: absence from a listing is not proof the order was never placed.
821 if (found == unconfirmed_.end() || (serialized() && !order_id)) {
822 return;
823 }
824 // The row first: it is recognized by the reference about to change, and left behind it would be
825 // counted beside the rebuilt one.
826 drop_reserved_row(*found);
827 if (order_id) {
828 // The primary part only: a broker's row is matched on it everywhere, and the listing that
829 // carries the order supplies the rest of the reference.
830 found->ref_.primary_ = *order_id;
831 found->row_.ref_.primary_ = *order_id;
832 // Named now, so a fresh start: a hand-cleared hold may have let it miss its history meanwhile.
833 found->history_missed_ = false;
834 if (found->unexplained_since_) {
835 found->unexplained_since_ = freshness_instant();
836 }
837 } else {
838 unconfirmed_.erase(found);
839 }
840 confirm_reservations();
841 }
842 }
843
885 bool serialized() const noexcept { return journals_to_a_broker && !overlapping_; }
886
899 if (placements_at_broker_ != 0 || recovered_) {
900 throw std::logic_error("allow_overlapping_orders must be chosen before recovery and before trading");
901 }
902 overlapping_ = true;
903 }
904
912 auto state = ObligationState::NONE;
913 if (!serialized()) {
914 return state;
915 }
916 for (const auto& entry : unconfirmed_) {
917 if (!(entry.instrument_ == instrument)) {
918 continue;
919 }
920 const auto owed = state_of(entry);
921 if (obligation_weight(state) < obligation_weight(owed)) {
922 state = owed;
923 }
924 }
925 return state;
926 }
927
933 std::vector<Obligation> obligations(const InstrumentId& instrument) const {
934 auto owed = obligations();
935 owed.erase(
936 std::remove_if(owed.begin(), owed.end(), [&](const Obligation& one) { return !(one.instrument_ == instrument); }),
937 owed.end()
938 );
939 return owed;
940 }
941
946 std::vector<Obligation> obligations() const {
947 std::vector<Obligation> owed;
948 if (!serialized()) {
949 return owed;
950 }
951 for (const auto& entry : unconfirmed_) {
952 Obligation obligation;
953 obligation.instrument_ = entry.instrument_;
954 obligation.journal_id_ = entry.journal_ ? entry.journal_->id_ : std::string();
955 obligation.order_id_ = entry.ref_.primary_;
956 obligation.side_ = entry.side_;
957 obligation.quantity_ = entry.expected_;
958 obligation.sent_at_ = entry.sent_at_;
959 obligation.state_ = state_of(entry);
960 owed.push_back(std::move(obligation));
961 }
962 return owed;
963 }
964
980 bool release_obligations([[maybe_unused]] const InstrumentId& instrument, [[maybe_unused]] const std::string& reason) {
981 bool written = true;
982 if constexpr (can_serialize) {
983 for (auto at = unconfirmed_.begin(); at != unconfirmed_.end();) {
984 if (!(at->instrument_ == instrument)) {
985 ++at;
986 continue;
987 }
988 auto line = closing_line(*at, JournalOutcome::CONFIRMED, std::nullopt, "released by an operator: " + reason);
989 if (!executor_.record(line)) {
990 written = false;
991 note_executor_failure(false);
992 ++at;
993 continue;
994 }
995 set_boundary(instrument, line.recorded_at_, /*strict=*/true);
996 drop_reserved_row(*at);
997 at = unconfirmed_.erase(at);
998 }
999 confirm_reservations();
1000 }
1001 return written;
1002 }
1003
1017 void restore_obligations([[maybe_unused]] const JournalReplay& replay) {
1018 recovered_ = true;
1019 if constexpr (can_serialize) {
1020 if (!serialized()) {
1021 return;
1022 }
1023 restore_from(replay);
1024 }
1025 }
1026
1033 std::size_t orders_awaiting_confirmation() const noexcept { return orders_awaiting_confirmation_; }
1034
1043 std::size_t obligations_unexplained() const noexcept {
1044 if (!serialized()) {
1045 return 0;
1046 }
1047 return static_cast<std::size_t>(std::count_if(unconfirmed_.begin(), unconfirmed_.end(), [](const Unconfirmed& entry) {
1048 return entry.operator_only_ || entry.residual_.coefficient() != 0;
1049 }));
1050 }
1051
1067 void set_confirmation_timeout(std::chrono::nanoseconds timeout) { confirmation_timeout_ = timeout; }
1068
1073 std::size_t orders_unconfirmed_timed_out() const noexcept { return orders_unconfirmed_timed_out_; }
1074
1088 std::optional<std::chrono::system_clock::time_point> unconfirmed_awaiting_evidence() {
1089 // Against the portfolio as it stands: a listing just installed may carry, or no longer carry, an
1090 // order, and no pass has run since.
1091 confirm_reservations();
1092 std::optional<std::chrono::system_clock::time_point> earliest;
1093 const auto now = freshness_instant();
1094 for (const auto& entry : unconfirmed_) {
1095 if (!awaits_evidence(entry, now)) {
1096 continue;
1097 }
1098 if (!earliest || entry.sent_at_ < *earliest) {
1099 earliest = entry.sent_at_;
1100 }
1101 }
1102 return earliest;
1103 }
1104
1118 void confirm_from_history(const std::vector<OrderSnapshot>& evidence) {
1119 if (serialized()) {
1120 confirm_serialized_from_history(evidence);
1121 return;
1122 }
1123 confirm_reservations();
1124 const auto now = freshness_instant();
1125 for (auto& entry : unconfirmed_) {
1126 entry.consulted_ = awaits_evidence(entry, now);
1127 if (!entry.consulted_) {
1128 continue;
1129 }
1130 // An entry not yet named matches no row: a row whose number is missing would read as equal.
1131 if (entry.ref_.primary_.empty()) {
1132 continue;
1133 }
1134 const auto* row = history_row_for(
1135 evidence, entry.ref_.primary_, entry.instrument_, entry.side_,
1136 entry.sent_at_ - order_time_tolerance_, entry.sent_at_ + order_time_tolerance_
1137 );
1138 if (row != nullptr && (entry.session_ended_ || !strategy::order_is_open(row->status_))) {
1139 if (decimal_less(row->execution_.filled_quantity_, entry.expected_)) {
1140 entry.expected_ = row->execution_.filled_quantity_;
1141 }
1142 entry.settled_ = true;
1143 // Holdings get a full timeout from here to show it (`confirm_reservations`).
1144 entry.unexplained_since_ = now;
1145 }
1146 }
1147 // Past the timeout and the history had no answer: noted, and judged by `confirm_reservations` against
1148 // the next holdings read, as a settled order is. Marking here, on the clock, marked a fill one
1149 // holdings read early on a loop polling slower than the timeout (the listing, and so this history,
1150 // is read before holdings in each iteration).
1151 for (auto& entry : unconfirmed_) {
1152 if (entry.consulted_ && !entry.settled_ && timed_out(entry, now)) {
1153 entry.history_missed_ = true;
1154 }
1155 }
1156 // Retires what the history just settled, and withholds what the holdings in hand already condemn.
1157 confirm_reservations();
1158 }
1159
1198 // Once per iteration, so the context shows what this iteration's holdings confirmed even when no
1199 // pass runs (`unconfirmed_orders`).
1200 confirm_reservations();
1201 if (guard_.halted(strategy::HaltReason::OPERATOR) ||
1202 guard_.halted(strategy::HaltReason::LOCAL_FAILURE)) {
1203 retained_targets_.erase(
1204 std::remove_if(
1205 retained_targets_.begin(), retained_targets_.end(),
1206 [](const RetainedTarget& kept) { return kept.for_phase_; }
1207 ),
1208 retained_targets_.end()
1209 );
1210 kept_intents_.clear();
1211 }
1212 // **A statement still queued is newer than anything retained.** Releasing retained work appends it to
1213 // the queue, behind what a caller driving the runner itself has queued, where it would be settled as
1214 // the latest word on its instrument. So what a queued target or order speaks about is forgotten first,
1215 // as `settle` would forget it (a `CancelIntent` names an order, not an instrument, and forgets nothing).
1216 for (const auto& queued : context_.queued()) {
1217 forget_retained(instruction_instrument(queued));
1218 }
1219 if ((retained_targets_.empty() && kept_intents_.empty()) ||
1220 context_.portfolio().as_of_ == std::chrono::system_clock::time_point{} ||
1221 context_.portfolio().listing_newer_than_holdings() || guard_.blocking_reason()) {
1222 return Report{};
1223 }
1224 const bool phase_permits = guard_.permits_phase(context_.phase());
1225 std::vector<RetainedTarget> kept;
1226 bool released = false;
1227 const auto as_of = context_.portfolio().as_of_;
1228 // Kept because the broker could not be reached: not offered again until it has answered since.
1229 const auto awaiting_broker = [&](const auto& entry) { return entry.after_ && !(*entry.after_ < as_of); };
1230 for (auto& entry : retained_targets_) {
1231 if ((entry.for_phase_ && !phase_permits) || awaiting_broker(entry) ||
1232 (entry.until_accounted_ && owes(entry.target_.instrument_))) {
1233 kept.push_back(std::move(entry));
1234 continue;
1235 }
1236 context_.submit(std::move(entry.target_));
1237 released = true;
1238 }
1239 retained_targets_ = std::move(kept);
1240 // Kept intents follow, in order, once the phase permits; each goes out only if the holding it was
1241 // decided against is unchanged (and, for a stop, no stop has come to rest meanwhile).
1242 if (phase_permits && !kept_intents_.empty()) {
1243 auto intents = std::move(kept_intents_);
1244 kept_intents_.clear();
1245 for (auto& entry : intents) {
1246 if (awaiting_broker(entry)) {
1247 kept_intents_.push_back(std::move(entry));
1248 continue;
1249 }
1250 const auto held = context_.portfolio().net_quantity_of(entry.intent_.instrument());
1251 const auto gap = held ? decimal_subtract(*held, entry.held_) : std::nullopt;
1252 // A weight is planned again as the position it states, against the holding at release, so
1253 // the book it was decided against does not bind it.
1254 const bool weight = strategy::size_kind_is_target(entry.intent_.size().kind());
1255 const bool same_book = weight || (gap && gap->coefficient() == 0);
1256 const bool stop_now_rests = entry.intent_.trigger() && stop_listed(entry.intent_.instrument());
1257 if (!same_book || stop_now_rests) {
1258 ++kept_intents_dropped_;
1259 continue;
1260 }
1261 context_.submit(std::move(entry.intent_));
1262 released = true;
1263 }
1264 }
1265 if (!released) {
1266 return Report{};
1267 }
1268 return settle();
1269 }
1270
1276 std::size_t intents_kept_for_phase() const noexcept { return kept_intents_.size(); }
1277
1283 std::size_t kept_intents_dropped() const noexcept { return kept_intents_dropped_; }
1284
1289 std::size_t targets_retained() const noexcept { return retained_targets_.size(); }
1290
1296 std::size_t targets_retained_for_phase() const noexcept {
1297 return static_cast<std::size_t>(std::count_if(
1298 retained_targets_.begin(), retained_targets_.end(),
1299 [](const RetainedTarget& kept) { return kept.for_phase_; }
1300 ));
1301 }
1302
1304 void clear_retained_targets() noexcept {
1305 retained_targets_.clear();
1306 kept_intents_.clear();
1307 }
1308
1314 void clear_deferred_targets() noexcept { deferred_targets_.clear(); }
1315
1326 std::size_t obligations_awaiting_account() const noexcept {
1327 if (!serialized()) {
1328 return 0;
1329 }
1330 return static_cast<std::size_t>(std::count_if(unconfirmed_.begin(), unconfirmed_.end(), [](const Unconfirmed& entry) {
1331 return !entry.operator_only_ && (!entry.listed_now_ || entry.withdrawn_);
1332 }));
1333 }
1334
1346 bool withdrawal_accepted_for(const WorkingOrder<Tag>& working) const {
1347 if constexpr (keeps_sent_orders_until_listed) {
1348 for (const auto& entry : unconfirmed_) {
1349 if (entry.withdrawn_ && !entry.ref_.primary_.empty() && working.ref_.primary_ == entry.ref_.primary_ &&
1350 working.order_.order_id_ == entry.ref_.primary_ && own_row(working.order_, entry)) {
1351 return true;
1352 }
1353 }
1354 }
1355 return false;
1356 }
1357
1367 Report retry_deferred_on_one_snapshot() { return release_deferred(nullptr); }
1368
1373 std::size_t instructions_unavailable() const noexcept { return instructions_unavailable_; }
1374
1379 std::size_t cancels_rejected() const noexcept { return cancels_rejected_; }
1384 std::size_t cancels_refused() const noexcept { return cancels_refused_; }
1389 std::size_t cancels_unresolved() const noexcept { return cancels_unresolved_; }
1394 std::size_t cancels_indeterminate() const noexcept { return cancels_indeterminate_; }
1395
1404 const std::optional<BrokerRefusal>& last_broker_refusal() const noexcept {
1405 return last_broker_refusal_;
1406 }
1407
1416 const std::optional<strategy::Rejection>& last_unbuildable() const noexcept {
1417 return last_unbuildable_;
1418 }
1419
1425 tolerance_ = std::move(tolerance);
1426 }
1427
1436 tolerance_.minimum_quantity_ = std::move(quantity);
1437 }
1438
1448 portfolio_value_ = std::move(value);
1449 portfolio_value_currency_.reset();
1450 }
1451
1461 portfolio_value_ = std::move(value.amount_);
1462 portfolio_value_currency_ = std::move(value.currency_);
1463 }
1464
1466 void clear_portfolio_value() noexcept {
1467 portfolio_value_.reset();
1468 portfolio_value_currency_.reset();
1469 }
1470
1476 dispatch::start(strategy_, context_);
1477 return settle();
1478 }
1479
1488 dispatch::stop(strategy_, context_);
1489 return settle();
1490 }
1491
1499 dispatch::stop(strategy_, context_);
1500 return settle();
1501 }
1502
1510 Report settle_closing() { return settle(); }
1511
1525#if defined(ATL_HAS_EIGEN)
1526 context_.record_bar(event);
1527#endif
1528 context_.set_last_price(event.instrument_, event.bar_.close_);
1529 context_.set_now(event.period_end_);
1530 confirm_reservations();
1531 dispatch::bar(strategy_, context_, event);
1532 return settle();
1533 }
1534
1553 void feed_warmup(const strategy::BarEvent& event) {
1554#if defined(ATL_HAS_EIGEN)
1555 context_.record_bar(event);
1556#endif
1557 context_.set_last_price(event.instrument_, event.bar_.close_);
1558 context_.set_now(event.period_end_);
1559 dispatch::bar(strategy_, context_, event);
1560 warmup_instructions_discarded_ += context_.drain().size();
1561 }
1562
1574 void record(const strategy::BarEvent& event) {
1575#if defined(ATL_HAS_EIGEN)
1576 context_.record_bar(event);
1577#endif
1578 context_.set_last_price(event.instrument_, event.bar_.close_);
1579 context_.set_now(event.period_end_);
1580 }
1581
1594#if defined(ATL_HAS_EIGEN)
1595 context_.record_bar(event);
1596#else
1597 static_cast<void>(event);
1598#endif
1599 }
1600
1612 confirm_reservations();
1613 dispatch::bars(strategy_, context_, slice);
1614 return settle();
1615 }
1616
1626 void feed_warmup(const strategy::BarSlice& slice) {
1627 dispatch::bars(strategy_, context_, slice);
1628 warmup_instructions_discarded_ += context_.drain().size();
1629 }
1630
1635 std::size_t warmup_instructions_discarded() const noexcept { return warmup_instructions_discarded_; }
1636
1643 context_.set_last_price(event.quote_.instrument_, event.quote_.last_price_);
1644 context_.set_now(event.quote_.received_at_);
1645 confirm_reservations();
1646 dispatch::quote(strategy_, context_, event);
1647 return settle();
1648 }
1649
1656 confirm_reservations();
1657 dispatch::fill(strategy_, context_, event);
1658 return settle();
1659 }
1660
1666 Report feed(const OrderSnapshot& event) {
1667 confirm_reservations();
1668 dispatch::order_update(strategy_, context_, event);
1669 return settle();
1670 }
1671
1678 context_.set_phase(phase);
1679 // A DAY order does not outlive its session, whatever its history row says afterwards: a broker may
1680 // leave an expired order's remainder on the row (KIS reads it from `rmn_qty`), so the row's filled
1681 // quantity is taken as final once the session has closed (`confirm_from_history`).
1683 for (auto& entry : unconfirmed_) {
1684 if (time_in_force_expires_at_close(entry.row_.order_.time_in_force_)) {
1685 entry.session_ended_ = true;
1686 }
1687 }
1688 }
1689 confirm_reservations();
1690 dispatch::phase(strategy_, context_, phase);
1691 return settle();
1692 }
1693
1694private:
1695 // Drains the queue and turns it into a report.
1696 //
1697 // **Cancellations are collected before any order is placed**, across the whole queue, rather than
1698 // per instruction. That is the sequencing `Plan` documents as the caller's obligation, and here
1699 // is the caller: a replacement placed before its predecessor is withdrawn is briefly a double
1700 // position, while cancelling first leaves the instrument unrepresented for a moment instead.
1701 //
1702 // A guard refusal drops that instruction and nothing else. One instrument breaching a position
1703 // ceiling must not silence the rest of a cross-sectional rebalance.
1704 Report settle() {
1705 // Before anything is planned, because this pass has not yet added anything of its own: the
1706 // probe must not read this runner's intentions as the broker's report. The portfolio can still
1707 // carry a reservation an *earlier* pass persisted, which is why those rows are marked and why
1708 // the probe skips them rather than relying on when it is called.
1710 // After the probe, which must read the broker's report rather than this runner's rows, and before
1711 // anything is planned: whatever replaced `working_` since the last pass, each order this runner
1712 // sent is counted again at what nothing yet explains (`unconfirmed_orders`).
1713 confirm_reservations();
1714
1715 Report report;
1716 auto instructions = context_.drain();
1717 kept_this_pass_.clear();
1718 drop_superseded_deferred(instructions);
1719 std::vector<strategy::PlannedOrder> pending;
1720 // The instruction each pending order came from, aligned with `pending`, so a phase refusal can
1721 // keep the target that produced it (`retain_for_phase`). Points into `instructions`, which lives
1722 // for the whole pass.
1723 std::vector<const Instruction<Tag>*> origin;
1724 // Whether a later instruction of this pass speaks about `source`'s instrument (`source` is one of
1725 // `instructions`): the latest statement wins.
1726 const auto restated_later = [&instructions](const Instruction<Tag>& source) {
1727 const auto instrument = instruction_instrument(source);
1728 const auto* const end = instructions.data() + instructions.size();
1729 for (const auto* later = &source + 1; later < end; ++later) {
1730 if (instruction_instrument(*later) == instrument) {
1731 return true;
1732 }
1733 }
1734 return false;
1735 };
1736
1737 // **One withdrawal per order per pass.** A strategy that clears what is resting and then states
1738 // a target asks twice for the same order: the explicit `CancelIntent` goes out, and then
1739 // `plan_for` reconciles the target against `context_.portfolio()` -- which does not carry the
1740 // cancellation just sent -- and plans a withdrawal for the same row. Two requests, and a broker
1741 // answering the second with "already cancelled" then reads as a rejected withdrawal in the
1742 // report.
1743 //
1744 // **Only the duplicate send is suppressed; the picture planning reads is untouched.** Removing
1745 // the row from that view is the tempting fix and it is unsafe: `SendOutcome::ACCEPTED` on a
1746 // withdrawal means the broker took the *request*, and `PENDING_CANCEL` counts as open in
1747 // `strategy::order_is_open`. Sizing a replacement against an emptied book would place a fresh
1748 // order of the full target while the original may still fill -- doubling a position to save a
1749 // request. Reconciliation keeps treating a possibly-working order as working, which is the
1750 // right answer until a listing says otherwise.
1751 //
1752 // Keyed on *attempted*, not on accepted: no outcome here means the order is gone, and a
1753 // withdrawal that was refused locally will be refused the same way a second time.
1754 std::vector<OrderRef<Tag>> attempted;
1755 const auto attempt_cancel = [this, &attempted, &report](const OrderRef<Tag>& ref) {
1756 // An unaddressed reference is exempt. Two of them compare equal, so deduping would collapse
1757 // two distinct unsendable withdrawals into one report entry and hide the second.
1758 if (!ref.primary_.empty() || !ref.secondary_.empty()) {
1759 for (const auto& seen : attempted) {
1760 if (seen == ref) {
1761 return;
1762 }
1763 }
1764 attempted.push_back(ref);
1765 }
1766 send_cancel(ref, report);
1767 };
1768 // Serialized: the instruments a plan in this pass places on, so a second plan for one of them is
1769 // withheld before its withdrawals go out (`serial_hold`).
1770 std::vector<InstrumentId> claimed;
1771
1772 for (const auto& instruction : instructions) {
1773 // **A later target or order replaces a retained target**: the strategy has spoken again about this
1774 // instrument, and replaying what it said before would override it. A withheld target is retained
1775 // again below, as the latest. A `CancelIntent` names an order, not an instrument, and leaves it.
1776 forget_retained(instruction_instrument(instruction));
1777 if (const auto* cancel = std::get_if<strategy::CancelIntent<Tag>>(&instruction)) {
1778 attempt_cancel(cancel->ref_);
1779 continue;
1780 }
1781
1782 const auto planned = plan_for(instruction);
1783 // **Checked before a single cancellation goes out, and on the plan rather than on its parts.**
1784 //
1785 // `plan_for` derives both halves from `context_.portfolio()`: which rows oppose the target, and
1786 // what remains to be placed. So a plan is only as good as the snapshot under it, and the
1787 // cancellations are sent below -- before the guard has seen the placements at all. Deciding the
1788 // halves separately is how a reprice becomes a bare withdrawal.
1789 //
1790 // Only a plan that would cancel something is stopped. One that only places is left to the guard
1791 // exactly as before, so a direct `OrderIntent` keeps reporting as a refusal rather than quietly
1792 // changing bucket.
1793 const bool would_cancel = !planned.cancel_.empty();
1794 if (const auto reason = plan_withhold_reason(would_cancel)) {
1795 // **A missing account stops the whole plan; a hold on placements stops only a plan that
1796 // would also withdraw something.**
1797 //
1798 // The difference is which of the two the *guard* can be relied on to catch. A held placement
1799 // is refused by `RiskGuard` a few lines below, with the hold named on its own report entry,
1800 // and that is the better result -- so a plan with nothing to cancel is left to it. What the
1801 // guard cannot catch is data: a snapshot nobody has read is not a hold, and the loop's hold
1802 // for it is not yet installed at every point an instruction can be settled. A source's
1803 // `read_order_list` runs before `PollingLoop` installs any hold and is handed a mutable
1804 // runner, so a target settled inside that call reaches here with the guard clear. Deciding
1805 // absence and staleness here, on the snapshot itself, is what makes the rule hold at every
1806 // send site rather than at most of them.
1807 // A missing-account reason is only ever returned when it should stop this plan, so it needs
1808 // no second test here. A hold on placements stops only a plan that would also withdraw
1809 // something: a placement-only plan is left to `RiskGuard` below, which refuses it with the
1810 // hold named on its own report entry, and that is the better result.
1811 const bool data_is_missing = *reason != WithheldPlanReason::PLACEMENT_HELD;
1812 if (data_is_missing || would_cancel) {
1813 // The whole plan, before any of its parts were counted separately. **Four reasons reach
1814 // here** and this counts them together: an account never read, one older than the stated
1815 // maximum, one read before the listing it would be reconciled with, and a hold refusing
1816 // placements for a plan that also wanted to withdraw something -- the last two on a perfectly
1817 // fresh portfolio. Counted at all because the loop's own gate
1818 // does not always describe it: with `allow_absent_portfolio` the gate can be open while this
1819 // fires.
1820 ++plans_withheld_;
1821 std::string detail = withhold_detail(*reason);
1822 if (data_is_missing) {
1823 if (retain_withheld(instruction)) {
1824 detail = "retained for the next holdings read";
1825 } else if (*reason == WithheldPlanReason::PORTFOLIO_SUPERSEDED &&
1826 std::holds_alternative<strategy::OrderIntent>(instruction)) {
1827 detail = "dropped: an order intent states a size against the mis-ordered picture";
1828 }
1829 }
1830 report.withheld_plans_.push_back(WithheldPlan<Tag>{
1831 *reason,
1832 instruction_instrument(instruction),
1833 instruction_tag(instruction),
1834 planned.cancel_,
1835 planned.place_.size(),
1836 std::move(detail),
1837 });
1838 continue;
1839 }
1840 }
1841 // **Serialized: decided on the plan, before any of its withdrawals go.** A plan whose placement will
1842 // be withheld must not withdraw first: a reprice would become a bare withdrawal, and a protective
1843 // order could be withdrawn with its replacement left waiting. A plan that only withdraws is not
1844 // held.
1845 // A target behind a resting stop places later: its plan withdraws the stop and waits
1846 // (`STOP_ORDER_RESTING`), so it is decided here too, or the stop could go with nothing to follow it.
1847 const bool defers = !planned.cancel_.empty() &&
1848 std::any_of(planned.rejected_.begin(), planned.rejected_.end(), [](const strategy::Rejection& rejection) {
1849 return rejection.reason_ == strategy::RejectReason::STOP_ORDER_RESTING;
1850 });
1851 if (serialized() && (!planned.place_.empty() || defers)) {
1852 std::optional<std::string> hold;
1853 for (const auto& order : planned.place_) {
1854 hold = serial_hold(order.instrument_, claimed);
1855 if (hold) {
1856 break;
1857 }
1858 }
1859 if (!hold && defers) {
1860 hold = serial_hold(instruction_instrument(instruction), claimed);
1861 }
1862 if (hold) {
1863 ++plans_withheld_;
1864 if (retain_withheld(instruction, /*for_phase=*/false, context_.portfolio().as_of_)) {
1865 *hold += "; retained for a later holdings read";
1866 }
1867 report.withheld_plans_.push_back(WithheldPlan<Tag>{
1869 instruction_instrument(instruction),
1870 instruction_tag(instruction),
1871 planned.cancel_,
1872 planned.place_.size(),
1873 std::move(*hold),
1874 });
1875 continue;
1876 }
1877 for (const auto& order : planned.place_) {
1878 claimed.push_back(order.instrument_);
1879 }
1880 if (defers) {
1881 claimed.push_back(instruction_instrument(instruction));
1882 }
1883 }
1884 // **Serialized: a target that could only withdraw an order this runner still owes is kept**, and its
1885 // withdrawals go out all the same (`withdraws_owed`). Nothing is reported as withheld: nothing was.
1886 if (serialized() && planned.place_.empty() && !defers && withdraws_owed(instruction, planned.cancel_)) {
1887 retain_withheld(instruction, /*for_phase=*/false, context_.portfolio().as_of_, /*until_accounted=*/true);
1888 }
1889 for (const auto& ref : planned.cancel_) {
1890 attempt_cancel(ref);
1891 }
1892 defer_if_waiting_on_stop(instruction, planned);
1893 for (const auto& rejection : planned.rejected_) {
1894 // Reconciliation's own refusal: it could not build a valid instruction at all, which is a
1895 // different fact from a broker saying no. **Not a diagnosis**: the same bucket holds a
1896 // quantity that rounded away on the lot, which is a deliberate drop. The reason is on the
1897 // entry.
1898 ++orders_unbuildable_;
1899 last_unbuildable_ = rejection;
1900 report.rejected_.push_back(rejection);
1901 }
1902 for (const auto& order : planned.place_) {
1903 pending.push_back(order);
1904 origin.push_back(&instruction);
1905 }
1906 }
1907
1908 strategy::GuardInputs inputs;
1909 // When the order actually goes out (`send_instant`): a target kept at a ledger mark, or one
1910 // released at a holdings read, is sent between bars, and a send recorded at the bar that decided it
1911 // would already look a window old to the next bar.
1912 inputs.now_ = send_instant();
1913 inputs.phase_ = context_.phase();
1914
1915 // **Every order in this pass is checked against a view that already contains the orders this
1916 // pass has sent.** Without that, a limit is re-evaluated from the same starting point for every
1917 // order, and two orders that each fit can breach it together: with a hundred sellable and no
1918 // resting order, two sells of sixty both pass and twenty shares are sold that were not there.
1919 //
1920 // A copy per pass, mutated as orders are accepted, closes that within one `settle`.
1921 // `persist_reservation` closes the same hazard *across* passes: always for a runner that sends to
1922 // a broker listing working orders, and otherwise for the limits that read what is outstanding.
1923 // Nothing has to recognise its own order in a later listing: a working-order poll assigns
1924 // `working_` wholesale (a holdings poll keeps it), so the broker's own answer replaces the
1925 // reservation when it arrives. Between a placement and that poll, the context portfolio is
1926 // therefore the broker's last report **plus** what this runner has sent since -- which is what a
1927 // target and a limit have to size against, and why the rows are marked so that anything inferring
1928 // the broker's behaviour from the listing can tell them apart.
1929 auto view = context_.portfolio();
1930 // Resolved once for the pass. The currencies and marks do not move as orders are accepted --
1931 // only quantities do, and the guard reads those from the view -- and resolving per order would
1932 // repeat a map lookup per instrument for every instrument. Built only when the ceiling is on, so
1933 // a caller who has not asked for it pays nothing.
1934 if (guard_.limits().max_gross_exposure_) {
1935 inputs.exposure_basis_ = exposure_basis_for(view, pending);
1936 }
1937 for (std::size_t at = 0; at < pending.size(); ++at) {
1938 auto& order = pending[at];
1939 const auto& source_instruction = *origin[at];
1940 // Checked before the guards, because an unresolved instrument is a safety hold rather than a
1941 // limit breach: reporting a notional ceiling for an order that was never going to be sent
1942 // would point an operator at the wrong problem.
1943 // Checked before the ledger and before the guards, and checked *per order* rather than once
1944 // at the top. A journal that failed on the first order of a batch will fail on the second, so
1945 // a batch that carried on would produce one unexplained refusal per instrument. This is also
1946 // what makes the condition hold for a direct `feed()`, which never reaches `PollingLoop`'s
1947 // gate at all.
1948 if (guard_.halted(strategy::HaltReason::LOCAL_FAILURE)) {
1949 ++instructions_unavailable_;
1950 report.unavailable_.push_back(
1951 WithheldOrder{
1953 order,
1954 guard_.hold_detail(strategy::HaltReason::LOCAL_FAILURE),
1955 }
1956 );
1957 continue;
1958 }
1959 if (ledger_.requires_reconcile(order.instrument_)) {
1960 // The indeterminacy ledger, not a guard: this instrument has an outcome nobody knows, so
1961 // nothing further goes out for it until a listing settles the question. Counted apart from
1962 // every other origin because the operator's response is to reconcile rather than to relax a
1963 // limit or fix a price.
1964 ++orders_reconcile_withheld_;
1965 std::string detail(ledger_.detail(order.instrument_));
1966 // **Serialized: the target is kept**, as at the gate below, and planned again at a later holdings
1967 // read: withheld again while the instrument is marked, sent once a listing has settled it. Not when
1968 // a later instruction of this pass spoke about the instrument: every instruction is planned before
1969 // any order is sent, so keeping this one here would undo the later one.
1970 if (serialized() && !restated_later(source_instruction) &&
1971 retain_withheld(source_instruction, /*for_phase=*/false, context_.portfolio().as_of_)) {
1972 detail += "; retained for a later holdings read";
1973 }
1974 report.withheld_.push_back(WithheldOrder{WithheldReason::RECONCILE_REQUIRED, order, std::move(detail)});
1975 continue;
1976 }
1977
1978 // **Serialized, again at the send**: an order placed earlier in this pass on the same instrument is
1979 // an order not yet accounted for. The baseline is taken here, once, and the same value goes to the
1980 // journal and to the order's record (`track_unconfirmed`).
1981 std::optional<Decimal64> baseline;
1982 if (serialized()) {
1983 if (auto hold = serial_hold(order.instrument_, {})) {
1984 ++orders_awaiting_confirmation_;
1985 if (retain_withheld(source_instruction, /*for_phase=*/false, context_.portfolio().as_of_)) {
1986 *hold += "; retained for a later holdings read";
1987 }
1988 report.withheld_.push_back(WithheldOrder{WithheldReason::AWAITING_CONFIRMATION, order, std::move(*hold)});
1989 continue;
1990 }
1991 baseline = context_.portfolio().net_quantity_of(order.instrument_);
1992 }
1993
1994 fill_sellable_if_needed(view, order);
1995
1996 inputs.reference_price_ = context_.last_price(order.instrument_);
1997 // The currency the order is priced in, which only the reference data knows. A buying-power
1998 // limit is per currency, and an unresolved instrument leaves it absent rather than defaulting
1999 // to one: guessing here would spend one currency's budget on another's order.
2000 const auto reference = context_.instrument(order.instrument_);
2001 inputs.order_currency_ =
2002 reference ? std::optional<Currency>(reference->currency_) : std::nullopt;
2003 // `evaluate` rather than `check`, for one fact `check` drops: whether the phase limit admitted
2004 // this order only because it closes a position. Nothing downstream can recover that -- the
2005 // order, the portfolio and the report all look the same either way -- and it is the one thing
2006 // an operator needs to see, because it is this library sending into a session the operator's
2007 // own limits said not to trade in.
2008 auto decision = guard_.evaluate(order, view, inputs);
2009 if (decision.rejection_) {
2010 // **Counted here and not at the send site, because there is no send.** A buy left above a
2011 // withheld ceiling is the larger notional, so every limit that multiplies quantity by price
2012 // sees a bigger number than the clamped order would have shown it. (A sell left below a
2013 // withheld floor is the smaller notional; these limits are not what stops that one.)
2014 if (order.price_would_have_been_clamped_) {
2015 ++unclamped_orders_refused_;
2016 }
2017 ++orders_guard_refused_;
2018 if (decision.rejection_->reason_ == strategy::GuardReason::PHASE_NOT_PERMITTED) {
2019 retain_for_phase(source_instruction);
2020 }
2021 report.refused_.push_back(std::move(*decision.rejection_));
2022 continue;
2023 }
2024
2025 // **The route it is sent with, written into the order before anything records it.** A
2026 // strategy names no venue, and a binding may send one anyway (`SessionExecutor::default_route`).
2027 // Resolved here rather than only at the wire so the journal line, the row `reserve` keeps --
2028 // which `cancel_all` and every other instruction built from it restate -- and the report all
2029 // name the venue the broker was asked for, not the blank the strategy left.
2030 order.venue_ = requested_route(order.venue_);
2031 const auto outcome = place_order(order, baseline);
2032 // **Counted where the outcome is known, not where the exception was granted.** An admitted
2033 // order can still be refused by the sellable check, a buying-power limit or either ceiling and
2034 // never leave the process, so counting at the guard would report a proxy for the harm rather
2035 // than the harm. `REJECTED` counts: the request went out and the broker answered it, which is
2036 // exactly the nightly-rejection case this exception was chosen knowing about, and a witness
2037 // that stayed at zero through every one of them would be silent in the case it exists for.
2038 // `REFUSED_LOCALLY` and `UNAVAILABLE` do not: nothing was sent. This is the same division
2039 // `note_sent` already makes, for the same reason.
2040 if (decision.admitted_outside_phase_ && send_outcome_reached_broker(outcome)) {
2041 ++sends_outside_permitted_phase_;
2042 }
2043 // **Split on whether a broker was asked, which is the division `send_outcome_reached_broker`
2044 // already makes.** `REJECTED` means the exchange saw this price and refused it.
2045 // `REFUSED_LOCALLY` means the binding did -- and that is a price-dependent refusal too: a
2046 // limit price no adapter will send is a limit price the withheld band might have moved into
2047 // range. The concrete case is a sell at zero that `Price::checked` admits
2048 // because zero sits on a stated grid. Counting only the first left that invisible.
2049 // `INDETERMINATE` is excluded because it is not a refusal at all -- the request may well be
2050 // resting -- and `UNAVAILABLE` because it is local machinery being unavailable, which no price
2051 // affects.
2052 if (order.price_would_have_been_clamped_) {
2053 if (outcome == SendOutcome::REJECTED) {
2054 ++unclamped_orders_rejected_;
2055 } else if (outcome == SendOutcome::REFUSED_LOCALLY) {
2056 ++unclamped_orders_refused_;
2057 }
2058 }
2059 // After every send, not only after a refusal. A terminal journal write that fails leaves the
2060 // broker's own outcome intact -- the send happened, and saying otherwise would be a lie -- but
2061 // it still means the journal is broken, and the runner must stop.
2062 note_executor_failure(send_outcome_is_local_failure(outcome));
2063 switch (outcome) {
2065 // Counted against the rate limit only once the instruction actually went, so an order a
2066 // guard refused or the ledger withheld does not spend budget.
2067 guard_.note_sent(inputs.now_);
2068 ++placements_at_broker_;
2069 // And it is working now, as far as anything later in this pass is concerned, and its
2070 // money is spent. An `INDETERMINATE` send is counted the same way below, with no reference.
2071 reserve(
2072 view, order, inputs.reference_price_, inputs.order_currency_, last_ref(),
2073 guard_.limits().enforce_buying_power_
2074 );
2075 persist_reservation(order, inputs.reference_price_, inputs.order_currency_, last_ref());
2076 track_unconfirmed(order, last_ref(), /*indeterminate=*/false, baseline);
2077 report.would_place_.push_back(std::move(order));
2078 break;
2080 // A refusal is an answer: the order does not exist, so nothing needs reconciling. It
2081 // still consumed a request, which is what the broker will have counted.
2082 guard_.note_sent(inputs.now_);
2083 ++orders_broker_refused_;
2084 // An exit the phase limit let out of session, refused by a broker not taking orders then:
2085 // the target waits for the session instead of for its next bar, which may be a day away.
2086 if (decision.admitted_outside_phase_) {
2087 retain_for_phase(source_instruction, /*broker_answered=*/true);
2088 } else if (last_error_is(broker_rate_limited)) {
2089 // **Turned away for the broker's rate limit, which is about the moment.** Kept as an
2090 // unreachable send is, and offered again after a newer holdings read: dropped, a daily
2091 // strategy's opening order or protective stop refused once at a busy open waited a day
2092 // (in a simulated run, one `EGW00201` at 09:00 moved a buy to the next session and left a position
2093 // without its stop all day). The broker processed nothing, so a retry cannot double it.
2094 retain_for_phase(source_instruction, /*broker_answered=*/true, context_.portfolio().as_of_);
2095 }
2096 note_broker_refusal(order);
2097 report.rejected_.push_back(
2098 strategy::Rejection{
2099 strategy::RejectReason::BROKER_REFUSED, order.instrument_, order.tag_
2100 }
2101 );
2102 break;
2104 guard_.note_sent(inputs.now_);
2105 // Counted like an acceptance for the purpose below: the order may exist, and a listing
2106 // read before this moment cannot show it either way.
2107 ++placements_at_broker_;
2108 ledger_.mark(order.instrument_, "an order request has an unknown outcome");
2109 ++orders_indeterminate_;
2110 // **Counted as if accepted, until the sweep says what became of it.** The ledger
2111 // withholds the instrument meanwhile; but once a sweep finds the order in the broker's
2112 // listing it clears the ledger, and a market order that filled is then in no listing while
2113 // holdings may not show it yet, so a target sent it again (twice the target, in one
2114 // process). Reserved and tracked here with the holding at send as the baseline, and handed
2115 // its reference by `resolve_indeterminate_placement`. **No reference** is passed on purpose:
2116 // `last_ref()` still names the previous accepted order, and a row carrying it could be
2117 // withdrawn in that order's place.
2118 // A serialized runner records it whatever its executor reports: it owns the instrument until
2119 // accounted for.
2120 if constexpr (keeps_sent_orders_until_listed) {
2121 if (serialized() || runner_detail::reports_last_ref<Executor>::value) {
2122 reserve(
2123 view, order, inputs.reference_price_, inputs.order_currency_, std::nullopt,
2124 guard_.limits().enforce_buying_power_
2125 );
2126 persist_reservation(order, inputs.reference_price_, inputs.order_currency_, std::nullopt);
2127 track_unconfirmed(order, std::nullopt, /*indeterminate=*/true, baseline);
2128 }
2129 }
2130 report.indeterminate_.push_back(std::move(order));
2131 break;
2133 // **No `note_sent`**: the binding refused it, so no request went out and the broker
2134 // counted nothing. Spending rate-limit budget here would refuse a later order that would
2135 // have been allowed, for a request nobody made. Reported as a rejection because that is
2136 // what it is from the order's point of view -- it does not exist, and nothing needs
2137 // reconciling -- and the runner keeps trading, because the next instruction may be
2138 // perfectly sendable. The reason is carried on the entry itself rather than left to
2139 // `last_error()`: a later accepted order in the same pass clears that, so a report can
2140 // outlive the only record of which refusal this was.
2141 ++orders_binding_refused_;
2142 if (decision.admitted_outside_phase_) {
2143 retain_for_phase(source_instruction);
2144 } else if (last_error_is(broker_unreachable)) {
2145 // **The broker could not be reached, so the decision is kept rather than lost.** Dropped,
2146 // a daily strategy's target or protective stop released at the open into a dropped
2147 // connection waited for its next bar, a day away (in a simulated run, a 30-minute outage at 09:00 moved a
2148 // buy to the next session and left a position without its stop all day). Kept as a phase
2149 // refusal is -- discarded by a stop, an operator's halt or a local failure, replaced by a
2150 // later statement, an intent checked against the book -- and offered again only after a
2151 // holdings read newer than this one, which is the evidence the broker answers again: a
2152 // retry every tick into a dead connection would wait out a connect timeout per instrument
2153 // per iteration, and hold up a stop behind it.
2154 retain_for_phase(source_instruction, /*broker_answered=*/false, context_.portfolio().as_of_);
2155 }
2156 report.rejected_.push_back(
2157 strategy::Rejection{
2158 strategy::RejectReason::BINDING_REFUSED, order.instrument_, order.tag_
2159 }
2160 );
2161 break;
2163 // No `note_sent`: nothing went out, so the broker counted nothing and no rate-limit
2164 // budget was spent. The ledger is not marked either -- an instruction that was never
2165 // attempted leaves no doubt about the position, and withholding the instrument would be
2166 // withholding it for the wrong reason.
2167 ++instructions_unavailable_;
2168 report.unavailable_.push_back(
2169 WithheldOrder{
2171 order,
2173 guard_.hold_detail(strategy::HaltReason::LOCAL_FAILURE) :
2174 std::string("local machinery refused to send"),
2175 }
2176 );
2177 break;
2178 }
2179 }
2180
2181 // The pass's instructions die with it; so do the pointers kept to deduplicate them.
2182 kept_this_pass_.clear();
2183 report.log_ = context_.log().take();
2184 return report;
2185 }
2186
2187 // Sends one cancellation, and marks the instrument when its outcome is unknown.
2188 //
2189 // A cancellation that fails in transit leaves the same question an order does -- the order may
2190 // now be withdrawn or may still be resting -- so it obliges the same reconciliation. A
2191 // cancellation is not gated by the ledger, because withdrawing an order is the one action that
2192 // cannot make an unresolved position worse.
2193 // Asks the broker what may be sold, but only when nothing else can answer.
2194 //
2195 // Four conditions, and every one of them is there to avoid a request:
2196 //
2197 // - the executor can ask at all -- a dry run and a replay cannot, and compile to nothing here;
2198 // - the caller enforces the limit, so nobody pays for a figure no check reads;
2199 // - the order is a sell, since the figure says nothing about buying;
2200 // - and the snapshot has not already answered, which at a `HOLDINGS` broker it always has.
2201 //
2202 // The answer is written to the context's portfolio as well as to this pass's view, so a later
2203 // pass in the same holdings cycle reuses it rather than asking again. The next holdings poll
2204 // replaces the positions wholesale and the figure goes with them, which is the right lifetime: it
2205 // is exactly as fresh as the position it describes.
2206 void fill_sellable_if_needed(
2207 strategy::Portfolio<Tag>& view,
2208 const strategy::PlannedOrder& order
2209 ) {
2210 if constexpr (runner_detail::fills_sellable<Executor>::value) {
2211 if (!guard_.limits().enforce_sellable_quantity_ || order.side_ != OrderSide::SELL) {
2212 return;
2213 }
2214 if (view.sellable_quantity_of(order.instrument_)) {
2215 return;
2216 }
2217 const auto answered = executor_.sellable_quantity(order.instrument_);
2218 if (!answered) {
2219 // Left absent, so the guard refuses for the reason it already has. Reporting a failure to
2220 // establish the figure as a sellable quantity of zero would be a different claim.
2221 return;
2222 }
2223 write_sellable(view, order.instrument_, *answered);
2224 auto persisted = context_.portfolio();
2225 write_sellable(persisted, order.instrument_, *answered);
2226 context_.set_portfolio(std::move(persisted));
2227 } else {
2228 static_cast<void>(view);
2229 static_cast<void>(order);
2230 }
2231 }
2232
2233 // Sends one order, stating the baseline to the journal in serialized mode.
2234 SendOutcome place_order(const strategy::PlannedOrder& order, [[maybe_unused]] const std::optional<Decimal64>& baseline) {
2235 if constexpr (can_serialize) {
2236 if (serialized()) {
2237 return executor_.place(order, baseline);
2238 }
2239 }
2240 return executor_.place(order);
2241 }
2242
2243 // Returns the venue the executor sends an order asking for `asked` to; `asked` where it does not route.
2244 Venue requested_route(Venue asked) const {
2245 if constexpr (runner_detail::resolves_route<Executor>::value) {
2246 return executor_.requested_route(asked);
2247 } else {
2248 return asked;
2249 }
2250 }
2251
2252 // Returns the reference the broker issued for the order just accepted, where there is one.
2253 //
2254 // Nothing where the executor has no broker to have been answered by, which is every dry run and
2255 // every replay.
2256 std::optional<OrderRef<Tag>> last_ref() const {
2257 if constexpr (runner_detail::reports_last_ref<Executor>::value) {
2258 return executor_.last_ref();
2259 } else {
2260 return std::nullopt;
2261 }
2262 }
2263
2264 // An order the broker accepted, or one whose request failed in transit, that holdings have not yet
2265 // shown in full; see `unconfirmed_orders`.
2266 struct Unconfirmed {
2267 // Empty while the order's outcome is unknown; see `resolve_indeterminate_placement`.
2268 OrderRef<Tag> ref_;
2269 InstrumentId instrument_;
2270 OrderSide side_{OrderSide::BUY};
2271 // What holdings must still show: the order's quantity, or less once its history says so.
2272 Decimal64 expected_;
2273 // The instrument's net holding when it was sent.
2274 Decimal64 baseline_;
2275 // The reserved row as `reserve` wrote it; rebuilt with the residual quantity.
2276 WorkingOrder<Tag> row_;
2277 std::chrono::system_clock::time_point sent_at_{};
2278 // Whether any listing has carried it, and whether the latest one does.
2279 bool listed_{false};
2280 bool listed_now_{false};
2281 // Whether this runner's withdrawal of it was accepted.
2282 bool withdrawn_{false};
2283 // Whether a terminal history row has capped `expected_`.
2284 bool settled_{false};
2285 // Whether it is a DAY order and the session it was sent in has closed since.
2286 bool session_ended_{false};
2287 // Whether the current `confirm_from_history` looked for it.
2288 bool consulted_{false};
2289 // Whether the history, read past the timeout, had no terminal row for it.
2290 bool history_missed_{false};
2291 // The quantity nothing explains, as of the last rebuild, and since when some has been.
2292 Decimal64 residual_;
2293 std::optional<std::chrono::system_clock::time_point> unexplained_since_;
2294 // Serialized mode (`serialized`). The order's journal line, whose id names it to a sweep and to the
2295 // line that accounts for it.
2296 std::optional<JournalEntry> journal_;
2297 // Whether `ref_` came from the broker's own acceptance, so a cancellation can be built from it; a
2298 // restored order or one a sweep named carries the order id alone.
2299 bool broker_ref_{false};
2300 // Restored from the journal at startup: its history is read once even though no listing carried it.
2301 bool restored_{false};
2302 // Only an operator may account for it (`release_obligations`).
2303 bool operator_only_{false};
2304 // Whether its timeout was counted, which happens once (and, for one not yet named, marked the ledger).
2305 bool noticed_{false};
2306 // Whether its name is the broker's own: given at acceptance, or confirmed by a dated history row of
2307 // its send. A name a sweep gave from a listing may belong to another day's order.
2308 bool identified_{false};
2309 // The holdings read it was decided against; an older one says nothing about it.
2310 std::chrono::system_clock::time_point baseline_at_{};
2311 // Why it is held for an operator, while that hold's `HELD` line is not yet written.
2312 std::string unwritten_hold_;
2313 };
2314
2315 // ---------------------------------------------------------------------------------------------------
2316 // Serialized mode (`serialized`): one order per instrument until holdings or its own history account
2317 // for it. In file order: tracking (`track_serialized`), confirmation (`confirm_serialized`), the gate
2318 // (`serial_hold`), evidence (`examine_evidence`), the journal writes (`discharge`, `persist_hold`) and
2319 // restore (`restore_from`). Confirmation and the gate use these only when `serialized()`;
2320 // `release_obligations` uses some whenever `can_serialize`.
2321 // ---------------------------------------------------------------------------------------------------
2322
2323 // `track_unconfirmed` for a serialized runner: every order, named or not, with the baseline the send
2324 // stated and the journal line it wrote. The gate guaranteed the holdings.
2325 void track_serialized(
2326 const strategy::PlannedOrder& order, const std::optional<OrderRef<Tag>>& ref, bool indeterminate,
2327 const Decimal64& baseline
2328 ) {
2329 const auto& portfolio = context_.portfolio();
2330 Unconfirmed entry;
2331 if (ref && !indeterminate) {
2332 entry.ref_ = *ref;
2333 entry.broker_ref_ = !ref->primary_.empty();
2334 entry.identified_ = entry.broker_ref_;
2335 }
2336 entry.baseline_at_ = portfolio.as_of_;
2337 entry.instrument_ = order.instrument_;
2338 entry.side_ = order.side_;
2339 entry.expected_ = order.quantity_;
2340 entry.baseline_ = baseline;
2341 entry.row_ = portfolio.working_.back();
2342 entry.sent_at_ = freshness_instant();
2343 entry.residual_ = order.quantity_;
2344 entry.journal_ = executor_.last_entry();
2345 if (entry.sent_at_ != std::chrono::system_clock::time_point{}) {
2346 entry.unexplained_since_ = entry.sent_at_;
2347 }
2348 unconfirmed_.push_back(std::move(entry));
2349 }
2350
2351 // Whether a reserved row carries the reference the broker gave when it accepted the order.
2352 bool cancellable_reservation(const WorkingOrder<Tag>& working) const {
2353 if (working.ref_.primary_.empty()) {
2354 return false;
2355 }
2356 for (const auto& entry : unconfirmed_) {
2357 if (is_reserved_row_of(working, entry)) {
2358 return entry.broker_ref_;
2359 }
2360 }
2361 return true;
2362 }
2363
2364 // The serialized half of `confirm_reservations`: one order per instrument, so no group arithmetic.
2365 //
2366 // Each order is accounted for, with a `CONFIRMED` line written first, when it is named, no listing
2367 // shows it open, and either its history capped what it filled at zero, or a holdings read shows
2368 // exactly its baseline plus what it filled. Holdings past that hold it for an operator. Anything else
2369 // rebuilds its reserved row from what nothing explains yet; a timeout is counted once and releases
2370 // nothing (only an order not yet named marks the ledger).
2371 void confirm_serialized(strategy::Portfolio<Tag>& portfolio, std::chrono::system_clock::time_point now) {
2372 const auto zero = Decimal64::parse("0");
2373 const auto epoch = std::chrono::system_clock::time_point{};
2374 const auto equal = [](const Decimal64& a, const Decimal64& b) { return !decimal_less(a, b) && !decimal_less(b, a); };
2375 for (auto at = unconfirmed_.begin(); at != unconfirmed_.end();) {
2376 auto& entry = *at;
2377 persist_hold(entry);
2378 // An actual read, and not one older than the read the order was decided against.
2379 const bool read = portfolio.as_of_ != epoch && !(portfolio.as_of_ < entry.baseline_at_);
2380 // What a listing still shows of it: that part cannot have reached holdings.
2381 Decimal64 listed = zero;
2382 entry.listed_now_ = false;
2383 std::optional<Decimal64> listed_filled;
2384 if (const auto* working = broker_row_for(portfolio, entry)) {
2385 entry.listed_ = true;
2386 entry.listed_now_ = true;
2387 listed = decimal_subtract(working->order_.quantity_, working->order_.execution_.filled_quantity_).value_or(zero);
2388 listed_filled = working->order_.execution_.filled_quantity_;
2389 }
2390 // A listing of its own reporting more filled than it was for, or than its history said it ended with,
2391 // contradicts what is known: held for an operator before the listing can move on and the contradiction
2392 // be forgotten. (Holding is the safe direction, so an undated row may prove it.)
2393 if (listed_filled && !entry.operator_only_ &&
2394 (decimal_less(entry.row_.order_.quantity_, *listed_filled) ||
2395 (entry.settled_ && decimal_less(entry.expected_, *listed_filled)))) {
2396 hold_for_operator(
2397 entry, "an order this runner sent" + named(entry) + " is listed with " + listed_filled->str() +
2398 " filled, more than " + (entry.settled_ ? "its history said it ended with (" + entry.expected_.str() + ")" :
2399 "it was for")
2400 );
2401 }
2402 // The holding, and its movement in the order's direction since the baseline, from an actual read.
2403 std::optional<Decimal64> held;
2404 std::optional<Decimal64> moved;
2405 if (read) {
2406 held = portfolio.net_quantity_of(entry.instrument_);
2407 if (held) {
2408 moved = entry.side_ == OrderSide::BUY ? decimal_subtract(*held, entry.baseline_) :
2409 decimal_subtract(entry.baseline_, *held);
2410 }
2411 }
2412 const bool is_named = !entry.ref_.primary_.empty();
2413 if (!entry.operator_only_ && is_named && entry.identified_ && !entry.listed_now_) {
2414 if (entry.expected_.coefficient() == 0) {
2415 if (discharge(entry, std::nullopt, "its own history row shows it ended with nothing filled")) {
2416 at = unconfirmed_.erase(at);
2417 continue;
2418 }
2419 } else if (moved && equal(*moved, entry.expected_)) {
2420 if (discharge(entry, held, "holdings show " + held->str() + ": its baseline " + entry.baseline_.str() +
2421 " and the " + entry.expected_.str() + " it filled")) {
2422 set_boundary(entry.instrument_, portfolio.as_of_, /*strict=*/false);
2423 at = unconfirmed_.erase(at);
2424 continue;
2425 }
2426 } else if (moved && decimal_less(entry.expected_, *moved)) {
2427 hold_for_operator(
2428 entry, "holdings moved " + moved->str() + " from the baseline " + entry.baseline_.str() + ", more than the " +
2429 entry.expected_.str() + " an order this runner sent" + named(entry) + " can explain"
2430 );
2431 }
2432 }
2433
2434 // What nothing explains yet: what it expects, less what holdings show of it, less what is listed.
2435 Decimal64 covered = zero;
2436 const auto fillable = decimal_subtract(entry.expected_, listed).value_or(zero);
2437 if (!entry.operator_only_ && moved && decimal_less(zero, *moved) && decimal_less(zero, fillable)) {
2438 covered = decimal_less(*moved, fillable) ? *moved : fillable;
2439 }
2440 auto residual = decimal_subtract(fillable, covered).value_or(entry.expected_);
2441 if (!decimal_less(zero, residual)) {
2442 mark_explained(entry);
2443 ++at;
2444 continue;
2445 }
2446 entry.residual_ = residual;
2447 // Not from the epoch: a restored order's clock starts with the first real instant.
2448 if (!entry.unexplained_since_ && now != epoch) {
2449 entry.unexplained_since_ = now;
2450 }
2451 if (!entry.noticed_ && !entry.operator_only_ && (entry.settled_ || entry.history_missed_ || !is_named) &&
2452 timed_out(entry, now) && portfolio.as_of_ - *entry.unexplained_since_ > confirmation_timeout_) {
2453 // A notice, not a release: the order still holds its instrument, and later evidence can still
2454 // account for it. A named one marks no ledger: the gate already withholds the instrument while it
2455 // is owed, and a mark would outlive the holdings that account for it. An unnamed one is accounted
2456 // for only by an operator.
2457 entry.noticed_ = true;
2458 ++orders_unconfirmed_timed_out_;
2459 if (!is_named) {
2460 ledger_.mark(
2461 entry.instrument_,
2462 "an order this runner sent" + named(entry) +
2463 " was not accounted for by holdings within the confirmation timeout; it still holds the instrument"
2464 );
2465 }
2466 }
2467 // Not shown while the ledger holds an order not yet named; see the overlapping branch.
2468 if (!is_named && ledger_.requires_reconcile(entry.instrument_)) {
2469 ++at;
2470 continue;
2471 }
2472 rebuild_residual_row(portfolio, entry, residual);
2473 ++at;
2474 }
2475 }
2476
2477 // The instant after which a holdings read may size the next order on an instrument (serialized
2478 // mode): the read that accounted for its last order (that read itself qualifies), or the instant an
2479 // operator released it or a previous process confirmed it (only a later read qualifies).
2480 struct Boundary {
2481 InstrumentId instrument_;
2482 std::chrono::system_clock::time_point at_{};
2483 bool strict_{false};
2484 };
2485
2486 // Records `instrument`'s boundary, replacing an earlier one.
2487 void set_boundary(const InstrumentId& instrument, std::chrono::system_clock::time_point at, bool strict) {
2488 for (auto& boundary : boundaries_) {
2489 if (boundary.instrument_ == instrument) {
2490 if (boundary.at_ < at || (!(at < boundary.at_) && strict)) {
2491 boundary.at_ = at;
2492 boundary.strict_ = strict;
2493 }
2494 return;
2495 }
2496 }
2497 boundaries_.push_back(Boundary{instrument, at, strict});
2498 }
2499
2500 // Why the holdings in hand cannot be the baseline of an order on `instrument`, or nothing.
2501 std::optional<std::string> baseline_missing(const InstrumentId& instrument) const {
2502 const auto& portfolio = context_.portfolio();
2503 if (portfolio.as_of_ == std::chrono::system_clock::time_point{}) {
2504 return std::string("holdings have not been read, so the order would have no baseline");
2505 }
2506 if (!portfolio.net_quantity_of(instrument)) {
2507 return std::string("the holding's rows do not add up, so the order would have no baseline");
2508 }
2509 for (const auto& boundary : boundaries_) {
2510 if (boundary.instrument_ == instrument &&
2511 (boundary.strict_ ? !(boundary.at_ < portfolio.as_of_) : portfolio.as_of_ < boundary.at_)) {
2512 return std::string(
2513 "the holdings were read before the last order on this instrument was accounted for; waiting for a "
2514 "newer read"
2515 );
2516 }
2517 }
2518 return std::nullopt;
2519 }
2520
2521 // Why a serialized runner may not place on `instrument` now, or nothing; `claimed` lists the
2522 // instruments an earlier plan in this pass places on.
2523 std::optional<std::string> serial_hold(const InstrumentId& instrument, const std::vector<InstrumentId>& claimed) const {
2524 if (!can_serialize) {
2525 return std::string(
2526 "this runner journals to a broker but cannot serialize its orders (it needs a broker that lists working "
2527 "orders, and an executor with last_entry and record); see allow_overlapping_orders"
2528 );
2529 }
2530 if (!recovered_) {
2531 return std::string("nothing is placed before runner::recover_runner has run");
2532 }
2533 for (const auto& entry : unconfirmed_) {
2534 if (!(entry.instrument_ == instrument)) {
2535 continue;
2536 }
2537 switch (state_of(entry)) {
2539 return "an order on this instrument" + named(entry) + " is held for an operator (release_obligations)";
2541 return std::string("an order on this instrument has an unknown outcome and no sweep has named it");
2544 break;
2545 }
2546 return "an order on this instrument" + named(entry) + " is not yet accounted for by holdings";
2547 }
2548 for (const auto& id : claimed) {
2549 if (id == instrument) {
2550 return std::string("an earlier instruction in this pass places on this instrument");
2551 }
2552 }
2553 return baseline_missing(instrument);
2554 }
2555
2556 static std::string named(const Unconfirmed& entry) {
2557 return entry.ref_.primary_.empty() ? std::string() : " (" + entry.ref_.primary_ + ")";
2558 }
2559
2560 // Whether a history row's status ends the original order, so its filled quantity is final.
2561 // `CANCEL_REJECTED` and `REPLACE_REJECTED` say a request about it failed, not that it ended.
2562 static bool ends_the_order(OrderStatus status) noexcept {
2563 switch (status) {
2564 case OrderStatus::FILLED:
2565 case OrderStatus::CANCELED:
2566 case OrderStatus::REJECTED:
2567 return true;
2568 // Not an end of this order: a replacement or a refused change leaves it, or its successor, to settle.
2569 case OrderStatus::UNKNOWN:
2570 case OrderStatus::PENDING:
2571 case OrderStatus::PENDING_CANCEL:
2572 case OrderStatus::PENDING_REPLACE:
2573 case OrderStatus::PARTIAL_FILLED:
2574 case OrderStatus::CANCEL_REJECTED:
2575 case OrderStatus::REPLACE_REJECTED:
2576 case OrderStatus::REPLACED:
2577 return false;
2578 }
2579 return false;
2580 }
2581
2582 // The rows that are `entry`'s own order: its number, instrument and side, dated within the tolerance of
2583 // its send. An undated row is nobody's in particular.
2584 bool own_row(const OrderSnapshot& row, const Unconfirmed& entry) const {
2585 return row.order_id_ == entry.ref_.primary_ && row.instrument_ == entry.instrument_ && row.side_ == entry.side_ &&
2586 row.ordered_at_ && !(*row.ordered_at_ < entry.sent_at_ - order_time_tolerance_) &&
2587 !(entry.sent_at_ + order_time_tolerance_ < *row.ordered_at_);
2588 }
2589
2590 // The history row that settles a serialized order: every row with its number, instrument and side (and,
2591 // where dated, inside its window) is dated, ended it, and filled the same, no more than it was for.
2592 // Otherwise none: an undated row may be another day's order, and `CANCEL_REJECTED` beside `CANCELED`
2593 // means the pages disagree about whether it ended.
2594 const OrderSnapshot* ended_row_for(const std::vector<OrderSnapshot>& evidence, const Unconfirmed& entry) const {
2595 const OrderSnapshot* found = nullptr;
2596 for (const auto& row : evidence) {
2597 if (row.order_id_ != entry.ref_.primary_ || !(row.instrument_ == entry.instrument_) || row.side_ != entry.side_) {
2598 continue;
2599 }
2600 if (row.ordered_at_ && !own_row(row, entry)) {
2601 continue;
2602 }
2603 if (!row.ordered_at_ || !ends_the_order(row.status_) ||
2604 decimal_less(entry.row_.order_.quantity_, row.execution_.filled_quantity_) ||
2605 (found != nullptr && (decimal_less(found->execution_.filled_quantity_, row.execution_.filled_quantity_) ||
2606 decimal_less(row.execution_.filled_quantity_, found->execution_.filled_quantity_)))) {
2607 return nullptr;
2608 }
2609 found = &row;
2610 }
2611 return found;
2612 }
2613
2614 // What a serialized order owes: held for an operator, unknown, or named and waiting.
2615 static ObligationState state_of(const Unconfirmed& entry) noexcept {
2616 return entry.operator_only_ ? ObligationState::OPERATOR :
2617 entry.ref_.primary_.empty() ? ObligationState::UNNAMED :
2619 }
2620
2621 // Takes in everything history rows say about each named order, before anything can account for it,
2622 // whether or not this read was scheduled for it:
2623 // - a row of its own confirms a name a sweep gave it;
2624 // - a row of its own reporting more filled than it was for, or, once its final quantity is known, a
2625 // different final quantity or more filled than it, holds it for an operator;
2626 // - rows that all say it ended (`ended_row_for`) cap what holdings must show, unless a listing still
2627 // shows it open.
2628 void examine_evidence(const std::vector<OrderSnapshot>& evidence) {
2629 const auto now = freshness_instant();
2630 const auto differ = [](const Decimal64& a, const Decimal64& b) { return decimal_less(a, b) || decimal_less(b, a); };
2631 for (auto& entry : unconfirmed_) {
2632 if (entry.operator_only_ || entry.ref_.primary_.empty()) {
2633 continue;
2634 }
2635 std::optional<std::string> contradiction;
2636 for (const auto& row : evidence) {
2637 if (!own_row(row, entry)) {
2638 continue;
2639 }
2640 entry.identified_ = true;
2641 const auto& filled = row.execution_.filled_quantity_;
2642 if (decimal_less(entry.row_.order_.quantity_, filled)) {
2643 contradiction = "its history row reports " + filled.str() + " filled, more than the " +
2644 entry.row_.order_.quantity_.str() + " it was for";
2645 } else if (entry.settled_ && (decimal_less(entry.expected_, filled) ||
2646 (ends_the_order(row.status_) && differ(entry.expected_, filled)))) {
2647 contradiction = "its history rows report " + filled.str() + " filled after reporting it ended with " +
2648 entry.expected_.str();
2649 }
2650 if (contradiction) {
2651 break;
2652 }
2653 }
2654 if (contradiction) {
2655 hold_for_operator(entry, "an order this runner sent" + named(entry) + ": " + *contradiction);
2656 continue;
2657 }
2658 if (entry.settled_ || broker_row_for(context_.portfolio(), entry) != nullptr) {
2659 continue;
2660 }
2661 if (const auto* row = ended_row_for(evidence, entry)) {
2662 entry.expected_ = row->execution_.filled_quantity_;
2663 entry.settled_ = true;
2664 // Holdings get a full timeout from here to show it (`confirm_serialized`).
2665 entry.unexplained_since_ = now;
2666 }
2667 }
2668 }
2669
2670 // `confirm_from_history` for a serialized runner: what the rows say is taken in before anything can
2671 // account for an order (`examine_evidence`); then, as for an overlapping runner, an order consulted past
2672 // the timeout with no answer is noted for the next holdings read.
2673 void confirm_serialized_from_history(const std::vector<OrderSnapshot>& evidence) {
2674 examine_evidence(evidence);
2675 confirm_reservations();
2676 const auto now = freshness_instant();
2677 for (auto& entry : unconfirmed_) {
2678 entry.consulted_ = awaits_evidence(entry, now);
2679 if (entry.consulted_ && !entry.settled_ && timed_out(entry, now)) {
2680 entry.history_missed_ = true;
2681 }
2682 }
2683 confirm_reservations();
2684 }
2685
2686 // `awaits_evidence` for a serialized runner.
2687 bool awaits_evidence_serialized(const Unconfirmed& entry, std::chrono::system_clock::time_point now) const {
2688 // An unnamed order waits for a sweep or an operator, and a held one for the operator: no history row
2689 // can be matched to the first or account for the second.
2690 if (entry.operator_only_ || entry.ref_.primary_.empty()) {
2691 return false;
2692 }
2693 // A name a sweep gave needs a row of the order's own day, however much of it holdings already show.
2694 if (!entry.identified_) {
2695 return true;
2696 }
2697 if (entry.residual_.coefficient() == 0 || entry.settled_ || entry.history_missed_) {
2698 return false;
2699 }
2700 if (timed_out(entry, now)) {
2701 return true;
2702 }
2703 // A restored order may have left every listing while no process was running.
2704 return !entry.listed_now_ && (entry.listed_ || entry.withdrawn_ || entry.restored_);
2705 }
2706
2707 // The line that accounts for, or holds, an order (serialized mode).
2708 static JournalEntry closing_line(
2709 const Unconfirmed& entry, JournalOutcome outcome, std::optional<Decimal64> holding, std::string detail
2710 ) {
2711 JournalEntry line = entry.journal_ ? *entry.journal_ : JournalEntry{};
2712 line.outcome_ = outcome;
2713 line.order_id_ = entry.ref_.primary_;
2714 line.baseline_ = std::move(holding);
2715 line.detail_ = std::move(detail);
2716 line.requires_operator_ = journal_outcome_requires_operator(outcome);
2717 return line;
2718 }
2719
2720 // Writes the line accounting for `entry`; on failure the runner stops sending and `entry` stays.
2721 bool discharge([[maybe_unused]] const Unconfirmed& entry, [[maybe_unused]] std::optional<Decimal64> holding,
2722 [[maybe_unused]] std::string detail) {
2723 if constexpr (can_serialize) {
2724 auto line = closing_line(entry, JournalOutcome::CONFIRMED, std::move(holding), std::move(detail));
2725 if (executor_.record(line)) {
2726 return true;
2727 }
2728 note_executor_failure(false);
2729 }
2730 return false;
2731 }
2732
2733 // Holds `entry` for an operator. In memory at once, whatever the journal does: a contradiction is never
2734 // forgotten. Durably as soon as the `HELD` line is written; until then the runner stops sending, and every
2735 // refresh writes it again (`persist_hold`).
2736 void hold_for_operator(Unconfirmed& entry, const std::string& detail) {
2737 entry.operator_only_ = true;
2738 entry.unwritten_hold_ = detail;
2739 ledger_.mark(entry.instrument_, detail + "; an operator must account for it (release_obligations)");
2740 persist_hold(entry);
2741 }
2742
2743 // Writes the `HELD` line of a hold not yet in the journal; on failure the runner stops sending.
2744 void persist_hold([[maybe_unused]] Unconfirmed& entry) {
2745 if constexpr (can_serialize) {
2746 if (entry.unwritten_hold_.empty()) {
2747 return;
2748 }
2749 auto line = closing_line(entry, JournalOutcome::HELD, std::nullopt, entry.unwritten_hold_);
2750 if (executor_.record(line)) {
2751 entry.unwritten_hold_.clear();
2752 } else {
2753 note_executor_failure(false);
2754 }
2755 }
2756 }
2757
2758 // The unnamed order a serialized sweep answered: the one whose journal line is `journal_id`, or none. A
2759 // guess could name another order, and a late answer to one already accounted for finds nothing.
2760 typename std::vector<Unconfirmed>::iterator unnamed_by_journal_id(const std::string& journal_id) {
2761 return std::find_if(unconfirmed_.begin(), unconfirmed_.end(), [&](const Unconfirmed& entry) {
2762 return !journal_id.empty() && entry.journal_ && entry.journal_->id_ == journal_id && entry.ref_.primary_.empty();
2763 });
2764 }
2765
2766 // Restores what a journal left unaccounted for; see `restore_obligations`.
2767 void restore_from(const JournalReplay& replay) {
2768 const auto same = [](const std::optional<Decimal64>& a, const std::optional<Decimal64>& b) {
2769 return a.has_value() == b.has_value() && (!a || (!decimal_less(*a, *b) && !decimal_less(*b, *a)));
2770 };
2771 // Every line repeats the instruction; a `CONFIRMED` or `HELD` line carries another holding.
2772 const auto same_instruction = [&](const JournalEntry& a, const JournalEntry& b) {
2773 return a.instrument_ == b.instrument_ && a.side_ == b.side_ && a.type_ == b.type_ &&
2774 a.time_in_force_ == b.time_in_force_ && a.session_ == b.session_ && a.venue_ == b.venue_ &&
2775 same(a.quantity_, b.quantity_) && same(a.price_, b.price_) && same(a.trigger_price_, b.trigger_price_) &&
2776 same(a.baseline_, b.baseline_);
2777 };
2778 struct Fold {
2779 JournalEntry line_;
2780 std::chrono::system_clock::time_point sent_at_{};
2781 std::string name_;
2782 // Whether the name came with the broker's acceptance rather than from a sweep.
2783 bool accepted_{false};
2784 bool done_{false};
2785 bool operator_only_{false};
2786 std::string why_;
2787 };
2788 std::vector<Fold> folds;
2789 // Each id's fold, so a long journal is read in one pass.
2790 std::unordered_map<std::string, std::size_t> fold_of;
2791 for (const auto& line : replay.entries_) {
2792 if (line.action_ != JournalAction::PLACE || line.instrument_.symbol_.empty()) {
2793 continue;
2794 }
2795 const bool closing = journal_outcome_is_closing_account(line.outcome_);
2796 const auto known = fold_of.find(line.id_);
2797 auto fold = known == fold_of.end() ? folds.end() : folds.begin() + static_cast<std::ptrdiff_t>(known->second);
2798 if (fold == folds.end()) {
2799 fold_of.emplace(line.id_, folds.size());
2800 folds.push_back(Fold{line, line.recorded_at_, {}, false, false, false, {}});
2801 fold = std::prev(folds.end());
2802 if (closing) {
2803 fold->line_.baseline_.reset();
2804 }
2805 } else {
2806 if (!closing && !same_instruction(fold->line_, line)) {
2807 fold->operator_only_ = true;
2808 fold->why_ = "its journal lines disagree about the order";
2809 }
2810 if (line.recorded_at_ < fold->sent_at_) {
2811 fold->sent_at_ = line.recorded_at_;
2812 }
2813 }
2814 if (line.requires_operator_ && !journal_outcome_requires_operator(line.outcome_)) {
2815 fold->operator_only_ = true;
2816 fold->why_ = line.detail_.empty() ? std::string("a damaged journal line") : line.detail_;
2817 }
2818 switch (line.outcome_) {
2821 if (!line.order_id_.empty()) {
2822 fold->name_ = line.order_id_;
2823 // The broker's reply named it (`ACCEPTED`), not a sweep (`RESOLVED`).
2824 fold->accepted_ = journal_outcome_answers_send(line.outcome_);
2825 }
2826 break;
2828 fold->done_ = true;
2829 set_boundary(line.instrument_, line.recorded_at_, /*strict=*/true);
2830 break;
2832 fold->done_ = true;
2833 break;
2835 fold->operator_only_ = true;
2836 fold->why_ = line.detail_;
2837 break;
2840 break;
2841 }
2842 }
2843
2844 std::vector<Unconfirmed> restored;
2845 // Why each restored order is held for an operator, aligned with `restored`.
2846 std::vector<std::string> why;
2847 for (auto& fold : folds) {
2848 if (fold.done_) {
2849 continue;
2850 }
2851 Unconfirmed entry;
2852 entry.journal_ = fold.line_;
2853 entry.ref_.primary_ = fold.name_;
2854 entry.instrument_ = fold.line_.instrument_;
2855 entry.side_ = fold.line_.side_;
2856 entry.expected_ = fold.line_.quantity_;
2857 entry.baseline_ = fold.line_.baseline_.value_or(Decimal64::parse("0"));
2858 entry.sent_at_ = fold.sent_at_;
2859 // On the shared clock (`serialized`): a holdings read older than the order's own line says nothing.
2860 entry.baseline_at_ = fold.sent_at_;
2861 entry.identified_ = fold.accepted_;
2862 entry.residual_ = fold.line_.quantity_;
2863 entry.restored_ = true;
2864 entry.operator_only_ = fold.operator_only_ || !fold.line_.baseline_;
2865 if (!fold.line_.baseline_ && fold.why_.empty()) {
2866 fold.why_ = "its journal line records no baseline (an overlapping or older runner wrote it)";
2867 }
2868 // The reserved row, rebuilt from the journal's record of the order.
2869 strategy::PlannedOrder order;
2870 order.instrument_ = fold.line_.instrument_;
2871 order.side_ = fold.line_.side_;
2872 order.type_ = fold.line_.type_;
2873 order.time_in_force_ = fold.line_.time_in_force_;
2874 order.session_ = fold.line_.session_;
2875 order.venue_ = fold.line_.venue_.value_or(Venue::UNSPECIFIED);
2876 order.quantity_ = fold.line_.quantity_;
2877 order.price_ = fold.line_.price_;
2878 order.trigger_price_ = fold.line_.trigger_price_;
2879 strategy::Portfolio<Tag> scratch;
2880 reserve(scratch, order, std::nullopt, std::nullopt, entry.ref_, /*spends=*/false);
2881 entry.row_ = scratch.working_.back();
2882 restored.push_back(std::move(entry));
2883 why.push_back(fold.why_);
2884 }
2885 for (std::size_t at = 0; at < restored.size(); ++at) {
2886 for (std::size_t other = 0; other < restored.size(); ++other) {
2887 if (other != at && restored[other].instrument_ == restored[at].instrument_) {
2888 restored[at].operator_only_ = true;
2889 if (why[at].empty()) {
2890 why[at] = "another order on its instrument is also unaccounted for";
2891 }
2892 }
2893 }
2894 }
2895 for (std::size_t at = 0; at < restored.size(); ++at) {
2896 if (restored[at].operator_only_) {
2897 ledger_.mark(
2898 restored[at].instrument_,
2899 "a previous process left an order" + named(restored[at]) + " only an operator may account for (" +
2900 why[at] + "); see release_obligations"
2901 );
2902 }
2903 unconfirmed_.push_back(std::move(restored[at]));
2904 }
2905 confirm_reservations();
2906 }
2907 // Remembers an order the broker just accepted, so a listing that lacks it cannot retire its
2908 // reservation; see `unconfirmed_orders`. Called right after `persist_reservation` wrote its row.
2909 //
2910 // `indeterminate`: an order whose request failed in transit, tracked with no reference until a sweep
2911 // names it (`resolve_indeterminate_placement`).
2912 void track_unconfirmed(
2913 [[maybe_unused]] const strategy::PlannedOrder& order, [[maybe_unused]] const std::optional<OrderRef<Tag>>& ref,
2914 [[maybe_unused]] bool indeterminate = false, [[maybe_unused]] const std::optional<Decimal64>& baseline = std::nullopt
2915 ) {
2916 if constexpr (keeps_sent_orders_until_listed) {
2917 if constexpr (can_serialize) {
2918 if (serialized() && baseline && !context_.portfolio().working_.empty()) {
2919 track_serialized(order, ref, indeterminate, *baseline);
2920 return;
2921 }
2922 }
2923 track_overlapping(order, ref, indeterminate);
2924 }
2925 }
2926
2927 // `track_unconfirmed` without serialization.
2928 //
2929 // Only an order with a broker reference and a portfolio that has been read: without a reference no
2930 // listing or history row can be matched to it, and without a read portfolio there is no baseline. Those
2931 // keep the old behaviour, where the next listing retires the row. An order of unknown outcome shows no
2932 // row while the indeterminacy ledger withholds its instrument (nothing can be placed there anyway, and a
2933 // resting order's own listed row would be counted twice) and does not time out; only holdings can cover it.
2934 void track_overlapping(const strategy::PlannedOrder& order, const std::optional<OrderRef<Tag>>& ref, bool indeterminate) {
2935 const auto& portfolio = context_.portfolio();
2936 if ((!indeterminate && (!ref || ref->primary_.empty())) ||
2937 portfolio.as_of_ == std::chrono::system_clock::time_point{} || portfolio.working_.empty()) {
2938 return;
2939 }
2940 const auto held = portfolio.net_quantity_of(order.instrument_);
2941 if (!held) {
2942 return;
2943 }
2944 Unconfirmed entry;
2945 if (ref) {
2946 entry.ref_ = *ref;
2947 }
2948 entry.instrument_ = order.instrument_;
2949 entry.side_ = order.side_;
2950 entry.expected_ = order.quantity_;
2951 entry.baseline_ = *held;
2952 entry.row_ = portfolio.working_.back();
2953 entry.sent_at_ = freshness_instant();
2954 entry.residual_ = order.quantity_;
2955 entry.unexplained_since_ = entry.sent_at_;
2956 unconfirmed_.push_back(std::move(entry));
2957 }
2958
2959 // Records that the broker accepted this runner's withdrawal of `ref`, if it is a sent order still counted.
2960 void note_withdrawal_accepted(const OrderRef<Tag>& ref) {
2961 for (auto& entry : unconfirmed_) {
2962 // The primary part alone, as everywhere a broker's own row is matched to the order.
2963 if (!entry.ref_.primary_.empty() && entry.ref_.primary_ == ref.primary_) {
2964 entry.withdrawn_ = true;
2965 }
2966 }
2967 }
2968
2969 // Whether some of `entry`'s quantity has been unexplained for longer than the confirmation timeout.
2970 bool timed_out(const Unconfirmed& entry, std::chrono::system_clock::time_point now) const {
2971 return entry.unexplained_since_ && now - *entry.unexplained_since_ > confirmation_timeout_;
2972 }
2973
2974 // How far a broker's order time may stand from this runner's own record of the order. Order numbers
2975 // repeat only across days, so an hour tells two orders apart and
2976 // still allows for a clock that is off.
2977 static constexpr std::chrono::hours order_time_tolerance_{1};
2978
2979 // The history row that answers for an order: a row with its number and instrument (and side, if
2980 // given) whose order time, where stated, is within `[earliest, latest]`. Null if there is none, or if
2981 // two such rows disagree on what filled or on whether the order is still open: a number reused on
2982 // another day must not answer for this order, and when the rows cannot tell which is which, neither
2983 // does. Rows that agree are one answer (a dated read can return an undated row once per day).
2984 static const OrderSnapshot* history_row_for(
2985 const std::vector<OrderSnapshot>& evidence, const std::string& order_id, const InstrumentId& instrument,
2986 std::optional<OrderSide> side, std::optional<std::chrono::system_clock::time_point> earliest,
2987 std::optional<std::chrono::system_clock::time_point> latest
2988 ) {
2989 const OrderSnapshot* found = nullptr;
2990 for (const auto& row : evidence) {
2991 if (row.order_id_ != order_id || !(row.instrument_ == instrument) || (side && row.side_ != *side)) {
2992 continue;
2993 }
2994 if (row.ordered_at_ && ((earliest && *row.ordered_at_ < *earliest) || (latest && *latest < *row.ordered_at_))) {
2995 continue;
2996 }
2997 if (found == nullptr) {
2998 found = &row;
2999 } else if (decimal_less(found->execution_.filled_quantity_, row.execution_.filled_quantity_) ||
3000 decimal_less(row.execution_.filled_quantity_, found->execution_.filled_quantity_) ||
3001 strategy::order_is_open(found->status_) != strategy::order_is_open(row.status_)) {
3002 return nullptr;
3003 }
3004 }
3005 return found;
3006 }
3007
3008 // Whether only the broker's history can settle `entry` now; see `unconfirmed_awaiting_evidence`.
3009 bool awaits_evidence(const Unconfirmed& entry, std::chrono::system_clock::time_point now) const {
3010 return serialized() ? awaits_evidence_serialized(entry, now) : awaits_evidence_overlapping(entry, now);
3011 }
3012
3013 bool awaits_evidence_overlapping(const Unconfirmed& entry, std::chrono::system_clock::time_point now) const {
3014 // A settled entry has had its answer: what it filled is known, and only holdings remain to show it.
3015 // One the history had no answer for past the timeout waits on holdings too, without a read per poll.
3016 // One with no reference yet never times out *while the ledger holds its instrument*: a sweep will
3017 // name the order or forget it (`resolve_indeterminate_placement`). Once something else lifted the
3018 // hold -- an operator clearing an `ambiguous` sweep by hand -- it is an ordinary entry again: its
3019 // history read finds no row, and a holdings read a timeout on withholds the instrument, counted,
3020 // rather than leaving an order that may never have existed reserved for the rest of the run.
3021 if (entry.residual_.coefficient() == 0 || entry.settled_ || entry.history_missed_ || entry.operator_only_ ||
3022 (entry.ref_.primary_.empty() && ledger_.requires_reconcile(entry.instrument_))) {
3023 return false;
3024 }
3025 if (timed_out(entry, now)) {
3026 return true;
3027 }
3028 return !entry.listed_now_ && (entry.listed_ || entry.withdrawn_ || entry.restored_);
3029 }
3030
3031 // Whether `working` is this runner's reserved row for `entry`.
3032 static bool is_reserved_row_of(const WorkingOrder<Tag>& working, const Unconfirmed& entry) {
3033 return working.reserved_ && working.ref_.primary_ == entry.ref_.primary_ &&
3034 working.order_.instrument_ == entry.instrument_ && working.order_.side_ == entry.side_;
3035 }
3036
3037 // The broker's own open row for `entry`'s order in `portfolio` (never a reservation), or none.
3038 static const WorkingOrder<Tag>* broker_row_for(const strategy::Portfolio<Tag>& portfolio, const Unconfirmed& entry) {
3039 if (entry.ref_.primary_.empty()) {
3040 return nullptr;
3041 }
3042 for (const auto& working : portfolio.working_) {
3043 if (!working.reserved_ && working.ref_.primary_ == entry.ref_.primary_ &&
3044 working.order_.instrument_ == entry.instrument_ && working.order_.side_ == entry.side_ &&
3045 strategy::order_is_open(working.order_.status_)) {
3046 return &working;
3047 }
3048 }
3049 return nullptr;
3050 }
3051
3052 // Puts `entry`'s reserved row back into `portfolio` for the quantity nothing explains yet.
3053 static void rebuild_residual_row(strategy::Portfolio<Tag>& portfolio, const Unconfirmed& entry, const Decimal64& residual) {
3054 auto row = entry.row_;
3055 row.order_.quantity_ = residual;
3056 row.amendable_quantity_ = residual;
3057 portfolio.working_.push_back(std::move(row));
3058 }
3059
3060 // Marks `entry` fully explained by holdings and listing; a later gap starts over, with a fresh timeout and
3061 // a fresh history read.
3062 static void mark_explained(Unconfirmed& entry) {
3063 entry.residual_ = Decimal64::parse("0");
3064 entry.unexplained_since_.reset();
3065 entry.history_missed_ = false;
3066 }
3067
3068 // The newest unnamed order on `instrument` and `side`: the sweep answers the order just sent, and an
3069 // older one still unnamed is left over from a hold cleared by hand without naming it.
3070 typename std::vector<Unconfirmed>::iterator newest_unnamed(const InstrumentId& instrument, OrderSide side) {
3071 const auto newest = std::find_if(unconfirmed_.rbegin(), unconfirmed_.rend(), [&](const Unconfirmed& entry) {
3072 return entry.ref_.primary_.empty() && entry.instrument_ == instrument && entry.side_ == side;
3073 });
3074 return newest == unconfirmed_.rend() ? unconfirmed_.end() : std::prev(newest.base());
3075 }
3076
3077 // Removes `entry`'s reserved row from the context portfolio.
3078 void drop_reserved_row(const Unconfirmed& entry) {
3079 auto portfolio = context_.portfolio();
3080 portfolio.working_.erase(
3081 std::remove_if(
3082 portfolio.working_.begin(), portfolio.working_.end(),
3083 [&](const WorkingOrder<Tag>& working) { return is_reserved_row_of(working, entry); }
3084 ),
3085 portfolio.working_.end()
3086 );
3087 context_.set_portfolio(std::move(portfolio));
3088 }
3089
3090 // Rebuilds the reserved row of every sent order still counted, and forgets those holdings confirm.
3091 //
3092 // Run before every handler and every pass, so whatever replaced `working_` since -- a listing, or a
3093 // source of the caller's own -- the strategy and the guard see each order's unexplained quantity.
3094 // `confirm_serialized` and `confirm_overlapping` say when an order counts as confirmed.
3095 void confirm_reservations() {
3096 if constexpr (keeps_sent_orders_until_listed) {
3097 if (unconfirmed_.empty()) {
3098 return;
3099 }
3100 const auto now = freshness_instant();
3101 auto portfolio = context_.portfolio();
3102 // Every row this list owns comes out first, and the residual goes back in below.
3103 portfolio.working_.erase(
3104 std::remove_if(
3105 portfolio.working_.begin(), portfolio.working_.end(),
3106 [&](const WorkingOrder<Tag>& working) {
3107 for (const auto& entry : unconfirmed_) {
3108 if (is_reserved_row_of(working, entry)) {
3109 return true;
3110 }
3111 }
3112 return false;
3113 }
3114 ),
3115 portfolio.working_.end()
3116 );
3117 if (serialized()) {
3118 confirm_serialized(portfolio, now);
3119 } else {
3120 confirm_overlapping(portfolio, now);
3121 }
3122 context_.set_portfolio(std::move(portfolio));
3123 }
3124 }
3125
3126 // The overlapping half of `confirm_reservations`: orders grouped per instrument, credited in the order
3127 // they were sent.
3128 //
3129 // - **covered**: holdings movement since the first unconfirmed order's baseline on the instrument, in
3130 // the orders' direction, credited in the order they were sent, each up to what it expects. Orders
3131 // on both sides of one instrument are credited only when holdings equal the baseline plus all of
3132 // them, since a movement could be either side's;
3133 // - **listed**: what a listing still shows remaining for the same reference, instrument and side;
3134 // - the row is put back with `expected - covered - listed`, when that is positive, and **never
3135 // spends buying power again** -- the pass that sent the order already did;
3136 // - the entry ends once `covered` reaches what it expects.
3137 void confirm_overlapping(strategy::Portfolio<Tag>& portfolio, std::chrono::system_clock::time_point now) {
3138 const auto zero = Decimal64::parse("0");
3139 std::vector<bool> retire(unconfirmed_.size(), false);
3140 // Settled by history, or past the timeout with no history answer, and still not in a holdings
3141 // read taken a full timeout later: withheld, with what holdings did show credited to it.
3142 std::vector<bool> expired(unconfirmed_.size(), false);
3143 std::vector<bool> done(unconfirmed_.size(), false);
3144 for (std::size_t first = 0; first < unconfirmed_.size(); ++first) {
3145 if (done[first]) {
3146 continue;
3147 }
3148 const auto& instrument = unconfirmed_[first].instrument_;
3149 const auto direction = unconfirmed_[first].side_;
3150 std::vector<std::size_t> group;
3151 bool mixed = false;
3152 for (std::size_t at = first; at < unconfirmed_.size(); ++at) {
3153 if (unconfirmed_[at].instrument_ == instrument) {
3154 group.push_back(at);
3155 done[at] = true;
3156 mixed = mixed || unconfirmed_[at].side_ != direction;
3157 }
3158 }
3159 // `a` signed in the group's direction: what a buy adds to a long holding, a sell takes away.
3160 const auto signed_in = [&](const Unconfirmed& entry, const Decimal64& quantity) {
3161 return entry.side_ == direction ? quantity : decimal_subtract(zero, quantity).value_or(zero);
3162 };
3163
3164 // What a listing still shows of each order: that part cannot have reached holdings.
3165 std::vector<Decimal64> listed(group.size(), zero);
3166 std::vector<Decimal64> fillable(group.size(), zero);
3167 for (std::size_t at = 0; at < group.size(); ++at) {
3168 auto& entry = unconfirmed_[group[at]];
3169 entry.listed_now_ = false;
3170 if (const auto* working = broker_row_for(portfolio, entry)) {
3171 entry.listed_ = true;
3172 entry.listed_now_ = true;
3173 listed[at] = decimal_subtract(working->order_.quantity_, working->order_.execution_.filled_quantity_)
3174 .value_or(zero);
3175 }
3176 const auto left = decimal_subtract(entry.expected_, listed[at]).value_or(zero);
3177 fillable[at] = decimal_less(zero, left) ? left : zero;
3178 }
3179
3180 // The holdings movement since the group's baseline -- the first entry's, which a retirement
3181 // below advances -- in the group's direction.
3182 std::optional<Decimal64> moved;
3183 if (const auto held = portfolio.net_quantity_of(instrument)) {
3184 moved = direction == OrderSide::BUY ? decimal_subtract(*held, unconfirmed_[first].baseline_) :
3185 decimal_subtract(unconfirmed_[first].baseline_, *held);
3186 }
3187 std::vector<Decimal64> covered(group.size(), zero);
3188 if (moved && !mixed) {
3189 // Credited in the order they were sent, each up to the part no listing still shows.
3190 auto left = *moved;
3191 for (std::size_t at = 0; at < group.size() && decimal_less(zero, left); ++at) {
3192 covered[at] = decimal_less(left, fillable[at]) ? left : fillable[at];
3193 left = decimal_subtract(left, covered[at]).value_or(zero);
3194 }
3195 } else if (moved) {
3196 // Both sides: a movement could be either side's, so it counts only when it is exactly what
3197 // all of them would make. A resting order a listing shows in full adds nothing, so a
3198 // protective stop does not hold up the buy beside it.
3199 std::optional<Decimal64> total = zero;
3200 for (std::size_t at = 0; at < group.size() && total; ++at) {
3201 total = decimal_add(*total, signed_in(unconfirmed_[group[at]], fillable[at]));
3202 }
3203 // A sum of zero is no evidence: two opposite orders that have not filled at all look like
3204 // two that both have. They wait for their history, and once it has said what each one
3205 // filled, holdings that did not move are evidence after all. That includes the resting order
3206 // above, which waits for no history (it once held them up until they timed out).
3207 bool all_settled = true;
3208 for (std::size_t at = 0; at < group.size(); ++at) {
3209 all_settled = all_settled && (unconfirmed_[group[at]].settled_ || fillable[at].coefficient() == 0);
3210 }
3211 if (total && (total->coefficient() != 0 || all_settled) && !decimal_less(*total, *moved) &&
3212 !decimal_less(*moved, *total)) {
3213 covered = fillable;
3214 }
3215 }
3216
3217 // Retired entries advance the baseline the rest are measured from, so the movement that
3218 // confirmed one is never counted again for the next.
3219 std::optional<Decimal64> advanced = unconfirmed_[first].baseline_;
3220 for (std::size_t at = 0; at < group.size(); ++at) {
3221 auto& entry = unconfirmed_[group[at]];
3222 if (!decimal_less(covered[at], entry.expected_)) {
3223 retire[group[at]] = true;
3224 if (advanced) {
3225 advanced = direction == OrderSide::BUY ?
3226 decimal_add(*advanced, signed_in(entry, entry.expected_)) :
3227 decimal_subtract(*advanced, signed_in(entry, entry.expected_));
3228 }
3229 continue;
3230 }
3231 auto residual = decimal_subtract(entry.expected_, covered[at]).value_or(zero);
3232 residual = decimal_subtract(residual, listed[at]).value_or(zero);
3233 if (!decimal_less(zero, residual)) {
3234 mark_explained(entry);
3235 continue;
3236 }
3237 entry.residual_ = residual;
3238 if (!entry.unexplained_since_) {
3239 entry.unexplained_since_ = now;
3240 }
3241 // Judged by the holdings read, not the clock: the listing (and so the history) is polled
3242 // before holdings in each iteration, and on a loop polling slower than the timeout the
3243 // clock alone marked a fill one holdings read before it would have shown.
3244 if ((entry.settled_ || entry.history_missed_) && timed_out(entry, now) &&
3245 portfolio.as_of_ - *entry.unexplained_since_ > confirmation_timeout_) {
3246 // A holdings answer read a whole timeout after the quantity became unexplained (or after the
3247 // history settled it) does not show it, and the history either said what it filled or had
3248 // nothing to say: an outside trade, a disagreement or an order nobody here can account for.
3249 // Withheld for the operator.
3250 ++orders_unconfirmed_timed_out_;
3251 ledger_.mark(
3252 entry.instrument_,
3253 entry.settled_ ?
3254 "an order this runner sent (" + entry.ref_.primary_ +
3255 ") filled by the order history, and holdings did not show it within the confirmation "
3256 "timeout" :
3257 std::string("an order this runner sent") +
3258 (entry.ref_.primary_.empty() ? " (its reply lost, never named by a sweep)" :
3259 " (" + entry.ref_.primary_ + ")") +
3260 " was not confirmed by holdings, the order list or the order history in time"
3261 );
3262 expired[group[at]] = true;
3263 // What holdings did show of it stays credited to it, not to the next order.
3264 if (advanced) {
3265 advanced = direction == OrderSide::BUY ? decimal_add(*advanced, signed_in(entry, covered[at])) :
3266 decimal_subtract(*advanced, signed_in(entry, covered[at]));
3267 }
3268 continue;
3269 }
3270 // Not shown while the ledger holds an order not yet named: no placement on the instrument can
3271 // go out then, and beside the broker's own row for the same order a resting one would count
3272 // twice, so a target would withdraw the real one. The sweep that lifts the hold names it.
3273 if (entry.ref_.primary_.empty() && ledger_.requires_reconcile(entry.instrument_)) {
3274 continue;
3275 }
3276 rebuild_residual_row(portfolio, entry, residual);
3277 }
3278 for (const auto at : group) {
3279 if (!retire[at] && advanced) {
3280 unconfirmed_[at].baseline_ = *advanced;
3281 }
3282 }
3283 }
3284
3285 for (std::size_t at = unconfirmed_.size(); at-- > 0;) {
3286 if (retire[at] || expired[at]) {
3287 unconfirmed_.erase(unconfirmed_.begin() + static_cast<std::ptrdiff_t>(at));
3288 }
3289 }
3290 }
3291
3292 // Carries this pass's reservation into the next one.
3293 //
3294 // **The in-pass view dies with the pass**, so without this a second pass before the next poll
3295 // starts from the broker's last word again and can re-spend what the first pass already
3296 // committed. The fix needs no matcher, because a working-order poll *replaces* what it
3297 // refreshes: `poll_working_orders` assigns `working_` wholesale, so an untracked reservation
3298 // written here is discarded the moment the broker's own listing arrives. (A holdings poll does not: it refreshes
3299 // positions and keeps `working_`.) Nothing has to recognise its own order in a listing -- which is
3300 // the half of reserve-and-match that carries a matcher's failure modes.
3301 //
3302 // **Always, for a runner that sends to a broker which lists working orders.** Target
3303 // reconciliation reads the same portfolio, and an order this runner just sent has to count as
3304 // working there, whatever the limits: a target restated on the next pass would otherwise be sent
3305 // again. It was once scoped to the limits below, and the cold start showed the cost --
3306 // `SessionSource` then fed a whole history read one pass per bar before the next listing, so a
3307 // target strategy under default limits sent one order per history bar (`tests/test-live.cpp`,
3308 // `[own-orders]`). It now feeds history through `feed_warmup`, which sends nothing; the reservation
3309 // still covers every other pass before a listing (a runner fed by hand, two names in one bar
3310 // poll, or bar polls that come faster than the listing's). A listing and holdings bound its life
3311 // (`unconfirmed_orders`), so the loop holds such a run while no listing will come -- the source
3312 // publishes none, no working-order cadence is set, or the last listing is older than the portfolio
3313 // age policy (`PollingLoop::gate_for`).
3314 //
3315 // **Scoped to the limits that read it everywhere else.** A dry run, `live::bind_observer` and a
3316 // custom executor that reaches no broker write a row with no broker reference: a later target
3317 // that opposes it would ask to withdraw an order identified by nothing, and a dry run has no
3318 // listing to replace the row at all. A broker that publishes no working-order inquiry never
3319 // replaces it either. For them the reservation persists only for the limits that size against
3320 // what is outstanding, as before. `max_position_quantity_` is one of those limits: its ceiling
3321 // counts same-side working orders, so an accepted order that stopped being visible at the end of a
3322 // pass would let the next bar place another of the same size.
3323 //
3324 // A custom executor that reaches a broker but reports no reference (`reports_last_ref`) is still
3325 // counted as keeping its orders: a duplicate order is the worse failure, and the listing the loop
3326 // insists on retires the unaddressable row.
3327 //
3328 // A listing replaces `working_` wholesale, and so used to retire the row whether or not it carried the
3329 // order -- a market order that filled at once is in no listing, and holdings lagging the fill did not
3330 // show it either. For a runner that keeps sent orders until listed, the row is now rebuilt
3331 // before every pass from what nothing explains yet (`track_unconfirmed`, `unconfirmed_orders`).
3332 void persist_reservation(
3333 const strategy::PlannedOrder& order,
3334 const std::optional<Decimal64>& reference_price,
3335 const std::optional<Currency>& currency,
3336 const std::optional<OrderRef<Tag>>& ref
3337 ) {
3338 const bool sells = guard_.limits().enforce_sellable_quantity_ && order.side_ == OrderSide::SELL;
3339 const bool buys = guard_.limits().enforce_buying_power_ && order.side_ == OrderSide::BUY;
3340 const bool capped = guard_.limits().max_position_quantity_.has_value();
3341 // The exposure ceiling reads what is outstanding for the same reason the position ceiling does,
3342 // so it needs the reservation for the same reason. Left out, a caller enabling only this limit
3343 // would get the within-pass fix and not the across-pass one: ten bought on each of two bars,
3344 // with no poll between, passes twice against a ceiling one of them should have reached. That is
3345 // precisely the omission `max_position_quantity_` once had.
3346 const bool exposed = guard_.limits().max_gross_exposure_.has_value();
3347 // A stop is read by target reconciliation whatever the limits, which refuses a target while one
3348 // rests (`RejectReason::STOP_ORDER_RESTING`). Not persisted, a stop placed on this pass would be
3349 // invisible to a target on the next one until a working-order poll listed it -- and "flatten"
3350 // would then go out beside the stop, both of them sells.
3351 const bool conditional = carries_trigger_price(order.type_);
3352 // A broker that will list the order back, reached by this runner: the listing and holdings
3353 // account for the row (`track_unconfirmed`), so the reservation is always written. See above for
3354 // why every other runner keeps the scoping.
3355 if (!keeps_sent_orders_until_listed && !sells && !buys && !capped && !exposed && !conditional) {
3356 return;
3357 }
3358 auto persisted = context_.portfolio();
3359 reserve(
3360 persisted, order, reference_price, currency, ref,
3361 guard_.limits().enforce_buying_power_
3362 );
3363 context_.set_portfolio(std::move(persisted));
3364 }
3365
3366 // Resolves what every instrument the exposure ceiling must value is denominated in, and marked at.
3367 //
3368 // **This is the half `RiskGuard` cannot do**, which is why the guard takes it as an input rather
3369 // than deriving it. A position states its own currency and last price; a working order on an
3370 // instrument with no position states neither, and an opening buy is exactly that case. The runner
3371 // is where reference data and the quote cache live, so it is where the lookup belongs -- the same
3372 // reasoning that puts `order_currency_` here.
3373 //
3374 // **An instrument this cannot resolve is left out, and leaving it out is how the refusal
3375 // happens.** `projected_gross_exposure` returns nothing when the basis does not cover something
3376 // the portfolio holds or has working, and the guard turns that into `VALUE_NOT_KNOWN`. Inventing
3377 // a currency or a price here would be a silent pass, which this check exists to prevent.
3378 //
3379 // **Only fields somebody explicitly set are read**, which rules out the position's own
3380 // `market_value_.currency_` as a fallback however tempting it is. `Currency` default-constructs to
3381 // KRW and `HoldingPosition::last_price_` to zero, so an untouched position does not read as
3382 // unresolved -- it reads as Korean won marked at nothing, resolves, and contributes zero to the
3383 // sum. That is the silent omission this refusal exists to prevent, wearing the costume of an
3384 // answer. It is the same trap `backtest::ReplayLedger` was in.
3385 //
3386 // So: the currency comes from registered reference data, which is also where `order_currency_`
3387 // comes from, so the two cannot disagree about one instrument. The mark comes from the quote cache
3388 // or from a position's `last_price_` **when that price is not zero** -- a mark of zero is not a
3389 // valuation, and accepting one would value the position at nothing.
3390 //
3391 // The cost, and it is the intended direction: an account holding an instrument the caller never
3392 // registered refuses every order while the ceiling is on. Visible and diagnosable, rather than a
3393 // ceiling quietly measuring part of the portfolio.
3394 std::vector<strategy::InstrumentBasis> exposure_basis_for(
3395 const strategy::Portfolio<Tag>& view,
3396 const std::vector<strategy::PlannedOrder>& pending
3397 ) const {
3398 std::vector<InstrumentId> instruments;
3399 const auto note = [&instruments](const InstrumentId& id) {
3400 for (const auto& seen : instruments) {
3401 if (seen == id) {
3402 return;
3403 }
3404 }
3405 instruments.push_back(id);
3406 };
3407 for (const auto& position : view.positions_) {
3408 // Flat rows carry no exposure and are skipped for the reason `projected_gross_exposure` gives:
3409 // a replay keeps a lot after it returns to flat, and requiring a mark for one would let a
3410 // closed position refuse orders in an instrument it has nothing to do with.
3411 if (position.quantity_.coefficient() == 0) {
3412 continue;
3413 }
3414 note(position.instrument_);
3415 }
3416 for (const auto& working : view.working_) {
3417 if (strategy::order_is_open(working.order_.status_)) {
3418 note(working.order_.instrument_);
3419 }
3420 }
3421 for (const auto& order : pending) {
3422 note(order.instrument_);
3423 }
3424
3425 std::vector<strategy::InstrumentBasis> basis;
3426 basis.reserve(instruments.size());
3427 for (const auto& id : instruments) {
3428 const HoldingPosition* position = nullptr;
3429 for (const auto& held : view.positions_) {
3430 if (held.instrument_ == id) {
3431 position = &held;
3432 break;
3433 }
3434 }
3435
3436 const auto reference = context_.instrument(id);
3437 if (!reference) {
3438 continue;
3439 }
3440
3441 // **Positive, not merely non-zero.** `Context::set_last_price` stores whatever a bar or quote
3442 // carried, with no validation, so a malformed price reaches here from market data rather than
3443 // only from a caller mistake. A negative mark would make an instrument's exposure negative and
3444 // let it offset real holdings until the ceiling passed anything at all; zero values the
3445 // position at nothing, which is the silent omission this resolution exists to refuse. Either
3446 // leaves the instrument unresolved, and the guard turns that into `VALUE_NOT_KNOWN`.
3447 const auto zero = Decimal64::parse("0");
3448 const auto quoted = context_.last_price(id);
3449 // **A quote that is present and not a price leaves the instrument unresolved**, rather than
3450 // falling through to the position's older mark. Falling back would be the quiet outcome: the
3451 // ceiling would answer from a stale figure while the current quote said something impossible,
3452 // and nothing would report that the data was wrong. Absent is a different thing from invalid,
3453 // and only absent gets the fallback.
3454 if (quoted && !decimal_less(zero, *quoted)) {
3455 continue;
3456 }
3457 auto price = quoted;
3458 if (!price && position != nullptr && decimal_less(zero, position->last_price_)) {
3459 price = position->last_price_;
3460 }
3461 if (!price) {
3462 continue;
3463 }
3464 basis.push_back(strategy::InstrumentBasis{id, reference->currency_, *price});
3465 }
3466 return basis;
3467 }
3468
3469 // Writes a sellable quantity onto the position for `instrument`, when there is one.
3470 static void write_sellable(
3471 strategy::Portfolio<Tag>& portfolio,
3472 const InstrumentId& instrument,
3473 const Decimal64& quantity
3474 ) {
3475 for (auto& position : portfolio.positions_) {
3476 if (position.instrument_ == instrument) {
3477 position.sellable_quantity_ = quantity;
3478 // **The inquiry is its own reading, so it carries its own basis -- on this row only.** The
3479 // holdings poll that built this portfolio established nothing about a figure it never
3480 // reported, and the inquiry answered for *this* instrument and no other. Writing the basis to
3481 // the portfolio would let one instrument's declaration certify every other row's figure: a
3482 // populated but undeclared quantity on another instrument would become enforceable, and a
3483 // wrong-sized sell of it would reach the wire. An executor that declares nothing leaves this
3484 // unestablished, and the guard then refuses.
3485 position.sellable_basis_ = runner_detail::sellable_inquiry_basis_of<Executor>();
3486 return;
3487 }
3488 }
3489 }
3490
3491 // Records an accepted order in `view` so the rest of the pass sizes against it.
3492 //
3493 // Written as a working order because that is what it now is, and because the alternative --
3494 // adjusting the position -- would be a lie: nothing has filled.
3495 //
3496 // **It carries the reference the broker just issued**, and that is not cosmetic. No guard reads a
3497 // reference, but reconciliation does: a row opposing the next target is put on the cancel list by
3498 // its `ref_`, and a synthetic row with an empty one would have the runner ask a broker to
3499 // withdraw an order identified by nothing. The shape and the amendable quantity are filled for
3500 // the same reason -- `cancel_all` builds the instruction from them, and a broker that requires a
3501 // quantity would be sent zero. An order just accepted and unfilled is amendable in full.
3502 //
3503 // Where the executor could not supply a reference -- a dry run, a replay, a custom executor
3504 // that does not report one, or an order whose reply was lost (passed no reference on purpose: the
3505 // executor's last one belongs to an earlier order) -- the row is still written, because a limit
3506 // that ignored the order would be the worse failure. Whether such a row can still be withdrawn is the executor's
3507 // question, not this one's: a replay's engine matches a cancellation on instrument and side and
3508 // needs no reference at all, while `live::SessionExecutor` refuses to send one to a broker.
3509 //
3510 // `spends` decides whether a buy also draws down the published buying-power reading, and it is
3511 // the caller's buying-power check rather than anything this function can see. The two were once
3512 // entangled: a caller who had enabled only the position ceiling watched
3513 // `portfolio().buying_power_in()` fall after a buy, which is a public figure a strategy is free
3514 // to size its next order from -- a behaviour change well past enforcing a ceiling.
3515 static void reserve(
3516 strategy::Portfolio<Tag>& view,
3517 const strategy::PlannedOrder& order,
3518 const std::optional<Decimal64>& reference_price,
3519 const std::optional<Currency>& currency,
3520 const std::optional<OrderRef<Tag>>& ref,
3521 bool spends
3522 ) {
3523 WorkingOrder<Tag> working;
3524 working.order_.instrument_ = order.instrument_;
3525 working.order_.side_ = order.side_;
3526 working.order_.status_ = OrderStatus::PENDING;
3527 working.order_.type_ = order.type_;
3528 working.order_.session_ = order.session_;
3529 // The venue this runner asked the order to be routed to, which is
3530 // `PlannedOrder::venue_` -- already resolved against the binding's default
3531 // route before the send -- and **not** the instrument's: the instrument's is
3532 // identity, and an order whose identity carried a route would stop matching
3533 // its own holding. A broker's own listing fills this field from what it
3534 // routed the order to; until one arrives, this row is what `cancel_all`
3535 // reads, and leaving it `UNSPECIFIED` would withdraw the order at the
3536 // broker's default venue rather than at the one it went to.
3537 working.order_.venue_ = order.venue_;
3538 working.order_.price_ = order.price_;
3539 // A stop's trigger, so the reserved row has the shape the broker's listing will report --
3540 // `cancel_all` restates it and `OrderUpdateWatch` compares it.
3541 working.order_.trigger_price_ = order.trigger_price_;
3542 working.order_.time_in_force_ = order.time_in_force_;
3543 working.order_.quantity_ = order.quantity_;
3544 working.order_.execution_.filled_quantity_ = Decimal64::parse("0");
3545 working.amendable_quantity_ = order.quantity_;
3546 if (ref) {
3547 working.ref_ = *ref;
3548 }
3549 // Marked as ours. A limit sizing against what is outstanding must count it; anything inferring
3550 // the broker's own behaviour from the listing must not, because this row is arithmetic rather
3551 // than something the broker said.
3552 working.reserved_ = true;
3553 view.working_.push_back(std::move(working));
3554
3555 // A buy also spends money that the rest of the pass may not spend again. The estimate is the
3556 // same one the guard checked the order against -- its own price, or the last quote for a market
3557 // order -- and it is an *estimate*: a market order fills where it fills, and commission is not
3558 // in it. Under-reserving would be the dangerous direction, and neither of those makes the
3559 // reservation smaller than the order's stated cost.
3560 if (!spends || order.side_ != OrderSide::BUY || !currency) {
3561 return;
3562 }
3563 const auto price = order.price_ ? order.price_ : reference_price;
3564 if (!price) {
3565 return;
3566 }
3567 const auto spent = decimal_multiply(order.quantity_, *price);
3568 if (!spent) {
3569 return;
3570 }
3571 for (auto& reading : view.buying_power_) {
3572 if (!(reading.cash_.currency_ == *currency)) {
3573 continue;
3574 }
3575 if (const auto left = decimal_subtract(reading.cash_.amount_, *spent)) {
3576 reading.cash_.amount_ = *left;
3577 }
3578 return;
3579 }
3580 }
3581
3582 // Returns why a plan derived from the current portfolio must not be acted on, or nothing.
3583 //
3584 // **The policy is evaluated here, against the snapshot in hand, rather than being pushed in as a
3585 // verdict somebody else computed earlier.** A `bool` set by the loop would be a judgement about
3586 // whatever portfolio was current when the loop last looked, and the loop looks at points a source
3587 // callback can get between: `PollingLoop::gate_for` calls `read_order_list` -- which is handed a
3588 // mutable runner and can feed -- before it ever reaches its own age check. A plan settled inside
3589 // that call would be checked against a stale verdict. Asking the question at settlement time makes
3590 // the answer structurally current, and there is no invalidation protocol to get wrong.
3591 // Drops the deferred targets `instructions` supersede.
3592 //
3593 // Every pass, so a target or order the strategy states for an instrument -- in answer to an order
3594 // update, a bar, anything -- replaces the fallback before `retry_deferred` could release it. See
3595 // `deferred_targets` for the whole contract.
3596 void drop_superseded_deferred(const std::vector<Instruction<Tag>>& instructions) {
3597 if (deferred_targets_.empty()) {
3598 return;
3599 }
3600 const auto drop = [this](const InstrumentId& instrument) {
3601 for (auto at = deferred_targets_.begin(); at != deferred_targets_.end(); ++at) {
3602 if (at->target_.instrument_ == instrument) {
3603 deferred_targets_.erase(at);
3604 return;
3605 }
3606 }
3607 };
3608 for (const auto& instruction : instructions) {
3609 if (const auto* target = std::get_if<strategy::TargetPosition>(&instruction)) {
3610 drop(target->instrument_);
3611 } else if (const auto* intent = std::get_if<strategy::OrderIntent>(&instruction)) {
3612 drop(intent->instrument());
3613 }
3614 }
3615 }
3616
3617 // The release loop behind `retry_deferred` and `retry_deferred_on_one_snapshot`. `evidence` is the
3618 // broker's history, or null for a caller whose own ledger decided every stop.
3619 Report release_deferred(const std::vector<OrderSnapshot>* evidence) {
3620 bool released = false;
3621 for (auto at = deferred_targets_.begin(); at != deferred_targets_.end();) {
3622 // Serialized, a stop this runner placed is not done until it is accounted for (`serialized`).
3623 if (stop_listed(at->target_.instrument_) ||
3624 obligation_state(at->target_.instrument_) != ObligationState::NONE) {
3625 ++at;
3626 continue;
3627 }
3628 if (evidence != nullptr) {
3629 bool decided = true;
3630 bool filled = false;
3631 for (const auto& stop : at->stops_) {
3632 // Matched to the order time its listing showed, where it showed one. Without it (KIS lists no
3633 // time), a row of another day under the same number cannot be told apart unless two rows
3634 // disagree.
3635 std::optional<std::chrono::system_clock::time_point> earliest;
3636 std::optional<std::chrono::system_clock::time_point> latest;
3637 if (stop.ordered_at_) {
3638 earliest = *stop.ordered_at_ - order_time_tolerance_;
3639 latest = *stop.ordered_at_ + order_time_tolerance_;
3640 }
3641 const auto* row = history_row_for(
3642 *evidence, stop.ref_.primary_, at->target_.instrument_, std::nullopt, earliest, latest
3643 );
3644 if (row == nullptr) {
3645 decided = false;
3646 continue;
3647 }
3648 if (row->execution_.filled_quantity_.coefficient() != 0) {
3649 filled = true;
3650 } else if (strategy::order_is_open(row->status_)) {
3651 decided = false;
3652 }
3653 }
3654 if (filled) {
3655 ++deferred_dropped_after_fill_;
3656 at = deferred_targets_.erase(at);
3657 continue;
3658 }
3659 if (!decided) {
3660 ++at;
3661 continue;
3662 }
3663 }
3664 context_.submit(at->target_);
3665 at = deferred_targets_.erase(at);
3666 released = true;
3667 }
3668 // **Nothing released, nothing settled.** A pass has effects of its own -- it observes the
3669 // sellable convention from the portfolio -- and this runs on every working-order poll.
3670 if (!released) {
3671 return Report{};
3672 }
3673 // Exempt from `PORTFOLIO_SUPERSEDED` for this pass only; see `plan_withhold_reason`. Scoped, so a
3674 // settle that throws cannot leave the exemption on for every plan after it.
3675 struct Exempt {
3676 bool& flag_;
3677 explicit Exempt(bool& flag) : flag_(flag) { flag_ = true; }
3678 ~Exempt() { flag_ = false; }
3679 Exempt(const Exempt&) = delete;
3680 Exempt& operator=(const Exempt&) = delete;
3681 } exempt(releasing_deferred_);
3682 return settle();
3683 }
3684
3685 // Drops the retained target on `instrument`, if there is one.
3686 void forget_retained(const InstrumentId& instrument) {
3687 if (instrument.symbol_.empty()) {
3688 return;
3689 }
3690 retained_targets_.erase(
3691 std::remove_if(
3692 retained_targets_.begin(), retained_targets_.end(),
3693 [&](const RetainedTarget& kept) { return kept.target_.instrument_ == instrument; }
3694 ),
3695 retained_targets_.end()
3696 );
3697 // Every kept intent for the instrument, not the first: a later statement supersedes all of them.
3698 kept_intents_.erase(
3699 std::remove_if(
3700 kept_intents_.begin(), kept_intents_.end(),
3701 [&](const KeptIntent& kept) { return kept.intent_.instrument() == instrument; }
3702 ),
3703 kept_intents_.end()
3704 );
3705 }
3706
3707 // Keeps a target withheld for want of a trustworthy account, to plan again once there is one.
3708 //
3709 // Withheld because the account was never read, is too old, or predates the listing: each is a fact
3710 // about the data, and the next holdings read can change it. One per instrument, the latest kept, as
3711 // for `deferred_targets_`. Returns false, retaining nothing:
3712 //
3713 // - for anything but a `TargetPosition`. An `OrderIntent` states a quantity against the picture that
3714 // was wrong, and replaying it later would act on a decision nobody re-made. (An intent the market
3715 // phase stopped is a different case -- the picture was right, the hour was not -- and is kept by
3716 // `keep_intent_for_phase`, with its holding checked at release.)
3717 // - while a stop, an operator's halt or a local failure holds. What a strategy says then -- `on_stop`
3718 // included -- must not be replayed once the hold lifts.
3719 //
3720 // `until_accounted` keeps it past later holdings reads too, until the instrument owes no order; see
3721 // `withdraws_owed`.
3722 bool retain_withheld(
3723 const Instruction<Tag>& instruction, bool for_phase = false,
3724 std::optional<std::chrono::system_clock::time_point> after = std::nullopt, bool until_accounted = false
3725 ) {
3726 const auto* target = std::get_if<strategy::TargetPosition>(&instruction);
3727 if (target == nullptr) {
3728 return false;
3729 }
3730 if (guard_.halted(strategy::HaltReason::STOPPING) || guard_.halted(strategy::HaltReason::OPERATOR) ||
3731 guard_.halted(strategy::HaltReason::LOCAL_FAILURE)) {
3732 return false;
3733 }
3734 // The latest statement wins, and so does its reason: a target retained for the account and then
3735 // restated and refused for the phase waits for the phase.
3736 for (auto& kept : retained_targets_) {
3737 if (kept.target_.instrument_ == target->instrument_) {
3738 kept = RetainedTarget{*target, for_phase, after, until_accounted};
3739 return true;
3740 }
3741 }
3742 retained_targets_.push_back(RetainedTarget{*target, for_phase, after, until_accounted});
3743 return true;
3744 }
3745
3746 // Whether a serialized target's plan, which places nothing, withdraws an order this runner still owes on
3747 // the target's instrument: its reserved row, or the broker's own row for it.
3748 //
3749 // Such an order has often filled already, so the withdrawal is refused and the picture the plan was made
3750 // from is the stale one. The withdrawal is still sent (the order may be resting), and the target is kept
3751 // until every order on the instrument is accounted for, then planned again against holdings that show
3752 // what it did. Not planned again before that: a second withdrawal of a finished or already withdrawn
3753 // order is refused, and a broker's refusal can read as an unknown outcome that holds the instrument.
3754 //
3755 // Compared by the order's id, instrument and side, as `is_reserved_row_of` and `broker_row_for` do: a
3756 // restored order carries the id alone, while the row the broker lists carries its whole reference.
3757 bool withdraws_owed(const Instruction<Tag>& instruction, const std::vector<OrderRef<Tag>>& cancel) const {
3758 const auto* target = std::get_if<strategy::TargetPosition>(&instruction);
3759 if (target == nullptr || cancel.empty() || unconfirmed_.empty()) {
3760 return false;
3761 }
3762 for (const auto& working : context_.portfolio().working_) {
3763 if (!(working.order_.instrument_ == target->instrument_) ||
3764 std::find(cancel.begin(), cancel.end(), working.ref_) == cancel.end()) {
3765 continue;
3766 }
3767 for (const auto& entry : unconfirmed_) {
3768 const bool listed_row_of = !working.reserved_ && !entry.ref_.primary_.empty() &&
3769 working.ref_.primary_ == entry.ref_.primary_ && working.order_.instrument_ == entry.instrument_ &&
3770 working.order_.side_ == entry.side_;
3771 if (listed_row_of || is_reserved_row_of(working, entry)) {
3772 return true;
3773 }
3774 }
3775 }
3776 return false;
3777 }
3778
3779 // Whether this runner owes an order on `instrument` (sent, and not yet accounted for).
3780 bool owes(const InstrumentId& instrument) const {
3781 return std::any_of(unconfirmed_.begin(), unconfirmed_.end(), [&](const Unconfirmed& entry) {
3782 return entry.instrument_ == instrument;
3783 });
3784 }
3785
3786 // Keeps a target whose order the market phase stopped, for the first permitted phase.
3787 //
3788 // Two routes reach it, both only for a runner that sends to a broker: the phase limit refused an
3789 // order that opens or grows a position, or it admitted one that reduces a position outside the
3790 // phase and the broker (or the binding) refused it -- which is what a broker not taking orders at
3791 // midnight does. See `retry_retained_targets`.
3792 void retain_for_phase(
3793 const Instruction<Tag>& instruction, bool broker_answered = false,
3794 std::optional<std::chrono::system_clock::time_point> after = std::nullopt
3795 ) {
3796 if constexpr (runner_detail::sends_to_a_broker<Executor>::value) {
3797 if (retain_withheld(instruction, /*for_phase=*/true, after)) {
3798 return;
3799 }
3800 keep_intent_for_phase(instruction, broker_answered, after);
3801 } else {
3802 (void)instruction;
3803 (void)broker_answered;
3804 (void)after;
3805 }
3806 }
3807
3808 // Whether the error behind the placement just refused satisfies `predicate` (`broker_unreachable`,
3809 // `broker_rate_limited`).
3810 //
3811 // Read immediately after the send, which is the only moment `last_error()` is this order's. An
3812 // executor that states no error has said nothing about why, so its refusals stay final.
3813 template <typename Predicate>
3814 bool last_error_is(Predicate predicate) const {
3815 if constexpr (runner_detail::states_last_error<Executor>::value) {
3816 const auto& error = executor_.last_error();
3817 return error && predicate(*error);
3818 } else {
3819 (void)predicate;
3820 return false;
3821 }
3822 }
3823
3824 // Keeps an order intent whose order the market phase stopped, for the first permitted phase.
3825 //
3826 // The intent half of `retain_for_phase`: a daily strategy's protective stop, decided on the bar fed
3827 // at midnight and refused by a broker not taking orders then, would otherwise be restated at the
3828 // next midnight and refused again, and the position would never be protected live. Kept once per
3829 // instruction (one instruction can produce two refused orders), in the order stated, with the
3830 // instrument's holding at this moment, which `retry_retained_targets` checks before releasing it;
3831 // any intent carrying a trigger, a stop buy as much as a stop sell, is also dropped there when a stop
3832 // rests on the instrument. **Not** while a stop, an operator's halt or a local failure holds; **not** an intent carrying an
3833 // idempotency key that a broker answered, because a second order would reuse the key.
3834 void keep_intent_for_phase(
3835 const Instruction<Tag>& instruction, bool broker_answered,
3836 std::optional<std::chrono::system_clock::time_point> after = std::nullopt
3837 ) {
3838 const auto* intent = std::get_if<strategy::OrderIntent>(&instruction);
3839 if (intent == nullptr) {
3840 return;
3841 }
3842 if (guard_.halted(strategy::HaltReason::STOPPING) || guard_.halted(strategy::HaltReason::OPERATOR) ||
3843 guard_.halted(strategy::HaltReason::LOCAL_FAILURE)) {
3844 return;
3845 }
3846 if (broker_answered && intent->client_order_id()) {
3847 return;
3848 }
3849 for (const auto* seen : kept_this_pass_) {
3850 if (seen == &instruction) {
3851 return;
3852 }
3853 }
3854 const auto held = context_.portfolio().net_quantity_of(intent->instrument());
3855 if (!held) {
3856 return;
3857 }
3858 kept_this_pass_.push_back(&instruction);
3859 kept_intents_.push_back(KeptIntent{*intent, *held, after});
3860 }
3861
3862 // Returns whether the portfolio lists an open stop order on `instrument`.
3863 bool stop_listed(const InstrumentId& instrument) const {
3864 for (const auto& working : context_.portfolio().working_) {
3865 if (working.order_.instrument_ == instrument &&
3866 strategy::order_is_open(working.order_.status_) &&
3867 strategy::is_stop_order(working.order_)) {
3868 return true;
3869 }
3870 }
3871 return false;
3872 }
3873
3874 // Keeps a target whose plan is waiting on a stop's withdrawal; see `deferred_targets`.
3875 void defer_if_waiting_on_stop(
3876 const Instruction<Tag>& instruction,
3877 const strategy::Plan<Tag>& planned
3878 ) {
3879 const auto* target = std::get_if<strategy::TargetPosition>(&instruction);
3880 if (target == nullptr) {
3881 return;
3882 }
3883 for (const auto& rejection : planned.rejected_) {
3884 if (rejection.reason_ != strategy::RejectReason::STOP_ORDER_RESTING) {
3885 continue;
3886 }
3887 // The stops it waits on, as the listing shows them now -- `PENDING_CANCEL` included, since that
3888 // is exactly a stop whose outcome is not yet known.
3889 DeferredTarget deferred;
3890 deferred.target_ = *target;
3891 deferred.deferred_at_ = context_.now();
3892 for (const auto& working : context_.portfolio().working_) {
3893 if (working.order_.instrument_ == target->instrument_ &&
3894 strategy::order_is_open(working.order_.status_) &&
3895 strategy::is_stop_order(working.order_)) {
3896 deferred.stops_.push_back({working.ref_, working.order_.ordered_at_});
3897 }
3898 }
3899 for (auto& kept : deferred_targets_) {
3900 if (kept.target_.instrument_ == target->instrument_) {
3901 kept = std::move(deferred);
3902 ++targets_deferred_;
3903 return;
3904 }
3905 }
3906 deferred_targets_.push_back(std::move(deferred));
3907 ++targets_deferred_;
3908 return;
3909 }
3910 }
3911
3912 std::optional<WithheldPlanReason> plan_withhold_reason(bool would_cancel) const {
3913 const auto& portfolio = context_.portfolio();
3914 // **A picture the library can prove is mis-ordered, so no opt-out reaches it.** The opt-outs below
3915 // accept an account nobody read or one read too long ago; this one was read, recently, and before
3916 // the listing it would be reconciled with -- which is the order in which a fill between the two
3917 // reads makes a position vanish. Exempt only while a deferred target is being released: that
3918 // release fires on the stop's own history showing it ended unfilled, so the listing change behind
3919 // it is by construction not a fill.
3920 if (!releasing_deferred_ && portfolio.listing_newer_than_holdings()) {
3922 }
3923 // A default-constructed `Portfolio` carries the epoch, and no broker reports a snapshot taken in
3924 // 1970. The same sentinel the loop uses; both writers of `as_of_` in this library stamp it.
3925 const bool never_read = portfolio.as_of_ == std::chrono::system_clock::time_point{};
3926 const bool too_old = !never_read && maximum_portfolio_age_ &&
3927 freshness_instant() - portfolio.as_of_ > *maximum_portfolio_age_;
3928
3929 if (never_read || too_old) {
3930 // **A plan that would withdraw something is stopped whatever was opted out of.** The opt-outs
3931 // say the operator accepts *sizing* against an account this library cannot vouch for. They do
3932 // not say it should withdraw orders it cannot account for, and the two are not the same
3933 // concession: an account that merely looks flat makes a resting protective order look opposed.
3934 if (would_cancel) {
3936 }
3937 // A placement is the half the opt-outs are about, and it is also the half a driving loop's
3938 // hold would otherwise catch -- so it is stopped here only for a run that asked for the
3939 // account to be known.
3940 if (require_known_portfolio_ && !(never_read && allow_absent_portfolio_)) {
3942 }
3943 // **Falls through rather than returning.** An opted-out placement still has to face the hold
3944 // check below: returning early here was a defect, because it let a plan out under an operator's
3945 // halt on the strength of an unrelated opt-out.
3946 }
3947 // Any placement hold, not an enumerated few: the harm is that the replacement will be refused
3948 // while the withdrawal has already gone, and every reason that refuses a placement does that.
3949 if (guard_.blocking_reason()) {
3951 }
3952 return std::nullopt;
3953 }
3954
3955 // Returns the instant a snapshot's age is measured against.
3956 //
3957 // The later of the decision instant and the last clock reading a loop supplied, so neither can be
3958 // used to make an old snapshot look fresh: a late bar cannot rewind the clock, and a loop that has
3959 // not spoken yet cannot erase the decision instant.
3960 std::chrono::system_clock::time_point freshness_instant() const {
3961 return observed_at_ > context_.now() ? observed_at_ : context_.now();
3962 }
3963
3964 // Returns the instant a send is counted at in the order-rate window: the later of the decision
3965 // instant and the loop's reading, or the decision instant alone where no loop has given a reading.
3966 // A replay never does, and its decisions may all lie before the epoch an unset reading equals.
3967 std::chrono::system_clock::time_point send_instant() const {
3968 return observed_at_ == std::chrono::system_clock::time_point{} ? context_.now() : freshness_instant();
3969 }
3970
3971 // Returns the detail recorded with a withheld plan, naming the hold where there is one.
3972 std::string withhold_detail(WithheldPlanReason reason) const {
3973 if (reason != WithheldPlanReason::PLACEMENT_HELD) {
3974 return {};
3975 }
3976 const auto holding = guard_.blocking_reason();
3977 return holding ? std::string(strategy::halt_reason_name(*holding)) : std::string{};
3978 }
3979
3980 // Returns the instrument an instruction is about, for reporting a plan that was not acted on.
3981 static InstrumentId instruction_instrument(const Instruction<Tag>& instruction) {
3982 if (const auto* target = std::get_if<strategy::TargetPosition>(&instruction)) {
3983 return target->instrument_;
3984 }
3985 if (const auto* order = std::get_if<strategy::OrderIntent>(&instruction)) {
3986 return order->instrument();
3987 }
3988 return InstrumentId{};
3989 }
3990
3991 // Returns the tag an instruction carries, so a withheld plan can be attributed to it.
3992 static std::string instruction_tag(const Instruction<Tag>& instruction) {
3993 if (const auto* target = std::get_if<strategy::TargetPosition>(&instruction)) {
3994 return target->tag_;
3995 }
3996 if (const auto* order = std::get_if<strategy::OrderIntent>(&instruction)) {
3997 return order->tag();
3998 }
3999 return {};
4000 }
4001
4002 void send_cancel(const OrderRef<Tag>& ref, Report& report) {
4003 // Gated like a placement, and in the same order. A cancellation is the one instruction that
4004 // cannot make an unresolved *position* worse, which is why the ledger does not gate it -- but it
4005 // still goes through the journal, and a journal that cannot record it must not let it be sent
4006 // unrecorded.
4007 if (guard_.halted(strategy::HaltReason::LOCAL_FAILURE)) {
4008 ++instructions_unavailable_;
4009 report.unavailable_cancel_.push_back(ref);
4010 return;
4011 }
4012 // And a halt stops this runner *sending*, which a cancellation is -- but only a halt that is
4013 // about this instruction. The argument for refusing here is `StopIntent` reaching this runner
4014 // from a signal and from a file: exempting cancellations wholesale would let
4015 // `echo withdraw > /var/run/atl/stop` send an instruction that an operator's own halt forbade,
4016 // and a kill switch a control file can partly bypass is not one. An operator who wants out
4017 // resumes first, which is one deliberate act and is visible in the report either way.
4018 //
4019 // **That argument covers the holds somebody set, and not the holds that merely mean this runner
4020 // is uninformed.** Refusing to withdraw a resting order because the *holdings* poll failed is
4021 // worse than the exposure it guards: the order keeps resting and can fill, and the reference it
4022 // would be withdrawn by came from a working-order read that succeeded. So the question asked is
4023 // `InstructionClass::WITHDRAWAL`, and `strategy::halt_blocks` holds the division and its
4024 // reasoning. The row is still resolved below, and a reference the snapshot cannot place is
4025 // reported rather than sent blind.
4026 if (const auto holding =
4027 guard_.blocking_reason_for(strategy::InstructionClass::WITHDRAWAL)) {
4028 ++cancels_refused_;
4029 report.refused_cancel_.push_back(RefusedCancel<Tag>{ref, *holding});
4030 return;
4031 }
4032 // Resolved against the portfolio first, because building a cancellation needs the order's own
4033 // quantity on a broker that requires one. A reference not in the snapshot means the snapshot is
4034 // stale, and sending a cancellation for an order the runner cannot describe is exactly the kind
4035 // of blind instruction that should be visible instead.
4036 // The broker's own row first: a reserved row for the same order is this runner's arithmetic.
4037 const WorkingOrder<Tag>* working = nullptr;
4038 for (const auto& row : context_.portfolio().working_) {
4039 if (row.ref_ == ref && (working == nullptr || (working->reserved_ && !row.reserved_))) {
4040 working = &row;
4041 }
4042 }
4043 if (working == nullptr) {
4044 ++cancels_unresolved_;
4045 report.unresolved_cancel_.push_back(ref);
4046 return;
4047 }
4048 // **Serialized: a reserved row whose reference the broker did not give is not sent.** A restored
4049 // order, or one a sweep named, carries the order id alone, which some brokers cannot cancel by; and a
4050 // row with no id addresses nothing. Refused as the binding would, before anything is sent.
4051 if (working->reserved_ && serialized() && !cancellable_reservation(*working)) {
4052 ++cancels_rejected_;
4053 report.rejected_cancel_.push_back(RejectedCancel<Tag>{ref, /*by_broker=*/false});
4054 return;
4055 }
4056 const auto outcome = executor_.cancel(*working);
4057 note_executor_failure(send_outcome_is_local_failure(outcome));
4058 // Switched over rather than tested one outcome at a time, deliberately. Three different outcomes
4059 // once fell through to `would_cancel_` -- the bucket that says the
4060 // broker withdrew the order -- because each fix handled the case it was told about and left the
4061 // rest. A switch with no default makes the next added enumerator a compile error in the library's
4062 // test and example builds (`-Werror=switch`), which is the only version of this that stays fixed.
4063 switch (outcome) {
4065 // The broker took the request. Whether the order stops working is a later fact, which only
4066 // its own listing reports: cancellation is asynchronous, and a broker that answers here can
4067 // still reject the withdrawal afterwards.
4068 //
4069 // **Counted against the rate limit, like a placement.** A withdrawal is a request the
4070 // broker counted, and a window that does not see it is not the window the operator
4071 // configured -- a `WITHDRAW` over thirty resting orders is thirty requests the guard was
4072 // blind to, and the next placement is then checked against a window it believes is empty.
4073 // It is still never *refused* by that limit: `send_cancel` calls no `check`, because a rate
4074 // limit must never be the reason an order cannot be withdrawn.
4075 guard_.note_sent(send_instant());
4076 // A sent order still counted may have filled before the withdrawal; its history decides.
4077 note_withdrawal_accepted(ref);
4078 report.would_cancel_.push_back(ref);
4079 return;
4081 // The broker refused the request, which is an answer: nothing needs reconciling. It is not
4082 // a withdrawal that happened, and it is not proof the order is working either -- a fill is
4083 // one of the ordinary reasons a withdrawal is refused. It still spent a request.
4084 guard_.note_sent(send_instant());
4085 ++cancels_rejected_;
4086 report.rejected_cancel_.push_back(RejectedCancel<Tag>{ref, /*by_broker=*/true});
4087 return;
4089 // Marked *and* reported apart from `would_cancel_`. The mark was always here; reporting it
4090 // as a withdrawal that happened was not right beside it, because the order may still be
4091 // resting and a caller reading `would_cancel_` would believe it is not.
4092 ledger_.mark(working->order_.instrument_, "a cancellation request has an unknown outcome");
4093 // Counted like an acceptance, for the same reason a placement is: the request may well have
4094 // reached the broker, and a window that assumes it did not is the one that under-counts.
4095 guard_.note_sent(send_instant());
4096 ++cancels_indeterminate_;
4097 report.indeterminate_cancel_.push_back(ref);
4098 return;
4100 // No `note_sent`, for the reason the placement path gives: nothing was sent, so the broker
4101 // counted nothing, and a rate limit must never become the reason a later order cannot go.
4102 // Reported beside the broker's own refusals because what it means for the order is the
4103 // same -- it was not withdrawn -- while `by_broker_` on the entry says which of the two
4104 // it was. Not `last_error()`: a later instruction in the same pass clears that.
4105 ++cancels_rejected_;
4106 report.rejected_cancel_.push_back(RejectedCancel<Tag>{ref, /*by_broker=*/false});
4107 return;
4109 // Not reported as a withdrawal that happened, because it did not. A cancellation nobody
4110 // sent must not read as one the broker has.
4111 ++instructions_unavailable_;
4112 report.unavailable_cancel_.push_back(ref);
4113 return;
4114 }
4115 }
4116
4117 // Plans a weight-sized intent as the target position it states (`strategy::weight_target`).
4118 //
4119 // **A resting stop refuses it, and nothing is withdrawn.** Target reconciliation withdraws a
4120 // resting stop when a trade is needed and waits for the stop's own history before trading, which
4121 // only a `TargetPosition` in the instruction stream can do (`defer_if_waiting_on_stop`); a weight
4122 // intent is not one, and an intent has never touched a stop. So a weight on an instrument with a
4123 // stop resting is refused `STOP_ORDER_RESTING` and the stop stays -- unless the holding already
4124 // meets it, when nothing is traded or withdrawn at all.
4125 strategy::Plan<Tag> plan_weight(
4126 const strategy::OrderIntent& intent,
4127 const strategy::ReconcileInputs& inputs
4128 ) const {
4129 strategy::Plan<Tag> plan;
4130 const auto converted = strategy::weight_target(intent, inputs);
4131 if (!converted.target_) {
4132 plan.rejected_.push_back(strategy::Rejection{
4133 converted.refused_.value_or(strategy::RejectReason::NOT_REPRESENTABLE), intent.instrument(),
4134 intent.tag()});
4135 return plan;
4136 }
4137 if (stop_listed(intent.instrument())) {
4138 // Met already: nothing to trade, and the stop protects what the weight means to hold, which
4139 // is `STOP_ORDER_RESTING`'s own rule for a met target. Unlike a target, nothing else on the
4140 // instrument is withdrawn either.
4141 const auto held = context_.portfolio().net_quantity_of(intent.instrument());
4142 const auto gap = held ? decimal_subtract(*held, converted.target_->target_quantity_) : std::nullopt;
4143 if (gap && gap->coefficient() == 0) {
4144 return plan;
4145 }
4146 plan.rejected_.push_back(strategy::Rejection{
4148 return plan;
4149 }
4150 return strategy::reconcile(*converted.target_, context_.portfolio(), inputs);
4151 }
4152
4153 // Reconciles one queued instruction against the current portfolio.
4154 //
4155 // The reference data is held in a local for the duration of the call, because
4156 // `ReconcileInputs` borrows it rather than copying: an `Instrument` carries a tick table, and
4157 // copying one per instruction per bar would be paying repeatedly for a value that does not
4158 // change.
4159 strategy::Plan<Tag> plan_for(const Instruction<Tag>& instruction) {
4160 const auto id = instrument_of(instruction);
4161 const auto reference = context_.instrument(id);
4162 const auto band = band_for(id);
4163
4164 strategy::ReconcileInputs inputs;
4165 inputs.instrument_ = reference ? &*reference : nullptr;
4166 inputs.reference_price_ = context_.last_price(id);
4167 inputs.limits_ = band.applied_;
4168 inputs.portfolio_value_ = portfolio_value_;
4169 inputs.portfolio_value_currency_ = portfolio_value_currency_;
4170 inputs.tolerance_ = tolerance_;
4171 inputs.reserved_rows_are_unconfirmed_ = keeps_sent_orders_until_listed;
4172
4173 auto plan = [&] {
4174 if (const auto* target = std::get_if<strategy::TargetPosition>(&instruction)) {
4175 return strategy::reconcile(*target, context_.portfolio(), inputs);
4176 }
4177 if (const auto* intent = std::get_if<strategy::OrderIntent>(&instruction)) {
4178 // **A weight is a holding, so it is planned as the position it states.** Reconciled as an
4179 // order it would buy the whole allocation again at every rebalance, which is what
4180 // `SizeKind::TARGET_WEIGHT` promises it does not do; as a target it is diffed against the
4181 // position and the working orders, like any target.
4182 if (strategy::size_kind_is_target(intent->size().kind())) {
4183 return plan_weight(*intent, inputs);
4184 }
4185 return strategy::reconcile(*intent, context_.portfolio(), inputs);
4186 }
4187 return strategy::Plan<Tag>{};
4188 }();
4189
4190 // Counted per plan rather than per order, and counted even when the plan placed nothing: the
4191 // figure answers "how often did this run decline to use a band it was holding", which is a fact
4192 // about the policy. Whether it cost anything is the other counter's question.
4193 if (band.withheld_) {
4194 ++price_bands_withheld_;
4195 mark_withheld_band(plan, *band.withheld_);
4196 }
4197 return plan;
4198 }
4199
4200 // Records what the broker said about `order`, where the executor can say.
4201 //
4202 // Called at the `REJECTED` outcome and nowhere else, which is the only moment the answer is this
4203 // order's: `SessionExecutor::last_error()` is cleared by the next accepted instruction in the same
4204 // pass, so reading it later would attribute somebody else's silence to this refusal -- or nothing
4205 // at all.
4206 void note_broker_refusal(const strategy::PlannedOrder& order) {
4207 if constexpr (runner_detail::states_last_error<Executor>::value) {
4208 const auto& error = executor_.last_error();
4209 if (!error) {
4210 // **Cleared, not left alone.** An executor may answer `REJECTED` and state nothing; the
4211 // counter still moves, and a record kept from an *earlier* refusal would then be handed to
4212 // the next run as though it described this one -- the wrong instrument under the wrong tag.
4213 // `SessionExecutor` always states something on this path, so this is about a binding
4214 // somebody else writes: the contract is "the last refusal, or nothing", never "the last
4215 // refusal that happened to come with words".
4216 last_broker_refusal_.reset();
4217 return;
4218 }
4220 refusal.code_ = error->broker_code_;
4221 refusal.message_ = error->message_;
4222 refusal.instrument_ = order.instrument_;
4223 refusal.tag_ = order.tag_;
4224 last_broker_refusal_ = std::move(refusal);
4225 } else {
4226 static_cast<void>(order);
4227 }
4228 }
4229
4230 // Returns which instrument a queued instruction concerns.
4231 static InstrumentId instrument_of(const Instruction<Tag>& instruction) {
4232 if (const auto* target = std::get_if<strategy::TargetPosition>(&instruction)) {
4233 return target->instrument_;
4234 }
4235 if (const auto* intent = std::get_if<strategy::OrderIntent>(&instruction)) {
4236 return intent->instrument();
4237 }
4238 return InstrumentId{};
4239 }
4240
4241 // A band and what its age is measured from; see `set_price_limits`.
4242 struct DatedBand {
4243 // The band as the broker published it.
4244 PriceLimits limits_;
4245 // When this runner was told about it, which is all an unstamped band has.
4246 std::chrono::system_clock::time_point received_at_{};
4247 // Returns the instant the band's age is measured from; see `set_price_limits`.
4248 //
4249 // **The exchange's stamp may only make a band look older, never fresher.** It is there to catch
4250 // a successful fetch of a week-old band, which is an ageing question; a stamp *later* than the
4251 // moment this process received it answers nothing -- it is two clocks disagreeing, by ordinary
4252 // skew at best and by a broker's bug at worst -- and taking it at face value would let a band
4253 // date itself into the future and outlive any stated maximum. Capping it at the receipt instant
4254 // keeps the case it exists for and gives the other one no purchase, with no invented tolerance
4255 // for how far apart two clocks may be.
4256 std::chrono::system_clock::time_point as_of() const noexcept {
4257 if (!limits_.exchange_time_) {
4258 return received_at_;
4259 }
4260 return *limits_.exchange_time_ < received_at_ ? *limits_.exchange_time_ : received_at_;
4261 }
4262 };
4263 // Which band a plan may use, and which it was denied; see `band_for`.
4264 struct BandDecision {
4265 // The band to clamp with, when there is one this runner will stand behind.
4266 std::optional<PriceLimits> applied_;
4267 // The band that was held back, when one was, so the decision can be counted.
4268 std::optional<PriceLimits> withheld_;
4269 };
4270
4271 // Returns the band to apply to `id`'s prices, and separately the one that was held back.
4272 //
4273 // Exactly one of the two is filled when a band is registered, and neither when none is. The
4274 // withheld half exists so the decision can be counted and reported: a band silently dropped is
4275 // the same failure as a band silently applied.
4276 BandDecision band_for(const InstrumentId& id) const {
4277 BandDecision decision;
4278 const auto found = price_limits_.find(id);
4279 if (found == price_limits_.end()) {
4280 return decision;
4281 }
4282 if (!maximum_price_limits_age_ && !allow_unbounded_price_limits_age_) {
4283 // No policy stated. Not applied, and deliberately not the same as an absent band: the caller
4284 // asked for clamping and is getting none, which `price_bands_withheld` reports and a driving
4285 // loop names in its notes.
4286 decision.withheld_ = found->second.limits_;
4287 return decision;
4288 }
4289 // **A band dated after the instant it is being aged at is not applied, whatever the policy.**
4290 // `as_of` can no longer exceed the receipt stamp, so the only way here is a caller moving
4291 // `set_observed_at` backwards -- reporting day 20, registering a band, then reporting day 10.
4292 // The subtraction below would then be negative and pass every positive maximum, so a band from
4293 // the caller's own future would clamp. A duration this runner cannot compute is not an age, and
4294 // a band whose age is unknown is the case the whole policy exists for.
4295 if (found->second.as_of() > freshness_instant()) {
4296 decision.withheld_ = found->second.limits_;
4297 return decision;
4298 }
4299 if (maximum_price_limits_age_ &&
4300 freshness_instant() - found->second.as_of() > *maximum_price_limits_age_) {
4301 decision.withheld_ = found->second.limits_;
4302 return decision;
4303 }
4304 decision.applied_ = found->second.limits_;
4305 return decision;
4306 }
4307
4308 // Marks the orders in `plan` whose price the band that was **not** applied would have moved.
4309 //
4310 // A statement about the planned price, not a prediction about the wire: clamping happens before
4311 // tick rounding (`reconcile_detail::prepare_price`), so a bound that binds here could in principle
4312 // round back to the same price. What the flag says is the checkable thing -- this order's price
4313 // lies outside the withheld band on the side the clamp corrects.
4314 static void mark_withheld_band(strategy::Plan<Tag>& plan, const PriceLimits& withheld) {
4315 for (auto& order : plan.place_) {
4316 if (!order.price_) {
4317 continue;
4318 }
4319 bool would_move = false;
4320 static_cast<void>(strategy::reconcile_detail::clamp_to_limits(
4321 *order.price_, order.side_, std::optional<PriceLimits>(withheld), would_move
4322 ));
4323 order.price_would_have_been_clamped_ = would_move;
4324 }
4325 }
4326
4327 Strategy strategy_;
4328 strategy::RiskGuard guard_;
4329 // Placements sent that may have left an order resting; see `placements_at_broker`.
4330 std::size_t placements_at_broker_{0};
4331 // Orders sent under a phase the limits did not permit; see `sends_outside_permitted_phase`.
4332 std::size_t sends_outside_permitted_phase_{0};
4333 // How old a snapshot may be before a plan derived from it is withheld; see the setter.
4334 std::optional<std::chrono::nanoseconds> maximum_portfolio_age_;
4335 // The last wall-clock instant a driving loop reported; see `set_observed_at`.
4336 std::chrono::system_clock::time_point observed_at_{};
4337 // Whether the caller stated this runner may act with no snapshot at all; see the setter.
4338 bool allow_absent_portfolio_{false};
4339 // Whether a placement needs a portfolio that has actually been read; see the setter.
4340 //
4341 // Defaulted from the executor: a runner that can reach a broker requires it, and one that cannot
4342 // does not. That is what makes the rule hold for a live runner assembled by hand rather than only
4343 // for one the binding factories produced.
4344 bool require_known_portfolio_{runner_detail::sends_to_a_broker<Executor>::value};
4345 bool probing_sellable_{false};
4346 strategy::SellableConventionProbe<Tag> sellable_probe_;
4347 Context context_;
4348 // Records the first local failure, keeping it rather than overwriting with a later one.
4349 void note_local_failure(std::string detail) {
4350 // The *first* reason is kept, not the latest: a storage failure cascades, and the reason the
4351 // cascade started is the one an operator needs. Taking the hold again would only replace it.
4353 guard_.halt(strategy::HaltReason::LOCAL_FAILURE, std::move(detail));
4354 }
4355 }
4356
4357 // Makes the executor's own local failure sticky on the runner, if it reported one.
4358 //
4359 // Called after every send rather than only after a refusal, because the two are not the same
4360 // event. A *pre-send* journal write that fails produces `UNAVAILABLE` and nothing was sent. A
4361 // *terminal* journal write that fails happens after the broker has already answered: the send
4362 // really did happen, so the outcome passed back stays the broker's, and only this call records
4363 // that the journal is no longer trustworthy.
4364 void note_executor_failure(bool refused) {
4365 const auto failures = executor_journal_failures();
4366 if (failures > acknowledged_journal_failures_) {
4367 auto detail = executor_journal_error();
4368 note_local_failure(
4369 detail.empty() ? std::string("the order journal could not be written") : std::move(detail)
4370 );
4371 return;
4372 }
4373 if (refused && failures == 0) {
4374 // An executor with no journal behind it can still refuse locally, and it reports no count.
4375 // Nothing to acknowledge, so a refusal always halts and a resume that does not fix the cause
4376 // simply halts again on the next send -- which is the truthful outcome.
4377 note_local_failure("local machinery refused to send");
4378 }
4379 }
4380
4381 // Returns how many journal writes the executor has had fail, or zero when it cannot say.
4382 std::size_t executor_journal_failures() const { return executor_journal_failures_impl(0); }
4383
4384 template <typename E = Executor>
4385 auto executor_journal_failures_impl(int) const
4386 -> decltype(std::declval<const E&>().journal_failures()) {
4387 return executor_.journal_failures();
4388 }
4389
4390 std::size_t executor_journal_failures_impl(...) const { return 0; }
4391
4392 // Returns what the executor says went wrong, or empty when it reports nothing or cannot.
4393 std::string executor_journal_error() const { return executor_journal_error_impl(0); }
4394
4395 template <typename E = Executor>
4396 auto executor_journal_error_impl(int) const
4397 -> decltype(std::declval<const E&>().last_journal_error(), std::string()) {
4398 return executor_.last_journal_error();
4399 }
4400
4401 std::string executor_journal_error_impl(...) const { return {}; }
4402
4403 Executor executor_;
4404 IndeterminacyLedger ledger_;
4405 std::size_t acknowledged_journal_failures_{0};
4406 std::map<InstrumentId, DatedBand> price_limits_;
4407 // How old a band may be before it is not applied; see `set_maximum_price_limits_age`.
4408 std::optional<std::chrono::nanoseconds> maximum_price_limits_age_;
4409 // Whether the caller stated this runner applies a band of any age; see the setter.
4410 bool allow_unbounded_price_limits_age_{false};
4411 // Plans built while a band was held and not applied; see `price_bands_withheld`.
4412 std::size_t price_bands_withheld_{0};
4413 // Sent orders a broker refused after a withheld band would have moved them; see the accessor.
4414 std::size_t unclamped_orders_rejected_{0};
4415 // Orders nothing sent to a broker while carrying an unclamped price; see the accessor.
4416 std::size_t unclamped_orders_refused_{0};
4417 // Instructions that failed by each origin, and refusals a broker made; see the accessors.
4418 std::size_t orders_unbuildable_{0};
4419 // @copydoc Runner::orders_unbuildable_
4420 std::size_t orders_reconcile_withheld_{0};
4421 // @copydoc Runner::orders_unbuildable_
4422 std::size_t orders_guard_refused_{0};
4423 // @copydoc Runner::orders_unbuildable_
4424 std::size_t orders_binding_refused_{0};
4425 // @copydoc Runner::orders_unbuildable_
4426 std::size_t orders_broker_refused_{0};
4427 // @copydoc Runner::orders_unbuildable_
4428 std::size_t orders_indeterminate_{0};
4429 // Withdrawals that failed, by origin; see `cancels_rejected`.
4430 std::size_t cancels_rejected_{0};
4431 // @copydoc Runner::cancels_rejected_
4432 std::size_t cancels_refused_{0};
4433 // @copydoc Runner::cancels_rejected_
4434 std::size_t cancels_unresolved_{0};
4435 // @copydoc Runner::cancels_rejected_
4436 std::size_t cancels_indeterminate_{0};
4437 // Plans held back whole, for any of three reasons; see `plans_withheld`.
4438 std::size_t plans_withheld_{0};
4439 std::size_t warmup_instructions_discarded_{0};
4440 // A stop a deferred target waits on, with the order time its listing showed (if any) to tell it from
4441 // another day's order under the same number.
4442 struct DeferredStop {
4443 OrderRef<Tag> ref_;
4444 std::optional<std::chrono::system_clock::time_point> ordered_at_;
4445 };
4446 // One target waiting for a stop's withdrawal, and when a listing first showed the stop gone.
4447 struct DeferredTarget {
4448 // The target, exactly as the strategy stated it.
4449 strategy::TargetPosition target_;
4450 // The stops it waits on, whose own history rows decide its release.
4451 std::vector<DeferredStop> stops_;
4452 // When it was deferred, which bounds the history that can hold those rows.
4453 std::chrono::system_clock::time_point deferred_at_{};
4454 };
4455 // Targets waiting for a stop's withdrawal; see `deferred_targets`.
4456 std::vector<DeferredTarget> deferred_targets_;
4457
4458 std::vector<Unconfirmed> unconfirmed_;
4459 // Serialized mode; see `serialized`.
4460 static constexpr bool can_serialize =
4461 keeps_sent_orders_until_listed && runner_detail::serializes<Executor>::value;
4462 bool overlapping_{false};
4463 bool recovered_{false};
4464 std::vector<Boundary> boundaries_;
4465 std::size_t orders_awaiting_confirmation_{0};
4466 std::chrono::nanoseconds confirmation_timeout_{std::chrono::minutes(10)};
4467 std::size_t orders_unconfirmed_timed_out_{0};
4468 // Targets withheld for want of a trustworthy account, one per instrument; see `retry_retained_targets`.
4469 // A retained target, and whether it waits for the market phase rather than for the account.
4470 struct RetainedTarget {
4471 strategy::TargetPosition target_;
4472 bool for_phase_{false};
4473 // Kept because the broker could not be reached: planned again only after a later holdings read.
4474 std::optional<std::chrono::system_clock::time_point> after_;
4475 // Kept behind an order it could only withdraw: planned again only once the instrument owes no order.
4476 bool until_accounted_{false};
4477 };
4478 std::vector<RetainedTarget> retained_targets_;
4479 // An order intent kept for the phase, and the instrument's net holding when it was kept.
4480 struct KeptIntent {
4481 strategy::OrderIntent intent_;
4482 Decimal64 held_;
4483 // As for `RetainedTarget::after_`.
4484 std::optional<std::chrono::system_clock::time_point> after_;
4485 };
4486 std::vector<KeptIntent> kept_intents_;
4487 std::size_t kept_intents_dropped_{0};
4488 // The instructions kept for the phase in the current pass, so each is kept once.
4489 std::vector<const Instruction<Tag>*> kept_this_pass_;
4490 // True only inside a deferred release's settle, which `plan_withhold_reason` exempts.
4491 bool releasing_deferred_{false};
4492 // How many were ever deferred; see `targets_deferred`.
4493 std::size_t targets_deferred_{0};
4494 // How many were dropped after a fill; see `deferred_dropped_after_fill`.
4495 std::size_t deferred_dropped_after_fill_{0};
4496 // Instructions local machinery was unavailable to send; see the accessor.
4497 std::size_t instructions_unavailable_{0};
4498 // What the broker said about the most recent order it refused; see the accessor.
4499 std::optional<BrokerRefusal> last_broker_refusal_;
4500 std::optional<strategy::Rejection> last_unbuildable_;
4501 std::optional<Decimal64> portfolio_value_;
4502 std::optional<Currency> portfolio_value_currency_;
4503 strategy::TradeTolerance tolerance_;
4504};
4505
4523template <typename Strategy, typename CapsType = strategy::NoCaps>
4525
4526} // namespace runner
4527} // namespace algotradelib
An exact signed decimal: an int64 coefficient times 10^-scale.
Definition types.hpp:297
static Decimal64 parse(std::string_view input)
Parses a decimal string exactly (no binary floating point).
Definition types.hpp:323
What a strategy's handlers receive as ctx: everything the strategy may read, and where it queues what...
Definition context.hpp:81
std::vector< Instruction< BrokerTag > > drain()
Removes and returns the queued requests (for runners).
Definition context.hpp:263
The instruments with an order of unknown outcome, where nothing may be placed until the broker's orde...
An executor that sends nothing and records what it was given (the dry run's default).
Runs one strategy: feeds it events, reconciles what it asks for, checks the risk limits,...
Definition runner.hpp:297
std::size_t cancels_rejected() const noexcept
Returns how many cancellations were refused, by the broker or the binding (RejectedCancel::by_broker_...
Definition runner.hpp:1379
void resume_after_local_failure()
Clears the local-failure hold, after an operator has fixed the cause.
Definition runner.hpp:408
std::size_t intents_kept_for_phase() const noexcept
Returns how many order intents are kept for the market phase, an unreachable broker or a rate limit; ...
Definition runner.hpp:1276
Report feed(const strategy::BarEvent &event)
Feeds one bar: caches it, sets the last price and the clock to the bar's period end,...
Definition runner.hpp:1524
std::optional< std::string > local_failure() const
Returns why local machinery stopped sending, if it did.
Definition runner.hpp:395
const std::optional< BrokerRefusal > & last_broker_refusal() const noexcept
Returns the broker's code and message for the most recent order it refused (SendOutcome::REJECTED; at...
Definition runner.hpp:1404
Report notify_stop()
Calls on_stop and settles what it asked for while PollingLoop's STOPPING hold stands,...
Definition runner.hpp:1498
std::size_t kept_intents_dropped() const noexcept
Returns how many kept intents were dropped at release, because the holding they were decided against ...
Definition runner.hpp:1283
void require_known_portfolio(bool required)
Sets whether placing requires holdings to have been read and fresh.
Definition runner.hpp:462
void clear_deferred_targets() noexcept
Discards every target waiting on a resting stop (deferred_targets). The orders this runner owes are n...
Definition runner.hpp:1314
std::size_t cancels_refused() const noexcept
Returns how many cancellations a hold refused before sending.
Definition runner.hpp:1384
std::size_t cancels_unresolved() const noexcept
Returns how many cancellations named an order the portfolio does not list.
Definition runner.hpp:1389
void probe_sellable_convention(bool enabled=true)
Turns on the strategy::SellableConventionProbe, which infers whether the broker's sellable figure alr...
Definition runner.hpp:503
void set_portfolio_value(Decimal64 value)
Sets the portfolio's total value, which a weight-sized intent needs live.
Definition runner.hpp:1447
std::chrono::system_clock::time_point observed_at() const noexcept
Returns the instant last set by set_observed_at.
Definition runner.hpp:442
std::size_t orders_broker_refused() const noexcept
Returns how many orders the broker refused (SendOutcome::REJECTED; a request was spent); see last_bro...
Definition runner.hpp:677
std::vector< Obligation > obligations(const InstrumentId &instrument) const
Lists the orders a serialized runner still owes on an instrument, for an operator.
Definition runner.hpp:933
void set_maximum_portfolio_age(std::optional< std::chrono::nanoseconds > age)
Sets how old the holdings may be before a reconciliation plan computed from them is held back (Withhe...
Definition runner.hpp:423
std::size_t plans_withheld() const noexcept
Returns how many whole reconciliation plans were held back (any WithheldPlanReason: holdings absent,...
Definition runner.hpp:691
Report feed(const strategy::QuoteEvent &event)
Feeds one quote: sets the last price and the clock, calls on_quote, and settles.
Definition runner.hpp:1642
void set_observed_at(std::chrono::system_clock::time_point at)
Sets the current wall-clock instant, used to measure the age of holdings and price bands.
Definition runner.hpp:436
Report retry_deferred(const std::vector< OrderSnapshot > &evidence)
Decides each waiting target against the broker's order history, and settles what it released.
Definition runner.hpp:759
void resolve_indeterminate_placement(const InstrumentId &instrument, OrderSide side, const std::optional< std::string > &order_id, const std::string &journal_id={})
Tells the runner what a reconciliation sweep found for an order of unknown outcome.
Definition runner.hpp:811
Strategy & strategy() noexcept
Returns the strategy.
Definition runner.hpp:537
std::size_t placements_at_broker() const noexcept
Returns how many placements may have left an order at the broker (accepted or of unknown outcome).
Definition runner.hpp:478
std::size_t orders_guard_refused() const noexcept
Returns how many orders the risk guard refused, for a limit or a hold; see orders_unbuildable.
Definition runner.hpp:665
void allow_unbounded_price_limits_age()
Applies price bands of any age (replacing a stated maximum).
Definition runner.hpp:604
std::size_t orders_awaiting_confirmation() const noexcept
Returns how many orders a serialized runner withheld because another order on the instrument was not ...
Definition runner.hpp:1033
void feed_warmup(const strategy::BarEvent &event)
Feeds one bar as history: like feed, but whatever the strategy asks for is discarded (counted in warm...
Definition runner.hpp:1553
Context & context() noexcept
Returns the context, to arrange its state (instruments, portfolio) before feeding events.
Definition runner.hpp:548
Report feed(const OrderSnapshot &event)
Feeds one order update: calls on_order_update and settles.
Definition runner.hpp:1666
const Executor & executor() const noexcept
Returns the executor.
Definition runner.hpp:366
Runner(Strategy strategy={}, strategy::RiskLimits limits={}, std::size_t series_capacity=512, Executor executor={})
Constructs a runner.
Definition runner.hpp:351
Report start()
Calls on_start and settles what it asked for.
Definition runner.hpp:1475
const strategy::SellableConventionProbe< Tag > & sellable_convention() const noexcept
Returns the sellable-convention probe.
Definition runner.hpp:524
std::size_t orders_unbuildable() const noexcept
Returns how many orders reconciliation could not build (any strategy::RejectReason before sending,...
Definition runner.hpp:654
void feed_warmup(const strategy::BarSlice &slice)
Delivers one period's bars as history: like feed(const strategy::BarSlice&), but whatever the strateg...
Definition runner.hpp:1626
void allow_absent_portfolio()
Lets this runner place orders before holdings have ever been read.
Definition runner.hpp:450
strategy::RiskGuard & guard() noexcept
Returns the risk guard, to halt or resume trading.
Definition runner.hpp:559
std::size_t warmup_instructions_discarded() const noexcept
Returns how many instructions feed_warmup discarded.
Definition runner.hpp:1635
Report retry_retained_targets()
Re-submits retained targets and kept intents once they can be acted on, and settles them.
Definition runner.hpp:1197
Report retry_deferred_on_one_snapshot()
Releases and settles every waiting target whose stops are no longer listed, without reading history.
Definition runner.hpp:1367
std::size_t orders_indeterminate() const noexcept
Returns how many orders had an unknown outcome (SendOutcome::INDETERMINATE, which at KIS,...
Definition runner.hpp:683
Report feed(const strategy::Fill &event)
Feeds one execution: calls on_fill and settles.
Definition runner.hpp:1655
void allow_overlapping_orders()
Lets several orders on one instrument be outstanding at once, as an unjournaled runner does,...
Definition runner.hpp:898
Report stop()
Calls on_stop and settles what it asked for.
Definition runner.hpp:1487
void set_minimum_trade(Decimal64 quantity)
Sets only the share-count floor of the trade tolerance.
Definition runner.hpp:1435
std::size_t unconfirmed_orders() const noexcept
Returns how many sent orders are still counted as reserved until the broker accounts for them.
Definition runner.hpp:784
void set_maximum_price_limits_age(std::optional< std::chrono::nanoseconds > age)
Sets how old a price band may be and still be applied to an order's price.
Definition runner.hpp:597
std::size_t targets_retained_for_phase() const noexcept
Returns how many retained targets wait for the market phase, an unreachable broker or a rate limit.
Definition runner.hpp:1296
bool release_obligations(const InstrumentId &instrument, const std::string &reason)
Accounts for every order a serialized runner still owes on an instrument, on an operator's word.
Definition runner.hpp:980
std::vector< strategy::TargetPosition > deferred_targets() const
Returns the targets waiting for a stop on their instrument to be cancelled (at most one per instrumen...
Definition runner.hpp:711
const std::optional< strategy::Rejection > & last_unbuildable() const noexcept
Returns why reconciliation could not build the most recent instruction it refused.
Definition runner.hpp:1416
std::size_t targets_deferred() const noexcept
Returns how many targets have waited for a stop's cancellation; see deferred_targets.
Definition runner.hpp:724
void halt_locally(std::string detail)
Stops this runner sending, with the LOCAL_FAILURE hold, until resume_after_local_failure.
Definition runner.hpp:472
void observe_sellable_convention()
Shows the current portfolio to the sellable-convention probe, if it is on.
Definition runner.hpp:511
void set_confirmation_timeout(std::chrono::nanoseconds timeout)
Sets how long a sent order's quantity may stay unexplained before it is counted as timed out (ten min...
Definition runner.hpp:1067
void restore_obligations(const JournalReplay &replay)
Restores the orders a previous process left unaccounted for; runner::recover_runner calls it,...
Definition runner.hpp:1017
Report settle_closing()
Settles instructions already queued on the context, without calling a handler.
Definition runner.hpp:1510
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
std::vector< Obligation > obligations() const
Lists every order a serialized runner still owes, on any instrument.
Definition runner.hpp:946
bool serialized() const noexcept
Tests whether this runner is serialized: at most one order per instrument that holdings have not yet ...
Definition runner.hpp:885
const strategy::RiskGuard & guard() const noexcept
Returns the risk guard.
Definition runner.hpp:554
std::size_t sends_outside_permitted_phase() const noexcept
Returns how many orders were sent in a market phase RiskLimits::permitted_phases_ does not permit,...
Definition runner.hpp:490
Report feed(strategy::MarketPhase phase)
Feeds a market phase change: sets it on the context, calls on_phase, and settles.
Definition runner.hpp:1677
IndeterminacyLedger & ledger() noexcept
Returns the ledger, so an operator who has read the broker's order list can clear an entry.
Definition runner.hpp:385
std::size_t targets_retained() const noexcept
Returns how many targets are retained, for any reason; see retry_retained_targets.
Definition runner.hpp:1289
void set_portfolio_value(Money value)
Sets the portfolio's total value and its currency.
Definition runner.hpp:1460
void record(const strategy::BarEvent &event)
Prepares one bar for a slice: caches it (with a linear algebra backend) and sets the last price and t...
Definition runner.hpp:1574
const Context & context() const noexcept
Returns the context.
Definition runner.hpp:543
std::size_t obligations_awaiting_account() const noexcept
Returns how many orders a serialized runner owes that a later read could account for without anything...
Definition runner.hpp:1326
Executor & executor() noexcept
Returns the executor.
Definition runner.hpp:371
bool withdrawal_accepted_for(const WorkingOrder< Tag > &working) const
Tests whether working is a row of an order this runner sent whose withdrawal the broker has already a...
Definition runner.hpp:1346
std::size_t obligations_unexplained() const noexcept
Returns how many orders a serialized runner owes that nothing yet explains: no listing shows them res...
Definition runner.hpp:1043
Report feed(const strategy::BarSlice &slice)
Delivers one period's bars to on_bars and settles what the strategy asked for.
Definition runner.hpp:1611
const IndeterminacyLedger & ledger() const noexcept
Returns the ledger of instruments withheld pending reconciliation or an operator: an order of unknown...
Definition runner.hpp:380
std::size_t orders_binding_refused() const noexcept
Returns how many orders the binding refused to send (no request made); see orders_unbuildable.
Definition runner.hpp:670
std::size_t deferred_dropped_after_fill() const noexcept
Returns how many waiting targets were dropped because a stop they waited on had filled.
Definition runner.hpp:730
std::size_t orders_reconcile_withheld() const noexcept
Returns how many orders were held back because the instrument was in the ledger (an order of unknown ...
Definition runner.hpp:660
void clear_retained_targets() noexcept
Discards every retained target and kept intent (PollingLoop does this when stopping).
Definition runner.hpp:1304
std::size_t orders_unconfirmed_timed_out() const noexcept
Returns how many sent orders timed out unconfirmed; see set_confirmation_timeout.
Definition runner.hpp:1073
ObligationState obligation_state(const InstrumentId &instrument) const
Returns what a serialized runner still owes on an instrument.
Definition runner.hpp:911
std::optional< std::chrono::system_clock::time_point > unconfirmed_awaiting_evidence()
Returns from when the broker's order history must be read to settle sent orders.
Definition runner.hpp:1088
std::size_t instructions_unavailable() const noexcept
Returns how many instructions (placements and cancellations) were not sent because local machinery wa...
Definition runner.hpp:1373
std::size_t price_bands_withheld() const noexcept
Returns how many plans were built while a band was known for the instrument but not applied (counted ...
Definition runner.hpp:613
void record_history_only(const strategy::BarEvent &event)
Caches one bar without moving the last price or the clock, and without calling a handler.
Definition runner.hpp:1593
std::size_t unclamped_orders_refused() const noexcept
Returns how many orders carrying a price a withheld band would have clamped were refused before sendi...
Definition runner.hpp:639
void refresh_unconfirmed()
Rebuilds the reserved working rows against the portfolio as it is now.
Definition runner.hpp:791
bool price_limits_age_policy_stated() const noexcept
Tests whether a price-band age policy has been stated (a maximum, or any age).
Definition runner.hpp:619
void confirm_from_history(const std::vector< OrderSnapshot > &evidence)
Settles reserved orders against the broker's order history; sends nothing.
Definition runner.hpp:1118
std::size_t cancels_indeterminate() const noexcept
Returns how many cancellations had an unknown outcome (the order may or may not have been cancelled).
Definition runner.hpp:1394
void set_trade_tolerance(strategy::TradeTolerance tolerance)
Sets how small a drift from a target is not worth an order; see strategy::TradeTolerance.
Definition runner.hpp:1424
const Strategy & strategy() const noexcept
Returns the strategy, to read the state it kept.
Definition runner.hpp:532
std::optional< std::chrono::system_clock::time_point > deferred_awaiting_evidence() const
Returns from when the broker's order history must be read to decide waiting targets.
Definition runner.hpp:737
void clear_portfolio_value() noexcept
Forgets the portfolio value, so weighted intents are refused again.
Definition runner.hpp:1466
std::size_t unclamped_orders_rejected() const noexcept
Returns how many sent orders the broker refused while carrying a price a withheld band would have cla...
Definition runner.hpp:632
const std::string & tag() const noexcept
Returns the tag.
Definition intent.hpp:410
const SizeSpec & size() const noexcept
Returns how much to trade.
Definition intent.hpp:392
const InstrumentId & instrument() const noexcept
Returns the instrument to trade.
Definition intent.hpp:368
Checks planned orders against RiskLimits, holds trading stopped, and counts orders sent.
Definition risk.hpp:706
void halt() noexcept
Takes the operator's hold (HaltReason::OPERATOR): no order or cancellation is sent until resume().
Definition risk.hpp:746
bool halted() const noexcept
Tests whether any hold is in force.
Definition risk.hpp:784
Infers a broker's SellableConvention from successive portfolios.
SizeKind kind() const noexcept
Returns how the size is expressed.
Definition intent.hpp:198
#define ATL_ASSERT_STRATEGY(S, Ctx)
Asserts at compile time that S is a strategy a runner with context Ctx can drive.
Definition concept.hpp:712
BrokerError refusal()
Returns the error every account-side member of DataOnlyTraits answers with.
Runs a strategy: the runner, the polling loop, the order journal, and stopping.
Definition clock.hpp:7
bool broker_unreachable(const BrokerError &error) noexcept
Tests whether an error means the broker could not be reached (as opposed to the binding refusing the ...
constexpr bool journal_outcome_answers_send(JournalOutcome outcome) noexcept
Tests whether a line with this outcome answers the send itself (the broker's reply,...
Definition journal.hpp:234
Runner< Strategy, CapsType, DryTag, RecordingExecutor< DryTag > > DryRunner
A runner that sends nothing: handlers fire and instructions are recorded by a RecordingExecutor.
Definition runner.hpp:4524
constexpr bool journal_outcome_is_closing_account(JournalOutcome outcome) noexcept
Tests whether a line with this outcome is a serialized runner's closing account of a placement (it co...
Definition journal.hpp:295
constexpr bool send_outcome_reached_broker(SendOutcome outcome) noexcept
Tests whether an outcome means a request reached the broker.
SendOutcome
What became of one instruction (a placement or a cancellation) an executor was asked to send.
Definition execution.hpp:30
@ UNAVAILABLE
Local machinery failed (such as the order journal not being writable), so nothing was sent.
Definition execution.hpp:55
@ INDETERMINATE
The outcome is unknown (see outcome_of): the request failed in transit or was cancelled,...
Definition execution.hpp:48
@ REJECTED
The broker refused the instruction (for a placement, the order does not exist); nothing needs reconci...
Definition execution.hpp:34
@ ACCEPTED
The broker accepted the instruction (for a placement, the order exists).
Definition execution.hpp:32
@ REFUSED_LOCALLY
The binding refused the instruction before sending it (such as an idempotency key at a broker without...
Definition execution.hpp:63
WithheldPlanReason
Why the runner held back a whole reconciliation plan (cancellations and placements) before sending an...
@ PLACEMENT_HELD
A hold stops placements, so the plan's cancellations are held too (a reprice must not become a bare c...
@ PORTFOLIO_ABSENT
No holdings have been read, so the plan was computed against an account that looks flat (and could ca...
@ AWAITING_CONFIRMATION
A serialized runner may not place on the instrument yet, so the plan's cancellations are held too: an...
@ PORTFOLIO_STALE
The holdings the plan was computed from are older than the configured maximum age.
@ PORTFOLIO_SUPERSEDED
The working-order listing is newer than the holdings the plan was sized against (see strategy::Portfo...
JournalOutcome
What a journal line says about an instruction.
Definition journal.hpp:93
@ CONFIRMED
A serialized runner accounted for the placement: holdings showed what it filled, its history showed i...
Definition journal.hpp:124
@ INDETERMINATE
The outcome is unknown (SendOutcome::INDETERMINATE: a failure in transit, an unreadable reply,...
Definition journal.hpp:108
@ RESOLVED
A recovery sweep read the broker's order list and closed the question.
Definition journal.hpp:116
@ REJECTED
The instruction was refused, by the broker or before it was sent, so it does not exist.
Definition journal.hpp:103
@ ACCEPTED
The broker accepted the instruction; it exists.
Definition journal.hpp:97
@ HELD
Evidence contradicted the placement (holdings moved past what it can explain), so only an operator ma...
Definition journal.hpp:129
@ PENDING
Written before the send; whether the instruction exists is not yet known.
Definition journal.hpp:95
constexpr bool journal_outcome_requires_operator(JournalOutcome outcome) noexcept
Tests whether a line with this outcome is written with requires_operator_ by design,...
Definition journal.hpp:316
@ LOCAL_FAILURE
Local machinery cannot send (see SendOutcome::UNAVAILABLE).
@ AWAITING_CONFIRMATION
A serialized runner already has an order on this instrument that holdings have not yet accounted for ...
@ RECONCILE_REQUIRED
An earlier instruction for this instrument has an unknown outcome; nothing is sent until the broker's...
constexpr bool send_outcome_is_local_failure(SendOutcome outcome) noexcept
Tests whether an outcome means the runner's own machinery failed (not the broker, and not a check tha...
ObligationState
What a serialized runner still owes on an instrument (Runner::obligation_state).
@ NAMED
An order the broker named, waiting for its history and holdings to account for it.
@ OPERATOR
An order only an operator may account for (Runner::release_obligations).
@ NONE
Nothing: an order may be placed.
@ UNNAMED
An order whose existence is unknown (its reply was lost and no sweep named it).
constexpr int obligation_weight(ObligationState state) noexcept
Returns how much an obligation state weighs when one instrument owes several orders: the instrument r...
bool broker_rate_limited(const BrokerError &error) noexcept
Tests whether an error is the broker's rate-limit refusal (such as KIS EGW00201).
std::variant< OrderIntent, TargetPosition, CancelIntent< Broker > > Instruction
One request a strategy queued: an order, a target position or a cancellation.
Definition context.hpp:45
void stop(S &strategy, Ctx &context)
Calls strategy.on_stop(context) if the strategy declares on_stop; otherwise does nothing.
Definition concept.hpp:168
What a strategy sees and emits: events, intents, the portfolio, reconciliation and risk limits.
Definition concept.hpp:16
@ PHASE_NOT_PERMITTED
The market phase is not in RiskLimits::permitted_phases_.
Definition risk.hpp:182
MarketPhase
Where the clock stands relative to the trading session.
Definition event.hpp:30
bool order_is_open(OrderStatus status)
Tests whether an order in a given status may still trade.
Definition portfolio.hpp:20
@ WITHDRAWAL
A cancellation of an order the caller named.
Definition risk.hpp:100
WeightTarget weight_target(const OrderIntent &intent, const ReconcileInputs &inputs)
Converts a weight-sized intent into the target position it states.
constexpr bool market_phase_session_ended(MarketPhase phase) noexcept
Tests whether a phase means the day's session is over (or never opened), so a day order no longer wor...
Definition event.hpp:79
@ BROKER_REFUSED
The broker refused the instruction (a request was sent).
Definition reconcile.hpp:57
@ NOT_REPRESENTABLE
The decimal arithmetic did not fit, or the portfolio's rows did not add up.
Definition reconcile.hpp:46
@ STOP_ORDER_RESTING
A target needs a trade while a stop order rests on the instrument; the stop is cancelled first and no...
Definition reconcile.hpp:80
@ BINDING_REFUSED
The binding refused to send the instruction, so no request was made.
Definition reconcile.hpp:64
constexpr std::string_view halt_reason_name(HaltReason reason) noexcept
Returns a lowercase name for a hold, such as "portfolio_stale", for logs.
Definition risk.hpp:144
constexpr bool size_kind_is_target(SizeKind kind) noexcept
Tests whether a size states a holding to reach (a target), rather than an amount to trade.
Definition intent.hpp:120
bool is_stop_order(const OrderSnapshot &order)
Tests whether a working order is a stop order: a trigger-price type, or one carrying a trigger price.
Plan< Broker > reconcile(const TargetPosition &target, const Portfolio< Broker > &portfolio, const ReconcileInputs &inputs)
Plans the cancellations and the order that move a holding to a target position.
HaltReason
Who or what is holding trading stopped.
Definition risk.hpp:29
@ LOCAL_FAILURE
Local machinery failed, such as the order journal not being writable; nothing is sent.
Definition risk.hpp:39
@ OPERATOR
A person stopped trading with RiskGuard::halt(); only RiskGuard::resume() clears it.
Definition risk.hpp:31
@ 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.
OrderStatus
Where an order stands in its lifecycle.
Definition types.hpp:1879
std::optional< Decimal64 > decimal_add(const Decimal64 &lhs, const Decimal64 &rhs)
Adds two decimals exactly.
Definition types.hpp:509
constexpr bool carries_trigger_price(OrderType type) noexcept
Tests whether an order type takes a trigger price.
Definition types.hpp:1774
std::optional< Decimal64 > decimal_subtract(const Decimal64 &lhs, const Decimal64 &rhs)
Subtracts two decimals exactly.
Definition types.hpp:530
constexpr bool time_in_force_expires_at_close(TimeInForce condition) noexcept
Tests whether an order with this condition expires at the session's close, so its filled quantity is ...
Definition types.hpp:1822
Venue
A trading venue: an exchange, or the combined view across exchanges.
Definition types.hpp:43
bool decimal_less(const Decimal64 &lhs, const Decimal64 &rhs)
Compares two decimals numerically, whatever their scales.
Definition types.hpp:665
std::optional< Decimal64 > decimal_multiply(const Decimal64 &lhs, const Decimal64 &rhs)
Multiplies two decimals exactly; the scales add.
Definition types.hpp:571
OrderSide
The side of an order: its economic direction, buy or sell.
Definition types.hpp:1683
Reconciliation: turning a strategy's intents and targets into the orders and cancellations to send.
Decimal64 close_
The last trade price.
std::string message_
A human-readable description, for logs; do not parse it.
Definition types.hpp:772
std::string broker_code_
The broker's own error code, verbatim (for example KIS's EGW00201).
Definition types.hpp:761
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
A broker's reference to one order, used to amend or cancel it.
Definition types.hpp:2111
The state of one order, as a broker listing reported it.
Definition types.hpp:2008
std::string order_id_
The broker's order number.
Definition types.hpp:2010
The highest and lowest price an instrument may trade at today.
Decimal64 last_price_
The last traded price, as the broker reported it.
Definition types.hpp:874
std::chrono::system_clock::time_point received_at_
When this process received the quote, on this machine's clock.
Definition types.hpp:880
InstrumentId instrument_
The instrument.
Definition types.hpp:872
An order still open to modification or cancellation.
Definition types.hpp:2290
OrderRef< Broker > ref_
The reference to amend or cancel the order with.
Definition types.hpp:2294
OrderSnapshot order_
The order's state.
Definition types.hpp:2292
The broker's own code and message for a refusal, kept for diagnosis.
What one runner pass decided and did, sorted by outcome (returned by Runner::feed).
Definition runner.hpp:192
bool quiet() const noexcept
Tests whether the pass did nothing at all.
Definition runner.hpp:260
std::vector< OrderRef< BrokerTag > > indeterminate_cancel_
Cancellations with an unknown outcome; the order may or may not still rest.
Definition runner.hpp:235
std::vector< OrderRef< BrokerTag > > unavailable_cancel_
Cancellations not sent because local machinery was unavailable.
Definition runner.hpp:244
std::vector< strategy::Rejection > rejected_
Instructions not placed: reconciliation could not build them, the broker refused them (BROKER_REFUSED...
Definition runner.hpp:208
std::vector< WithheldOrder > withheld_
Orders held back because an earlier order on the instrument has an unknown outcome (RECONCILE_REQUIRE...
Definition runner.hpp:217
std::vector< strategy::GuardRejection > refused_
Instructions the risk guard refused (a limit or a hold).
Definition runner.hpp:210
std::vector< RefusedCancel< BrokerTag > > refused_cancel_
Cancellations refused by a hold that blocks withdrawals (OPERATOR, STOPPING; see strategy::halt_block...
Definition runner.hpp:252
std::vector< strategy::PlannedOrder > would_place_
Orders that passed every check and were sent (in a dry run, would have been).
Definition runner.hpp:194
std::vector< RejectedCancel< BrokerTag > > rejected_cancel_
Cancellations that were refused, by the broker or the binding.
Definition runner.hpp:233
std::vector< OrderRef< BrokerTag > > unresolved_cancel_
Cancellations naming an order the portfolio does not list; not sent.
Definition runner.hpp:237
std::vector< WithheldPlan< BrokerTag > > withheld_plans_
Whole plans (cancellations and placements) held back before any part was sent; see WithheldPlanReason...
Definition runner.hpp:222
std::vector< WithheldOrder > unavailable_
Orders not sent because local machinery was unavailable (SendOutcome::UNAVAILABLE,...
Definition runner.hpp:242
std::vector< std::string > log_
Lines the strategy logged during the pass.
Definition runner.hpp:254
std::vector< OrderRef< BrokerTag > > would_cancel_
Cancellations the broker accepted.
Definition runner.hpp:201
std::vector< strategy::PlannedOrder > indeterminate_
Orders whose outcome is unknown (SendOutcome::INDETERMINATE, which at KIS, Toss and Kiwoom includes m...
Definition runner.hpp:227
The broker tag of a dry run, whose order references reach no broker.
Definition runner.hpp:270
static constexpr std::string_view name
The name an order reference prints: "dryrun".
Definition runner.hpp:272
The contents of a journal, including what could not be read.
Definition journal.hpp:464
One order a serialized runner still owes (Runner::obligations).
std::string order_id_
The broker's order id; empty while unnamed.
Decimal64 quantity_
The quantity holdings must still account for.
ObligationState state_
Its state: NAMED, UNNAMED or OPERATOR.
std::chrono::system_clock::time_point sent_at_
When it was sent, on the journal's clock.
InstrumentId instrument_
The instrument.
std::string journal_id_
The id of the order's journal lines.
A cancellation a hold refused (nothing was sent), and which hold.
Definition runner.hpp:161
strategy::HaltReason reason_
The hold that stopped it.
Definition runner.hpp:165
OrderRef< BrokerTag > ref_
The order that was to be cancelled.
Definition runner.hpp:163
A cancellation that was refused, by the broker or before it was sent.
Definition runner.hpp:173
bool by_broker_
true if the broker refused it; false if it was refused before sending (by the binding,...
Definition runner.hpp:180
OrderRef< BrokerTag > ref_
The order that was to be cancelled.
Definition runner.hpp:175
One bar, delivered to on_bar.
Definition event.hpp:202
std::chrono::system_clock::time_point period_end_
When the bar's period ended: the earliest moment the bar could be known.
Definition event.hpp:218
InstrumentId instrument_
The instrument the bar is for.
Definition event.hpp:204
Every instrument's bar of one interval for one period end, delivered to on_bars.
Definition event.hpp:298
One execution of an order the strategy placed, delivered to on_fill.
Definition event.hpp:407
MarketPhase phase_
The market phase the order would be sent in.
Definition risk.hpp:357
std::chrono::system_clock::time_point now_
The current instant; RiskLimits::rate_window_ counts back from it.
Definition risk.hpp:355
std::optional< Currency > order_currency_
The currency the order is priced in, from its reference data.
Definition risk.hpp:374
std::optional< Decimal64 > reference_price_
A price to value a market order, which carries none.
Definition risk.hpp:359
std::vector< InstrumentBasis > exposure_basis_
The currency and mark of every instrument held or working, plus the order's.
Definition risk.hpp:367
A capability set in which the broker implements nothing optional.
Definition concept.hpp:360
std::vector< PlannedOrder > place_
The orders to place, after the cancellations.
std::vector< Rejection > rejected_
The instructions not placed, with reasons.
TimeInForce time_in_force_
How long the order stays valid.
OrderSession session_
The trading session.
Venue venue_
Where to route the order; see OrderRequest::venue_.
bool price_would_have_been_clamped_
Whether a price band was known but not applied (too old), and would have clamped this price.
std::string tag_
The tag of the intent or target the order came from.
std::optional< Decimal64 > trigger_price_
The trigger price of a STOP_LIMIT order, exactly as stated; empty otherwise.
OrderType type_
The order type.
InstrumentId instrument_
The instrument to trade.
std::optional< Decimal64 > price_
The limit price, after clamping and rounding; empty for a market order.
Decimal64 quantity_
The quantity, after rounding onto the lot.
std::vector< WorkingOrder< Broker > > working_
Orders still outstanding at the broker.
Definition portfolio.hpp:77
std::optional< Decimal64 > net_quantity_of(const InstrumentId &instrument) const
Returns the net quantity held of an instrument, summed over every row.
std::vector< BuyingPower > buying_power_
What may be spent, per currency.
Definition portfolio.hpp:71
std::chrono::system_clock::time_point as_of_
When the holdings were received, on this machine's clock.
Definition portfolio.hpp:84
std::optional< Decimal64 > sellable_quantity_of(const InstrumentId &instrument) const
Returns the sellable quantity the broker reported for an instrument (first row).
std::vector< HoldingPosition > positions_
Every position row the broker reported.
Definition portfolio.hpp:56
bool listing_newer_than_holdings() const noexcept
Tests whether the working-order listing is newer than the holdings.
A quote the runner has just read, delivered to on_quote.
Definition event.hpp:390
QuoteSnapshot quote_
The quote, as the broker reported it.
Definition event.hpp:392
std::optional< Currency > portfolio_value_currency_
The currency of portfolio_value_; absent means the instrument's currency.
std::optional< Decimal64 > reference_price_
A price to size a notional or weight against, such as the last trade.
std::optional< PriceLimits > limits_
The day's price band, if the broker publishes one.
bool reserved_rows_are_unconfirmed_
Whether the portfolio's reserved rows (WorkingOrder::reserved_) are orders this runner sent that the ...
std::optional< Decimal64 > portfolio_value_
The total portfolio value, for weight sizing.
TradeTolerance tolerance_
How small a drift from a target is not worth an order.
const Instrument * instrument_
The instrument's reference data (tick grid, lot size, currency); required.
The limits the operator sets for RiskGuard.
Definition risk.hpp:268
How small a difference from a target is not worth an order.