316 runner_detail::sends_to_a_broker<Executor>::value &&
317 strategy::strategy_detail::declares_on_fill<Strategy>;
324 static constexpr bool takes_bar_slices = strategy::strategy_detail::declares_on_bars<Strategy>;
334 runner_detail::sends_to_a_broker<Executor>::value && CapsType::has_working_orders;
341 runner_detail::sends_to_a_broker<Executor>::value && runner_detail::journals<Executor>::value;
354 std::size_t series_capacity = 512,
358 guard_(std::move(limits)),
359 context_(series_capacity),
366 const Executor&
executor() const noexcept {
return executor_; }
371 Executor&
executor() noexcept {
return executor_; }
410 acknowledged_journal_failures_ = executor_journal_failures();
424 maximum_portfolio_age_ = age;
442 std::chrono::system_clock::time_point
observed_at() const noexcept {
return observed_at_; }
472 void halt_locally(std::string detail) { note_local_failure(std::move(detail)); }
491 return sends_outside_permitted_phase_;
515 if (probing_sellable_ && !context_.portfolio().listing_newer_than_holdings()) {
516 sellable_probe_.observe(context_.portfolio());
525 return sellable_probe_;
532 const Strategy&
strategy() const noexcept {
return strategy_; }
537 Strategy&
strategy() noexcept {
return strategy_; }
576 dated.received_at_ = observed_at_;
577 dated.limits_ = std::move(limits);
578 price_limits_.insert_or_assign(
id, std::move(dated));
598 maximum_price_limits_age_ = age;
600 allow_unbounded_price_limits_age_ =
false;
605 allow_unbounded_price_limits_age_ =
true;
606 maximum_price_limits_age_.reset();
620 return maximum_price_limits_age_.has_value() || allow_unbounded_price_limits_age_;
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_);
738 std::optional<std::chrono::system_clock::time_point> earliest;
739 for (
const auto& deferred : deferred_targets_) {
740 if (stop_listed(deferred.target_.instrument_)) {
743 if (!earliest || deferred.deferred_at_ < *earliest) {
744 earliest = deferred.deferred_at_;
760 return release_deferred(&evidence);
813 [[maybe_unused]]
const std::optional<std::string>& order_id, [[maybe_unused]]
const std::string& journal_id = {}
816 if (order_id && order_id->empty()) {
819 const auto found =
serialized() ? unnamed_by_journal_id(journal_id) : newest_unnamed(instrument, side);
821 if (found == unconfirmed_.end() || (
serialized() && !order_id)) {
826 drop_reserved_row(*found);
830 found->ref_.primary_ = *order_id;
831 found->row_.ref_.primary_ = *order_id;
833 found->history_missed_ =
false;
834 if (found->unexplained_since_) {
835 found->unexplained_since_ = freshness_instant();
838 unconfirmed_.erase(found);
840 confirm_reservations();
899 if (placements_at_broker_ != 0 || recovered_) {
900 throw std::logic_error(
"allow_overlapping_orders must be chosen before recovery and before trading");
916 for (
const auto& entry : unconfirmed_) {
917 if (!(entry.instrument_ == instrument)) {
920 const auto owed = state_of(entry);
936 std::remove_if(owed.begin(), owed.end(), [&](
const Obligation& one) { return !(one.instrument_ == instrument); }),
947 std::vector<Obligation> owed;
951 for (
const auto& entry : unconfirmed_) {
954 obligation.
journal_id_ = entry.journal_ ? entry.journal_->id_ : std::string();
955 obligation.
order_id_ = entry.ref_.primary_;
956 obligation.
side_ = entry.side_;
958 obligation.
sent_at_ = entry.sent_at_;
959 obligation.
state_ = state_of(entry);
960 owed.push_back(std::move(obligation));
982 if constexpr (can_serialize) {
983 for (
auto at = unconfirmed_.begin(); at != unconfirmed_.end();) {
984 if (!(at->instrument_ == instrument)) {
989 if (!executor_.record(line)) {
991 note_executor_failure(
false);
995 set_boundary(instrument, line.recorded_at_,
true);
996 drop_reserved_row(*at);
997 at = unconfirmed_.erase(at);
999 confirm_reservations();
1019 if constexpr (can_serialize) {
1023 restore_from(replay);
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;
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)) {
1098 if (!earliest || entry.sent_at_ < *earliest) {
1099 earliest = entry.sent_at_;
1120 confirm_serialized_from_history(evidence);
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_) {
1131 if (entry.ref_.primary_.empty()) {
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_
1139 if (
decimal_less(row->execution_.filled_quantity_, entry.expected_)) {
1140 entry.expected_ = row->execution_.filled_quantity_;
1142 entry.settled_ =
true;
1144 entry.unexplained_since_ = now;
1151 for (
auto& entry : unconfirmed_) {
1152 if (entry.consulted_ && !entry.settled_ && timed_out(entry, now)) {
1153 entry.history_missed_ =
true;
1157 confirm_reservations();
1200 confirm_reservations();
1203 retained_targets_.erase(
1205 retained_targets_.begin(), retained_targets_.end(),
1206 [](
const RetainedTarget& kept) { return kept.for_phase_; }
1208 retained_targets_.end()
1210 kept_intents_.clear();
1216 for (
const auto& queued : context_.queued()) {
1217 forget_retained(instruction_instrument(queued));
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()) {
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_;
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));
1236 context_.submit(std::move(entry.target_));
1239 retained_targets_ = std::move(kept);
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));
1250 const auto held = context_.portfolio().net_quantity_of(entry.intent_.instrument());
1251 const auto gap = held ?
decimal_subtract(*held, entry.held_) : std::nullopt;
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_;
1261 context_.submit(std::move(entry.intent_));
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_; }
1305 retained_targets_.clear();
1306 kept_intents_.clear();
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_);
1348 for (
const auto& entry : unconfirmed_) {
1349 if (entry.withdrawn_ && !entry.ref_.primary_.empty() && working.
ref_.primary_ == entry.ref_.primary_ &&
1405 return last_broker_refusal_;
1417 return last_unbuildable_;
1425 tolerance_ = std::move(tolerance);
1436 tolerance_.minimum_quantity_ = std::move(quantity);
1448 portfolio_value_ = std::move(value);
1449 portfolio_value_currency_.reset();
1461 portfolio_value_ = std::move(value.
amount_);
1462 portfolio_value_currency_ = std::move(value.
currency_);
1467 portfolio_value_.reset();
1468 portfolio_value_currency_.reset();
1476 dispatch::start(strategy_, context_);
1488 dispatch::stop(strategy_, context_);
1499 dispatch::stop(strategy_, context_);
1525#if defined(ATL_HAS_EIGEN)
1526 context_.record_bar(event);
1530 confirm_reservations();
1531 dispatch::bar(strategy_, context_, event);
1554#if defined(ATL_HAS_EIGEN)
1555 context_.record_bar(event);
1559 dispatch::bar(strategy_, context_, event);
1560 warmup_instructions_discarded_ += context_.drain().size();
1575#if defined(ATL_HAS_EIGEN)
1576 context_.record_bar(event);
1594#if defined(ATL_HAS_EIGEN)
1595 context_.record_bar(event);
1597 static_cast<void>(event);
1612 confirm_reservations();
1613 dispatch::bars(strategy_, context_, slice);
1627 dispatch::bars(strategy_, context_, slice);
1628 warmup_instructions_discarded_ += context_.drain().size();
1645 confirm_reservations();
1646 dispatch::quote(strategy_, context_, event);
1656 confirm_reservations();
1657 dispatch::fill(strategy_, context_, event);
1667 confirm_reservations();
1668 dispatch::order_update(strategy_, context_, event);
1678 context_.set_phase(phase);
1683 for (
auto& entry : unconfirmed_) {
1685 entry.session_ended_ =
true;
1689 confirm_reservations();
1690 dispatch::phase(strategy_, context_, phase);
1713 confirm_reservations();
1716 auto instructions = context_.
drain();
1717 kept_this_pass_.clear();
1718 drop_superseded_deferred(instructions);
1719 std::vector<strategy::PlannedOrder> pending;
1723 std::vector<const Instruction<Tag>*> origin;
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) {
1754 std::vector<OrderRef<Tag>> attempted;
1755 const auto attempt_cancel = [
this, &attempted, &report](
const OrderRef<Tag>& ref) {
1758 if (!ref.primary_.empty() || !ref.secondary_.empty()) {
1759 for (
const auto& seen : attempted) {
1764 attempted.push_back(ref);
1766 send_cancel(ref, report);
1770 std::vector<InstrumentId> claimed;
1772 for (
const auto& instruction : instructions) {
1776 forget_retained(instruction_instrument(instruction));
1777 if (
const auto* cancel = std::get_if<strategy::CancelIntent<Tag>>(&instruction)) {
1778 attempt_cancel(cancel->ref_);
1782 const auto planned = plan_for(instruction);
1793 const bool would_cancel = !planned.cancel_.empty();
1794 if (
const auto reason = plan_withhold_reason(would_cancel)) {
1812 if (data_is_missing || would_cancel) {
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";
1826 std::holds_alternative<strategy::OrderIntent>(instruction)) {
1827 detail =
"dropped: an order intent states a size against the mis-ordered picture";
1830 report.withheld_plans_.push_back(WithheldPlan<Tag>{
1832 instruction_instrument(instruction),
1833 instruction_tag(instruction),
1835 planned.place_.size(),
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;
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);
1859 if (!hold && defers) {
1860 hold = serial_hold(instruction_instrument(instruction), claimed);
1864 if (retain_withheld(instruction,
false, context_.portfolio().as_of_)) {
1865 *hold +=
"; retained for a later holdings read";
1867 report.withheld_plans_.push_back(WithheldPlan<Tag>{
1869 instruction_instrument(instruction),
1870 instruction_tag(instruction),
1872 planned.place_.size(),
1877 for (
const auto& order : planned.place_) {
1878 claimed.push_back(order.instrument_);
1881 claimed.push_back(instruction_instrument(instruction));
1886 if (
serialized() && planned.place_.empty() && !defers && withdraws_owed(instruction, planned.cancel_)) {
1887 retain_withheld(instruction,
false, context_.portfolio().as_of_,
true);
1889 for (
const auto& ref : planned.cancel_) {
1890 attempt_cancel(ref);
1892 defer_if_waiting_on_stop(instruction, planned);
1893 for (
const auto& rejection : planned.rejected_) {
1898 ++orders_unbuildable_;
1899 last_unbuildable_ = rejection;
1900 report.rejected_.push_back(rejection);
1902 for (
const auto& order : planned.place_) {
1903 pending.push_back(order);
1904 origin.push_back(&instruction);
1908 strategy::GuardInputs inputs;
1912 inputs.
now_ = send_instant();
1913 inputs.
phase_ = context_.phase();
1929 auto view = context_.portfolio();
1934 if (guard_.limits().max_gross_exposure_) {
1937 for (std::size_t at = 0; at < pending.size(); ++at) {
1938 auto& order = pending[at];
1939 const auto& source_instruction = *origin[at];
1949 ++instructions_unavailable_;
1950 report.unavailable_.push_back(
1959 if (ledger_.requires_reconcile(order.instrument_)) {
1964 ++orders_reconcile_withheld_;
1965 std::string detail(ledger_.detail(order.instrument_));
1970 if (
serialized() && !restated_later(source_instruction) &&
1971 retain_withheld(source_instruction,
false, context_.portfolio().as_of_)) {
1972 detail +=
"; retained for a later holdings read";
1981 std::optional<Decimal64> baseline;
1983 if (
auto hold = serial_hold(order.instrument_, {})) {
1984 ++orders_awaiting_confirmation_;
1985 if (retain_withheld(source_instruction,
false, context_.portfolio().as_of_)) {
1986 *hold +=
"; retained for a later holdings read";
1991 baseline = context_.portfolio().net_quantity_of(order.instrument_);
1994 fill_sellable_if_needed(view, order);
2000 const auto reference = context_.instrument(order.instrument_);
2002 reference ? std::optional<Currency>(reference->currency_) : std::nullopt;
2008 auto decision = guard_.evaluate(order, view, inputs);
2009 if (decision.rejection_) {
2014 if (order.price_would_have_been_clamped_) {
2015 ++unclamped_orders_refused_;
2017 ++orders_guard_refused_;
2019 retain_for_phase(source_instruction);
2021 report.refused_.push_back(std::move(*decision.rejection_));
2030 order.venue_ = requested_route(order.venue_);
2031 const auto outcome = place_order(order, baseline);
2041 ++sends_outside_permitted_phase_;
2052 if (order.price_would_have_been_clamped_) {
2054 ++unclamped_orders_rejected_;
2056 ++unclamped_orders_refused_;
2067 guard_.note_sent(inputs.
now_);
2068 ++placements_at_broker_;
2073 guard_.limits().enforce_buying_power_
2076 track_unconfirmed(order, last_ref(),
false, baseline);
2077 report.would_place_.push_back(std::move(order));
2082 guard_.note_sent(inputs.
now_);
2083 ++orders_broker_refused_;
2086 if (decision.admitted_outside_phase_) {
2087 retain_for_phase(source_instruction,
true);
2094 retain_for_phase(source_instruction,
true, context_.portfolio().as_of_);
2096 note_broker_refusal(order);
2097 report.rejected_.push_back(
2098 strategy::Rejection{
2104 guard_.note_sent(inputs.
now_);
2107 ++placements_at_broker_;
2108 ledger_.mark(order.instrument_,
"an order request has an unknown outcome");
2109 ++orders_indeterminate_;
2121 if (
serialized() || runner_detail::reports_last_ref<Executor>::value) {
2124 guard_.limits().enforce_buying_power_
2127 track_unconfirmed(order, std::nullopt,
true, baseline);
2130 report.indeterminate_.push_back(std::move(order));
2141 ++orders_binding_refused_;
2142 if (decision.admitted_outside_phase_) {
2143 retain_for_phase(source_instruction);
2154 retain_for_phase(source_instruction,
false, context_.portfolio().as_of_);
2156 report.rejected_.push_back(
2157 strategy::Rejection{
2167 ++instructions_unavailable_;
2168 report.unavailable_.push_back(
2174 std::string(
"local machinery refused to send"),
2182 kept_this_pass_.clear();
2183 report.log_ = context_.log().take();
2206 void fill_sellable_if_needed(
2207 strategy::Portfolio<Tag>& view,
2208 const strategy::PlannedOrder& order
2210 if constexpr (runner_detail::fills_sellable<Executor>::value) {
2211 if (!guard_.limits().enforce_sellable_quantity_ || order.
side_ != OrderSide::SELL) {
2217 const auto answered = executor_.sellable_quantity(order.
instrument_);
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));
2228 static_cast<void>(view);
2229 static_cast<void>(order);
2234 SendOutcome place_order(
const strategy::PlannedOrder& order, [[maybe_unused]]
const std::optional<Decimal64>& baseline) {
2235 if constexpr (can_serialize) {
2237 return executor_.place(order, baseline);
2240 return executor_.place(order);
2245 if constexpr (runner_detail::resolves_route<Executor>::value) {
2246 return executor_.requested_route(asked);
2256 std::optional<OrderRef<Tag>> last_ref()
const {
2257 if constexpr (runner_detail::reports_last_ref<Executor>::value) {
2258 return executor_.last_ref();
2260 return std::nullopt;
2266 struct Unconfirmed {
2269 InstrumentId instrument_;
2272 Decimal64 expected_;
2274 Decimal64 baseline_;
2276 WorkingOrder<Tag> row_;
2277 std::chrono::system_clock::time_point sent_at_{};
2279 bool listed_{
false};
2280 bool listed_now_{
false};
2282 bool withdrawn_{
false};
2284 bool settled_{
false};
2286 bool session_ended_{
false};
2288 bool consulted_{
false};
2290 bool history_missed_{
false};
2292 Decimal64 residual_;
2293 std::optional<std::chrono::system_clock::time_point> unexplained_since_;
2296 std::optional<JournalEntry> journal_;
2299 bool broker_ref_{
false};
2301 bool restored_{
false};
2303 bool operator_only_{
false};
2305 bool noticed_{
false};
2308 bool identified_{
false};
2310 std::chrono::system_clock::time_point baseline_at_{};
2312 std::string unwritten_hold_;
2325 void track_serialized(
2326 const strategy::PlannedOrder& order,
const std::optional<OrderRef<Tag>>& ref,
bool indeterminate,
2327 const Decimal64& baseline
2329 const auto& portfolio = context_.portfolio();
2331 if (ref && !indeterminate) {
2333 entry.broker_ref_ = !ref->primary_.empty();
2334 entry.identified_ = entry.broker_ref_;
2336 entry.baseline_at_ = portfolio.as_of_;
2338 entry.side_ = order.
side_;
2340 entry.baseline_ = baseline;
2341 entry.row_ = portfolio.working_.back();
2342 entry.sent_at_ = freshness_instant();
2344 entry.journal_ = executor_.last_entry();
2345 if (entry.sent_at_ != std::chrono::system_clock::time_point{}) {
2346 entry.unexplained_since_ = entry.sent_at_;
2348 unconfirmed_.push_back(std::move(entry));
2352 bool cancellable_reservation(
const WorkingOrder<Tag>& working)
const {
2353 if (working.ref_.primary_.empty()) {
2356 for (
const auto& entry : unconfirmed_) {
2357 if (is_reserved_row_of(working, entry)) {
2358 return entry.broker_ref_;
2371 void confirm_serialized(strategy::Portfolio<Tag>& portfolio, std::chrono::system_clock::time_point now) {
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();) {
2377 persist_hold(entry);
2379 const bool read = portfolio.
as_of_ != epoch && !(portfolio.
as_of_ < entry.baseline_at_);
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_;
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)))) {
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() +
")" :
2403 std::optional<Decimal64> held;
2404 std::optional<Decimal64> moved;
2408 moved = entry.side_ == OrderSide::BUY ?
decimal_subtract(*held, entry.baseline_) :
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);
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_,
false);
2423 at = unconfirmed_.erase(at);
2426 }
else if (moved &&
decimal_less(entry.expected_, *moved)) {
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"
2435 Decimal64 covered = zero;
2436 const auto fillable =
decimal_subtract(entry.expected_, listed).value_or(zero);
2438 covered =
decimal_less(*moved, fillable) ? *moved : fillable;
2440 auto residual =
decimal_subtract(fillable, covered).value_or(entry.expected_);
2442 mark_explained(entry);
2446 entry.residual_ = residual;
2448 if (!entry.unexplained_since_ && now != epoch) {
2449 entry.unexplained_since_ = now;
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_) {
2457 entry.noticed_ =
true;
2458 ++orders_unconfirmed_timed_out_;
2462 "an order this runner sent" + named(entry) +
2463 " was not accounted for by holdings within the confirmation timeout; it still holds the instrument"
2468 if (!is_named && ledger_.requires_reconcile(entry.instrument_)) {
2472 rebuild_residual_row(portfolio, entry, residual);
2481 InstrumentId instrument_;
2482 std::chrono::system_clock::time_point at_{};
2483 bool strict_{
false};
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)) {
2492 boundary.strict_ = strict;
2497 boundaries_.push_back(Boundary{instrument, at, strict});
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");
2507 return std::string(
"the holding's rows do not add up, so the order would have no baseline");
2509 for (
const auto& boundary : boundaries_) {
2510 if (boundary.instrument_ == instrument &&
2511 (boundary.strict_ ? !(boundary.at_ < portfolio.
as_of_) : portfolio.
as_of_ < boundary.at_)) {
2513 "the holdings were read before the last order on this instrument was accounted for; waiting for a "
2518 return std::nullopt;
2523 std::optional<std::string> serial_hold(
const InstrumentId& instrument,
const std::vector<InstrumentId>& claimed)
const {
2524 if (!can_serialize) {
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"
2531 return std::string(
"nothing is placed before runner::recover_runner has run");
2533 for (
const auto& entry : unconfirmed_) {
2534 if (!(entry.instrument_ == instrument)) {
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");
2546 return "an order on this instrument" + named(entry) +
" is not yet accounted for by holdings";
2548 for (
const auto&
id : claimed) {
2549 if (
id == instrument) {
2550 return std::string(
"an earlier instruction in this pass places on this instrument");
2553 return baseline_missing(instrument);
2556 static std::string named(
const Unconfirmed& entry) {
2557 return entry.ref_.primary_.empty() ? std::string() :
" (" + entry.ref_.primary_ +
")";
2562 static bool ends_the_order(
OrderStatus status)
noexcept {
2564 case OrderStatus::FILLED:
2565 case OrderStatus::CANCELED:
2566 case OrderStatus::REJECTED:
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:
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_);
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_) {
2600 if (row.ordered_at_ && !own_row(row, entry)) {
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_)))) {
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()) {
2635 std::optional<std::string> contradiction;
2636 for (
const auto& row : evidence) {
2637 if (!own_row(row, entry)) {
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();
2650 if (contradiction) {
2654 if (contradiction) {
2655 hold_for_operator(entry,
"an order this runner sent" + named(entry) +
": " + *contradiction);
2658 if (entry.settled_ || broker_row_for(context_.portfolio(), entry) !=
nullptr) {
2661 if (
const auto* row = ended_row_for(evidence, entry)) {
2662 entry.expected_ = row->execution_.filled_quantity_;
2663 entry.settled_ =
true;
2665 entry.unexplained_since_ = now;
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;
2683 confirm_reservations();
2687 bool awaits_evidence_serialized(
const Unconfirmed& entry, std::chrono::system_clock::time_point now)
const {
2690 if (entry.operator_only_ || entry.ref_.primary_.empty()) {
2694 if (!entry.identified_) {
2697 if (entry.residual_.coefficient() == 0 || entry.settled_ || entry.history_missed_) {
2700 if (timed_out(entry, now)) {
2704 return !entry.listed_now_ && (entry.listed_ || entry.withdrawn_ || entry.restored_);
2708 static JournalEntry closing_line(
2709 const Unconfirmed& entry,
JournalOutcome outcome, std::optional<Decimal64> holding, std::string detail
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);
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) {
2725 if (executor_.record(line)) {
2728 note_executor_failure(
false);
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);
2744 void persist_hold([[maybe_unused]] Unconfirmed& entry) {
2745 if constexpr (can_serialize) {
2746 if (entry.unwritten_hold_.empty()) {
2750 if (executor_.record(line)) {
2751 entry.unwritten_hold_.clear();
2753 note_executor_failure(
false);
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();
2767 void restore_from(
const JournalReplay& replay) {
2768 const auto same = [](
const std::optional<Decimal64>& a,
const std::optional<Decimal64>& b) {
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_);
2780 std::chrono::system_clock::time_point sent_at_{};
2783 bool accepted_{
false};
2785 bool operator_only_{
false};
2788 std::vector<Fold> folds;
2790 std::unordered_map<std::string, std::size_t> fold_of;
2791 for (
const auto& line : replay.entries_) {
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());
2803 fold->line_.baseline_.reset();
2806 if (!closing && !same_instruction(fold->line_, line)) {
2807 fold->operator_only_ =
true;
2808 fold->why_ =
"its journal lines disagree about the order";
2810 if (line.recorded_at_ < fold->sent_at_) {
2811 fold->sent_at_ = line.recorded_at_;
2815 fold->operator_only_ =
true;
2816 fold->why_ = line.detail_.empty() ? std::string(
"a damaged journal line") : line.detail_;
2818 switch (line.outcome_) {
2821 if (!line.order_id_.empty()) {
2822 fold->name_ = line.order_id_;
2829 set_boundary(line.instrument_, line.recorded_at_,
true);
2835 fold->operator_only_ =
true;
2836 fold->why_ = line.detail_;
2844 std::vector<Unconfirmed> restored;
2846 std::vector<std::string> why;
2847 for (
auto& fold : folds) {
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_;
2858 entry.sent_at_ = fold.sent_at_;
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)";
2869 strategy::PlannedOrder order;
2871 order.
side_ = fold.line_.side_;
2872 order.
type_ = fold.line_.type_;
2874 order.
session_ = fold.line_.session_;
2875 order.
venue_ = fold.line_.venue_.value_or(Venue::UNSPECIFIED);
2877 order.
price_ = fold.line_.price_;
2879 strategy::Portfolio<Tag> scratch;
2880 reserve(scratch, order, std::nullopt, std::nullopt, entry.ref_,
false);
2881 entry.row_ = scratch.
working_.back();
2882 restored.push_back(std::move(entry));
2883 why.push_back(fold.why_);
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";
2895 for (std::size_t at = 0; at < restored.size(); ++at) {
2896 if (restored[at].operator_only_) {
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"
2903 unconfirmed_.push_back(std::move(restored[at]));
2905 confirm_reservations();
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
2917 if constexpr (can_serialize) {
2918 if (
serialized() && baseline && !context_.portfolio().working_.empty()) {
2919 track_serialized(order, ref, indeterminate, *baseline);
2923 track_overlapping(order, ref, indeterminate);
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()) {
2949 entry.side_ = order.
side_;
2951 entry.baseline_ = *held;
2952 entry.row_ = portfolio.
working_.back();
2953 entry.sent_at_ = freshness_instant();
2955 entry.unexplained_since_ = entry.sent_at_;
2956 unconfirmed_.push_back(std::move(entry));
2960 void note_withdrawal_accepted(
const OrderRef<Tag>& ref) {
2961 for (
auto& entry : unconfirmed_) {
2963 if (!entry.ref_.primary_.empty() && entry.ref_.primary_ == ref.primary_) {
2964 entry.withdrawn_ =
true;
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_;
2977 static constexpr std::chrono::hours order_time_tolerance_{1};
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
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)) {
2994 if (row.ordered_at_ && ((earliest && *row.ordered_at_ < *earliest) || (latest && *latest < *row.ordered_at_))) {
2997 if (found ==
nullptr) {
2999 }
else if (
decimal_less(found->execution_.filled_quantity_, row.execution_.filled_quantity_) ||
3000 decimal_less(row.execution_.filled_quantity_, found->execution_.filled_quantity_) ||
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);
3013 bool awaits_evidence_overlapping(
const Unconfirmed& entry, std::chrono::system_clock::time_point now)
const {
3021 if (entry.residual_.coefficient() == 0 || entry.settled_ || entry.history_missed_ || entry.operator_only_ ||
3022 (entry.ref_.primary_.empty() && ledger_.requires_reconcile(entry.instrument_))) {
3025 if (timed_out(entry, now)) {
3028 return !entry.listed_now_ && (entry.listed_ || entry.withdrawn_ || entry.restored_);
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_;
3038 static const WorkingOrder<Tag>* broker_row_for(
const strategy::Portfolio<Tag>& portfolio,
const Unconfirmed& entry) {
3039 if (entry.ref_.primary_.empty()) {
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_ &&
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));
3062 static void mark_explained(Unconfirmed& entry) {
3064 entry.unexplained_since_.reset();
3065 entry.history_missed_ =
false;
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;
3074 return newest == unconfirmed_.rend() ? unconfirmed_.end() : std::prev(newest.base());
3078 void drop_reserved_row(
const Unconfirmed& entry) {
3079 auto portfolio = context_.portfolio();
3083 [&](
const WorkingOrder<Tag>& working) { return is_reserved_row_of(working, entry); }
3087 context_.set_portfolio(std::move(portfolio));
3095 void confirm_reservations() {
3097 if (unconfirmed_.empty()) {
3100 const auto now = freshness_instant();
3101 auto portfolio = context_.portfolio();
3106 [&](
const WorkingOrder<Tag>& working) {
3107 for (const auto& entry : unconfirmed_) {
3108 if (is_reserved_row_of(working, entry)) {
3118 confirm_serialized(portfolio, now);
3120 confirm_overlapping(portfolio, now);
3122 context_.set_portfolio(std::move(portfolio));
3137 void confirm_overlapping(strategy::Portfolio<Tag>& portfolio, std::chrono::system_clock::time_point now) {
3139 std::vector<bool> retire(unconfirmed_.size(),
false);
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) {
3148 const auto& instrument = unconfirmed_[first].instrument_;
3149 const auto direction = unconfirmed_[first].side_;
3150 std::vector<std::size_t> group;
3152 for (std::size_t at = first; at < unconfirmed_.size(); ++at) {
3153 if (unconfirmed_[at].instrument_ == instrument) {
3154 group.push_back(at);
3156 mixed = mixed || unconfirmed_[at].side_ != direction;
3160 const auto signed_in = [&](
const Unconfirmed& entry,
const Decimal64& quantity) {
3161 return entry.side_ == direction ? quantity :
decimal_subtract(zero, quantity).value_or(zero);
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_)
3176 const auto left =
decimal_subtract(entry.expected_, listed[at]).value_or(zero);
3182 std::optional<Decimal64> moved;
3184 moved = direction == OrderSide::BUY ?
decimal_subtract(*held, unconfirmed_[first].baseline_) :
3187 std::vector<Decimal64> covered(group.size(), zero);
3188 if (moved && !mixed) {
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];
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]));
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);
3211 if (total && (total->coefficient() != 0 || all_settled) && !
decimal_less(*total, *moved) &&
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]];
3223 retire[group[at]] =
true;
3225 advanced = direction == OrderSide::BUY ?
3226 decimal_add(*advanced, signed_in(entry, entry.expected_)) :
3231 auto residual =
decimal_subtract(entry.expected_, covered[at]).value_or(zero);
3234 mark_explained(entry);
3237 entry.residual_ = residual;
3238 if (!entry.unexplained_since_) {
3239 entry.unexplained_since_ = now;
3244 if ((entry.settled_ || entry.history_missed_) && timed_out(entry, now) &&
3245 portfolio.
as_of_ - *entry.unexplained_since_ > confirmation_timeout_) {
3250 ++orders_unconfirmed_timed_out_;
3254 "an order this runner sent (" + entry.ref_.primary_ +
3255 ") filled by the order history, and holdings did not show it within the confirmation "
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"
3262 expired[group[at]] =
true;
3265 advanced = direction == OrderSide::BUY ?
decimal_add(*advanced, signed_in(entry, covered[at])) :
3273 if (entry.ref_.primary_.empty() && ledger_.requires_reconcile(entry.instrument_)) {
3276 rebuild_residual_row(portfolio, entry, residual);
3278 for (
const auto at : group) {
3279 if (!retire[at] && advanced) {
3280 unconfirmed_[at].baseline_ = *advanced;
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));
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
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();
3346 const bool exposed = guard_.limits().max_gross_exposure_.has_value();
3355 if (!keeps_sent_orders_until_listed && !sells && !buys && !capped && !exposed && !conditional) {
3358 auto persisted = context_.portfolio();
3360 persisted, order, reference_price, currency, ref,
3361 guard_.limits().enforce_buying_power_
3363 context_.set_portfolio(std::move(persisted));
3394 std::vector<strategy::InstrumentBasis> exposure_basis_for(
3395 const strategy::Portfolio<Tag>& view,
3396 const std::vector<strategy::PlannedOrder>& pending
3398 std::vector<InstrumentId> instruments;
3399 const auto note = [&instruments](
const InstrumentId& id) {
3400 for (
const auto& seen : instruments) {
3405 instruments.push_back(
id);
3407 for (
const auto& position : view.
positions_) {
3411 if (position.quantity_.coefficient() == 0) {
3414 note(position.instrument_);
3416 for (
const auto& working : view.
working_) {
3418 note(working.order_.instrument_);
3421 for (
const auto& order : pending) {
3425 std::vector<strategy::InstrumentBasis> basis;
3426 basis.reserve(instruments.size());
3427 for (
const auto&
id : instruments) {
3428 const HoldingPosition* position =
nullptr;
3430 if (held.instrument_ ==
id) {
3436 const auto reference = context_.instrument(
id);
3448 const auto quoted = context_.last_price(
id);
3457 auto price = quoted;
3458 if (!price && position !=
nullptr &&
decimal_less(zero, position->last_price_)) {
3459 price = position->last_price_;
3464 basis.push_back(strategy::InstrumentBasis{id, reference->currency_, *price});
3470 static void write_sellable(
3471 strategy::Portfolio<Tag>& portfolio,
3472 const InstrumentId& instrument,
3473 const Decimal64& quantity
3475 for (
auto& position : portfolio.
positions_) {
3476 if (position.instrument_ == instrument) {
3477 position.sellable_quantity_ = quantity;
3485 position.sellable_basis_ = runner_detail::sellable_inquiry_basis_of<Executor>();
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,
3523 WorkingOrder<Tag> working;
3525 working.order_.side_ = order.
side_;
3526 working.order_.status_ = OrderStatus::PENDING;
3527 working.order_.type_ = order.
type_;
3528 working.order_.session_ = order.
session_;
3537 working.order_.venue_ = order.
venue_;
3538 working.order_.price_ = order.
price_;
3543 working.order_.quantity_ = order.
quantity_;
3545 working.amendable_quantity_ = order.
quantity_;
3547 working.ref_ = *ref;
3552 working.reserved_ =
true;
3553 view.
working_.push_back(std::move(working));
3560 if (!spends || order.
side_ != OrderSide::BUY || !currency) {
3563 const auto price = order.
price_ ? order.
price_ : reference_price;
3572 if (!(reading.cash_.currency_ == *currency)) {
3576 reading.cash_.amount_ = *left;
3596 void drop_superseded_deferred(
const std::vector<
Instruction<Tag>>& instructions) {
3597 if (deferred_targets_.empty()) {
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);
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());
3619 Report release_deferred(
const std::vector<OrderSnapshot>* evidence) {
3620 bool released =
false;
3621 for (
auto at = deferred_targets_.begin(); at != deferred_targets_.end();) {
3623 if (stop_listed(at->target_.instrument_) ||
3628 if (evidence !=
nullptr) {
3629 bool decided =
true;
3630 bool filled =
false;
3631 for (
const auto& stop : at->stops_) {
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_;
3641 const auto* row = history_row_for(
3642 *evidence,
stop.ref_.primary_, at->target_.instrument_, std::nullopt, earliest, latest
3644 if (row ==
nullptr) {
3648 if (row->execution_.filled_quantity_.coefficient() != 0) {
3655 ++deferred_dropped_after_fill_;
3656 at = deferred_targets_.erase(at);
3664 context_.submit(at->target_);
3665 at = deferred_targets_.erase(at);
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_);
3686 void forget_retained(
const InstrumentId& instrument) {
3687 if (instrument.symbol_.empty()) {
3690 retained_targets_.erase(
3692 retained_targets_.begin(), retained_targets_.end(),
3693 [&](
const RetainedTarget& kept) { return kept.target_.instrument_ == instrument; }
3695 retained_targets_.end()
3698 kept_intents_.erase(
3700 kept_intents_.begin(), kept_intents_.end(),
3701 [&](
const KeptIntent& kept) { return kept.intent_.instrument() == instrument; }
3722 bool retain_withheld(
3724 std::optional<std::chrono::system_clock::time_point> after = std::nullopt,
bool until_accounted =
false
3726 const auto* target = std::get_if<strategy::TargetPosition>(&instruction);
3727 if (target ==
nullptr) {
3736 for (
auto& kept : retained_targets_) {
3737 if (kept.target_.instrument_ == target->instrument_) {
3738 kept = RetainedTarget{*target, for_phase, after, until_accounted};
3742 retained_targets_.push_back(RetainedTarget{*target, for_phase, after, until_accounted});
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()) {
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()) {
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)) {
3780 bool owes(
const InstrumentId& instrument)
const {
3781 return std::any_of(unconfirmed_.begin(), unconfirmed_.end(), [&](
const Unconfirmed& entry) {
3782 return entry.instrument_ == instrument;
3792 void retain_for_phase(
3794 std::optional<std::chrono::system_clock::time_point> after = std::nullopt
3796 if constexpr (runner_detail::sends_to_a_broker<Executor>::value) {
3797 if (retain_withheld(instruction,
true, after)) {
3800 keep_intent_for_phase(instruction, broker_answered, after);
3803 (void)broker_answered;
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);
3834 void keep_intent_for_phase(
3836 std::optional<std::chrono::system_clock::time_point> after = std::nullopt
3838 const auto* intent = std::get_if<strategy::OrderIntent>(&instruction);
3839 if (intent ==
nullptr) {
3846 if (broker_answered && intent->client_order_id()) {
3849 for (
const auto* seen : kept_this_pass_) {
3850 if (seen == &instruction) {
3854 const auto held = context_.portfolio().net_quantity_of(intent->instrument());
3858 kept_this_pass_.push_back(&instruction);
3859 kept_intents_.push_back(KeptIntent{*intent, *held, after});
3863 bool stop_listed(
const InstrumentId& instrument)
const {
3864 for (
const auto& working : context_.portfolio().working_) {
3865 if (working.order_.instrument_ == instrument &&
3875 void defer_if_waiting_on_stop(
3877 const strategy::Plan<Tag>& planned
3879 const auto* target = std::get_if<strategy::TargetPosition>(&instruction);
3880 if (target ==
nullptr) {
3883 for (
const auto& rejection : planned.
rejected_) {
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_ &&
3896 deferred.stops_.push_back({working.ref_, working.order_.ordered_at_});
3899 for (
auto& kept : deferred_targets_) {
3900 if (kept.target_.instrument_ == target->instrument_) {
3901 kept = std::move(deferred);
3902 ++targets_deferred_;
3906 deferred_targets_.push_back(std::move(deferred));
3907 ++targets_deferred_;
3912 std::optional<WithheldPlanReason> plan_withhold_reason(
bool would_cancel)
const {
3913 const auto& portfolio = context_.portfolio();
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_;
3929 if (never_read || too_old) {
3940 if (require_known_portfolio_ && !(never_read && allow_absent_portfolio_)) {
3949 if (guard_.blocking_reason()) {
3952 return std::nullopt;
3960 std::chrono::system_clock::time_point freshness_instant()
const {
3961 return observed_at_ > context_.now() ? observed_at_ : context_.now();
3967 std::chrono::system_clock::time_point send_instant()
const {
3968 return observed_at_ == std::chrono::system_clock::time_point{} ? context_.now() : freshness_instant();
3976 const auto holding = guard_.blocking_reason();
3981 static InstrumentId instruction_instrument(
const Instruction<Tag>& instruction) {
3982 if (
const auto* target = std::get_if<strategy::TargetPosition>(&instruction)) {
3983 return target->instrument_;
3985 if (
const auto* order = std::get_if<strategy::OrderIntent>(&instruction)) {
3986 return order->instrument();
3988 return InstrumentId{};
3993 if (
const auto* target = std::get_if<strategy::TargetPosition>(&instruction)) {
3994 return target->tag_;
3996 if (
const auto* order = std::get_if<strategy::OrderIntent>(&instruction)) {
3997 return order->tag();
4002 void send_cancel(
const OrderRef<Tag>& ref, Report& report) {
4008 ++instructions_unavailable_;
4009 report.unavailable_cancel_.push_back(ref);
4026 if (
const auto holding =
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_))) {
4043 if (working ==
nullptr) {
4044 ++cancels_unresolved_;
4045 report.unresolved_cancel_.push_back(ref);
4051 if (working->reserved_ && serialized() && !cancellable_reservation(*working)) {
4052 ++cancels_rejected_;
4056 const auto outcome = executor_.cancel(*working);
4075 guard_.note_sent(send_instant());
4077 note_withdrawal_accepted(ref);
4078 report.would_cancel_.push_back(ref);
4084 guard_.note_sent(send_instant());
4085 ++cancels_rejected_;
4092 ledger_.mark(working->order_.instrument_,
"a cancellation request has an unknown outcome");
4095 guard_.note_sent(send_instant());
4096 ++cancels_indeterminate_;
4097 report.indeterminate_cancel_.push_back(ref);
4105 ++cancels_rejected_;
4111 ++instructions_unavailable_;
4112 report.unavailable_cancel_.push_back(ref);
4125 strategy::Plan<Tag> plan_weight(
4126 const strategy::OrderIntent& intent,
4127 const strategy::ReconcileInputs& inputs
4129 strategy::Plan<Tag> plan;
4131 if (!converted.target_) {
4132 plan.
rejected_.push_back(strategy::Rejection{
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) {
4146 plan.
rejected_.push_back(strategy::Rejection{
4160 const auto id = instrument_of(instruction);
4161 const auto reference = context_.instrument(
id);
4162 const auto band = band_for(
id);
4164 strategy::ReconcileInputs inputs;
4165 inputs.
instrument_ = reference ? &*reference :
nullptr;
4167 inputs.
limits_ = band.applied_;
4174 if (
const auto* target = std::get_if<strategy::TargetPosition>(&instruction)) {
4177 if (
const auto* intent = std::get_if<strategy::OrderIntent>(&instruction)) {
4183 return plan_weight(*intent, inputs);
4187 return strategy::Plan<Tag>{};
4193 if (band.withheld_) {
4194 ++price_bands_withheld_;
4195 mark_withheld_band(plan, *band.withheld_);
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();
4216 last_broker_refusal_.reset();
4224 last_broker_refusal_ = std::move(refusal);
4226 static_cast<void>(order);
4232 if (
const auto* target = std::get_if<strategy::TargetPosition>(&instruction)) {
4233 return target->instrument_;
4235 if (
const auto* intent = std::get_if<strategy::OrderIntent>(&instruction)) {
4238 return InstrumentId{};
4244 PriceLimits limits_;
4246 std::chrono::system_clock::time_point received_at_{};
4256 std::chrono::system_clock::time_point as_of() const noexcept {
4257 if (!limits_.exchange_time_) {
4258 return received_at_;
4260 return *limits_.exchange_time_ < received_at_ ? *limits_.exchange_time_ : received_at_;
4264 struct BandDecision {
4266 std::optional<PriceLimits> applied_;
4268 std::optional<PriceLimits> withheld_;
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()) {
4282 if (!maximum_price_limits_age_ && !allow_unbounded_price_limits_age_) {
4286 decision.withheld_ = found->second.limits_;
4295 if (found->second.as_of() > freshness_instant()) {
4296 decision.withheld_ = found->second.limits_;
4299 if (maximum_price_limits_age_ &&
4300 freshness_instant() - found->second.as_of() > *maximum_price_limits_age_) {
4301 decision.withheld_ = found->second.limits_;
4304 decision.applied_ = found->second.limits_;
4314 static void mark_withheld_band(strategy::Plan<Tag>& plan,
const PriceLimits& withheld) {
4315 for (
auto& order : plan.
place_) {
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
4328 strategy::RiskGuard guard_;
4330 std::size_t placements_at_broker_{0};
4332 std::size_t sends_outside_permitted_phase_{0};
4334 std::optional<std::chrono::nanoseconds> maximum_portfolio_age_;
4336 std::chrono::system_clock::time_point observed_at_{};
4338 bool allow_absent_portfolio_{
false};
4344 bool require_known_portfolio_{runner_detail::sends_to_a_broker<Executor>::value};
4345 bool probing_sellable_{
false};
4346 strategy::SellableConventionProbe<Tag> sellable_probe_;
4349 void note_local_failure(std::string detail) {
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();
4369 detail.empty() ? std::string(
"the order journal could not be written") : std::move(detail)
4373 if (refused && failures == 0) {
4377 note_local_failure(
"local machinery refused to send");
4382 std::size_t executor_journal_failures()
const {
return executor_journal_failures_impl(0); }
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();
4390 std::size_t executor_journal_failures_impl(...)
const {
return 0; }
4393 std::string executor_journal_error()
const {
return executor_journal_error_impl(0); }
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();
4401 std::string executor_journal_error_impl(...)
const {
return {}; }
4405 std::size_t acknowledged_journal_failures_{0};
4406 std::map<InstrumentId, DatedBand> price_limits_;
4408 std::optional<std::chrono::nanoseconds> maximum_price_limits_age_;
4410 bool allow_unbounded_price_limits_age_{
false};
4412 std::size_t price_bands_withheld_{0};
4414 std::size_t unclamped_orders_rejected_{0};
4416 std::size_t unclamped_orders_refused_{0};
4418 std::size_t orders_unbuildable_{0};
4420 std::size_t orders_reconcile_withheld_{0};
4422 std::size_t orders_guard_refused_{0};
4424 std::size_t orders_binding_refused_{0};
4426 std::size_t orders_broker_refused_{0};
4428 std::size_t orders_indeterminate_{0};
4430 std::size_t cancels_rejected_{0};
4432 std::size_t cancels_refused_{0};
4434 std::size_t cancels_unresolved_{0};
4436 std::size_t cancels_indeterminate_{0};
4438 std::size_t plans_withheld_{0};
4439 std::size_t warmup_instructions_discarded_{0};
4442 struct DeferredStop {
4444 std::optional<std::chrono::system_clock::time_point> ordered_at_;
4447 struct DeferredTarget {
4449 strategy::TargetPosition target_;
4451 std::vector<DeferredStop> stops_;
4453 std::chrono::system_clock::time_point deferred_at_{};
4456 std::vector<DeferredTarget> deferred_targets_;
4458 std::vector<Unconfirmed> unconfirmed_;
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};
4470 struct RetainedTarget {
4471 strategy::TargetPosition target_;
4472 bool for_phase_{
false};
4474 std::optional<std::chrono::system_clock::time_point> after_;
4476 bool until_accounted_{
false};
4478 std::vector<RetainedTarget> retained_targets_;
4481 strategy::OrderIntent intent_;
4484 std::optional<std::chrono::system_clock::time_point> after_;
4486 std::vector<KeptIntent> kept_intents_;
4487 std::size_t kept_intents_dropped_{0};
4489 std::vector<const Instruction<Tag>*> kept_this_pass_;
4491 bool releasing_deferred_{
false};
4493 std::size_t targets_deferred_{0};
4495 std::size_t deferred_dropped_after_fill_{0};
4497 std::size_t instructions_unavailable_{0};
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_;