4#include "./portfolio.hpp"
6#include <algotradelib/core>
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";
200template <
typename Broker>
287namespace reconcile_detail {
290inline std::optional<Decimal64> lot_of(
const Instrument& instrument) {
294 if (instrument.
lot_size_->coefficient() <= 0) {
312inline std::optional<Decimal64> round_to_lot(
313 const Decimal64& quantity,
314 const std::optional<Decimal64>& lot,
325 adjusted = !(*rounded == quantity);
340inline std::optional<Decimal64> clamp_to_limits(
341 const Decimal64& price,
343 const std::optional<PriceLimits>& limits,
350 if (side == OrderSide::BUY && limits->upper_ &&
decimal_less(*limits->upper_, price)) {
352 return limits->upper_;
354 if (side == OrderSide::SELL && limits->lower_ &&
decimal_less(price, *limits->lower_)) {
356 return limits->lower_;
369inline std::optional<Decimal64> prepare_price(
370 const Decimal64& wanted,
372 const PriceRule& rule,
373 const std::optional<PriceLimits>& limits,
377 const auto within = clamp_to_limits(wanted, side, limits, clamped);
381 const auto on_grid = rule.round_to_tick(*within, side);
385 rounded = !(*on_grid == *within);
396inline bool rounded_out_of_band(
397 const Decimal64& wanted,
398 const Decimal64& priced,
400 const std::optional<PriceLimits>& limits
405 if (side == OrderSide::BUY) {
418inline bool below_tolerance(
419 const Decimal64& drift,
420 const Decimal64& already_working,
421 const ReconcileInputs& inputs
423 const auto& tolerance = inputs.tolerance_;
425 if (tolerance.minimum_quantity_ &&
decimal_less(drift, *tolerance.minimum_quantity_)) {
429 if (tolerance.minimum_notional_ && inputs.reference_price_) {
431 if (value &&
decimal_less(*value, *tolerance.minimum_notional_)) {
438 if (tolerance.minimum_fraction_of_working_ && already_working.coefficient() > 0) {
441 if (fraction &&
decimal_less(*fraction, *tolerance.minimum_fraction_of_working_)) {
488template <
typename Broker>
514 const auto held = *net;
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_)) {
535 const bool met = !buying && !selling;
536 for (
const auto& working : portfolio.
working_) {
537 if (!(working.order_.instrument_ == target.
instrument_) ||
541 if (is_stop(working.order_)) {
545 plan.
cancel_.push_back(working.ref_);
549 plan.
cancel_.push_back(working.ref_);
565 for (
const auto& working : portfolio.
working_) {
566 if (!(working.order_.instrument_ == target.
instrument_) ||
570 const bool opposes = (buying && working.order_.side_ == OrderSide::SELL) ||
571 (selling && working.order_.side_ == OrderSide::BUY) || (!buying && !selling);
573 plan.
cancel_.push_back(working.ref_);
576 if (!buying && !selling) {
580 const OrderSide side = buying ? OrderSide::BUY : OrderSide::SELL;
581 const auto magnitude = buying ? *needed :
decimal_subtract(zero, *needed).value_or(zero);
605 std::optional<Decimal64>(*remaining);
606 if (drift && reconcile_detail::below_tolerance(*drift, already, inputs)) {
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_);
624 const auto left =
decimal_subtract(working.order_.quantity_, working.order_.execution_.filled_quantity_);
646 if (remaining->coefficient() == 0) {
656 const auto lot = reconcile_detail::lot_of(*inputs.
instrument_);
657 const auto sized = reconcile_detail::round_to_lot(*remaining, lot, order.
quantity_rounded_);
661 if (sized->coefficient() == 0) {
669 const auto priced = reconcile_detail::prepare_price(
680 if (reconcile_detail::rounded_out_of_band(target.
limit_->value(), *priced, side, inputs.
limits_)) {
689 plan.
place_.push_back(std::move(order));
709template <
typename Broker>
728 const auto sizing_price = intent.
limit() ?
729 std::optional<Decimal64>(intent.
limit()->value()) :
745 if (!sizing_price || sizing_price->coefficient() <= 0) {
765 if (!sizing_price || sizing_price->coefficient() <= 0) {
774 const auto shares =
decimal_divide(*allocation, *sizing_price, 0);
790 const auto lot = reconcile_detail::lot_of(*inputs.
instrument_);
791 const auto sized = reconcile_detail::round_to_lot(wanted, lot, order.
quantity_rounded_);
795 if (sized->coefficient() == 0) {
800 if (intent.
limit()) {
801 const auto priced = reconcile_detail::prepare_price(
802 intent.
limit()->value(),
812 if (reconcile_detail::rounded_out_of_band(intent.
limit()->value(), *priced, intent.
side(), inputs.
limits_)) {
835 plan.
place_.push_back(std::move(order));
884 const auto price = intent.
limit() ?
885 std::optional<Decimal64>(intent.
limit()->value()) :
887 if (!price || price->coefficient() <= 0) {
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) :
904 out.
target_ = std::move(target);
An exact signed decimal: an int64 coefficient times 10^-scale.
std::int64_t coefficient() const noexcept
Returns the coefficient.
static Decimal64 parse(std::string_view input)
Parses a decimal string exactly (no binary floating point).
One order a strategy asks the runner to place.
TimeInForce time_in_force() const noexcept
Returns how long the order stays valid.
const std::string & tag() const noexcept
Returns the tag.
const std::optional< std::string > & client_order_id() const noexcept
Returns the idempotency key.
const SizeSpec & size() const noexcept
Returns how much to trade.
const InstrumentId & instrument() const noexcept
Returns the instrument to trade.
OrderSide side() const noexcept
Returns the side.
const std::optional< Price > & trigger() const noexcept
Returns the trigger price.
static OrderIntent limit(InstrumentId instrument, OrderSide side, SizeSpec size, Price limit, TimeInForce time_in_force=TimeInForce::DAY)
Makes a limit order.
const Decimal64 & as_target_weight() const
Returns the target weight.
const Decimal64 & as_quantity() const
Returns the quantity.
SizeKind kind() const noexcept
Returns how the size is expressed.
const Money & as_notional() const
Returns the notional amount.
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.
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.
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.
@ BROKER_REFUSED
The broker refused the instruction (a request was sent).
@ QUANTITY_ROUNDS_TO_ZERO
The quantity rounded down to zero on the lot.
@ NO_REFERENCE_DATA
No reference data for the instrument, so neither the tick grid nor the lot is known.
@ NOT_REPRESENTABLE
The decimal arithmetic did not fit, or the portfolio's rows did not add up.
@ STOP_ORDER_RESTING
A target needs a trade while a stop order rests on the instrument; the stop is cancelled first and no...
@ TRIGGER_OFF_GRID
A stop-limit order's trigger price is not on the instrument's tick grid.
@ UNSUPPORTED_WEIGHT
A weight-sized intent with something a holding cannot express: a sell, a negative weight,...
@ NO_PRICE_AVAILABLE
A price was needed to size the order and none was available.
@ BINDING_REFUSED
The binding refused to send the instruction, so no request was made.
@ NEGATIVE_SIZE
An intent's size is negative: a quantity or a notional amount below zero.
@ NO_PERMITTED_PRICE
Rounding the limit onto the grid, in the cautious direction, would move it out of the day's band: no ...
@ UNKNOWN_PRICE_GRID
The instrument's tick grid is unknown, so a limit price cannot be made valid.
@ NO_PORTFOLIO_VALUE
A weight needed the portfolio's value, and none (or a non-positive one, or one in another currency) w...
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.
@ QUANTITY
A number of shares or contracts.
@ TARGET_WEIGHT
A fraction of portfolio value to hold; the runner trades the difference.
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).
std::optional< Decimal64 > decimal_add(const Decimal64 &lhs, const Decimal64 &rhs)
Adds two decimals exactly.
OrderType
How an order's price is decided (its price basis).
@ LIMIT
지정가: fills at the stated price or better.
@ STOP_LIMIT
스톱지정가: a limit released when a trigger price is reached.
@ MARKET
시장가: fills at whatever the book offers.
constexpr bool carries_trigger_price(OrderType type) noexcept
Tests whether an order type takes a trigger price.
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...
OrderSession
The trading session an order is submitted into.
@ REGULAR
The continuous regular session.
std::optional< Decimal64 > decimal_subtract(const Decimal64 &lhs, const Decimal64 &rhs)
Subtracts two decimals exactly.
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.
Venue
A trading venue: an exchange, or the combined view across exchanges.
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.
TimeInForce
How long an order stays valid and what happens to an unfilled remainder.
@ DAY
Rests until the session ends; an unfilled remainder is cancelled then.
OrderSide
The side of an order: its economic direction, buy or sell.
Identifies an instrument: its country, venue and symbol.
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.
The state of one order, as a broker listing reported it.
OrderType type_
The price basis; UNKNOWN if the broker's code was not recognized.
std::optional< Decimal64 > trigger_price_
The trigger price, whenever the broker reports a positive one.
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.
std::vector< WorkingOrder< Broker > > working_
Orders still outstanding at the broker.
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.
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.
InstrumentId instrument_
The instrument.
std::string tag_
A label carried to the orders and their fills.
Decimal64 target_quantity_
The position to hold, signed: negative is short, and zero means flat.
std::optional< Price > limit_
The limit price for the orders the runner places; without one they are market orders.
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.