Runs a runner for a live session: polls a source on a schedule, gates placements, derives the market phase, and honours stop requests.
More...
|
| | PollingLoop (Runner &runner, Source &source, PollSchedule schedule={}, Clock clock={}) |
| | Constructs a loop.
|
| const Clock & | clock () const noexcept |
| | Returns the clock.
|
| Clock & | clock () noexcept |
| | Returns the clock, for a test to advance.
|
| const PollSchedule & | schedule () const noexcept |
| | Returns the schedule.
|
| void | set_maximum_portfolio_age (std::chrono::nanoseconds age) |
| | Sets how old the holdings (and, for a runner that sends to a broker, the working-order listing) may be before placements are held (PORTFOLIO_STALE).
|
| void | set_closing_wait (std::chrono::nanoseconds limit) |
| | Sets how long a WITHDRAW or FLATTEN may wait before its closing pass (30 seconds by default).
|
| void | set_owed_order_limit (std::chrono::nanoseconds limit) |
| | Sets how long a serialized runner's order may stay owed with nobody able to account for it before the loop stops placing (ten minutes by default).
|
| void | allow_unbounded_portfolio_age () |
| | Accepts holdings of any age, instead of stating a maximum.
|
| void | set_maximum_price_limits_age (std::optional< std::chrono::nanoseconds > age) |
| | Sets how old a price band may be and still be applied; passed to the runner (see Runner::set_maximum_price_limits_age).
|
| void | allow_unbounded_price_limits_age () |
| | Applies price bands of any age (see Runner::allow_unbounded_price_limits_age).
|
| void | allow_absent_portfolio () |
| | Lets the run place orders before holdings have ever been read (for a broker without a holdings inquiry, or a run deliberately without that poll).
|
| void | set_session_window (strategy::SessionWindow window) |
| | Sets the session window the market phase is derived from (KRX by default, as in a backtest).
|
| const strategy::SessionWindow & | session_window () const noexcept |
| | Returns the session window.
|
| void | set_failure_policy (FailurePolicy policy) |
| | Sets how many consecutive failures of one poll kind stop the loop.
|
| void | set_control_file (FileStopChannel channel) |
| | Sets the control file an operator writes a stop intent into.
|
| LoopReport | run (StopSource &stop, std::size_t max_iterations=0) |
| | Runs until a stop is requested, a poll kind exhausts its failure budget, or max_iterations iterations have run.
|
| const IterationReport & | last () const noexcept |
| | Returns what the last iteration did.
|
| void | set_maximum_buying_power_age (std::chrono::nanoseconds age) |
| | Sets how old buying-power readings may be.
|
template<typename
Runner, typename Source, typename Clock = SteadyClock>
class algotradelib::runner::PollingLoop< Runner, Source, Clock >
Runs a runner for a live session: polls a source on a schedule, gates placements, derives the market phase, and honours stop requests.
Each iteration: check for a stop (a StopSource or the control file); derive and install the market phase from the SessionWindow and the polled calendar; make sure the broker's order list has been read once (until it has, placements are held); poll whatever is due, in all_poll_kinds order, deriving the phase again before each poll; announce a phase change; retry retained targets.
Placements are held through holds on the runner's strategy::RiskGuard: ORDER_LIST_UNREAD, PORTFOLIO_ABSENT, PORTFOLIO_STALE, BUYING_POWER_UNREAD and POLL_FAILING are data holds, under which named cancellations still go (see strategy::halt_blocks), so a stop can withdraw known orders even when the order list could not be read. STOPPING, taken while a stop is honoured and left in place after it, blocks cancellations too; the loop lifts it only for its own closing instructions. The loop never takes or releases OPERATOR or LOCAL_FAILURE, which also block cancellations.
State a holdings age policy (set_maximum_portfolio_age or allow_unbounded_portfolio_age): without one, every placement is held as PORTFOLIO_STALE.
A phase the loop cannot establish is reported (final_phase_ and a note), not held. A stop is honoured with a closing pass (see StopIntent); LoopReport::closing_complete() says whether everything asked for was accepted, which is not the same as being flat. The closing orders are allowed in any phase but still face every other risk check (sellable quantity, ceilings).
- Note
- If you write a source: install the trading calendar during PollKind::CALENDAR only, and do not feed the runner from that poll.
- Template Parameters
-
Definition at line 976 of file loop.hpp.
template<typename
Runner, typename Source, typename Clock = SteadyClock>
Sets how long a WITHDRAW or FLATTEN may wait before its closing pass (30 seconds by default).
A closing pass is built from the holdings and the order list in hand. Right after this runner sent an order they do not show it yet, and a pass built then withdraws what it can see and closes nothing (LoopReport::closing_source_unverified_, closing_unconfirmed_). So a stop that closes first lets the loop read both again – working orders, then holdings, no sooner than five seconds after each was last asked, by anything – while it holds STOPPING, and runs its pass as soon as:
- the picture is verified;
- no order of this runner awaits an account a read could give (Runner::obligations_awaiting_account);
- and no instrument that is held is marked in the indeterminacy ledger.
The iteration that takes the stop up reads as any other; with none of those missing its pass runs in that iteration. Only the wait's further reads are paced.
or when this limit has passed, measured on the clock's monotonic reading. The pass then runs as it always did and the report says the wait ran out (closing_wait_expired_). The limit bounds the wait, not the stop: a call already in flight, and the pass itself, take what they take.
It does not wait for a QUIESCE; for an order of this runner that is simply resting (the pass withdraws it, and a holding behind it is then closed by asking again); for an order held for an operator, or its mark; under an OPERATOR or LOCAL_FAILURE hold, when the pass can send nothing; or with a source that lists no working orders. It cannot tell every case no read will change: a mark made within the last minute or two, a mark nothing sweeps (a source with no journal), or an order whose reply was lost wait the whole limit. Zero or a negative limit disables the wait.
A supervisor's grace period has to cover this limit on top of the calls in flight and the pass.
- Parameters
-
Definition at line 1118 of file loop.hpp.
template<typename
Runner, typename Source, typename Clock = SteadyClock>
Sets how long a serialized runner's order may stay owed with nobody able to account for it before the loop stops placing (ten minutes by default).
Such an order is one whose outcome is unknown and that no sweep has named (ObligationState::UNNAMED), or one held for an operator (ObligationState::OPERATOR). Its instrument is withheld until Runner::release_obligations, and a strategy trading other instruments would go on beside it: a pair with one leg, a rotation holding a name it meant to sell. So the loop acts in two steps.
The hold. Once the limit has passed on the loop's clock, the loop holds strategy::HaltReason::STOPPING: nothing a strategy asks for goes out, a withdrawal included. It is taken where the loop checks its holds, before every poll and once more after an iteration's polls, and is not a deadline enforced at each send: everything up to the next check runs on the last one. A poll already begun when the limit passes (a slow read that then feeds a handler) is not interrupted, and neither is the end of an iteration, where the phase handler and then the retry of kept targets both follow the last check. So an order can still go out after the limit, until the loop's next check. Every order-list read that begins after a check that found the limit passed runs under the hold, its handlers included, and the hold stays through a failed read, a start that cannot read the list, and a run abandoned for failing polls.
The stop. Once an order-list read has succeeded in an iteration that began after the limit and has still left the order so, the loop requests StopIntent::QUIESCE on the run's StopSource: nothing more is placed, nothing is cancelled or closed, and the order is still owed. LoopReport::owed_stop_ names it. If that read's sweep names the order instead, the hold is lifted and the run goes on. So the stop comes later than the limit, with the first successful order-list read in an iteration that began after it: a read in an iteration that began before the limit does not decide, and how long the next takes to come depends on the list's cadence, on the time the loop's own calls take, and on reads failing. The request stays on the StopSource, as a control-file word does, and stops any other loop sharing it. A restart restores the order and stops again at its first order-list read, having placed nothing, until a sweep names the order or it is released.
To trade again in this process, after reading the broker's order list and holdings for the order: Runner::release_obligations for its instrument, ledger().resolve_after_reading_orders if the ledger still holds the instrument (an unnamed order past the confirmation timeout and one held for an operator each mark it, and a release leaves the ledger alone), StopSource::clear, and guard().resume(strategy::HaltReason::STOPPING); the next order there waits for a holdings read received after the release.
An order the broker named never causes this, however old; it stays owed until holdings or its own history account for it, which is not guaranteed to happen (Runner::obligations). An order whose reply was lost and that the broker did accept causes it too, if no list shows it within the limit: the stop says nobody could account for the order, not that it failed. The limit has to exceed the time a sweep needs to name such an order: its matching window (two minutes by default) and whatever a listing lags. The ages are on the loop's wall clock against the journal's stamps; a clock set forward shortens them and one set back lengthens them.
- Parameters
-
| limit | The longest such an order may be owed; zero or negative never holds or stops for one. |
Definition at line 1170 of file loop.hpp.