algotradelib 0.0.0.9000
C++ header-only library for algorithmic trading
Loading...
Searching...
No Matches
sellable_convention.hpp
1#pragma once
2
3#include "./portfolio.hpp"
4
5#include <algotradelib/core>
6
7#include <cstddef>
8#include <string_view>
9#include <utility>
10#include <vector>
11
12namespace algotradelib {
13namespace strategy {
14
31
37inline constexpr std::string_view sellable_convention_name(SellableConvention convention) noexcept {
38 switch (convention) {
40 return "undetermined";
42 return "net_of_resting";
44 return "gross_of_resting";
46 return "inconsistent";
47 }
48 return "undetermined";
49}
50
59 std::size_t net_{0};
61 std::size_t gross_{0};
62
68 SellableConvention verdict() const noexcept {
69 if (net_ > 0 && gross_ > 0) {
71 }
72 if (net_ > 0) {
74 }
75 if (gross_ > 0) {
77 }
79 }
80
85 std::size_t total() const noexcept { return net_ + gross_; }
86};
87
103template <typename Broker>
105public:
110 void observe(const Portfolio<Broker>& portfolio) {
111 // **A figure of unknown coverage teaches nothing about resting orders, and would teach something
112 // false.** This probe reads the *distance* between what is held and what is sellable and watches
113 // how that distance moves. Both numbers have to describe the same shares for the distance to mean
114 // anything: if `rmnd_qty` is account-wide while `trde_able_qty` covers one venue -- exactly what
115 // `SellableBasis::Coverage::UNESTABLISHED` means at Kiwoom -- the distance is a venue split, and a
116 // position moving between venues would read as a resting convention. So an unenforceable basis is
117 // skipped whole rather than judged, and the verdict stays `UNDETERMINED`, which is true.
118 for (const auto& position : portfolio.positions_) {
119 const auto reading = portfolio.sellable_reading_for(position.instrument_);
120 if (!reading.basis_.is_enforceable()) {
121 // **Skipped, and the baseline is dropped with it.** Merely ignoring the reading would leave the
122 // previous one pending, so the *next* enforceable reading would be compared against a
123 // baseline from before the gap -- and the distance would appear to move by whatever happened
124 // in between. A declared 100/100/0, an undeclared 100/70/0, then a declared 100/70/30 would
125 // record a confident NET_OF_RESTING across a window the probe never observed.
126 //
127 // Resolved per instrument rather than per portfolio, because a per-instrument inquiry's basis
128 // lands on its own row: checking only the snapshot's would discard every Toss reading.
129 forget(position.instrument_);
130 continue;
131 }
132 if (!position.sellable_quantity_) {
133 // The broker did not publish it, so there is nothing to learn from this position.
134 continue;
135 }
136 const auto resting = reported_resting(portfolio, position.instrument_);
137 const Reading now{position.quantity_, *position.sellable_quantity_, resting};
138 if (auto* previous = find(last_, position.instrument_)) {
139 judge(position.instrument_, *previous, now);
140 *previous = now;
141 continue;
142 }
143 last_.push_back({position.instrument_, now});
144 }
145 }
146
152 void forget(const InstrumentId& instrument) {
153 for (auto it = last_.begin(); it != last_.end(); ++it) {
154 if (it->first == instrument) {
155 last_.erase(it);
156 return;
157 }
158 }
159 }
160
166 SellableEvidence evidence(const InstrumentId& instrument) const {
167 if (const auto* found = find(evidence_, instrument)) {
168 return *found;
169 }
170 return SellableEvidence{};
171 }
172
178 SellableEvidence total;
179 for (const auto& entry : evidence_) {
180 total.net_ += entry.second.net_;
181 total.gross_ += entry.second.gross_;
182 }
183 return total;
184 }
185
191
196 const std::vector<std::pair<InstrumentId, SellableEvidence>>& per_instrument() const noexcept {
197 return evidence_;
198 }
199
200private:
201 // Returns the sell quantity **the broker reported** resting, ignoring the runner's reservations.
202 //
203 // A runner adds a row of its own for an order it has just sent, so that the next order in the
204 // pass cannot re-sell the same shares. Those rows are this library's arithmetic, and counting
205 // them here would be circular: the resting quantity would rise with no change in the broker's
206 // sellable figure, and the probe would record `GROSS_OF_RESTING` on evidence it manufactured
207 // itself -- every time, for any broker, whenever the sellable check happened to be enabled.
208 static Decimal64 reported_resting(
209 const Portfolio<Broker>& portfolio,
210 const InstrumentId& instrument
211 ) {
212 Decimal64 total = Decimal64::parse("0");
213 for (const auto& order : portfolio.working_) {
214 if (order.reserved_ || !(order.order_.instrument_ == instrument) ||
215 order.order_.side_ != OrderSide::SELL) {
216 continue;
217 }
218 if (!order_is_open(order.order_.status_)) {
219 continue;
220 }
221 const auto remaining = decimal_subtract(
222 order.order_.quantity_, order.order_.execution_.filled_quantity_
223 );
224 if (!remaining || decimal_less(*remaining, Decimal64::parse("0"))) {
225 continue;
226 }
227 if (const auto sum = decimal_add(total, *remaining)) {
228 total = *sum;
229 }
230 }
231 return total;
232 }
233
234 // One position as a poll reported it.
235 struct Reading {
236 Decimal64 held_;
237 Decimal64 sellable_;
238 Decimal64 resting_;
239 };
240
241 // Returns whether two decimals are numerically equal, across differing scales.
242 static bool same(const Decimal64& lhs, const Decimal64& rhs) {
243 // `operator==` compares the coefficient and the scale, so "70" and "70.0" are not equal by it.
244 // A broker is free to change the scale it reports between two polls, and a probe that read that
245 // as a change would conclude from noise.
246 return !decimal_less(lhs, rhs) && !decimal_less(rhs, lhs);
247 }
248
249 // Records what the move from `before` to `now` says, if it says anything.
250 void judge(const InstrumentId& instrument, const Reading& before, const Reading& now) {
251 if (!same(before.held_, now.held_)) {
252 return;
253 }
254 const auto grew = decimal_subtract(now.resting_, before.resting_);
255 if (!grew || !decimal_less(Decimal64::parse("0"), *grew)) {
256 return;
257 }
258 const auto gap_before = decimal_subtract(before.held_, before.sellable_);
259 const auto gap_now = decimal_subtract(now.held_, now.sellable_);
260 if (!gap_before || !gap_now) {
261 return;
262 }
263 const auto moved = decimal_subtract(*gap_now, *gap_before);
264 if (!moved) {
265 return;
266 }
267
268 auto* entry = find(evidence_, instrument);
269 if (entry == nullptr) {
270 evidence_.push_back({instrument, SellableEvidence{}});
271 entry = &evidence_.back().second;
272 }
273 if (same(*moved, *grew)) {
274 ++entry->net_;
275 } else if (same(*moved, Decimal64::parse("0"))) {
276 ++entry->gross_;
277 }
278 // Anything else moved by an amount neither rule predicts -- a settlement in the same window,
279 // most likely -- and is discarded rather than forced into one of the two answers.
280 }
281
282 // Returns the entry for `instrument`, or null.
283 template <typename Entries>
284 static auto find(Entries& entries, const InstrumentId& instrument)
285 -> decltype(&entries.front().second) {
286 for (auto& entry : entries) {
287 if (entry.first == instrument) {
288 return &entry.second;
289 }
290 }
291 return nullptr;
292 }
293
294 std::vector<std::pair<InstrumentId, Reading>> last_;
295 std::vector<std::pair<InstrumentId, SellableEvidence>> evidence_;
296};
297
298} // namespace strategy
299} // namespace algotradelib
An exact signed decimal: an int64 coefficient times 10^-scale.
Definition types.hpp:297
static Decimal64 parse(std::string_view input)
Parses a decimal string exactly (no binary floating point).
Definition types.hpp:323
Infers a broker's SellableConvention from successive portfolios.
SellableEvidence evidence(const InstrumentId &instrument) const
Returns the evidence for one instrument.
SellableConvention verdict() const
Returns the broker's convention as observed so far.
SellableEvidence overall() const
Returns the evidence summed over every instrument.
const std::vector< std::pair< InstrumentId, SellableEvidence > > & per_instrument() const noexcept
Returns the evidence per instrument.
void observe(const Portfolio< Broker > &portfolio)
Records one polled portfolio, comparing it with the previous one.
void forget(const InstrumentId &instrument)
Discards the last recorded reading for an instrument, so the next one is not compared across a gap.
What a strategy sees and emits: events, intents, the portfolio, reconciliation and risk limits.
Definition concept.hpp:16
SellableConvention
Whether a broker's sellable quantity already excludes sell orders resting at the broker.
@ INCONSISTENT
Both behaviours were observed; no single rule fits.
@ GROSS_OF_RESTING
The figure did not move when a sell began resting: the broker includes them.
@ NET_OF_RESTING
The figure fell when a sell began resting: the broker excludes resting sells.
@ UNDETERMINED
Nothing conclusive has been observed yet.
bool order_is_open(OrderStatus status)
Tests whether an order in a given status may still trade.
Definition portfolio.hpp:20
constexpr std::string_view sellable_convention_name(SellableConvention convention) noexcept
Returns a lowercase name for a convention, such as "net_of_resting", for logs.
The library: broker-neutral types (core), and the modules below.
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
Identifies an instrument: its country, venue and symbol.
Definition types.hpp:78
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
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
How many observations supported each answer.
std::size_t total() const noexcept
Returns the number of observations.
std::size_t gross_
Observations where the figure did not move when a sell began resting.
SellableConvention verdict() const noexcept
Returns the convention the counts support.
std::size_t net_
Observations where the figure fell by the quantity that began resting.