algotradelib 0.0.0.9000
C++ header-only library for algorithmic trading
Loading...
Searching...
No Matches
reconcile.hpp
Go to the documentation of this file.
1#pragma once
2
3#include "./intent.hpp"
4#include "./portfolio.hpp"
5
6#include <algotradelib/core>
7
8#include <optional>
9#include <string>
10#include <utility>
11#include <vector>
12
13namespace algotradelib {
14namespace strategy {
15
27
95
101inline constexpr std::string_view reject_reason_name(RejectReason reason) noexcept {
102 switch (reason) {
104 return "no_reference_data";
106 return "unknown_price_grid";
108 return "no_price_available";
110 return "no_portfolio_value";
112 return "quantity_rounds_to_zero";
114 return "not_representable";
116 return "no_permitted_price";
118 return "broker_refused";
120 return "binding_refused";
122 return "trigger_off_grid";
124 return "stop_order_resting";
126 return "unsupported_weight";
128 return "negative_size";
129 }
130 return "unknown";
131}
132
142
191
200template <typename Broker>
201struct Plan {
203 std::vector<OrderRef<Broker>> cancel_;
205 std::vector<PlannedOrder> place_;
207 std::vector<Rejection> rejected_;
208
213 bool empty() const noexcept {
214 return cancel_.empty() && place_.empty() && rejected_.empty();
215 }
216};
217
235 std::optional<Decimal64> minimum_quantity_;
237 std::optional<Decimal64> minimum_notional_;
242 std::optional<Decimal64> minimum_fraction_of_working_;
243
248 bool stated() const noexcept {
249 return minimum_quantity_.has_value() || minimum_notional_.has_value() ||
251 }
252};
253
261 const Instrument* instrument_{nullptr};
263 std::optional<Decimal64> reference_price_;
265 std::optional<PriceLimits> limits_;
267 std::optional<Decimal64> portfolio_value_;
268
276 std::optional<Currency> portfolio_value_currency_;
285};
286
287namespace reconcile_detail {
288
290inline std::optional<Decimal64> lot_of(const Instrument& instrument) {
291 if (!instrument.lot_size_) {
292 return std::nullopt;
293 }
294 if (instrument.lot_size_->coefficient() <= 0) {
295 return std::nullopt;
296 }
297 return instrument.lot_size_;
298}
299
312inline std::optional<Decimal64> round_to_lot(
313 const Decimal64& quantity,
314 const std::optional<Decimal64>& lot,
315 bool& adjusted
316) {
317 adjusted = false;
318 if (!lot) {
319 return quantity;
320 }
321 const auto rounded = decimal_floor_to_multiple(quantity, *lot);
322 if (!rounded) {
323 return std::nullopt;
324 }
325 adjusted = !(*rounded == quantity);
326 return rounded;
327}
328
340inline std::optional<Decimal64> clamp_to_limits(
341 const Decimal64& price,
342 OrderSide side,
343 const std::optional<PriceLimits>& limits,
344 bool& adjusted
345) {
346 adjusted = false;
347 if (!limits) {
348 return price;
349 }
350 if (side == OrderSide::BUY && limits->upper_ && decimal_less(*limits->upper_, price)) {
351 adjusted = true;
352 return limits->upper_;
353 }
354 if (side == OrderSide::SELL && limits->lower_ && decimal_less(price, *limits->lower_)) {
355 adjusted = true;
356 return limits->lower_;
357 }
358 return price;
359}
360
369inline std::optional<Decimal64> prepare_price(
370 const Decimal64& wanted,
371 OrderSide side,
372 const PriceRule& rule,
373 const std::optional<PriceLimits>& limits,
374 bool& clamped,
375 bool& rounded
376) {
377 const auto within = clamp_to_limits(wanted, side, limits, clamped);
378 if (!within) {
379 return std::nullopt;
380 }
381 const auto on_grid = rule.round_to_tick(*within, side);
382 if (!on_grid) {
383 return std::nullopt;
384 }
385 rounded = !(*on_grid == *within);
386 return on_grid;
387}
388
396inline bool rounded_out_of_band(
397 const Decimal64& wanted,
398 const Decimal64& priced,
399 OrderSide side,
400 const std::optional<PriceLimits>& limits
401) {
402 if (!limits) {
403 return false;
404 }
405 if (side == OrderSide::BUY) {
406 return limits->lower_ && decimal_less(priced, *limits->lower_) && !decimal_less(wanted, *limits->lower_);
407 }
408 return limits->upper_ && decimal_less(*limits->upper_, priced) && !decimal_less(*limits->upper_, wanted);
409}
410
418inline bool below_tolerance(
419 const Decimal64& drift,
420 const Decimal64& already_working,
421 const ReconcileInputs& inputs
422) {
423 const auto& tolerance = inputs.tolerance_;
424
425 if (tolerance.minimum_quantity_ && decimal_less(drift, *tolerance.minimum_quantity_)) {
426 return true;
427 }
428
429 if (tolerance.minimum_notional_ && inputs.reference_price_) {
430 const auto value = decimal_multiply(drift, *inputs.reference_price_);
431 if (value && decimal_less(*value, *tolerance.minimum_notional_)) {
432 return true;
433 }
434 }
435
436 // Only meaningful against something resting: with nothing working there is no queue position to
437 // give up, so a drift is simply a new order and this floor has nothing to say about it.
438 if (tolerance.minimum_fraction_of_working_ && already_working.coefficient() > 0) {
439 // Six places, which resolves a fraction far finer than any threshold an operator would state.
440 const auto fraction = decimal_divide(drift, already_working, 6);
441 if (fraction && decimal_less(*fraction, *tolerance.minimum_fraction_of_working_)) {
442 return true;
443 }
444 }
445
446 return false;
447}
448
449} // namespace reconcile_detail
450
457inline bool is_stop_order(const OrderSnapshot& order) {
458 return carries_trigger_price(order.type_) || order.trigger_price_.has_value();
459}
460
488template <typename Broker>
490 const TargetPosition& target,
491 const Portfolio<Broker>& portfolio,
492 const ReconcileInputs& inputs
493) {
494 Plan<Broker> plan;
495 const auto reject = [&](RejectReason reason) {
496 plan.cancel_.clear();
497 plan.place_.clear();
498 plan.rejected_.push_back(Rejection{reason, target.instrument_, target.tag_});
499 return plan;
500 };
501
502 if (inputs.instrument_ == nullptr) {
503 return reject(RejectReason::NO_REFERENCE_DATA);
504 }
505
506 const auto zero = Decimal64::parse("0");
507 // Net across every position row, as the guards read it: a holding can be split across lots.
508 const auto net = portfolio.net_quantity_of(target.instrument_);
509 if (!net) {
510 // A snapshot whose own rows will not add up cannot say what reaching a target would take, and
511 // `value_or(zero)` here would read a broken listing as flat and order the whole target.
512 return reject(RejectReason::NOT_REPRESENTABLE);
513 }
514 const auto held = *net;
515 const auto needed = decimal_subtract(target.target_quantity_, held);
516 if (!needed) {
517 return reject(RejectReason::NOT_REPRESENTABLE);
518 }
519
520 const bool buying = decimal_less(zero, *needed);
521 const bool selling = decimal_less(*needed, zero);
522
523 // **Stops first; see `RejectReason::STOP_ORDER_RESTING`.** A row is a stop when its basis says so
524 // or it carries a trigger -- an adapter records a positive trigger even on a row whose basis reads
525 // otherwise, and that row may be a released stop.
526 const auto is_stop = [](const OrderSnapshot& order) { return is_stop_order(order); };
527 bool stop_resting = false;
528 for (const auto& working : portfolio.working_) {
529 if (working.order_.instrument_ == target.instrument_ &&
530 order_is_open(working.order_.status_) && is_stop(working.order_)) {
531 stop_resting = true;
532 }
533 }
534 if (stop_resting) {
535 const bool met = !buying && !selling;
536 for (const auto& working : portfolio.working_) {
537 if (!(working.order_.instrument_ == target.instrument_) ||
538 !order_is_open(working.order_.status_)) {
539 continue;
540 }
541 if (is_stop(working.order_)) {
542 // Withdrawn only when a trade is needed, and not again once its withdrawal is pending: a
543 // second request for an order already on its way out can only fail.
544 if (!met && !order_status_cancel_pending(working.order_.status_)) {
545 plan.cancel_.push_back(working.ref_);
546 }
547 } else if (met) {
548 // The holding meets the target, so every *other* working order is unwanted, as before.
549 plan.cancel_.push_back(working.ref_);
550 }
551 }
552 if (!met) {
553 plan.rejected_.push_back(
555 );
556 }
557 return plan;
558 }
559
560 // Step 2: withdraw what works against the target. With nothing needed, that is everything.
561 //
562 // Only orders that can still trade are withdrawn. A snapshot may carry an order that has since
563 // filled or been cancelled, and asking a broker to cancel one of those is a request that can
564 // only fail -- noise on a path where a real cancellation failure has to be noticed.
565 for (const auto& working : portfolio.working_) {
566 if (!(working.order_.instrument_ == target.instrument_) ||
567 !order_is_open(working.order_.status_)) {
568 continue;
569 }
570 const bool opposes = (buying && working.order_.side_ == OrderSide::SELL) ||
571 (selling && working.order_.side_ == OrderSide::BUY) || (!buying && !selling);
572 if (opposes) {
573 plan.cancel_.push_back(working.ref_);
574 }
575 }
576 if (!buying && !selling) {
577 return plan;
578 }
579
580 const OrderSide side = buying ? OrderSide::BUY : OrderSide::SELL;
581 const auto magnitude = buying ? *needed : decimal_subtract(zero, *needed).value_or(zero);
582 const auto already = portfolio.working_quantity(target.instrument_, side);
583
584 // Step 3: only the part not already in the market.
585 auto remaining = decimal_subtract(magnitude, already);
586 if (!remaining) {
587 return reject(RejectReason::NOT_REPRESENTABLE);
588 }
589
590 // Step 3a: a drift smaller than the tolerance is not worth an instruction.
591 //
592 // Measured on the *drift* -- how far the in-flight position is from the target -- and not on what
593 // would be sent. Those differ exactly in the case the tolerance exists for: 100 working against a
594 // target of 99 would withdraw the side and re-place 99, so the quantity sent is 99 while the drift
595 // is one share.
596 //
597 // Suppressed only when nothing has already been queued for cancellation. An order working the
598 // *wrong way* still has to be withdrawn however small the drift, because leaving it resting works
599 // against the target rather than merely failing to reach it. And returning an empty plan rather
600 // than the accumulated one matters for the same reason: withdrawing a working order and then
601 // declining to replace it would leave the position further from the target than doing nothing.
602 if (inputs.tolerance_.stated() && plan.cancel_.empty()) {
603 const auto drift = decimal_less(*remaining, zero) ?
604 decimal_subtract(zero, *remaining) :
605 std::optional<Decimal64>(*remaining);
606 if (drift && reconcile_detail::below_tolerance(*drift, already, inputs)) {
607 return Plan<Broker>{};
608 }
609 }
610
611 // Step 4: more working than wanted, so withdraw the side and send the whole remainder -- net of what
612 // this runner reserved for its own orders not yet shown by holdings, when those may have filled
613 // (`ReconcileInputs::reserved_rows_are_unconfirmed_`): their withdrawal is refused if they did, and a
614 // whole remainder beside them traded twice. One genuinely resting is withdrawn all the same;
615 // its history settles what it filled, and a later pass places the rest.
616 if (decimal_less(*remaining, zero)) {
617 Decimal64 reserved = zero;
618 for (const auto& working : portfolio.working_) {
619 if (working.order_.instrument_ == target.instrument_ &&
620 working.order_.side_ == side && order_is_open(working.order_.status_)) {
621 plan.cancel_.push_back(working.ref_);
622 if (inputs.reserved_rows_are_unconfirmed_ && working.reserved_) {
623 // A sum that will not compute refuses: undercounted, it would size the replacement larger.
624 const auto left = decimal_subtract(working.order_.quantity_, working.order_.execution_.filled_quantity_);
625 if (!left) {
626 return reject(RejectReason::NOT_REPRESENTABLE);
627 }
628 if (decimal_less(zero, *left)) {
629 const auto sum = decimal_add(reserved, *left);
630 if (!sum) {
631 return reject(RejectReason::NOT_REPRESENTABLE);
632 }
633 reserved = *sum;
634 }
635 }
636 }
637 }
638 remaining = decimal_subtract(magnitude, reserved);
639 if (!remaining) {
640 return reject(RejectReason::NOT_REPRESENTABLE);
641 }
642 if (!decimal_less(zero, *remaining)) {
643 return plan;
644 }
645 }
646 if (remaining->coefficient() == 0) {
647 return plan;
648 }
649
650 PlannedOrder order;
651 order.instrument_ = target.instrument_;
652 order.side_ = side;
654 order.tag_ = target.tag_;
655
656 const auto lot = reconcile_detail::lot_of(*inputs.instrument_);
657 const auto sized = reconcile_detail::round_to_lot(*remaining, lot, order.quantity_rounded_);
658 if (!sized) {
659 return reject(RejectReason::NOT_REPRESENTABLE);
660 }
661 if (sized->coefficient() == 0) {
662 // Rounded away entirely. A rejection rather than an empty plan, because the strategy asked
663 // for something and is getting nothing; silence here would look like a met target.
665 }
666 order.quantity_ = *sized;
667
668 if (target.limit_) {
669 const auto priced = reconcile_detail::prepare_price(
670 target.limit_->value(),
671 side,
672 inputs.instrument_->price_rule_,
673 inputs.limits_,
674 order.price_clamped_,
675 order.price_rounded_
676 );
677 if (!priced) {
679 }
680 if (reconcile_detail::rounded_out_of_band(target.limit_->value(), *priced, side, inputs.limits_)) {
682 }
683 order.type_ = OrderType::LIMIT;
684 order.price_ = priced;
685 } else {
686 order.type_ = OrderType::MARKET;
687 }
688
689 plan.place_.push_back(std::move(order));
690 return plan;
691}
692
709template <typename Broker>
711 const OrderIntent& intent,
712 const Portfolio<Broker>& portfolio,
713 const ReconcileInputs& inputs
714) {
715 (void)portfolio;
716 Plan<Broker> plan;
717 const auto reject = [&](RejectReason reason) {
718 plan.place_.clear();
719 plan.rejected_.push_back(Rejection{reason, intent.instrument(), intent.tag()});
720 return plan;
721 };
722
723 if (inputs.instrument_ == nullptr) {
724 return reject(RejectReason::NO_REFERENCE_DATA);
725 }
726
727 // A price to size against: the limit if there is one, otherwise whatever the runner supplied.
728 const auto sizing_price = intent.limit() ?
729 std::optional<Decimal64>(intent.limit()->value()) :
730 inputs.reference_price_;
731
732 Decimal64 wanted;
733 switch (intent.size().kind()) {
735 if (intent.size().as_quantity().coefficient() < 0) {
736 return reject(RejectReason::NEGATIVE_SIZE);
737 }
738 wanted = intent.size().as_quantity();
739 break;
740 case SizeKind::NOTIONAL: {
741 if (intent.size().as_notional().amount_.coefficient() < 0) {
742 return reject(RejectReason::NEGATIVE_SIZE);
743 }
744 // Not positive is no price to size by: divided by a negative one, a buy became a negative quantity.
745 if (!sizing_price || sizing_price->coefficient() <= 0) {
747 }
748 const auto shares =
749 decimal_divide(intent.size().as_notional().amount_, *sizing_price, 0);
750 if (!shares) {
751 return reject(RejectReason::NOT_REPRESENTABLE);
752 }
753 wanted = *shares;
754 break;
755 }
757 // The guards `weight_target` applies: a weight states a holding, which cannot be negative, and is
758 // a fraction of a value and sized by a price that must both be positive.
759 if (intent.size().as_target_weight().coefficient() < 0) {
761 }
762 if (!inputs.portfolio_value_ || inputs.portfolio_value_->coefficient() <= 0) {
764 }
765 if (!sizing_price || sizing_price->coefficient() <= 0) {
767 }
768 const auto allocation = decimal_multiply(
769 inputs.portfolio_value_.value(), intent.size().as_target_weight()
770 );
771 if (!allocation) {
772 return reject(RejectReason::NOT_REPRESENTABLE);
773 }
774 const auto shares = decimal_divide(*allocation, *sizing_price, 0);
775 if (!shares) {
776 return reject(RejectReason::NOT_REPRESENTABLE);
777 }
778 wanted = *shares;
779 break;
780 }
781 }
782
783 PlannedOrder order;
784 order.instrument_ = intent.instrument();
785 order.side_ = intent.side();
786 order.time_in_force_ = intent.time_in_force();
787 order.tag_ = intent.tag();
788 order.client_order_id_ = intent.client_order_id();
789
790 const auto lot = reconcile_detail::lot_of(*inputs.instrument_);
791 const auto sized = reconcile_detail::round_to_lot(wanted, lot, order.quantity_rounded_);
792 if (!sized) {
793 return reject(RejectReason::NOT_REPRESENTABLE);
794 }
795 if (sized->coefficient() == 0) {
797 }
798 order.quantity_ = *sized;
799
800 if (intent.limit()) {
801 const auto priced = reconcile_detail::prepare_price(
802 intent.limit()->value(),
803 intent.side(),
804 inputs.instrument_->price_rule_,
805 inputs.limits_,
806 order.price_clamped_,
807 order.price_rounded_
808 );
809 if (!priced) {
811 }
812 if (reconcile_detail::rounded_out_of_band(intent.limit()->value(), *priced, intent.side(), inputs.limits_)) {
814 }
815 order.type_ = OrderType::LIMIT;
816 order.price_ = priced;
817 } else {
818 order.type_ = OrderType::MARKET;
819 }
820
821 if (intent.trigger()) {
822 // Checked, never moved; see `RejectReason::TRIGGER_OFF_GRID`. An unknown grid answers neither
823 // way and is refused as it is for a limit.
824 const auto aligned = inputs.instrument_->price_rule_.on_tick(intent.trigger()->value());
825 if (!aligned) {
827 }
828 if (!*aligned) {
829 return reject(RejectReason::TRIGGER_OFF_GRID);
830 }
832 order.trigger_price_ = intent.trigger()->value();
833 }
834
835 plan.place_.push_back(std::move(order));
836 return plan;
837}
838
842 std::optional<TargetPosition> target_;
844 std::optional<RejectReason> refused_;
845};
846
862inline WeightTarget weight_target(const OrderIntent& intent, const ReconcileInputs& inputs) {
863 WeightTarget out;
864 const auto refuse = [&](RejectReason reason) {
865 out.refused_ = reason;
866 return out;
867 };
868 const auto& weight = intent.size().as_target_weight();
869 if (intent.side() != OrderSide::BUY || decimal_less(weight, Decimal64::parse("0")) ||
870 intent.trigger() || intent.time_in_force() != TimeInForce::DAY || intent.client_order_id()) {
872 }
873 if (inputs.instrument_ == nullptr) {
874 return refuse(RejectReason::NO_REFERENCE_DATA);
875 }
876 // A value of zero or less is not one a holding can be a fraction of: sized against it, a weight
877 // would become a liquidation (zero) or a short (negative) -- an account whose read failed and came
878 // back empty, or a replay that borrowed below zero equity, must not sell on that.
879 if (!inputs.portfolio_value_ || inputs.portfolio_value_->coefficient() <= 0 ||
881 !(*inputs.portfolio_value_currency_ == inputs.instrument_->currency_))) {
883 }
884 const auto price = intent.limit() ?
885 std::optional<Decimal64>(intent.limit()->value()) :
886 inputs.reference_price_;
887 if (!price || price->coefficient() <= 0) {
889 }
890 const auto allocation = decimal_multiply(*inputs.portfolio_value_, weight);
891 const auto shares = allocation ? decimal_divide(*allocation, *price, 0) : std::nullopt;
892 bool adjusted = false;
893 const auto wanted = shares ?
894 reconcile_detail::round_to_lot(*shares, reconcile_detail::lot_of(*inputs.instrument_), adjusted) :
895 std::nullopt;
896 if (!wanted) {
897 return refuse(RejectReason::NOT_REPRESENTABLE);
898 }
899 TargetPosition target;
900 target.instrument_ = intent.instrument();
901 target.target_quantity_ = *wanted;
902 target.limit_ = intent.limit();
903 target.tag_ = intent.tag();
904 out.target_ = std::move(target);
905 return out;
906}
907
908} // namespace strategy
909} // namespace algotradelib
An exact signed decimal: an int64 coefficient times 10^-scale.
Definition types.hpp:297
std::int64_t coefficient() const noexcept
Returns the coefficient.
Definition types.hpp:399
static Decimal64 parse(std::string_view input)
Parses a decimal string exactly (no binary floating point).
Definition types.hpp:323
One order a strategy asks the runner to place.
Definition intent.hpp:249
TimeInForce time_in_force() const noexcept
Returns how long the order stays valid.
Definition intent.hpp:386
const std::string & tag() const noexcept
Returns the tag.
Definition intent.hpp:410
const std::optional< std::string > & client_order_id() const noexcept
Returns the idempotency key.
Definition intent.hpp:416
const SizeSpec & size() const noexcept
Returns how much to trade.
Definition intent.hpp:392
const InstrumentId & instrument() const noexcept
Returns the instrument to trade.
Definition intent.hpp:368
OrderSide side() const noexcept
Returns the side.
Definition intent.hpp:374
const std::optional< Price > & trigger() const noexcept
Returns the trigger price.
Definition intent.hpp:404
static OrderIntent limit(InstrumentId instrument, OrderSide side, SizeSpec size, Price limit, TimeInForce time_in_force=TimeInForce::DAY)
Makes a limit order.
Definition intent.hpp:261
const Decimal64 & as_target_weight() const
Returns the target weight.
Definition intent.hpp:225
const Decimal64 & as_quantity() const
Returns the quantity.
Definition intent.hpp:205
SizeKind kind() const noexcept
Returns how the size is expressed.
Definition intent.hpp:198
const Money & as_notional() const
Returns the notional amount.
Definition intent.hpp:215
What a strategy asks the runner for: orders (OrderIntent), target positions (TargetPosition) and canc...
What a strategy sees and emits: events, intents, the portfolio, reconciliation and risk limits.
Definition concept.hpp:16
constexpr std::string_view reject_reason_name(RejectReason reason) noexcept
Returns a lowercase name for a reason, such as "no_reference_data", for logs.
bool order_is_open(OrderStatus status)
Tests whether an order in a given status may still trade.
Definition portfolio.hpp:20
WeightTarget weight_target(const OrderIntent &intent, const ReconcileInputs &inputs)
Converts a weight-sized intent into the target position it states.
RejectReason
Why an instruction was not placed.
Definition reconcile.hpp:34
@ BROKER_REFUSED
The broker refused the instruction (a request was sent).
Definition reconcile.hpp:57
@ QUANTITY_ROUNDS_TO_ZERO
The quantity rounded down to zero on the lot.
Definition reconcile.hpp:44
@ NO_REFERENCE_DATA
No reference data for the instrument, so neither the tick grid nor the lot is known.
Definition reconcile.hpp:36
@ NOT_REPRESENTABLE
The decimal arithmetic did not fit, or the portfolio's rows did not add up.
Definition reconcile.hpp:46
@ STOP_ORDER_RESTING
A target needs a trade while a stop order rests on the instrument; the stop is cancelled first and no...
Definition reconcile.hpp:80
@ TRIGGER_OFF_GRID
A stop-limit order's trigger price is not on the instrument's tick grid.
Definition reconcile.hpp:71
@ UNSUPPORTED_WEIGHT
A weight-sized intent with something a holding cannot express: a sell, a negative weight,...
Definition reconcile.hpp:85
@ NO_PRICE_AVAILABLE
A price was needed to size the order and none was available.
Definition reconcile.hpp:40
@ BINDING_REFUSED
The binding refused to send the instruction, so no request was made.
Definition reconcile.hpp:64
@ NEGATIVE_SIZE
An intent's size is negative: a quantity or a notional amount below zero.
Definition reconcile.hpp:93
@ NO_PERMITTED_PRICE
Rounding the limit onto the grid, in the cautious direction, would move it out of the day's band: no ...
Definition reconcile.hpp:55
@ UNKNOWN_PRICE_GRID
The instrument's tick grid is unknown, so a limit price cannot be made valid.
Definition reconcile.hpp:38
@ NO_PORTFOLIO_VALUE
A weight needed the portfolio's value, and none (or a non-positive one, or one in another currency) w...
Definition reconcile.hpp:42
bool is_stop_order(const OrderSnapshot &order)
Tests whether a working order is a stop order: a trigger-price type, or one carrying a trigger price.
@ NOTIONAL
An amount of money; the runner divides it by a price.
Definition intent.hpp:110
@ QUANTITY
A number of shares or contracts.
Definition intent.hpp:108
@ TARGET_WEIGHT
A fraction of portfolio value to hold; the runner trades the difference.
Definition intent.hpp:112
Plan< Broker > reconcile(const TargetPosition &target, const Portfolio< Broker > &portfolio, const ReconcileInputs &inputs)
Plans the cancellations and the order that move a holding to a target position.
The library: broker-neutral types (core), and the modules below.
std::optional< Decimal64 > decimal_floor_to_multiple(const Decimal64 &value, const Decimal64 &step)
Rounds down to a multiple of a step (toward negative infinity).
Definition types.hpp:639
std::optional< Decimal64 > decimal_add(const Decimal64 &lhs, const Decimal64 &rhs)
Adds two decimals exactly.
Definition types.hpp:509
OrderType
How an order's price is decided (its price basis).
Definition types.hpp:1707
@ LIMIT
지정가: fills at the stated price or better.
Definition types.hpp:1710
@ STOP_LIMIT
스톱지정가: a limit released when a trigger price is reached.
Definition types.hpp:1722
@ MARKET
시장가: fills at whatever the book offers.
Definition types.hpp:1712
constexpr bool carries_trigger_price(OrderType type) noexcept
Tests whether an order type takes a trigger price.
Definition types.hpp:1774
constexpr bool order_status_cancel_pending(OrderStatus status) noexcept
Tests whether a cancellation of an order in this status is already on its way, so asking again can on...
Definition types.hpp:1899
OrderSession
The trading session an order is submitted into.
Definition types.hpp:1839
@ REGULAR
The continuous regular session.
Definition types.hpp:1842
std::optional< Decimal64 > decimal_subtract(const Decimal64 &lhs, const Decimal64 &rhs)
Subtracts two decimals exactly.
Definition types.hpp:530
std::optional< Decimal64 > decimal_divide(const Decimal64 &lhs, const Decimal64 &rhs, std::uint8_t scale)
Divides two decimals, truncating toward zero at a given scale.
Definition types.hpp:602
Venue
A trading venue: an exchange, or the combined view across exchanges.
Definition types.hpp:43
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
TimeInForce
How long an order stays valid and what happens to an unfilled remainder.
Definition types.hpp:1797
@ DAY
Rests until the session ends; an unfilled remainder is cancelled then.
Definition types.hpp:1800
OrderSide
The side of an order: its economic direction, buy or sell.
Definition types.hpp:1683
Identifies an instrument: its country, venue and symbol.
Definition types.hpp:78
Reference data for one instrument.
Currency currency_
The currency it trades in.
PriceRule price_rule_
The price grid; kind UNKNOWN where the library has no table.
std::optional< Decimal64 > lot_size_
The order quantity increment, if the broker publishes one.
Decimal64 amount_
The amount.
Definition types.hpp:897
The state of one order, as a broker listing reported it.
Definition types.hpp:2008
OrderType type_
The price basis; UNKNOWN if the broker's code was not recognized.
Definition types.hpp:2018
std::optional< Decimal64 > trigger_price_
The trigger price, whenever the broker reports a positive one.
Definition types.hpp:2048
std::optional< bool > on_tick(const Decimal64 &price) const
Tests whether a price is on the grid.
What to send: first every cancellation, then every placement.
bool empty() const noexcept
Tests whether the plan is empty.
std::vector< PlannedOrder > place_
The orders to place, after the cancellations.
std::vector< Rejection > rejected_
The instructions not placed, with reasons.
std::vector< OrderRef< Broker > > cancel_
The orders to cancel, sent first.
One order the runner is to place.
TimeInForce time_in_force_
How long the order stays valid.
bool price_clamped_
Whether the price was clamped to the day's band (an aggressive limit beyond it).
OrderSession session_
The trading session.
std::optional< std::string > client_order_id_
The idempotency key, if the intent carried one.
Venue venue_
Where to route the order; see OrderRequest::venue_.
bool price_would_have_been_clamped_
Whether a price band was known but not applied (too old), and would have clamped this price.
std::string tag_
The tag of the intent or target the order came from.
bool quantity_rounded_
Whether the quantity was rounded down onto the lot.
bool price_rounded_
Whether the price was rounded onto the tick grid.
std::optional< Decimal64 > trigger_price_
The trigger price of a STOP_LIMIT order, exactly as stated; empty otherwise.
OrderType type_
The order type.
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.
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 > 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.
What reconciliation needs besides the intent and the portfolio; the runner fills it in.
std::optional< Currency > portfolio_value_currency_
The currency of portfolio_value_; absent means the instrument's currency.
std::optional< Decimal64 > reference_price_
A price to size a notional or weight against, such as the last trade.
std::optional< PriceLimits > limits_
The day's price band, if the broker publishes one.
bool reserved_rows_are_unconfirmed_
Whether the portfolio's reserved rows (WorkingOrder::reserved_) are orders this runner sent that the ...
std::optional< Decimal64 > portfolio_value_
The total portfolio value, for weight sizing.
TradeTolerance tolerance_
How small a drift from a target is not worth an order.
const Instrument * instrument_
The instrument's reference data (tick grid, lot size, currency); required.
One instruction that was not placed, and why.
InstrumentId instrument_
The instrument it was for.
RejectReason reason_
Why it was not placed.
std::string tag_
The tag of the intent or target it came from.
The position a strategy wants to hold in one instrument.
Definition intent.hpp:442
InstrumentId instrument_
The instrument.
Definition intent.hpp:444
std::string tag_
A label carried to the orders and their fills.
Definition intent.hpp:450
Decimal64 target_quantity_
The position to hold, signed: negative is short, and zero means flat.
Definition intent.hpp:446
std::optional< Price > limit_
The limit price for the orders the runner places; without one they are market orders.
Definition intent.hpp:448
How small a difference from a target is not worth an order.
std::optional< Decimal64 > minimum_notional_
The smallest drift worth trading, valued at the reference price.
bool stated() const noexcept
Tests whether any floor is set.
std::optional< Decimal64 > minimum_quantity_
The smallest drift worth trading, in shares.
std::optional< Decimal64 > minimum_fraction_of_working_
The smallest drift worth trading, as a fraction of the quantity already working (0....
The result of weight_target: a target position or a refusal, exactly one of them.
std::optional< TargetPosition > target_
The target position, if the weight could be sized.
std::optional< RejectReason > refused_
Why it could not be, otherwise.