algotradelib 0.0.0.9000
C++ header-only library for algorithmic trading
Loading...
Searching...
No Matches
risk.hpp
1#pragma once
2
3#include "./event.hpp"
4#include "./portfolio.hpp"
5#include "./reconcile.hpp"
6
7#include <algotradelib/core>
8
9#include <array>
10#include <chrono>
11#include <cstddef>
12#include <deque>
13#include <optional>
14#include <string>
15#include <string_view>
16#include <utility>
17#include <vector>
18
19namespace algotradelib {
20namespace strategy {
21
29enum class HaltReason {
31 OPERATOR,
39 LOCAL_FAILURE,
54 PORTFOLIO_ABSENT,
60 PORTFOLIO_STALE,
69};
70
82
84inline constexpr std::size_t halt_reason_count = all_halt_reasons.size();
85
102
114inline constexpr bool halt_blocks(HaltReason reason, InstructionClass klass) noexcept {
115 switch (klass) {
117 return true;
119 break;
120 }
121 switch (reason) {
125 return true;
126 // Guards against an opening order duplicating a predecessor's; a named withdrawal duplicates nothing.
128 // None of these says anything about a row the caller already named.
132 // A known resting order is still safe to withdraw when polling is abandoned.
134 return false;
135 }
136 return true;
137}
138
144inline constexpr std::string_view halt_reason_name(HaltReason reason) noexcept {
145 switch (reason) {
147 return "operator";
149 return "local_failure";
151 return "stopping";
153 return "order_list_unread";
155 return "portfolio_absent";
157 return "portfolio_stale";
159 return "buying_power_unread";
161 return "poll_failing";
162 }
163 return "";
164}
165
166static_assert(
167 lists_every_value(all_halt_reasons, [](HaltReason reason) { return !halt_reason_name(reason).empty(); }),
168 "all_halt_reasons must list every HaltReason"
169);
170
171/*
172 * Operator limits, checked before anything is sent. A guard refuses an order; it never resizes one.
173 */
174
210
216inline constexpr std::string_view guard_reason_name(GuardReason reason) noexcept {
217 switch (reason) {
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";
240 }
241 return "unknown";
242}
243
251 std::string tag_;
253 std::optional<HaltReason> halt_reason_;
255 std::optional<Decimal64> limit_;
257 std::optional<Decimal64> attempted_;
258};
259
270 std::optional<Decimal64> max_order_notional_;
277 std::optional<Decimal64> max_position_quantity_;
291 std::optional<Decimal64> max_gross_exposure_;
293 std::optional<std::size_t> max_orders_per_window_;
295 std::chrono::seconds rate_window_{60};
317 std::optional<std::vector<InstrumentId>> permitted_universe_;
331 std::optional<std::vector<MarketPhase>> permitted_phases_{
332 std::vector<MarketPhase>{MarketPhase::OPEN}
333 };
334};
335
351
355 std::chrono::system_clock::time_point now_{};
359 std::optional<Decimal64> reference_price_;
367 std::vector<InstrumentBasis> exposure_basis_;
374 std::optional<Currency> order_currency_;
375};
376
377namespace risk_detail {
378
380inline Decimal64 decimal_max(const Decimal64& lhs, const Decimal64& rhs) {
381 return decimal_less(lhs, rhs) ? rhs : lhs;
382}
383
385inline std::optional<Decimal64> decimal_magnitude(const Decimal64& value) {
386 const auto zero = Decimal64::parse("0");
387 if (!decimal_less(value, zero)) {
388 return value;
389 }
390 return decimal_subtract(zero, value);
391}
392
393} // namespace risk_detail
394
407
431template <typename Broker>
432std::optional<Decimal64> projected_gross_exposure(
433 const Portfolio<Broker>& portfolio,
434 const std::vector<InstrumentBasis>& basis,
435 const Currency& currency,
436 const PlannedOrder* candidate,
437 const std::optional<Decimal64>& candidate_price,
438 CandidateContribution contribution
439) {
440 const auto zero = Decimal64::parse("0");
441
442 // Every instrument the answer depends on: held, working, or about to be ordered. Collected first
443 // so each one is valued exactly once however many rows mention it.
444 std::vector<InstrumentId> instruments;
445 const auto note = [&instruments](const InstrumentId& id) {
446 for (const auto& seen : instruments) {
447 if (seen == id) {
448 return;
449 }
450 }
451 instruments.push_back(id);
452 };
453 for (const auto& position : portfolio.positions_) {
454 // A flat row (a closed position a broker or the replay ledger still lists) is not exposure and
455 // needs no basis; if it also has a working order, the loop below collects it.
456 if (position.quantity_.coefficient() == 0) {
457 continue;
458 }
459 note(position.instrument_);
460 }
461 for (const auto& working : portfolio.working_) {
462 if (order_is_open(working.order_.status_)) {
463 note(working.order_.instrument_);
464 }
465 }
466 if (candidate != nullptr) {
467 note(candidate->instrument_);
468 }
469
470 auto total = zero;
471 for (const auto& id : instruments) {
472 const InstrumentBasis* resolved = nullptr;
473 for (const auto& entry : basis) {
474 if (!(entry.instrument_ == id)) {
475 continue;
476 }
477 if (resolved != nullptr) {
478 // Two answers for one instrument. Picking either would be picking one of two currencies or
479 // two marks, and the caller resolved it twice for a reason this cannot see.
480 return std::nullopt;
481 }
482 resolved = &entry;
483 }
484 if (resolved == nullptr) {
485 return std::nullopt;
486 }
487 if (!(resolved->currency_ == currency)) {
488 continue;
489 }
490 // A mark must be positive: zero would omit the position, and a negative one (from bad market
491 // data) would offset real holdings.
492 if (!decimal_less(zero, resolved->price_)) {
493 return std::nullopt;
494 }
495
496 // Summed over every row: a holding can be split across lots.
497 auto held = zero;
498 for (const auto& position : portfolio.positions_) {
499 if (!(position.instrument_ == id)) {
500 continue;
501 }
502 const auto sum = decimal_add(held, position.quantity_);
503 if (!sum) {
504 return std::nullopt;
505 }
506 held = *sum;
507 }
508 auto buys = zero;
509 auto sells = zero;
510 auto price = resolved->price_;
511 for (const auto& working : portfolio.working_) {
512 if (!(working.order_.instrument_ == id) || !order_is_open(working.order_.status_)) {
513 continue;
514 }
515 const auto remaining = decimal_subtract(
516 working.order_.quantity_, working.order_.execution_.filled_quantity_
517 );
518 if (!remaining) {
519 return std::nullopt;
520 }
521 // A row filled beyond its quantity refuses (`working_quantity` skips it): a limit cannot infer
522 // from an inconsistent snapshot that nothing is outstanding.
523 if (decimal_less(*remaining, zero)) {
524 return std::nullopt;
525 }
526 const auto sum = decimal_add(
527 working.order_.side_ == OrderSide::BUY ? buys : sells, *remaining
528 );
529 if (!sum) {
530 return std::nullopt;
531 }
532 (working.order_.side_ == OrderSide::BUY ? buys : sells) = *sum;
533 if (working.order_.price_) {
534 // A non-positive price would silently lose to the mark in `max`; refuse instead.
535 if (!decimal_less(zero, *working.order_.price_)) {
536 return std::nullopt;
537 }
538 price = risk_detail::decimal_max(price, *working.order_.price_);
539 }
540 }
541
542 if (candidate != nullptr && candidate->instrument_ == id) {
543 // The candidate's price applies under both contributions, its quantity only under one, so the
544 // two projections differ only by the quantity.
545 const auto stated = candidate->price_ ? candidate->price_ : candidate_price;
546 if (stated) {
547 // Validated, not just offered to `max`: a negative price would vanish into the mark here and
548 // pass `max_order_notional_` as a negative value. Nothing upstream rules it out.
549 if (!decimal_less(zero, *stated)) {
550 return std::nullopt;
551 }
552 price = risk_detail::decimal_max(price, *stated);
553 }
554 bool adds_quantity = false;
555 switch (contribution) {
557 adds_quantity = true;
558 break;
560 break;
561 }
562 if (adds_quantity) {
563 const auto sum = decimal_add(
564 candidate->side_ == OrderSide::BUY ? buys : sells, candidate->quantity_
565 );
566 if (!sum) {
567 return std::nullopt;
568 }
569 (candidate->side_ == OrderSide::BUY ? buys : sells) = *sum;
570 }
571 }
572
573 const auto if_bought = decimal_add(held, buys);
574 const auto if_sold = decimal_subtract(held, sells);
575 if (!if_bought || !if_sold) {
576 return std::nullopt;
577 }
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) {
581 return std::nullopt;
582 }
583 const auto projected = risk_detail::decimal_max(*long_side, *short_side);
584
585 const auto value = decimal_multiply(projected, price);
586 if (!value) {
587 return std::nullopt;
588 }
589 const auto running = decimal_add(total, *value);
590 if (!running) {
591 return std::nullopt;
592 }
593 total = *running;
594 }
595 return total;
596}
597
618template <typename Broker>
619std::optional<bool> closes_toward_flat(
620 const Portfolio<Broker>& portfolio, const PlannedOrder& order
621) {
622 const auto zero = Decimal64::parse("0");
623
624 // Net across every row; a split holding must not understate `held`.
625 const auto net = portfolio.net_quantity_of(order.instrument_);
626 if (!net) {
627 return std::nullopt;
628 }
629 const auto held = *net;
630 // No row and a row of zero both mean nothing to close.
631 if (held.coefficient() == 0) {
632 return false;
633 }
634
635 // A non-positive quantity closes nothing; a negative one would otherwise satisfy the bound below.
636 if (!decimal_less(zero, order.quantity_)) {
637 return false;
638 }
639
640 const auto closing = decimal_less(zero, held) ? OrderSide::SELL : OrderSide::BUY;
641 if (order.side_ != closing) {
642 return false;
643 }
644
645 auto resting = zero;
646 for (const auto& working : portfolio.working_) {
647 if (!(working.order_.instrument_ == order.instrument_) || working.order_.side_ != closing) {
648 continue;
649 }
650 if (!order_is_open(working.order_.status_)) {
651 continue;
652 }
653 const auto remaining = decimal_subtract(
654 working.order_.quantity_, working.order_.execution_.filled_quantity_
655 );
656 // A row filled beyond its quantity is a contradiction; refuse rather than count zero.
657 if (!remaining || decimal_less(*remaining, zero)) {
658 return std::nullopt;
659 }
660 const auto sum = decimal_add(resting, *remaining);
661 if (!sum) {
662 return std::nullopt;
663 }
664 resting = *sum;
665 }
666
667 const auto committed = decimal_add(resting, order.quantity_);
668 if (!committed) {
669 return std::nullopt;
670 }
671 const auto magnitude = risk_detail::decimal_magnitude(held);
672 if (!magnitude) {
673 return std::nullopt;
674 }
675 // `<=`: an order that reaches flat exactly is a close (the usual liquidation case).
676 return !decimal_less(*magnitude, *committed);
677}
678
688 std::optional<GuardRejection> rejection_;
694};
695
707public:
712 explicit RiskGuard(RiskLimits limits = {}) : limits_(std::move(limits)) {}
713
718 const RiskLimits& limits() const noexcept { return limits_; }
719
725 bool permits_phase(MarketPhase phase) const noexcept {
726 if (!limits_.permitted_phases_) {
727 return true;
728 }
729 for (const auto allowed : *limits_.permitted_phases_) {
730 if (allowed == phase) {
731 return true;
732 }
733 }
734 return false;
735 }
736
746 void halt() noexcept { held_[index_of(HaltReason::OPERATOR)] = true; }
747
757 void halt(HaltReason reason, std::string detail) {
758 held_[index_of(reason)] = true;
759 details_[index_of(reason)] = std::move(detail);
760 }
761
766 void halt(HaltReason reason) { halt(reason, std::string()); }
767
769 void resume() noexcept { resume(HaltReason::OPERATOR); }
770
775 void resume(HaltReason reason) noexcept {
776 held_[index_of(reason)] = false;
777 details_[index_of(reason)].clear();
778 }
779
784 bool halted() const noexcept {
785 for (const auto held : held_) {
786 if (held) {
787 return true;
788 }
789 }
790 return false;
791 }
792
798 bool halted(HaltReason reason) const noexcept { return held_[index_of(reason)]; }
799
804 std::optional<HaltReason> blocking_reason() const noexcept {
806 }
807
813 std::optional<HaltReason> blocking_reason_for(InstructionClass klass) const noexcept {
814 for (std::size_t index = 0; index < halt_reason_count; ++index) {
815 const auto reason = static_cast<HaltReason>(index);
816 if (held_[index] && halt_blocks(reason, klass)) {
817 return reason;
818 }
819 }
820 return std::nullopt;
821 }
822
828 const std::string& hold_detail(HaltReason reason) const noexcept {
829 return details_[index_of(reason)];
830 }
831
836 std::vector<HaltReason> holds() const {
837 std::vector<HaltReason> held;
838 for (std::size_t index = 0; index < halt_reason_count; ++index) {
839 if (held_[index]) {
840 held.push_back(static_cast<HaltReason>(index));
841 }
842 }
843 return held;
844 }
845
854 void note_sent(std::chrono::system_clock::time_point when) {
855 sent_.push_back(when);
856 ++total_sent_;
857 }
858
867 std::size_t total_sent() const noexcept { return total_sent_; }
868
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_) {
878 ++count;
879 }
880 }
881 return count;
882 }
883
888 void forget_before(std::chrono::system_clock::time_point cutoff) {
889 while (!sent_.empty() && sent_.front() < cutoff) {
890 sent_.pop_front();
891 }
892 }
893
904 template <typename Broker>
905 std::optional<GuardRejection> check(
906 const PlannedOrder& order,
907 const Portfolio<Broker>& portfolio,
908 const GuardInputs& inputs
909 ) const {
910 return evaluate(order, portfolio, inputs).rejection_;
911 }
912
926 template <typename Broker>
928 const PlannedOrder& order,
929 const Portfolio<Broker>& portfolio,
930 const GuardInputs& inputs
931 ) const {
932 const auto refuse = [&order](GuardReason reason) {
933 GuardRejection rejection;
934 rejection.reason_ = reason;
935 rejection.instrument_ = order.instrument_;
936 rejection.tag_ = order.tag_;
937 return GuardDecision{std::optional<GuardRejection>(rejection), false};
938 };
939 const auto refuse_with =
940 [&order](GuardReason reason, Decimal64 limit, Decimal64 attempted) {
941 GuardRejection rejection;
942 rejection.reason_ = reason;
943 rejection.instrument_ = order.instrument_;
944 rejection.tag_ = order.tag_;
945 rejection.limit_ = std::move(limit);
946 rejection.attempted_ = std::move(attempted);
947 return GuardDecision{std::optional<GuardRejection>(rejection), false};
948 };
949 // Set by the phase check and reported only if the order survives every later limit, since
950 // `refuse` and `refuse_with` both build a decision with it false. An order the exception
951 // admitted and a ceiling then refused was not admitted.
952 bool admitted_outside_phase = false;
953
954 if (const auto holding = blocking_reason()) {
955 auto decision = refuse(GuardReason::TRADING_HALTED);
956 decision.rejection_->halt_reason_ = *holding;
957 return decision;
958 }
959
960 if (limits_.permitted_universe_) {
961 bool permitted = false;
962 for (const auto& allowed : *limits_.permitted_universe_) {
963 if (allowed == order.instrument_) {
964 permitted = true;
965 break;
966 }
967 }
968 if (!permitted) {
970 }
971 }
972
973 if (limits_.permitted_phases_) {
974 bool permitted = false;
975 for (const auto phase : *limits_.permitted_phases_) {
976 if (phase == inputs.phase_) {
977 permitted = true;
978 break;
979 }
980 }
981 if (!permitted) {
982 // The phase restricts what an order opens, not what it closes: an exit must stay expressible
983 // after the session. `closes_toward_flat` is narrower than the exposure rule, which would
984 // admit a sell that opens a short against a resting buy.
985 const auto closing = closes_toward_flat(portfolio, order);
986 // Unknown refuses: this branch only relaxes a restriction.
987 if (!closing || !*closing) {
989 }
990 admitted_outside_phase = true;
991 }
992 }
993
994 if (limits_.max_orders_per_window_ &&
995 recent_orders(inputs.now_) >= *limits_.max_orders_per_window_) {
996 return refuse_with(
998 Decimal64(static_cast<std::int64_t>(*limits_.max_orders_per_window_), 0),
999 Decimal64(static_cast<std::int64_t>(recent_orders(inputs.now_) + 1), 0)
1000 );
1001 }
1002
1003 // A market order carries no price, so its value has to come from the runner's last quote.
1004 // Without one, a notional or exposure limit cannot be evaluated, and a limit that cannot be
1005 // evaluated must refuse rather than wave the order through.
1006 const auto price = order.price_ ? order.price_ : inputs.reference_price_;
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) {
1011 return refuse(GuardReason::VALUE_NOT_KNOWN);
1012 }
1013
1014 std::optional<Decimal64> value;
1015 if (price) {
1016 value = decimal_multiply(order.quantity_, *price);
1017 if (needs_value && !value) {
1018 return refuse(GuardReason::VALUE_NOT_KNOWN);
1019 }
1020 }
1021
1022 if (limits_.max_order_notional_ && value &&
1023 decimal_less(*limits_.max_order_notional_, *value)) {
1024 return refuse_with(
1025 GuardReason::ORDER_NOTIONAL_TOO_LARGE, *limits_.max_order_notional_, *value
1026 );
1027 }
1028
1029 if (limits_.enforce_sellable_quantity_ && order.side_ == OrderSide::SELL) {
1030 // One read, so the figure and its basis come from the same row.
1031 const auto reading = portfolio.sellable_reading_for(order.instrument_);
1032 const auto& sellable = reading.quantity_;
1033 if (!sellable) {
1034 // Absent is unknown, and unknown refuses; the held quantity is not substituted.
1036 }
1037 // A figure whose basis is not established refuses even though present; this fails closed for
1038 // an adapter the library has never seen.
1039 if (!reading.basis_.is_enforceable()) {
1041 }
1042 // Every open sell (the broker's rows and this library's reservations) counts against the
1043 // figure. That over-refuses where the broker already deducted them and never under-refuses;
1044 // see `SellableBasis::DEDUCT`.
1045 const auto resting = portfolio.working_quantity(order.instrument_, OrderSide::SELL);
1046 const auto committed = decimal_add(resting, order.quantity_);
1047 if (!committed) {
1049 }
1050 if (decimal_less(*sellable, *committed)) {
1051 return refuse_with(GuardReason::QUANTITY_NOT_SELLABLE, *sellable, *committed);
1052 }
1053 }
1054
1055 if (limits_.enforce_buying_power_ && order.side_ == OrderSide::BUY) {
1056 if (!value) {
1057 // A price is present (`needs_value`), so the multiplication itself failed.
1058 return refuse(GuardReason::VALUE_NOT_KNOWN);
1059 }
1060 if (!inputs.order_currency_) {
1062 }
1063 const auto available = portfolio.buying_power_in(*inputs.order_currency_);
1064 if (!available) {
1065 // Absent is unknown, and unknown refuses. (`cash_in` is empty live, so no fallback.)
1067 }
1068 if (decimal_less(*available, *value)) {
1069 return refuse_with(GuardReason::INSUFFICIENT_BUYING_POWER, *available, *value);
1070 }
1071 }
1072
1073 if (limits_.max_position_quantity_) {
1074 const auto zero = Decimal64::parse("0");
1075 // Net across every position row, as reconciliation sizes: a split holding read from its first
1076 // row would refuse an exit to flat and pass a buy that both lots together put over the ceiling.
1077 const auto net = portfolio.net_quantity_of(order.instrument_);
1078 if (!net) {
1080 }
1081 const auto held = *net;
1082 // Orders already working on this side count, so two buys that each fit cannot together exceed
1083 // the ceiling. A resting opposite-side order is not counted as relief.
1084 const auto pending = portfolio.working_quantity(order.instrument_, order.side_);
1085 const auto committed = decimal_add(pending, order.quantity_);
1086 if (!committed) {
1088 }
1089 const auto signed_quantity = order.side_ == OrderSide::BUY ?
1090 std::optional<Decimal64>(*committed) :
1091 decimal_subtract(zero, *committed);
1092 if (!signed_quantity) {
1094 }
1095 const auto resulting = decimal_add(held, *signed_quantity);
1096 if (!resulting) {
1098 }
1099 // The ceiling is on magnitude, so a short of the same size is limited as a long is.
1100 const auto magnitude = decimal_less(*resulting, zero) ?
1101 decimal_subtract(zero, *resulting) :
1102 std::optional<Decimal64>(*resulting);
1103 if (!magnitude) {
1105 }
1106 if (decimal_less(*limits_.max_position_quantity_, *magnitude)) {
1107 return refuse_with(
1108 GuardReason::POSITION_LIMIT_EXCEEDED, *limits_.max_position_quantity_, *magnitude
1109 );
1110 }
1111 }
1112
1113 if (limits_.max_gross_exposure_) {
1114 // Per currency: an unresolved order currency refuses.
1115 if (!inputs.order_currency_) {
1116 return refuse(GuardReason::VALUE_NOT_KNOWN);
1117 }
1118 const auto resulting = projected_gross_exposure(
1119 portfolio, inputs.exposure_basis_, *inputs.order_currency_, &order, price,
1121 );
1122 if (!resulting) {
1123 // The basis does not describe the portfolio, or the arithmetic did not fit.
1124 return refuse(GuardReason::VALUE_NOT_KNOWN);
1125 }
1126 if (decimal_less(*limits_.max_gross_exposure_, *resulting)) {
1127 // Over the ceiling, refuse only an order that raises the projection, so an account already
1128 // over it can still sell out. Compare against `PRICE_ONLY` (same prices, no quantity), not
1129 // `nullptr`, so the difference is the order's quantity and not its limit price.
1130 const auto without = projected_gross_exposure(
1131 portfolio, inputs.exposure_basis_, *inputs.order_currency_, &order, price,
1133 );
1134 if (!without) {
1135 return refuse(GuardReason::VALUE_NOT_KNOWN);
1136 }
1137 if (decimal_less(*without, *resulting)) {
1138 return refuse_with(
1139 GuardReason::GROSS_EXPOSURE_EXCEEDED, *limits_.max_gross_exposure_, *resulting
1140 );
1141 }
1142 }
1143 }
1144
1145 return GuardDecision{std::nullopt, admitted_outside_phase};
1146 }
1147
1148private:
1149 // `reason`'s slot in the hold arrays.
1150 static constexpr std::size_t index_of(HaltReason reason) noexcept {
1151 return static_cast<std::size_t>(reason);
1152 }
1153
1154 RiskLimits limits_;
1155 // Which holds are in force, indexed by `HaltReason`; declaration order is priority order.
1156 std::array<bool, halt_reason_count> held_{};
1157 // Each hold's detail, empty where none or not held.
1158 std::array<std::string, halt_reason_count> details_;
1159 // Timestamps of orders sent, oldest first; a sliding window, not a counter reset on a boundary.
1160 std::deque<std::chrono::system_clock::time_point> sent_;
1161 std::size_t total_sent_{0};
1162};
1163
1164} // namespace strategy
1165} // namespace algotradelib
A three-letter currency code, such as KRW or USD (ISO 4217 style).
Definition types.hpp:234
An exact signed decimal: an int64 coefficient times 10^-scale.
Definition types.hpp:297
Decimal64()=default
Constructs zero.
static Decimal64 parse(std::string_view input)
Parses a decimal string exactly (no binary floating point).
Definition types.hpp:323
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.
Definition risk.hpp:874
void note_sent(std::chrono::system_clock::time_point when)
Records that a request was sent, for the rate limit.
Definition risk.hpp:854
void halt(HaltReason reason)
Takes a hold, with no description.
Definition risk.hpp:766
const RiskLimits & limits() const noexcept
Returns the limits.
Definition risk.hpp:718
bool halted(HaltReason reason) const noexcept
Tests whether one hold is in force.
Definition risk.hpp:798
void resume(HaltReason reason) noexcept
Releases a hold; releasing one that is not held does nothing.
Definition risk.hpp:775
RiskGuard(RiskLimits limits={})
Constructs a guard.
Definition risk.hpp:712
void resume() noexcept
Releases the operator's hold; other holds, if any, still stop trading.
Definition risk.hpp:769
std::size_t total_sent() const noexcept
Returns how many orders have been recorded with note_sent over the guard's life.
Definition risk.hpp:867
void halt(HaltReason reason, std::string detail)
Takes a hold, with a description.
Definition risk.hpp:757
bool permits_phase(MarketPhase phase) const noexcept
Tests whether a phase permits opening or increasing a position.
Definition risk.hpp:725
std::vector< HaltReason > holds() const
Returns every hold in force.
Definition risk.hpp:836
std::optional< GuardRejection > check(const PlannedOrder &order, const Portfolio< Broker > &portfolio, const GuardInputs &inputs) const
Checks one planned order against the limits and holds.
Definition risk.hpp:905
std::optional< HaltReason > blocking_reason_for(InstructionClass klass) const noexcept
Returns the hold that stops an instruction class, the most fundamental first.
Definition risk.hpp:813
void halt() noexcept
Takes the operator's hold (HaltReason::OPERATOR): no order or cancellation is sent until resume().
Definition risk.hpp:746
void forget_before(std::chrono::system_clock::time_point cutoff)
Forgets sends recorded before an instant, to bound memory.
Definition risk.hpp:888
bool halted() const noexcept
Tests whether any hold is in force.
Definition risk.hpp:784
std::optional< HaltReason > blocking_reason() const noexcept
Returns the hold that stops placements, the most fundamental first.
Definition risk.hpp:804
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.
Definition risk.hpp:927
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.
Definition concept.hpp:16
GuardReason
Which check refused an order.
Definition risk.hpp:176
@ SELLABLE_BASIS_UNESTABLISHED
A sellable quantity is present, but what it covers is not established.
Definition risk.hpp:206
@ ORDER_NOTIONAL_TOO_LARGE
The order's value exceeds RiskLimits::max_order_notional_.
Definition risk.hpp:184
@ ORDER_RATE_EXCEEDED
RiskLimits::max_orders_per_window_ orders were already sent in the window.
Definition risk.hpp:190
@ QUANTITY_NOT_SELLABLE
The sell exceeds the broker's sellable quantity, or the broker gave none.
Definition risk.hpp:199
@ POSITION_LIMIT_EXCEEDED
The position could exceed RiskLimits::max_position_quantity_.
Definition risk.hpp:186
@ VALUE_NOT_KNOWN
A limit needed a value that is not known.
Definition risk.hpp:197
@ INSUFFICIENT_BUYING_POWER
The buy costs more than the broker's buying power, or the broker gave none.
Definition risk.hpp:208
@ INSTRUMENT_NOT_PERMITTED
The instrument is not in RiskLimits::permitted_universe_.
Definition risk.hpp:180
@ PHASE_NOT_PERMITTED
The market phase is not in RiskLimits::permitted_phases_.
Definition risk.hpp:182
@ GROSS_EXPOSURE_EXCEEDED
The gross exposure could exceed RiskLimits::max_gross_exposure_.
Definition risk.hpp:188
@ TRADING_HALTED
A hold is in force; GuardRejection::halt_reason_ names it.
Definition risk.hpp:178
MarketPhase
Where the clock stands relative to the trading session.
Definition event.hpp:30
@ UNKNOWN
The calendar does not cover this date; nothing is known about it.
Definition event.hpp:32
@ OPEN
Inside the session.
Definition event.hpp:38
constexpr std::size_t halt_reason_count
The number of HaltReason values.
Definition risk.hpp:84
constexpr bool halt_blocks(HaltReason reason, InstructionClass klass) noexcept
Tests whether a hold stops an instruction of a given class.
Definition risk.hpp:114
std::optional< Decimal64 > projected_gross_exposure(const Portfolio< Broker > &portfolio, const std::vector< InstrumentBasis > &basis, const Currency &currency, 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.
Definition risk.hpp:432
CandidateContribution
What a candidate order adds to projected_gross_exposure.
Definition risk.hpp:401
@ QUANTITY_AND_PRICE
The candidate's quantity and price: the exposure if the order is sent.
Definition risk.hpp:403
@ PRICE_ONLY
The candidate's price only: the exposure without the order, valued at the same prices.
Definition risk.hpp:405
bool order_is_open(OrderStatus status)
Tests whether an order in a given status may still trade.
Definition portfolio.hpp:20
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.
Definition risk.hpp:619
InstructionClass
What an instruction can do to a position, which decides which holds stop it.
Definition risk.hpp:91
@ OPENING
A placement, which can open or enlarge a position. Every hold stops it.
Definition risk.hpp:93
@ WITHDRAWAL
A cancellation of an order the caller named.
Definition risk.hpp:100
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.
Definition risk.hpp:216
constexpr std::string_view halt_reason_name(HaltReason reason) noexcept
Returns a lowercase name for a hold, such as "portfolio_stale", for logs.
Definition risk.hpp:144
constexpr std::array< HaltReason, 8 > all_halt_reasons
Every HaltReason, in declaration order.
Definition risk.hpp:72
HaltReason
Who or what is holding trading stopped.
Definition risk.hpp:29
@ BUYING_POWER_UNREAD
A buying-power limit is enforced, but there is no current reading to check it against.
Definition risk.hpp:66
@ LOCAL_FAILURE
Local machinery failed, such as the order journal not being writable; nothing is sent.
Definition risk.hpp:39
@ PORTFOLIO_ABSENT
No holdings snapshot has been read yet, so what the account holds is unknown.
Definition risk.hpp:54
@ ORDER_LIST_UNREAD
The broker's order list has not been read since this process started.
Definition risk.hpp:48
@ OPERATOR
A person stopped trading with RiskGuard::halt(); only RiskGuard::resume() clears it.
Definition risk.hpp:31
@ PORTFOLIO_STALE
The holdings snapshot is older than the configured maximum age.
Definition risk.hpp:60
@ POLL_FAILING
A poll kept failing and the loop gave up, as opposed to STOPPING on request.
Definition risk.hpp:68
@ STOPPING
A stop was requested and the run is shutting down.
Definition risk.hpp:41
The library: broker-neutral types (core), and the modules below.
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.
Definition types.hpp:509
std::optional< Decimal64 > decimal_subtract(const Decimal64 &lhs, const Decimal64 &rhs)
Subtracts two decimals exactly.
Definition types.hpp:530
bool decimal_less(const Decimal64 &lhs, const Decimal64 &rhs)
Compares two decimals numerically, whatever their scales.
Definition types.hpp:665
std::optional< Decimal64 > decimal_multiply(const Decimal64 &lhs, const Decimal64 &rhs)
Multiplies two decimals exactly; the scales add.
Definition types.hpp:571
Reconciliation: turning a strategy's intents and targets into the orders and cancellations to send.
Identifies an instrument: its country, venue and symbol.
Definition types.hpp:78
What RiskGuard::evaluate concluded about one order.
Definition risk.hpp:686
bool admitted_outside_phase_
Whether the order was permitted only because it closes toward flat in a phase the limits do not permi...
Definition risk.hpp:693
std::optional< GuardRejection > rejection_
Why the order was refused, or std::nullopt if it was permitted.
Definition risk.hpp:688
What RiskGuard::check needs besides the order and the portfolio.
Definition risk.hpp:353
MarketPhase phase_
The market phase the order would be sent in.
Definition risk.hpp:357
std::chrono::system_clock::time_point now_
The current instant; RiskLimits::rate_window_ counts back from it.
Definition risk.hpp:355
std::optional< Currency > order_currency_
The currency the order is priced in, from its reference data.
Definition risk.hpp:374
std::optional< Decimal64 > reference_price_
A price to value a market order, which carries none.
Definition risk.hpp:359
std::vector< InstrumentBasis > exposure_basis_
The currency and mark of every instrument held or working, plus the order's.
Definition risk.hpp:367
One order a guard refused, with the limit and the value that exceeded it.
Definition risk.hpp:245
InstrumentId instrument_
The instrument of the refused order.
Definition risk.hpp:249
std::string tag_
The tag of the intent the order came from.
Definition risk.hpp:251
GuardReason reason_
Which check refused the order.
Definition risk.hpp:247
std::optional< Decimal64 > limit_
The limit that was exceeded, where the reason has one.
Definition risk.hpp:255
std::optional< HaltReason > halt_reason_
The hold that stopped the order, when reason_ is TRADING_HALTED; otherwise empty.
Definition risk.hpp:253
std::optional< Decimal64 > attempted_
The value the order would have reached, where the reason has one.
Definition risk.hpp:257
An instrument's currency and the price to value it at, for the exposure ceiling.
Definition risk.hpp:339
InstrumentId instrument_
The instrument.
Definition risk.hpp:341
Decimal64 price_
The mark to value it at.
Definition risk.hpp:349
Currency currency_
The currency it trades in.
Definition risk.hpp:343
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.
Definition portfolio.hpp:50
std::vector< WorkingOrder< Broker > > working_
Orders still outstanding at the broker.
Definition portfolio.hpp:77
std::optional< Decimal64 > buying_power_in(const Currency &currency) 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.
Definition portfolio.hpp:56
The limits the operator sets for RiskGuard.
Definition risk.hpp:268
bool enforce_sellable_quantity_
Whether to refuse a sell larger than the broker's sellable quantity.
Definition risk.hpp:308
bool enforce_buying_power_
Whether to refuse a buy that costs more than the broker's buying power.
Definition risk.hpp:315
std::optional< std::vector< InstrumentId > > permitted_universe_
The instruments that may be traded; absent means any.
Definition risk.hpp:317
std::optional< Decimal64 > max_gross_exposure_
The largest gross exposure the portfolio may reach, in the order's currency.
Definition risk.hpp:291
std::optional< Decimal64 > max_position_quantity_
The largest absolute position, in shares, any instrument may reach.
Definition risk.hpp:277
std::optional< std::size_t > max_orders_per_window_
The most orders that may be sent within rate_window_.
Definition risk.hpp:293
std::optional< Decimal64 > max_order_notional_
The largest value one order may have, in its own currency.
Definition risk.hpp:270
std::chrono::seconds rate_window_
The window max_orders_per_window_ counts over.
Definition risk.hpp:295
std::optional< std::vector< MarketPhase > > permitted_phases_
The market phases in which a position may be opened or increased; absent means any.
Definition risk.hpp:331