4#include "./portfolio.hpp"
7#include <algotradelib/core>
149 return "local_failure";
153 return "order_list_unread";
155 return "portfolio_absent";
157 return "portfolio_stale";
159 return "buying_power_unread";
161 return "poll_failing";
168 "all_halt_reasons must list every HaltReason"
219 return "trading_halted";
221 return "instrument_not_permitted";
223 return "phase_not_permitted";
225 return "order_notional_too_large";
227 return "position_limit_exceeded";
229 return "gross_exposure_exceeded";
231 return "order_rate_exceeded";
233 return "value_not_known";
235 return "quantity_not_sellable";
237 return "sellable_basis_unestablished";
239 return "insufficient_buying_power";
355 std::chrono::system_clock::time_point
now_{};
377namespace risk_detail {
385inline std::optional<Decimal64> decimal_magnitude(
const Decimal64& value) {
431template <
typename Broker>
434 const std::vector<InstrumentBasis>& basis,
437 const std::optional<Decimal64>& candidate_price,
444 std::vector<InstrumentId> instruments;
445 const auto note = [&instruments](
const InstrumentId& id) {
446 for (
const auto& seen : instruments) {
451 instruments.push_back(
id);
453 for (
const auto& position : portfolio.
positions_) {
456 if (position.quantity_.coefficient() == 0) {
459 note(position.instrument_);
461 for (
const auto& working : portfolio.
working_) {
463 note(working.order_.instrument_);
466 if (candidate !=
nullptr) {
471 for (
const auto&
id : instruments) {
473 for (
const auto& entry : basis) {
474 if (!(entry.instrument_ ==
id)) {
477 if (resolved !=
nullptr) {
484 if (resolved ==
nullptr) {
487 if (!(resolved->
currency_ == currency)) {
498 for (
const auto& position : portfolio.
positions_) {
499 if (!(position.instrument_ ==
id)) {
502 const auto sum =
decimal_add(held, position.quantity_);
510 auto price = resolved->
price_;
511 for (
const auto& working : portfolio.
working_) {
512 if (!(working.order_.instrument_ ==
id) || !
order_is_open(working.order_.status_)) {
516 working.order_.quantity_, working.order_.execution_.filled_quantity_
527 working.order_.side_ == OrderSide::BUY ? buys : sells, *remaining
532 (working.order_.side_ == OrderSide::BUY ? buys : sells) = *sum;
533 if (working.order_.price_) {
538 price = risk_detail::decimal_max(price, *working.order_.price_);
542 if (candidate !=
nullptr && candidate->
instrument_ ==
id) {
545 const auto stated = candidate->
price_ ? candidate->
price_ : candidate_price;
552 price = risk_detail::decimal_max(price, *stated);
554 bool adds_quantity =
false;
555 switch (contribution) {
557 adds_quantity =
true;
564 candidate->
side_ == OrderSide::BUY ? buys : sells, candidate->
quantity_
569 (candidate->
side_ == OrderSide::BUY ? buys : sells) = *sum;
575 if (!if_bought || !if_sold) {
578 const auto long_side = risk_detail::decimal_magnitude(*if_bought);
579 const auto short_side = risk_detail::decimal_magnitude(*if_sold);
580 if (!long_side || !short_side) {
583 const auto projected = risk_detail::decimal_max(*long_side, *short_side);
618template <
typename Broker>
629 const auto held = *net;
631 if (held.coefficient() == 0) {
640 const auto closing =
decimal_less(zero, held) ? OrderSide::SELL : OrderSide::BUY;
641 if (order.
side_ != closing) {
646 for (
const auto& working : portfolio.
working_) {
647 if (!(working.order_.instrument_ == order.
instrument_) || working.order_.side_ != closing) {
654 working.order_.quantity_, working.order_.execution_.filled_quantity_
671 const auto magnitude = risk_detail::decimal_magnitude(held);
726 if (!limits_.permitted_phases_) {
729 for (
const auto allowed : *limits_.permitted_phases_) {
730 if (allowed == phase) {
758 held_[index_of(reason)] =
true;
759 details_[index_of(reason)] = std::move(detail);
776 held_[index_of(reason)] =
false;
777 details_[index_of(reason)].clear();
785 for (
const auto held : held_) {
815 const auto reason =
static_cast<HaltReason>(index);
828 const std::string& hold_detail(
HaltReason reason)
const noexcept {
829 return details_[index_of(reason)];
836 std::vector<HaltReason>
holds()
const {
837 std::vector<HaltReason> held;
840 held.push_back(
static_cast<HaltReason>(index));
854 void note_sent(std::chrono::system_clock::time_point when) {
855 sent_.push_back(when);
867 std::size_t
total_sent() const noexcept {
return total_sent_; }
874 std::size_t
recent_orders(std::chrono::system_clock::time_point now)
const {
875 std::size_t count = 0;
876 for (
const auto when : sent_) {
877 if (now - when < limits_.rate_window_) {
889 while (!sent_.empty() && sent_.front() < cutoff) {
904 template <
typename Broker>
905 std::optional<GuardRejection>
check(
926 template <
typename Broker>
937 return GuardDecision{std::optional<GuardRejection>(rejection),
false};
939 const auto refuse_with =
945 rejection.
limit_ = std::move(limit);
947 return GuardDecision{std::optional<GuardRejection>(rejection),
false};
952 bool admitted_outside_phase =
false;
956 decision.rejection_->halt_reason_ = *holding;
960 if (limits_.permitted_universe_) {
961 bool permitted =
false;
962 for (
const auto& allowed : *limits_.permitted_universe_) {
973 if (limits_.permitted_phases_) {
974 bool permitted =
false;
975 for (
const auto phase : *limits_.permitted_phases_) {
976 if (phase == inputs.
phase_) {
987 if (!closing || !*closing) {
990 admitted_outside_phase =
true;
994 if (limits_.max_orders_per_window_ &&
998 Decimal64(
static_cast<std::int64_t
>(*limits_.max_orders_per_window_), 0),
1007 const bool needs_value =
1008 limits_.max_order_notional_.has_value() || limits_.max_gross_exposure_.has_value() ||
1009 (limits_.enforce_buying_power_ && order.
side_ == OrderSide::BUY);
1010 if (needs_value && !price) {
1014 std::optional<Decimal64> value;
1017 if (needs_value && !value) {
1022 if (limits_.max_order_notional_ && value &&
1029 if (limits_.enforce_sellable_quantity_ && order.
side_ == OrderSide::SELL) {
1032 const auto& sellable = reading.
quantity_;
1039 if (!reading.basis_.is_enforceable()) {
1055 if (limits_.enforce_buying_power_ && order.
side_ == OrderSide::BUY) {
1073 if (limits_.max_position_quantity_) {
1081 const auto held = *net;
1089 const auto signed_quantity = order.
side_ == OrderSide::BUY ?
1090 std::optional<Decimal64>(*committed) :
1092 if (!signed_quantity) {
1095 const auto resulting =
decimal_add(held, *signed_quantity);
1100 const auto magnitude =
decimal_less(*resulting, zero) ?
1102 std::optional<Decimal64>(*resulting);
1106 if (
decimal_less(*limits_.max_position_quantity_, *magnitude)) {
1113 if (limits_.max_gross_exposure_) {
1126 if (
decimal_less(*limits_.max_gross_exposure_, *resulting)) {
1150 static constexpr std::size_t index_of(
HaltReason reason)
noexcept {
1151 return static_cast<std::size_t
>(reason);
1156 std::array<bool, halt_reason_count> held_{};
1158 std::array<std::string, halt_reason_count> details_;
1160 std::deque<std::chrono::system_clock::time_point> sent_;
1161 std::size_t total_sent_{0};
A three-letter currency code, such as KRW or USD (ISO 4217 style).
An exact signed decimal: an int64 coefficient times 10^-scale.
Decimal64()=default
Constructs zero.
static Decimal64 parse(std::string_view input)
Parses a decimal string exactly (no binary floating point).
std::size_t recent_orders(std::chrono::system_clock::time_point now) const
Returns how many orders were sent within RiskLimits::rate_window_ before an instant.
void note_sent(std::chrono::system_clock::time_point when)
Records that a request was sent, for the rate limit.
void halt(HaltReason reason)
Takes a hold, with no description.
const RiskLimits & limits() const noexcept
Returns the limits.
bool halted(HaltReason reason) const noexcept
Tests whether one hold is in force.
void resume(HaltReason reason) noexcept
Releases a hold; releasing one that is not held does nothing.
RiskGuard(RiskLimits limits={})
Constructs a guard.
void resume() noexcept
Releases the operator's hold; other holds, if any, still stop trading.
std::size_t total_sent() const noexcept
Returns how many orders have been recorded with note_sent over the guard's life.
void halt(HaltReason reason, std::string detail)
Takes a hold, with a description.
bool permits_phase(MarketPhase phase) const noexcept
Tests whether a phase permits opening or increasing a position.
std::vector< HaltReason > holds() const
Returns every hold in force.
std::optional< GuardRejection > check(const PlannedOrder &order, const Portfolio< Broker > &portfolio, const GuardInputs &inputs) const
Checks one planned order against the limits and holds.
std::optional< HaltReason > blocking_reason_for(InstructionClass klass) const noexcept
Returns the hold that stops an instruction class, the most fundamental first.
void halt() noexcept
Takes the operator's hold (HaltReason::OPERATOR): no order or cancellation is sent until resume().
void forget_before(std::chrono::system_clock::time_point cutoff)
Forgets sends recorded before an instant, to bound memory.
bool halted() const noexcept
Tests whether any hold is in force.
std::optional< HaltReason > blocking_reason() const noexcept
Returns the hold that stops placements, the most fundamental first.
GuardDecision evaluate(const PlannedOrder &order, const Portfolio< Broker > &portfolio, const GuardInputs &inputs) const
Checks one planned order, and reports whether only the closing exception admitted it.
The events a runner delivers to a strategy's handlers, and the market phase.
What a strategy sees and emits: events, intents, the portfolio, reconciliation and risk limits.
GuardReason
Which check refused an order.
@ SELLABLE_BASIS_UNESTABLISHED
A sellable quantity is present, but what it covers is not established.
@ ORDER_NOTIONAL_TOO_LARGE
The order's value exceeds RiskLimits::max_order_notional_.
@ ORDER_RATE_EXCEEDED
RiskLimits::max_orders_per_window_ orders were already sent in the window.
@ QUANTITY_NOT_SELLABLE
The sell exceeds the broker's sellable quantity, or the broker gave none.
@ POSITION_LIMIT_EXCEEDED
The position could exceed RiskLimits::max_position_quantity_.
@ VALUE_NOT_KNOWN
A limit needed a value that is not known.
@ INSUFFICIENT_BUYING_POWER
The buy costs more than the broker's buying power, or the broker gave none.
@ INSTRUMENT_NOT_PERMITTED
The instrument is not in RiskLimits::permitted_universe_.
@ PHASE_NOT_PERMITTED
The market phase is not in RiskLimits::permitted_phases_.
@ GROSS_EXPOSURE_EXCEEDED
The gross exposure could exceed RiskLimits::max_gross_exposure_.
@ TRADING_HALTED
A hold is in force; GuardRejection::halt_reason_ names it.
MarketPhase
Where the clock stands relative to the trading session.
@ UNKNOWN
The calendar does not cover this date; nothing is known about it.
@ OPEN
Inside the session.
constexpr std::size_t halt_reason_count
The number of HaltReason values.
constexpr bool halt_blocks(HaltReason reason, InstructionClass klass) noexcept
Tests whether a hold stops an instruction of a given class.
std::optional< Decimal64 > projected_gross_exposure(const Portfolio< Broker > &portfolio, const std::vector< InstrumentBasis > &basis, const Currency ¤cy, const PlannedOrder *candidate, const std::optional< Decimal64 > &candidate_price, CandidateContribution contribution)
Computes the gross exposure in one currency if every outstanding order (and a candidate) filled.
CandidateContribution
What a candidate order adds to projected_gross_exposure.
@ QUANTITY_AND_PRICE
The candidate's quantity and price: the exposure if the order is sent.
@ PRICE_ONLY
The candidate's price only: the exposure without the order, valued at the same prices.
bool order_is_open(OrderStatus status)
Tests whether an order in a given status may still trade.
std::optional< bool > closes_toward_flat(const Portfolio< Broker > &portfolio, const PlannedOrder &order)
Tests whether an order only reduces a held position toward flat, without passing through it.
InstructionClass
What an instruction can do to a position, which decides which holds stop it.
@ OPENING
A placement, which can open or enlarge a position. Every hold stops it.
@ WITHDRAWAL
A cancellation of an order the caller named.
constexpr std::string_view guard_reason_name(GuardReason reason) noexcept
Returns a lowercase name for a refusal reason, such as "order_rate_exceeded", for logs.
constexpr std::string_view halt_reason_name(HaltReason reason) noexcept
Returns a lowercase name for a hold, such as "portfolio_stale", for logs.
constexpr std::array< HaltReason, 8 > all_halt_reasons
Every HaltReason, in declaration order.
HaltReason
Who or what is holding trading stopped.
@ BUYING_POWER_UNREAD
A buying-power limit is enforced, but there is no current reading to check it against.
@ LOCAL_FAILURE
Local machinery failed, such as the order journal not being writable; nothing is sent.
@ PORTFOLIO_ABSENT
No holdings snapshot has been read yet, so what the account holds is unknown.
@ ORDER_LIST_UNREAD
The broker's order list has not been read since this process started.
@ OPERATOR
A person stopped trading with RiskGuard::halt(); only RiskGuard::resume() clears it.
@ PORTFOLIO_STALE
The holdings snapshot is older than the configured maximum age.
@ POLL_FAILING
A poll kept failing and the loop gave up, as opposed to STOPPING on request.
@ STOPPING
A stop was requested and the run is shutting down.
The library: broker-neutral types (core), and the modules below.
constexpr bool lists_every_value(const std::array< Enum, N > &values, IsEnumerator is_enumerator) noexcept
Tests whether a list holds every value of an enum exactly once, in any order.
std::optional< Decimal64 > decimal_add(const Decimal64 &lhs, const Decimal64 &rhs)
Adds two decimals exactly.
std::optional< Decimal64 > decimal_subtract(const Decimal64 &lhs, const Decimal64 &rhs)
Subtracts two decimals exactly.
bool decimal_less(const Decimal64 &lhs, const Decimal64 &rhs)
Compares two decimals numerically, whatever their scales.
std::optional< Decimal64 > decimal_multiply(const Decimal64 &lhs, const Decimal64 &rhs)
Multiplies two decimals exactly; the scales add.
Reconciliation: turning a strategy's intents and targets into the orders and cancellations to send.
Identifies an instrument: its country, venue and symbol.
What RiskGuard::evaluate concluded about one order.
bool admitted_outside_phase_
Whether the order was permitted only because it closes toward flat in a phase the limits do not permi...
std::optional< GuardRejection > rejection_
Why the order was refused, or std::nullopt if it was permitted.
One order a guard refused, with the limit and the value that exceeded it.
InstrumentId instrument_
The instrument of the refused order.
std::string tag_
The tag of the intent the order came from.
GuardReason reason_
Which check refused the order.
std::optional< Decimal64 > limit_
The limit that was exceeded, where the reason has one.
std::optional< HaltReason > halt_reason_
The hold that stopped the order, when reason_ is TRADING_HALTED; otherwise empty.
std::optional< Decimal64 > attempted_
The value the order would have reached, where the reason has one.
An instrument's currency and the price to value it at, for the exposure ceiling.
InstrumentId instrument_
The instrument.
Decimal64 price_
The mark to value it at.
Currency currency_
The currency it trades in.
One order the runner is to place.
std::string tag_
The tag of the intent or target the order came from.
InstrumentId instrument_
The instrument to trade.
std::optional< Decimal64 > price_
The limit price, after clamping and rounding; empty for a market order.
Decimal64 quantity_
The quantity, after rounding onto the lot.
std::optional< Decimal64 > quantity_
The figure, or std::nullopt if no row of the instrument reported one.
What the account holds and which orders are still working, as a strategy sees it.
std::vector< WorkingOrder< Broker > > working_
Orders still outstanding at the broker.
std::optional< Decimal64 > buying_power_in(const Currency ¤cy) const
Returns the buying power in a currency.
std::optional< Decimal64 > net_quantity_of(const InstrumentId &instrument) const
Returns the net quantity held of an instrument, summed over every row.
Decimal64 working_quantity(const InstrumentId &instrument, OrderSide side) const
Returns the quantity still to fill on open orders for an instrument and side.
SellableReading sellable_reading_for(const InstrumentId &instrument) const
Returns an instrument's sellable quantity and its basis, taken from the same row.
std::vector< HoldingPosition > positions_
Every position row the broker reported.
The limits the operator sets for RiskGuard.
bool enforce_sellable_quantity_
Whether to refuse a sell larger than the broker's sellable quantity.
bool enforce_buying_power_
Whether to refuse a buy that costs more than the broker's buying power.
std::optional< std::vector< InstrumentId > > permitted_universe_
The instruments that may be traded; absent means any.
std::optional< Decimal64 > max_gross_exposure_
The largest gross exposure the portfolio may reach, in the order's currency.
std::optional< Decimal64 > max_position_quantity_
The largest absolute position, in shares, any instrument may reach.
std::optional< std::size_t > max_orders_per_window_
The most orders that may be sent within rate_window_.
std::optional< Decimal64 > max_order_notional_
The largest value one order may have, in its own currency.
std::chrono::seconds rate_window_
The window max_orders_per_window_ counts over.
std::optional< std::vector< MarketPhase > > permitted_phases_
The market phases in which a position may be opened or increased; absent means any.