986 : runner_(&
runner), source_(&source), schedule_(std::move(
schedule)), clock_(std::move(
clock)) {}
992 const Clock&
clock() const noexcept {
return clock_; }
997 Clock&
clock() noexcept {
return clock_; }
1011 struct InstructionOutcomes {
1012 std::size_t unbuildable_{0};
1013 std::size_t unconfirmed_timed_out_{0};
1014 std::size_t reconcile_withheld_{0};
1015 std::size_t guard_refused_{0};
1016 std::size_t binding_refused_{0};
1017 std::size_t broker_refused_{0};
1018 std::size_t indeterminate_{0};
1019 std::size_t cancels_rejected_{0};
1020 std::size_t cancels_refused_{0};
1021 std::size_t cancels_unresolved_{0};
1022 std::size_t cancels_indeterminate_{0};
1023 std::size_t plans_withheld_{0};
1024 std::size_t instructions_unavailable_{0};
1028 InstructionOutcomes instruction_outcomes()
const {
1029 InstructionOutcomes at;
1030 at.unbuildable_ = runner_->orders_unbuildable();
1031 at.unconfirmed_timed_out_ = runner_->orders_unconfirmed_timed_out();
1032 at.reconcile_withheld_ = runner_->orders_reconcile_withheld();
1033 at.guard_refused_ = runner_->orders_guard_refused();
1034 at.binding_refused_ = runner_->orders_binding_refused();
1035 at.broker_refused_ = runner_->orders_broker_refused();
1036 at.indeterminate_ = runner_->orders_indeterminate();
1037 at.cancels_rejected_ = runner_->cancels_rejected();
1038 at.cancels_refused_ = runner_->cancels_refused();
1039 at.cancels_unresolved_ = runner_->cancels_unresolved();
1040 at.cancels_indeterminate_ = runner_->cancels_indeterminate();
1041 at.plans_withheld_ = runner_->plans_withheld();
1042 at.instructions_unavailable_ = runner_->instructions_unavailable();
1047 static void note_instruction_outcomes(
1049 const InstructionOutcomes& before,
1050 const InstructionOutcomes& after
1052 report.orders_unbuildable_ = after.unbuildable_ - before.unbuildable_;
1053 report.orders_unconfirmed_timed_out_ = after.unconfirmed_timed_out_ - before.unconfirmed_timed_out_;
1054 report.orders_reconcile_withheld_ = after.reconcile_withheld_ - before.reconcile_withheld_;
1055 report.orders_guard_refused_ = after.guard_refused_ - before.guard_refused_;
1056 report.orders_binding_refused_ = after.binding_refused_ - before.binding_refused_;
1057 report.orders_broker_refused_ = after.broker_refused_ - before.broker_refused_;
1058 report.orders_indeterminate_ = after.indeterminate_ - before.indeterminate_;
1059 report.cancels_rejected_ = after.cancels_rejected_ - before.cancels_rejected_;
1060 report.cancels_refused_ = after.cancels_refused_ - before.cancels_refused_;
1061 report.cancels_unresolved_ = after.cancels_unresolved_ - before.cancels_unresolved_;
1062 report.cancels_indeterminate_ = after.cancels_indeterminate_ - before.cancels_indeterminate_;
1063 report.plans_withheld_ = after.plans_withheld_ - before.plans_withheld_;
1064 report.instructions_unavailable_ = after.instructions_unavailable_ - before.instructions_unavailable_;
1080 maximum_portfolio_age_ = age;
1085 runner_->set_maximum_portfolio_age(age);
1183 allow_unbounded_portfolio_age_ =
true;
1184 maximum_portfolio_age_.reset();
1185 runner_->set_maximum_portfolio_age(std::nullopt);
1194 runner_->set_maximum_price_limits_age(age);
1199 runner_->allow_unbounded_price_limits_age();
1210 allow_absent_portfolio_ =
true;
1214 runner_->allow_absent_portfolio();
1222 session_window_ = std::move(window);
1266 const auto outside_phase_before = runner_->sends_outside_permitted_phase();
1268 const auto bands_withheld_before = runner_->price_bands_withheld();
1269 const auto unclamped_rejected_before = runner_->unclamped_orders_rejected();
1270 const auto unclamped_refused_before = runner_->unclamped_orders_refused();
1275 const auto outcomes_before = instruction_outcomes();
1276 auto outcomes_after = outcomes_before;
1280 std::optional<BrokerRefusal> refusal_at_boundary;
1281 std::optional<strategy::Rejection> unbuildable_at_boundary;
1284 const auto charged_before = [&]() {
1286 return source_->charged_costs();
1291 for (
auto& due : next_due_) {
1294 for (
auto& count : consecutive_failures_) {
1298 warmup_discarded_at_start_ = warmup_discarded();
1300 kept_dropped_at_start_ = runner_->kept_intents_dropped();
1303 while (max_iterations == 0 || report.
iterations_ < max_iterations) {
1304 const auto iteration = step(stop, report);
1306 if (iteration.gate_) {
1311 if (iteration.phase_changed_) {
1318 outcomes_after = instruction_outcomes();
1319 refusal_at_boundary = runner_->last_broker_refusal();
1320 unbuildable_at_boundary = runner_->last_unbuildable();
1324 runner_->clear_retained_targets();
1325 if (
const auto intent = stop.intent()) {
1326 interrupt_stop(*intent, report);
1327 }
else if (owed_past_limit(clock_.now())) {
1332 report.
final_gate_ = runner_->guard().blocking_reason();
1336 if (
const auto intent = stop.intent()) {
1341 outcomes_after = instruction_outcomes();
1342 refusal_at_boundary = runner_->last_broker_refusal();
1343 unbuildable_at_boundary = runner_->last_unbuildable();
1347 const bool last_iteration = max_iterations != 0 && report.
iterations_ >= max_iterations;
1348 if (!last_iteration && closing_should_wait(*intent, report)) {
1349 clock_.wait_for(closing_until_next());
1352 finish_stop(*intent, report);
1356 clock_.wait_for(until_next(iteration.at_));
1367 runner_->sends_outside_permitted_phase() - outside_phase_before;
1370 runner_->unclamped_orders_rejected() - unclamped_rejected_before;
1372 runner_->unclamped_orders_refused() - unclamped_refused_before;
1375 outcomes_after = instruction_outcomes();
1376 refusal_at_boundary = runner_->last_broker_refusal();
1377 unbuildable_at_boundary = runner_->last_unbuildable();
1379 note_instruction_outcomes(report, outcomes_before, outcomes_after);
1402 !said_no_band_age_policy_) {
1403 said_no_band_age_policy_ =
true;
1404 report.
notes_.emplace_back(
1405 "no price band age policy: call set_maximum_price_limits_age or "
1406 "allow_unbounded_price_limits_age"
1412 note_sellable_convention(report);
1413 note_unestablished_phase(report);
1414 note_on_fill_never_fires(report);
1415 note_listing_outpaces_holdings(report);
1416 note_warmup_discarded(report);
1417 note_kept_intents_dropped(report);
1437 void note_sellable_convention(
LoopReport& report)
const {
1439 if (evidence.total() == 0) {
1443 "broker's sellable figure appears " +
1445 std::to_string(evidence.net_) +
" net, " + std::to_string(evidence.gross_) +
" gross)"
1450 std::size_t warmup_discarded()
const {
1451 if constexpr (runner_counts_warmup<Runner>::value) {
1464 void note_warmup_discarded(LoopReport& report)
const {
1465 const auto discarded = warmup_discarded() - warmup_discarded_at_start_;
1466 if (discarded == 0) {
1469 report.notes_.push_back(
1470 std::to_string(discarded) +
" instruction(s) decided on bars fed as history were not sent: "
1471 "only the newest bar of each instrument's read may send (poll bars no slower than their interval)"
1476 void note_kept_intents_dropped(LoopReport& report)
const {
1477 if constexpr (runner_counts_kept_drops<Runner>::value) {
1478 const auto dropped = runner_->kept_intents_dropped() - kept_dropped_at_start_;
1480 report.notes_.push_back(
1481 std::to_string(dropped) +
" order intent(s) kept for the market phase were not sent at the "
1482 "open: the instrument's holding had changed since they were decided, or a stop now rested"
1486 static_cast<void>(report);
1494 void note_on_fill_never_fires(LoopReport& report)
const {
1496 report.notes_.push_back(
1497 "strategy defines on_fill; it never fires on this runner (fills are not observed live: "
1498 "read positions from holdings, see writing-a-strategy.md)"
1501 static_cast<void>(report);
1511 void note_listing_outpaces_holdings(LoopReport& report)
const {
1512 const auto& listing = schedule_.working_orders_;
1513 const auto& holdings = schedule_.holdings_;
1514 if (listing && holdings && *listing < *holdings) {
1515 report.notes_.push_back(
1516 "working orders are polled more often than holdings; plans made between a listing and the "
1517 "next holdings read are withheld as portfolio_superseded"
1523 IterationReport step(StopSource& stop, LoopReport& report) {
1524 IterationReport iteration;
1525 iteration.at_ = clock_.now();
1529 check_control_file(stop, report);
1532 if (
stop.requested()) {
1533 runner_->clear_retained_targets();
1550 install_phase(iteration);
1557 runner_->set_observed_at(iteration.at_);
1561 runner_->require_known_portfolio(!allow_absent_portfolio_);
1570 hold(gate_for(stop, iteration.at_, report,
true));
1573 if (!is_due(kind, iteration.at_)) {
1588 install_phase(iteration);
1594 hold(gate_for(stop, iteration.at_, report,
false));
1595 const auto watch = watch_phase(iteration);
1603 const auto placements_before_poll = runner_->placements_at_broker();
1604 const auto outcome = source_->poll(kind, *runner_);
1606 working_asked_at_ = clock_.monotonic_now();
1608 holdings_asked_at_ = clock_.monotonic_now();
1610 note_revised_phase(std::string(
poll_kind_name(kind)) +
" poll", watch, report);
1616 note_working_orders_read(iteration.at_, placements_before_poll);
1618 ++report.attempted_[
static_cast<int>(kind)];
1619 iteration.polls_.emplace_back(kind, outcome);
1620 schedule_next(kind, iteration.at_);
1621 note_outcome(kind, outcome, report);
1623 iteration.fed_ =
true;
1625 if (report.abandoned_) {
1628 iteration.gate_ = runner_->guard().blocking_reason();
1637 runner_->observe_sellable_convention();
1645 install_phase(iteration);
1646 hold(gate_for(stop, iteration.at_, report,
false));
1650 if (announced_ != iteration.phase_) {
1668 if (runner_->context().now() < iteration.at_) {
1669 runner_->context().set_now(iteration.at_);
1671 announced_ = iteration.phase_;
1672 iteration.phase_changed_ =
true;
1675 const auto watch = watch_phase(iteration);
1676 runner_->feed(iteration.phase_);
1677 note_revised_phase(
"on_phase handler", watch, report);
1687 runner_->retry_retained_targets();
1692 iteration.gate_ = runner_->guard().blocking_reason();
1698 std::chrono::system_clock::time_point at_{};
1700 std::size_t sends_{0};
1704 PhaseWatch watch_phase(
const IterationReport& iteration)
const {
1705 return PhaseWatch{iteration.at_, iteration.phase_, runner_->guard().total_sent()};
1725 void note_revised_phase(
const std::string& what,
const PhaseWatch& before, LoopReport& report) {
1726 if (runner_->guard().total_sent() == before.sends_) {
1729 if (phase_at(before.at_) == before.phase_) {
1732 ++report.sends_under_a_revised_phase_;
1733 if (report.sends_under_a_revised_phase_ == 1) {
1741 report.notes_.push_back(
1742 "the " + what +
" changed the market phase and sent a request in the same call; check whether "
1743 "that request used the earlier phase. Only the calendar poll may write the trading calendar, "
1744 "and it must not send");
1766 void install_phase(IterationReport& iteration) {
1767 iteration.phase_ = phase_at(iteration.at_);
1771 phase_ever_known_ =
true;
1773 runner_->context().set_phase(iteration.phase_);
1787 const auto& calendar = runner_->context().calendar();
1788 return session_window_.phase(at, calendar.is_trading_day(session_window_.local_date(at)));
1796 std::string unreadable_calendar_rows()
const {
1797 const auto unreadable = runner_->context().calendar().unreadable_days_;
1798 if (unreadable == 0) {
1801 return "; the broker sent " + std::to_string(unreadable) +
" calendar row" +
1802 (unreadable == 1 ?
"" :
"s") +
" whose date could not be read";
1816 void note_unestablished_phase(LoopReport& report)
const {
1830 const bool ever_known = phase_ever_known_;
1836 if (!schedule_.calendar_) {
1837 reason =
"no calendar cadence is configured";
1839 reason =
"every calendar poll failed";
1840 }
else if (runner_->context().calendar().days_.empty()) {
1844 reason =
"a calendar poll succeeded but no calendar arrived (this broker may publish none)";
1845 reason += unreadable_calendar_rows();
1849 const auto date = session_window_.local_date(last_.at_);
1850 reason =
"the calendar does not cover " + std::to_string(date.year_) +
"-" +
1851 std::to_string(date.month_) +
"-" + std::to_string(date.day_);
1852 reason += unreadable_calendar_rows();
1854 report.notes_.push_back(
1855 std::string(ever_known ?
1856 "the market phase stopped being established (" :
1857 "the market phase was never established (") +
1858 reason +
"); a guard permitting only known phases refused every placement opening a position" +
1859 (ever_known ?
" after that" :
""));
1865 bool read_order_list_once(std::chrono::system_clock::time_point at, LoopReport& report) {
1869 const auto watch = PhaseWatch{at, phase_at(at), runner_->guard().total_sent()};
1873 const auto placements_before_read = runner_->placements_at_broker();
1874 const auto outcome = source_->read_order_list(*runner_);
1876 working_asked_at_ = clock_.monotonic_now();
1877 note_revised_phase(
"order-list read", watch, report);
1881 order_list_read_ =
true;
1882 report.order_list_read_ =
true;
1883 order_list_at_ = at;
1887 note_working_orders_read(at, placements_before_read);
1905 std::optional<strategy::HaltReason> gate_for(
1907 std::chrono::system_clock::time_point at,
1911 if (
stop.requested()) {
1924 const auto intent =
stop.intent();
1928 if (intent &&
stop_intent_closes(*intent) && !order_list_read_ && may_read && on_the_floor &&
1931 read_order_list_once(at, report);
1935 if (!order_list_read_) {
1944 const bool owed = owed_past_limit(clock_.now());
1952 if (!read_order_list_once(at, report)) {
1964 if (
auto owed = owed_too_long()) {
1965 report.notes_.push_back(owed_stop_note(*owed));
1966 report.owed_stop_ = std::move(owed);
1970 if (owed_past_limit(clock_.now())) {
1977 const auto as_of = runner_->context().portfolio().as_of_;
1978 if (as_of == std::chrono::system_clock::time_point{}) {
1994 if (!allow_absent_portfolio_) {
2013 if (!maximum_portfolio_age_ && !allow_unbounded_portfolio_age_) {
2014 if (!said_no_age_policy_) {
2015 said_no_age_policy_ =
true;
2016 report.notes_.emplace_back(
2017 "no portfolio age policy: call set_maximum_portfolio_age or allow_unbounded_portfolio_age"
2022 if (maximum_portfolio_age_ && as_of != std::chrono::system_clock::time_point{} &&
2023 at - as_of > *maximum_portfolio_age_) {
2034 if constexpr (runner_keeps_sent_orders<Runner>::value) {
2035 if (
const auto missing = listing_missing(at)) {
2036 if ((said_listing_ &
missing->first) == 0) {
2037 said_listing_ |=
missing->first;
2038 report.notes_.emplace_back(
missing->second);
2043 if (runner_->guard().limits().enforce_buying_power_) {
2046 if (
const auto unread = buying_power_unread(at)) {
2050 return std::nullopt;
2062 std::optional<strategy::HaltReason> buying_power_unread(
2063 std::chrono::system_clock::time_point at
2065 const auto& readings = runner_->context().portfolio().buying_power_;
2068 const auto usable = [&](
const BuyingPower& reading) {
2069 return !maximum_buying_power_age_ ||
2070 at - reading.received_at_ <= *maximum_buying_power_age_;
2072 if constexpr (source_names_buying_power_currencies<Source>::value) {
2073 const auto& wanted = source_->buying_power_currencies();
2074 if (!wanted.empty()) {
2075 for (
const auto& currency : wanted) {
2077 for (
const auto& reading : readings) {
2078 if (reading.cash_.currency_ == currency && usable(reading)) {
2087 return std::nullopt;
2090 if (readings.empty()) {
2093 for (
const auto& reading : readings) {
2094 if (!usable(reading)) {
2098 return std::nullopt;
2115 maximum_buying_power_age_ = age;
2124 if (
const auto intent = control_.
poll()) {
2125 stop.request(*intent);
2132 "control file names no intent: '" + control_.
unrecognized() +
"'");
2138 const int at =
static_cast<int>(kind);
2140 ++report.failed_[at];
2141 ++consecutive_failures_[at];
2142 if (consecutive_failures_[at] >= failure_policy_.consecutive_limit_) {
2143 report.abandoned_ =
true;
2144 report.abandoned_kind_ = kind;
2145 report.notes_.push_back(
2146 std::string(
"gave up after repeated failures polling ") + std::string(
poll_kind_name(kind)));
2162 consecutive_failures_[at] = 0;
2169 void begin_stop(LoopReport& report) {
2170 closing_ = ClosingWait{};
2171 closing_->began_ = clock_.monotonic_now();
2172 closing_->deadline_ = closing_->began_ + std::chrono::duration_cast<std::chrono::steady_clock::duration>(
2173 closing_wait_ > std::chrono::nanoseconds::zero() ? closing_wait_ : std::chrono::nanoseconds::zero());
2178 runner_->clear_retained_targets();
2179 runner_->clear_deferred_targets();
2198 const auto parting = runner_->notify_stop();
2203 report.stop_handler_refused_ = parting.refused_.size() + parting.refused_cancel_.size() +
2204 parting.rejected_.size() + parting.withheld_.size() + parting.withheld_plans_.size() +
2205 parting.unavailable_.size();
2206 report.stop_handler_sent_ = parting.would_place_.size() + parting.would_cancel_.size();
2212 void interrupt_stop(
StopIntent intent, LoopReport& report) {
2216 runner_->clear_deferred_targets();
2217 report.stopped_by_ = intent;
2219 report.closing_waited_ = std::chrono::duration_cast<std::chrono::nanoseconds>(
2220 clock_.monotonic_now() - closing_->began_);
2223 report.closing_interrupted_ =
true;
2224 report.notes_.push_back(
2225 std::string(
"the run was abandoned for failing polls with a ") + std::string(
stop_intent_name(intent)) +
2226 " pending: its closing pass did not run, nothing was withdrawn or closed by it, and the runner is"
2227 " left unable to send; read the broker's order list and holdings");
2232 void finish_stop(
StopIntent intent, LoopReport& report) {
2233 report.closing_waited_ = std::chrono::duration_cast<std::chrono::nanoseconds>(
2234 clock_.monotonic_now() - closing_->began_);
2235 closing_->reads_wanted_ =
false;
2243 const auto stale = runner_->settle_closing();
2244 report.stop_handler_refused_ += stale.refused_.size() + stale.refused_cancel_.size() +
2245 stale.rejected_.size() + stale.withheld_.size() + stale.withheld_plans_.size() + stale.unavailable_.size();
2246 report.stop_handler_sent_ += stale.would_place_.size() + stale.would_cancel_.size();
2250 runner_->clear_retained_targets();
2251 runner_->clear_deferred_targets();
2264 report.closing_source_unverified_ = !closing_source_verified(clock_.now());
2268 runner_->refresh_unconfirmed();
2271 report.closing_unconfirmed_ = runner_->obligations_unexplained();
2272 const auto& portfolio = runner_->context().portfolio();
2274 for (
const auto& working : portfolio.working_) {
2276 if (runner_->withdrawal_accepted_for(working)) {
2279 runner_->context().cancel(working.ref_,
"stop-withdraw");
2280 ++report.withdrawn_;
2299 for (
const auto& working : portfolio.working_) {
2300 if (runner_->withdrawal_accepted_for(working)) {
2303 runner_->context().cancel(working.ref_,
"stop-flatten");
2304 ++report.withdrawn_;
2314 std::vector<InstrumentId> closed;
2315 for (
const auto& position : portfolio.positions_) {
2317 for (
const auto& already : closed) {
2318 if (already == position.instrument_) {
2326 closed.push_back(position.instrument_);
2330 const auto net = portfolio.net_quantity_of(position.instrument_);
2334 ++report.closing_unsizable_;
2337 if (net->coefficient() == 0) {
2343 if (report.closing_source_unverified_) {
2353 ++report.closing_contended_;
2356 const bool held_long =
decimal_less(Decimal64{}, *net);
2357 const auto magnitude = held_long ?
2358 std::optional<Decimal64>(*net) :
2366 ++report.closing_unsizable_;
2369 const OrderSide closing_side = held_long ? OrderSide::SELL : OrderSide::BUY;
2370 const auto contending = portfolio.working_quantity(position.instrument_, closing_side);
2378 ++report.closing_unsizable_;
2381 if (contending.coefficient() != 0) {
2385 ++report.closing_contended_;
2387 if (
decimal_less(*sendable, Decimal64{}) || sendable->coefficient() == 0) {
2392 runner_->context().submit(
2394 position.instrument_,
2399 ++report.flattened_;
2422 const auto closing_at = clock_.now();
2423 runner_->context().set_phase(phase_at(closing_at));
2424 const auto watch = PhaseWatch{closing_at, phase_at(closing_at), runner_->guard().total_sent()};
2425 const auto closing = runner_->settle_closing();
2426 note_revised_phase(
"closing pass", watch, report);
2430 runner_->clear_retained_targets();
2431 runner_->clear_deferred_targets();
2433 report.closing_placed_ = closing.would_place_.size();
2437 report.closing_rejected_ = closing.rejected_.size() + closing.rejected_cancel_.size();
2438 report.closing_refused_ = closing.refused_.size() + closing.refused_cancel_.size();
2440 report.closing_withheld_ = closing.withheld_.size() + closing.withheld_plans_.size();
2441 report.closing_indeterminate_ =
2442 closing.indeterminate_.size() + closing.indeterminate_cancel_.size();
2443 report.closing_unavailable_ = closing.unavailable_.size() + closing.unavailable_cancel_.size();
2444 report.closing_unresolved_ = closing.unresolved_cancel_.size();
2448 if (!closing.refused_.empty() || !closing.refused_cancel_.empty()) {
2449 report.notes_.push_back(
2450 "a guard refused " + std::to_string(report.closing_refused_) +
2451 " closing instruction(s); the position is still held");
2453 if (!closing.rejected_.empty()) {
2459 report.notes_.push_back(
2460 std::to_string(closing.rejected_.size()) +
2461 " closing instruction(s) were not placed; the position is still held");
2463 if (!closing.rejected_cancel_.empty()) {
2464 report.notes_.push_back(
2465 std::to_string(closing.rejected_cancel_.size()) +
2466 " closing cancellation(s) were refused; read the broker's order list for their state");
2468 if (report.closing_withheld_ != 0) {
2469 report.notes_.push_back(
2470 "withheld " + std::to_string(report.closing_withheld_) +
2471 " closing instruction(s) pending reconciliation, or until an earlier order is accounted for;"
2472 " read the broker's order list");
2474 if (report.closing_unconfirmed_ != 0) {
2475 report.notes_.push_back(
2476 std::to_string(report.closing_unconfirmed_) +
2477 " order(s) this runner sent were not yet accounted for by holdings or a listing, so the closing pass"
2478 " could not withdraw or size around them; read the broker's order list and holdings");
2480 if (report.closing_wait_expired_) {
2481 report.notes_.push_back(
2482 "the stop waited " +
2483 std::to_string(std::chrono::duration_cast<std::chrono::seconds>(report.closing_waited_).count()) +
2484 " s for holdings and the order list to account for this runner's orders, or for a marked"
2485 " instrument to be resolved, and that did not happen; the closing pass was built from what was in"
2486 " hand (set_closing_wait)");
2488 if (report.closing_indeterminate_ != 0) {
2489 report.notes_.push_back(
2490 std::to_string(report.closing_indeterminate_) +
2491 " closing instruction(s) have an unknown outcome; read the broker's order list");
2493 if (report.closing_unavailable_ != 0) {
2494 report.notes_.push_back(
2495 "local machinery could not send " + std::to_string(report.closing_unavailable_) +
2496 " closing instruction(s); no broker was asked and the position is still held");
2498 if (report.stop_handler_refused_ != 0) {
2499 report.notes_.push_back(
2500 "the strategy's on_stop asked for " + std::to_string(report.stop_handler_refused_) +
2501 " instruction(s) during the stop; none were sent, because an operator's stop is not a"
2502 " moment for a strategy to trade");
2504 if (report.closing_contended_ != 0) {
2505 report.notes_.push_back(
2506 report.closing_source_unverified_ ?
2507 "no closing order was sent for " + std::to_string(report.closing_contended_) +
2508 " position(s), because an order this loop cannot see could close them too; they are"
2509 " still held and the withdrawals above are all that went" :
2510 std::to_string(report.closing_contended_) +
2511 " position(s) were closed only for the part no working order was already selling or"
2512 " buying; if those withdrawals succeed, part of the position is still held");
2514 if (report.closing_unsizable_ != 0) {
2523 report.notes_.push_back(
2524 "no closing order could be sized for " + std::to_string(report.closing_unsizable_) +
2525 " position(s): the holdings quantities would not add up or fit, or could not be combined"
2526 " with what is already working on the closing side; nothing was sent for them and they are"
2529 if (report.closing_source_unverified_) {
2530 report.notes_.push_back(
2531 std::string(
"the stop could not verify what was outstanding: ") + unverified_source_reason() +
2532 "; orders or positions a previous process left may remain");
2534 if (report.closing_unresolved_ != 0) {
2535 report.notes_.push_back(
2536 std::to_string(report.closing_unresolved_) +
2537 " closing cancellation(s) named an order the portfolio does not hold; read the broker's"
2552 void hold(std::optional<strategy::HaltReason> gate) {
2553 if (gate && !owns(*gate)) {
2556 if (held_ == gate) {
2560 runner_->guard().resume(*held_);
2563 runner_->guard().halt(*gate);
2570 std::optional<std::pair<unsigned, std::string>> listing_missing(
2571 std::chrono::system_clock::time_point at
2573 if constexpr (!source_provides_working_orders<Source>::value) {
2575 return std::make_pair(1u, std::string(
2576 "this runner keeps the orders it sends until a working-order listing or holdings account for "
2577 "them, and the source publishes no listing: placing holds (declare provides_working_orders and "
2581 if (!schedule_.working_orders_) {
2582 return std::make_pair(2u, std::string(
2583 "no working-order cadence: this runner keeps the orders it sends until a working-order "
2584 "listing or holdings account for them, so placing holds until PollSchedule::working_orders_ "
2588 if (maximum_portfolio_age_ && working_orders_at_ &&
2589 at - *working_orders_at_ > *maximum_portfolio_age_) {
2590 return std::make_pair(4u, std::string(
2591 "working-order listing older than the portfolio age policy: poll working orders "
2592 "(PollSchedule::working_orders_) more often than that age, by at least a tick; placing "
2593 "holds until it is read"
2596 return std::nullopt;
2603 std::optional<Obligation> owed_longer_than_limit(std::chrono::system_clock::time_point at)
const {
2604 std::optional<Obligation> oldest;
2605 if (owed_order_limit_ <= std::chrono::nanoseconds::zero()) {
2608 for (
auto& owed : runner_->obligations()) {
2612 if (at - owed.sent_at_ > owed_order_limit_ && (!oldest || owed.sent_at_ < oldest->sent_at_)) {
2613 oldest = std::move(owed);
2620 bool owed_past_limit(std::chrono::system_clock::time_point now)
const {
2621 return owed_longer_than_limit(now).has_value();
2629 std::optional<Obligation> owed_too_long()
const {
2630 if (!order_list_read_ || !order_list_at_) {
2631 return std::nullopt;
2633 return owed_longer_than_limit(*order_list_at_);
2637 static std::string owed_stop_note(
const Obligation& owed) {
2638 return "stopped placing: the " + std::string(owed.side_ == OrderSide::BUY ?
"buy" :
"sell") +
" of " +
2639 owed.quantity_.str() +
" " + owed.instrument_.symbol_ +
" sent " +
format_iso8601_utc(owed.sent_at_) +
2640 " (journal id " + owed.journal_id_ +
") is still owed, and " +
2642 ". Nothing was cancelled or closed. Read the broker's order list and holdings for it, then account for "
2643 "it with Runner::release_obligations; until then every start stops here again";
2651 bool closing_should_wait(
StopIntent intent, LoopReport& report) {
2652 if (!
stop_intent_closes(intent) || closing_wait_ <= std::chrono::nanoseconds::zero()) {
2657 if constexpr (!source_provides_working_orders<Source>::value) {
2665 const auto monotonic = clock_.monotonic_now();
2666 report.closing_waited_ = std::chrono::duration_cast<std::chrono::nanoseconds>(monotonic - closing_->began_);
2668 runner_->refresh_unconfirmed();
2669 closing_->reads_wanted_ = closing_needs_evidence(clock_.now());
2670 if (!closing_->reads_wanted_) {
2673 if (monotonic >= closing_->deadline_) {
2674 report.closing_wait_expired_ =
true;
2675 closing_->reads_wanted_ =
false;
2683 bool closing_needs_evidence(std::chrono::system_clock::time_point at)
const {
2684 if (!closing_source_verified(at) || runner_->obligations_awaiting_account() != 0) {
2689 const auto& portfolio = runner_->context().portfolio();
2690 for (
const auto& position : portfolio.positions_) {
2691 const auto net = portfolio.net_quantity_of(position.instrument_);
2692 if ((!net || net->coefficient() != 0) && runner_->ledger().requires_reconcile(position.instrument_) &&
2704 bool closing_read_due(
PollKind kind)
const {
2705 if (!closing_ || !closing_->reads_wanted_) {
2708 return !asked_within_floor(kind);
2712 bool asked_within_floor(
PollKind kind)
const {
2714 return asked && clock_.monotonic_now() - *asked < closing_reread_;
2720 std::chrono::nanoseconds closing_until_next()
const {
2721 auto wait = schedule_.tick_;
2722 const auto wall = clock_.now();
2724 const auto& due = next_due_[
static_cast<int>(kind)];
2728 wait = std::min(wait, std::chrono::duration_cast<std::chrono::nanoseconds>(*due - wall));
2730 const auto monotonic = clock_.monotonic_now();
2731 const auto until = [&](std::chrono::steady_clock::time_point when) {
2732 return std::chrono::duration_cast<std::chrono::nanoseconds>(when - monotonic);
2734 wait = std::min(wait, until(closing_->deadline_));
2735 for (
const auto* asked : {&working_asked_at_, &holdings_asked_at_}) {
2736 wait = std::min(wait, *asked ? until(**asked + closing_reread_) : std::chrono::nanoseconds::zero());
2738 return wait > std::chrono::nanoseconds::zero() ? wait : std::chrono::nanoseconds::zero();
2750 bool closing_source_verified(std::chrono::system_clock::time_point at)
const {
2751 if (!order_list_read_) {
2758 if (!working_orders_at_) {
2764 if (runner_->placements_at_broker() != placements_at_working_read_) {
2767 if (maximum_portfolio_age_ && at - *working_orders_at_ > *maximum_portfolio_age_) {
2770 const auto as_of = runner_->context().portfolio().as_of_;
2771 if (as_of == std::chrono::system_clock::time_point{}) {
2776 if (runner_->context().portfolio().listing_newer_than_holdings()) {
2779 return !maximum_portfolio_age_ || at - as_of <= *maximum_portfolio_age_;
2787 void note_working_orders_read(
2788 std::chrono::system_clock::time_point at, std::size_t placements_before_poll
2790 if constexpr (source_provides_working_orders<Source>::value) {
2791 working_orders_at_ = at;
2792 order_list_at_ = at;
2796 placements_at_working_read_ = placements_before_poll;
2799 (void)placements_before_poll;
2804 std::string unverified_source_reason()
const {
2805 if constexpr (!source_provides_working_orders<Source>::value) {
2806 return "this source does not publish the broker's outstanding orders";
2808 if (!order_list_read_ || !working_orders_at_) {
2809 return "the broker's order list has not been read";
2811 if (runner_->placements_at_broker() != placements_at_working_read_) {
2812 return "an order was sent after the working orders were last read";
2814 if (runner_->context().portfolio().as_of_ == std::chrono::system_clock::time_point{}) {
2815 return "no holdings snapshot has arrived";
2817 if (runner_->context().portfolio().listing_newer_than_holdings()) {
2818 return "the holdings were read before the working orders, so a fill between them is in neither";
2820 if (maximum_portfolio_age_ &&
2821 clock_.now() - *working_orders_at_ > *maximum_portfolio_age_) {
2822 return "the working orders were read longer ago than the configured maximum";
2824 return "the holdings snapshot is older than the configured maximum";
2828 bool is_due(
PollKind kind, std::chrono::system_clock::time_point at)
const {
2832 if (closing_ && closing_->reads_wanted_ &&
2834 return closing_read_due(kind);
2836 const auto interval = schedule_.interval_for(kind);
2840 const auto& due = next_due_[
static_cast<int>(kind)];
2841 return !due || at >= *due;
2845 void schedule_next(
PollKind kind, std::chrono::system_clock::time_point at) {
2846 const auto interval = schedule_.interval_for(kind);
2850 next_due_[
static_cast<int>(kind)] =
2851 at + std::chrono::duration_cast<std::chrono::system_clock::duration>(*interval);
2859 std::chrono::nanoseconds until_next(std::chrono::system_clock::time_point at)
const {
2860 auto wait = schedule_.tick_;
2862 const auto& due = next_due_[
static_cast<int>(kind)];
2866 const auto remaining =
2867 std::chrono::duration_cast<std::chrono::nanoseconds>(*due - at);
2868 if (remaining < wait) {
2872 return wait > std::chrono::nanoseconds::zero() ? wait : std::chrono::nanoseconds::zero();
2877 PollSchedule schedule_;
2879 FileStopChannel control_;
2880 FailurePolicy failure_policy_;
2889 bool phase_ever_known_{
false};
2890 std::optional<std::chrono::nanoseconds> maximum_portfolio_age_;
2893 std::chrono::nanoseconds closing_wait_{std::chrono::seconds(30)};
2894 std::chrono::nanoseconds owed_order_limit_{std::chrono::minutes(10)};
2895 static constexpr std::chrono::seconds closing_reread_{5};
2898 struct ClosingWait {
2899 std::chrono::steady_clock::time_point began_{};
2900 std::chrono::steady_clock::time_point deadline_{};
2901 bool reads_wanted_{
false};
2903 std::optional<ClosingWait> closing_;
2906 std::optional<std::chrono::steady_clock::time_point> working_asked_at_;
2907 std::optional<std::chrono::steady_clock::time_point> holdings_asked_at_;
2909 bool said_no_band_age_policy_{
false};
2911 bool allow_unbounded_portfolio_age_{
false};
2913 bool allow_absent_portfolio_{
false};
2915 bool said_no_age_policy_{
false};
2917 unsigned said_listing_{0};
2919 std::size_t warmup_discarded_at_start_{0};
2921 std::size_t kept_dropped_at_start_{0};
2922 std::optional<std::chrono::nanoseconds> maximum_buying_power_age_;
2923 std::optional<std::chrono::system_clock::time_point> next_due_[
poll_kind_count];
2925 bool order_list_read_{
false};
2945 std::optional<std::chrono::system_clock::time_point> working_orders_at_;
2949 std::optional<std::chrono::system_clock::time_point> order_list_at_;
2951 std::size_t placements_at_working_read_{0};
2954 std::optional<strategy::HaltReason> held_;
2955 IterationReport last_;