algotradelib 0.0.0.9000
C++ header-only library for algorithmic trading
Loading...
Searching...
No Matches
ledger.hpp
Go to the documentation of this file.
1#pragma once
2
4#include "../strategy/portfolio.hpp"
5#include "cost.hpp"
6
7#include <algotradelib/core>
8
9#include <optional>
10#include <string_view>
11#include <utility>
12#include <vector>
13
14namespace algotradelib {
15namespace backtest {
16
21
27 static constexpr std::string_view name = "backtest";
28};
29
55
62public:
69 cash_.push_back(Money{std::move(cash), std::move(currency)});
70 }
71
77 std::optional<Decimal64> cash_in(const Currency& currency) const {
78 for (const auto& amount : cash_) {
79 if (amount.currency_ == currency) {
80 return amount.amount_;
81 }
82 }
83 return std::nullopt;
84 }
85
91 std::optional<Decimal64> quantity_of(const InstrumentId& instrument) const {
92 for (const auto& lot : positions_) {
93 if (lot.instrument_ == instrument) {
94 return lot.quantity_;
95 }
96 }
97 return std::nullopt;
98 }
99
105 std::optional<Decimal64> average_price_of(const InstrumentId& instrument) const {
106 for (const auto& lot : positions_) {
107 if (lot.instrument_ == instrument && lot.quantity_.coefficient() != 0) {
108 return lot.average_purchase_price_;
109 }
110 }
111 return std::nullopt;
112 }
113
119 Decimal64 realized_in(const Currency& currency) const {
120 for (const auto& entry : realized_) {
121 if (entry.currency_ == currency) {
122 return entry.amount_;
123 }
124 }
125 return Decimal64::parse("0");
126 }
127
136 const std::vector<HoldingPosition>& positions() const noexcept { return positions_; }
137
142 const std::vector<Execution>& executions() const noexcept { return executions_; }
143
157 bool apply(const Execution& execution, const Currency& currency) {
158 const auto& fill = execution.fill_;
159 const auto zero = Decimal64::parse("0");
160
161 // Signed quantity: a sale reduces the position.
162 const auto signed_quantity = fill.side_ == OrderSide::BUY ?
163 std::optional<Decimal64>(fill.quantity_) :
164 decimal_subtract(zero, fill.quantity_);
165 if (!signed_quantity) {
166 return false;
167 }
168
169 const auto gross = decimal_multiply(fill.quantity_, fill.price_);
170 if (!gross) {
171 return false;
172 }
173 // Cash out on a buy, in on a sale, and commission out either way.
174 const auto cash_change = fill.side_ == OrderSide::BUY ?
175 decimal_subtract(zero, *gross) :
176 std::optional<Decimal64>(*gross);
177 if (!cash_change) {
178 return false;
179 }
180 const auto after_commission = decimal_subtract(*cash_change, execution.commission_);
181 if (!after_commission) {
182 return false;
183 }
184 // Both the broker's commission and the institution fee leave cash.
185 const auto after_fees = execution.institution_fee_ ?
186 decimal_subtract(*after_commission, *execution.institution_fee_) :
187 after_commission;
188 if (!after_fees) {
189 return false;
190 }
191 const auto after_tax = decimal_subtract(*after_fees, execution.tax_);
192 if (!after_tax) {
193 return false;
194 }
195
196 // Read, never opened. Opening the lot here is what made an early return leave a trace.
197 const auto* existing = find(fill.instrument_);
198 const auto held = existing != nullptr ? existing->quantity_ : zero;
199 const auto average = existing != nullptr ? existing->average_purchase_price_ : zero;
200
201 // A non-flat position cannot change currency; refuse rather than relabel it.
202 if (existing != nullptr && held.coefficient() != 0 &&
203 !(existing->market_value_.currency_ == currency)) {
204 return false;
205 }
206
207 const auto closing = closed_quantity(held, *signed_quantity);
208 if (!closing) {
209 return false;
210 }
211
212 // Realized profit exists only on the part that closed, and only against the old average.
213 std::optional<Decimal64> realized_total;
214 if (closing->coefficient() != 0) {
215 const auto moved = decimal_subtract(fill.price_, average);
216 if (!moved) {
217 return false;
218 }
219 auto banked = decimal_multiply(*closing, *moved);
220 if (!banked) {
221 return false;
222 }
223 // Closing a short banks the opposite sign: it profits when the price fell.
224 if (decimal_less(held, zero)) {
225 banked = decimal_subtract(zero, *banked);
226 if (!banked) {
227 return false;
228 }
229 }
230 realized_total = decimal_add(realized_in(currency), *banked);
231 if (!realized_total) {
232 return false;
233 }
234 }
235
236 const auto resulting = decimal_add(held, *signed_quantity);
237 if (!resulting) {
238 return false;
239 }
240
241 const auto valued = decimal_multiply(*resulting, fill.price_);
242 if (!valued) {
243 return false;
244 }
245
246 // The average price follows only the part that opened or added. A position crossing flat takes the
247 // fill price outright, because nothing of the old basis survives.
248 auto new_average = average;
249 if (resulting->coefficient() == 0) {
250 new_average = zero;
251 } else if (crossed_or_opened(held, *resulting)) {
252 new_average = fill.price_;
253 } else if (adding(held, *signed_quantity)) {
254 const auto blended = weighted_average(held, average, *signed_quantity, fill.price_);
255 if (!blended) {
256 return false;
257 }
258 new_average = *blended;
259 }
260 // Reducing without crossing leaves the average alone: selling half a position does not change
261 // what the remaining half cost.
262
263 const auto cash_total = decimal_add(
264 cash_in(currency).value_or(zero), *after_tax
265 );
266 if (!cash_total) {
267 return false;
268 }
269
270 // --- No arithmetic below can fail, and nothing below returns false -----------------------
271 auto* lot = find_or_open(fill.instrument_);
272 lot->average_purchase_price_ = new_average;
273 lot->quantity_ = *resulting;
274 lot->last_price_ = fill.price_;
275 // **The currency is recorded here because this is the only place it is known.** `mark` is not
276 // told one and neither is `equity_in`'s caller beyond the currency it is asking about, so a
277 // position that never wrote its own currency down cannot be told apart from one denominated in
278 // whatever the caller happened to ask for. `Currency` default-constructs to KRW
279 // (`Currency`'s default constructor), which makes the untold case silently *answer* rather than be absent --
280 // a USD position would read as Korean won to anything summing per currency.
281 lot->market_value_ = Money{*valued, currency};
282 set_amount(cash_, currency, *cash_total);
283 if (realized_total) {
284 set_amount(realized_, currency, *realized_total);
285 }
286 executions_.push_back(execution);
287 return true;
288 }
289
298 bool mark(const InstrumentId& instrument, const Decimal64& price) {
299 bool marked = true;
300 for (auto& lot : positions_) {
301 if (lot.instrument_ == instrument) {
302 // **Both fields move or neither does, and the caller is told which.** Two earlier versions
303 // each lost one half of that. Advancing the price and leaving the old `market_value_`
304 // handed a strategy reading `positions()` or `snapshot()` a stale number presented as
305 // current, with no error anywhere; skipping the row silently declined to apply a price the
306 // caller asked for. The currency is the one `apply` recorded -- a lot exists only because a
307 // fill opened it -- and it is the half nothing can invalidate.
308 const auto valued = decimal_multiply(lot.quantity_, price);
309 if (!valued) {
310 marked = false;
311 continue;
312 }
313 lot.last_price_ = price;
314 lot.market_value_ = Money{*valued, lot.market_value_.currency_};
315 }
316 }
317 return marked;
318 }
319
331 template <typename BrokerTag>
333 std::chrono::system_clock::time_point as_of
334 ) const {
336 portfolio.positions_ = positions_;
337 // Every held share is sellable: a model consistent with crediting sales on the fill (no
338 // settlement); `ReportCaveats` discloses it.
339 for (auto& position : portfolio.positions_) {
340 position.sellable_quantity_ = position.quantity_;
341 }
342 // The basis a producer of sellable figures must state: whole holding, deduct resting, unverified.
343 portfolio.sellable_basis_ = SellableBasis{
346 false,
347 };
348 portfolio.cash_ = cash_;
349 // Buying power modelled as this ledger's cash, stamped at the snapshot time; the report states
350 // the model.
351 portfolio.buying_power_.reserve(cash_.size());
352 for (const auto& amount : cash_) {
353 portfolio.buying_power_.push_back(BuyingPower{amount, as_of});
354 }
355 portfolio.as_of_ = as_of;
356 return portfolio;
357 }
358
367 std::optional<Decimal64> equity_in(const Currency& currency) const {
368 auto total = cash_in(currency).value_or(Decimal64::parse("0"));
369 for (const auto& lot : positions_) {
370 if (lot.quantity_.coefficient() == 0) {
371 continue;
372 }
373 // Only positions in `currency` (recorded on `market_value_` by `apply`); no conversion.
374 if (!(lot.market_value_.currency_ == currency)) {
375 continue;
376 }
377 // Computed from the last mark rather than read from `market_value_`.
378 const auto value = decimal_multiply(lot.quantity_, lot.last_price_);
379 if (!value) {
380 return std::nullopt;
381 }
382 const auto sum = decimal_add(total, *value);
383 if (!sum) {
384 return std::nullopt;
385 }
386 total = *sum;
387 }
388 return total;
389 }
390
391private:
392 // Returns the position in `instrument`, or nothing, **without creating one**.
393 const HoldingPosition* find(const InstrumentId& instrument) const {
394 for (const auto& lot : positions_) {
395 if (lot.instrument_ == instrument) {
396 return &lot;
397 }
398 }
399 return nullptr;
400 }
401
402 HoldingPosition* find_or_open(const InstrumentId& instrument) {
403 for (auto& lot : positions_) {
404 if (lot.instrument_ == instrument) {
405 return &lot;
406 }
407 }
408 HoldingPosition opened;
409 opened.instrument_ = instrument;
410 opened.quantity_ = Decimal64::parse("0");
411 opened.average_purchase_price_ = Decimal64::parse("0");
412 opened.last_price_ = Decimal64::parse("0");
413 positions_.push_back(opened);
414 return &positions_.back();
415 }
416
417 // Returns how much of `held` this trade closes, which is zero when it only adds.
418 static std::optional<Decimal64> closed_quantity(
419 const Decimal64& held,
420 const Decimal64& traded
421 ) {
422 const auto zero = Decimal64::parse("0");
423 const bool long_held = decimal_less(zero, held);
424 const bool short_held = decimal_less(held, zero);
425 const bool buying = decimal_less(zero, traded);
426 if ((long_held && buying) || (short_held && !buying) || held.coefficient() == 0) {
427 return zero;
428 }
429 const auto magnitude_held = long_held ? held : decimal_subtract(zero, held);
430 const auto magnitude_traded = buying ? traded : decimal_subtract(zero, traded);
431 if (!magnitude_held || !magnitude_traded) {
432 return std::nullopt;
433 }
434 return decimal_less(*magnitude_held, *magnitude_traded) ? *magnitude_held : *magnitude_traded;
435 }
436
437 // Returns whether the trade opened from flat or crossed through it.
438 static bool crossed_or_opened(const Decimal64& held, const Decimal64& resulting) {
439 const auto zero = Decimal64::parse("0");
440 if (held.coefficient() == 0) {
441 return true;
442 }
443 return decimal_less(zero, held) != decimal_less(zero, resulting);
444 }
445
446 // Returns whether the trade added to a position rather than reducing it.
447 static bool adding(const Decimal64& held, const Decimal64& traded) {
448 const auto zero = Decimal64::parse("0");
449 return decimal_less(zero, held) == decimal_less(zero, traded);
450 }
451
452 // Returns the average of two lots by magnitude, which is what adding to a position produces.
453 static std::optional<Decimal64> weighted_average(
454 const Decimal64& held,
455 const Decimal64& held_price,
456 const Decimal64& traded,
457 const Decimal64& traded_price
458 ) {
459 const auto zero = Decimal64::parse("0");
460 const auto held_magnitude = decimal_less(held, zero) ? decimal_subtract(zero, held) : held;
461 const auto traded_magnitude =
462 decimal_less(traded, zero) ? decimal_subtract(zero, traded) : traded;
463 if (!held_magnitude || !traded_magnitude) {
464 return std::nullopt;
465 }
466 const auto held_cost = decimal_multiply(*held_magnitude, held_price);
467 const auto traded_cost = decimal_multiply(*traded_magnitude, traded_price);
468 if (!held_cost || !traded_cost) {
469 return std::nullopt;
470 }
471 const auto total_cost = decimal_add(*held_cost, *traded_cost);
472 const auto total_quantity = decimal_add(*held_magnitude, *traded_magnitude);
473 if (!total_cost || !total_quantity || total_quantity->coefficient() == 0) {
474 return std::nullopt;
475 }
476 // Four places, which holds a tick on every grid this library knows and more than a price needs.
477 return decimal_divide(*total_cost, *total_quantity, 4);
478 }
479
480 // Writes `total` as the whole amount held in `currency`, appending the entry when there is none.
481 //
482 // Takes the **total** rather than a change, because the addition is the fallible half and `apply`
483 // has already done it before committing anything. What is left cannot fail, which is what lets
484 // `apply` promise the ledger is either wholly updated or untouched.
485 static void set_amount(
486 std::vector<Money>& amounts,
487 const Currency& currency,
488 const Decimal64& total
489 ) {
490 for (auto& amount : amounts) {
491 if (amount.currency_ == currency) {
492 amount.amount_ = total;
493 return;
494 }
495 }
496 amounts.push_back(Money{total, currency});
497 }
498
499 std::vector<HoldingPosition> positions_;
500 std::vector<Money> cash_;
501 std::vector<Money> realized_;
502 std::vector<Execution> executions_;
503};
504
505} // namespace backtest
506} // 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
static Decimal64 parse(std::string_view input)
Parses a decimal string exactly (no binary floating point).
Definition types.hpp:323
std::optional< Decimal64 > quantity_of(const InstrumentId &instrument) const
Returns the quantity held of an instrument.
Definition ledger.hpp:91
std::optional< Decimal64 > average_price_of(const InstrumentId &instrument) const
Returns the average price of the position in an instrument.
Definition ledger.hpp:105
const std::vector< HoldingPosition > & positions() const noexcept
Returns every position, including ones that went flat.
Definition ledger.hpp:136
std::optional< Decimal64 > equity_in(const Currency &currency) const
Returns total equity in a currency: cash plus positions at their last marks.
Definition ledger.hpp:367
const std::vector< Execution > & executions() const noexcept
Returns every execution applied.
Definition ledger.hpp:142
ReplayLedger(Decimal64 cash, Currency currency)
Constructs a ledger holding only cash.
Definition ledger.hpp:68
bool mark(const InstrumentId &instrument, const Decimal64 &price)
Marks positions in an instrument to a price, updating their market value.
Definition ledger.hpp:298
Decimal64 realized_in(const Currency &currency) const
Returns the profit realized by closing positions, in a currency (fees excluded).
Definition ledger.hpp:119
strategy::Portfolio< BrokerTag > snapshot(std::chrono::system_clock::time_point as_of) const
Returns the portfolio a strategy sees.
Definition ledger.hpp:332
bool apply(const Execution &execution, const Currency &currency)
Applies one execution to the position, cash and realized profit.
Definition ledger.hpp:157
std::optional< Decimal64 > cash_in(const Currency &currency) const
Returns the cash held in a currency.
Definition ledger.hpp:77
Trading costs for a backtest: commission (CommissionModel, CommissionPolicy), slippage (SlippageModel...
The events a runner delivers to a strategy's handlers, and the market phase.
Replays a strategy over stored bars: fill and cost models, the replay ledger, and the report.
Definition clock.hpp:14
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
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
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
What the broker says may be spent, in one currency.
Definition types.hpp:1126
One position row in a holdings snapshot.
Definition types.hpp:1017
InstrumentId instrument_
The instrument held (brokers report holdings without a trading venue).
Definition types.hpp:1019
Identifies an instrument: its country, venue and symbol.
Definition types.hpp:78
An exact amount of money in one currency.
Definition types.hpp:895
What a sellable quantity covers, and how RiskGuard must treat resting sells against it.
Definition types.hpp:918
@ WHOLE_HOLDING
Every share held of the instrument, whatever venue or lot.
Definition types.hpp:924
@ DEDUCT
Subtract every resting sell from the figure.
Definition types.hpp:942
The commission rate that charged an execution, as the backtest report lists it.
Definition cost.hpp:124
The broker tag of a replay's order references (they reach no broker).
Definition ledger.hpp:25
static constexpr std::string_view name
The name an order reference prints: "backtest".
Definition ledger.hpp:27
One execution a backtest produced, and what it cost.
Definition ledger.hpp:31
std::optional< Decimal64 > institution_fee_
The institution fee (유관기관제비용), if the rate was split; empty is not zero.
Definition ledger.hpp:41
Decimal64 commission_
The broker's commission; includes the institution fee when institution_fee_ is empty.
Definition ledger.hpp:35
AppliedRate applied_
The commission rate that charged this execution.
Definition ledger.hpp:45
Decimal64 slippage_
The price given up to slippage (already included in the fill price).
Definition ledger.hpp:47
strategy::Fill fill_
The fill, as on_fill receives it.
Definition ledger.hpp:33
Decimal64 tax_
The sale tax charged; zero unless a sell tax applies (NextBarFillModel::with_sell_tax).
Definition ledger.hpp:53
Currency currency_
The currency of the cost figures.
Definition ledger.hpp:43
One execution of an order the strategy placed, delivered to on_fill.
Definition event.hpp:407
What the account holds and which orders are still working, as a strategy sees it.
Definition portfolio.hpp:50
SellableBasis sellable_basis_
What the sellable_quantity_ figures on positions_ cover.
std::vector< Money > cash_
Cash per currency, in a backtest only.
Definition portfolio.hpp:64
std::vector< BuyingPower > buying_power_
What may be spent, per currency.
Definition portfolio.hpp:71
std::chrono::system_clock::time_point as_of_
When the holdings were received, on this machine's clock.
Definition portfolio.hpp:84
std::vector< HoldingPosition > positions_
Every position row the broker reported.
Definition portfolio.hpp:56