algotradelib 0.0.0.9000
C++ header-only library for algorithmic trading
Loading...
Searching...
No Matches
cost.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <algotradelib/core>
4
5#include <cstdint>
6#include <optional>
7#include <string>
8#include <string_view>
9#include <utility>
10
11namespace algotradelib {
12namespace backtest {
13
22
37 bool from_broker_{false};
38
46 std::optional<Decimal64> on(const Decimal64& quantity, const Decimal64& price) const {
47 const auto value = decimal_multiply(quantity, price);
48 if (!value) {
49 return std::nullopt;
50 }
51 auto charged = decimal_multiply(*value, rate_);
52 if (!charged) {
53 return std::nullopt;
54 }
55 // Truncated to whole currency units, as a broker charges.
56 const auto whole = decimal_divide(*charged, Decimal64::parse("1"), 0);
57 if (!whole) {
58 return std::nullopt;
59 }
60 return decimal_less(*whole, minimum_) ? minimum_ : *whole;
61 }
62
67 static CommissionModel none() { return CommissionModel{}; }
68
75 static CommissionModel of(Decimal64 rate, Decimal64 minimum = Decimal64::parse("0")) {
76 return CommissionModel{std::move(rate), std::move(minimum)};
77 }
78};
79
95 const CommissionSchedule& schedule,
96 Country country,
97 const Date& date
98) {
99 const auto selection = schedule.rate_for(country, date);
100 if (!selection.rate_) {
101 BrokerError error;
103 // Nothing was asked of anybody: this reads a schedule already in hand.
104 error.reached_broker_ = false;
105 // Distinct codes: a missing row wants a rate stated; an unreadable window wants a refetch.
106 if (selection.bounds_unreadable_) {
107 error.broker_code_ = "rate_bounds_unreadable";
108 error.message_ = "the published schedule holds a commission rate for that market whose date "
109 "bounds could not be read, so whether it applies on that date is unknown";
110 return Result<CommissionModel>(std::move(error));
111 }
112 error.broker_code_ = "no_rate_for_date";
113 error.message_ = "the published schedule states no commission rate for that market and date";
114 return Result<CommissionModel>(std::move(error));
115 }
116 const auto& rate = selection.rate_;
117 auto model = CommissionModel::of(rate->rate_);
118 // A caller's stated schedule stays the caller's: the report says where the rate it charged came from.
119 model.from_broker_ = commission_origin_is_broker(schedule.origin_);
120 return Result<CommissionModel>(std::move(model));
121}
122
126 Country country_{Country::KR};
132 std::optional<Decimal64> institution_rate_;
136 std::optional<Date> from_;
138 std::optional<Date> until_;
146 std::size_t executions_{0};
147
154 friend bool operator==(const AppliedRate& lhs, const AppliedRate& rhs) {
155 return lhs.country_ == rhs.country_ && lhs.venue_ == rhs.venue_ && lhs.rate_ == rhs.rate_ &&
156 lhs.institution_rate_ == rhs.institution_rate_ && lhs.minimum_ == rhs.minimum_ &&
157 lhs.from_ == rhs.from_ && lhs.until_ == rhs.until_ && lhs.origin_ == rhs.origin_;
158 }
159};
160
175
185public:
187 CommissionPolicy() = default;
188
195 CommissionPolicy policy;
196 policy.flat_ = std::move(model);
197 return policy;
198 }
199
212 CommissionSchedule schedule,
213 VenueKey asserted,
214 Country dating_market
215 ) {
216 if (commission_origin_names_tariff(schedule.origin_) && schedule.tariff_.empty()) {
217 return Result<CommissionPolicy>(refusal(
218 "commission_tariff_unnamed",
219 "a caller-stated commission schedule must name the tariff it was read from, because the "
220 "published table is channel by venue and nothing here can select the channel"
221 ));
222 }
223 if (asserted.kind() == VenueKeyKind::NORMALIZED && asserted.venue() == Venue::COMBINED) {
224 return Result<CommissionPolicy>(refusal(
225 "commission_venue_not_an_execution",
226 "COMBINED is consolidated routing rather than a venue an order matched on, so it cannot be "
227 "asserted as the venue a fill happened at"
228 ));
229 }
230 CommissionPolicy policy;
231 policy.schedule_ = std::move(schedule);
232 policy.asserted_ = std::move(asserted);
233 policy.dating_market_ = dating_market;
234 return Result<CommissionPolicy>(std::move(policy));
235 }
236
241 bool is_scheduled() const noexcept { return schedule_.has_value(); }
242
247 const VenueKey& asserted_venue() const noexcept { return asserted_; }
248
253 std::string_view tariff() const noexcept {
254 return schedule_ ? std::string_view(schedule_->tariff_) : std::string_view();
255 }
256
271 const Decimal64& quantity,
272 const Decimal64& price,
273 const Date& date,
274 Country country
275 ) const {
276 if (!schedule_) {
277 const auto charged = flat_.on(quantity, price);
278 if (!charged) {
279 return Result<CommissionCharge>(refusal(
280 "commission_not_representable",
281 "the commission on that execution does not fit an exact decimal"
282 ));
283 }
284 CommissionCharge result;
285 result.broker_ = *charged;
286 // A flat model is one inclusive figure: no institution split.
287 result.applied_.country_ = country;
288 result.applied_.rate_ = flat_.rate_;
289 result.applied_.minimum_ = flat_.minimum_;
290 result.applied_.origin_ = flat_.from_broker_ ?
293 return Result<CommissionCharge>(std::move(result));
294 }
295
296 // A market other than the dating market is refused if any of its rates is dated: the replay's
297 // date is another market's day, so which dated rate applies cannot be decided.
298 if (country != dating_market_ && schedule_->any_dated_rate_for(country)) {
299 BrokerError error;
301 error.reached_broker_ = false;
302 error.broker_code_ = "commission_date_not_market_local";
303 error.message_ = "this market's rates have dated bounds, and the replay's date " +
304 named(date) + " is that of another market's trading day, so which rate applies cannot be "
305 "decided here; price it in a replay whose session is that market's, or state rates with "
306 "no dated bounds";
307 return Result<CommissionCharge>(std::move(error));
308 }
309
310 const auto selection = schedule_->rate_for(country, asserted_, date);
311 const auto& rate = selection.rate_;
312 if (!rate) {
313 BrokerError error;
315 error.reached_broker_ = false;
316 // Name the date, and distinguish an unreadable window (refetch) from a missing rate (state one).
317 if (selection.bounds_unreadable_) {
318 error.broker_code_ = "rate_bounds_unreadable";
319 error.message_ = "the schedule holds a commission rate for " + described(asserted_) +
320 " in this instrument's market whose date bounds could not be read, so whether it applies "
321 "on " + named(date) + " is unknown; refetch the schedule or state the rates";
322 return Result<CommissionCharge>(std::move(error));
323 }
324 error.broker_code_ = "no_rate_for_date";
325 error.message_ = "the schedule states no commission rate for " + named(date) +
326 " at venue " + described(asserted_) + " in the market this instrument is listed in";
327 return Result<CommissionCharge>(std::move(error));
328 }
329
330 // No minimum on a scheduled rate: no schedule states one. A caller with a minimum uses `stated`.
331 const auto broker = component(quantity, price, rate->rate_, Decimal64::parse("0"));
332 if (!broker) {
333 return Result<CommissionCharge>(refusal(
334 "commission_not_representable",
335 "the commission on that execution does not fit an exact decimal"
336 ));
337 }
338 CommissionCharge result;
339 result.broker_ = *broker;
340 if (rate->institution_rate_) {
341 // Its own component, with no minimum (the minimum is the broker's).
342 const auto institution =
343 component(quantity, price, *rate->institution_rate_, Decimal64::parse("0"));
344 if (!institution) {
345 return Result<CommissionCharge>(refusal(
346 "commission_not_representable",
347 "the institution fee on that execution does not fit an exact decimal"
348 ));
349 }
350 result.institution_ = *institution;
351 }
352 result.applied_.country_ = country;
353 result.applied_.venue_ = rate->venue_;
354 result.applied_.rate_ = rate->rate_;
355 result.applied_.institution_rate_ = rate->institution_rate_;
356 result.applied_.from_ = rate->from_;
357 result.applied_.until_ = rate->until_;
358 result.applied_.origin_ = schedule_->origin_;
359 return Result<CommissionCharge>(std::move(result));
360 }
361
362private:
363 // The venue a refusal was asked about, as the asker spelled it.
364 static std::string described(const VenueKey& venue) {
365 switch (venue.kind()) {
367 return "(none asserted)";
369 return std::string(venue_name(venue.venue()));
371 return "'" + venue.raw_code().str() + "'";
372 }
373 return "(unknown)";
374 }
375
376 // `date` as `YYYY-MM-DD`.
377 static std::string named(const Date& date) {
378 const auto pad = [](unsigned value) {
379 return value < 10 ? "0" + std::to_string(value) : std::to_string(value);
380 };
381 return std::to_string(date.year_) + "-" + pad(date.month_) + "-" + pad(date.day_);
382 }
383
384 static BrokerError refusal(std::string code, std::string message) {
385 BrokerError error;
386 error.category_ = ErrorCategory::INVALID_ARGUMENT;
387 // Nothing was asked of anybody: this reads a schedule already in hand.
388 error.reached_broker_ = false;
389 error.broker_code_ = std::move(code);
390 error.message_ = std::move(message);
391 return error;
392 }
393
394 // One component's charge, truncated to whole units and floored at `minimum`.
395 static std::optional<Decimal64> component(
396 const Decimal64& quantity,
397 const Decimal64& price,
398 const Decimal64& rate,
399 const Decimal64& minimum
400 ) {
401 CommissionModel model;
402 model.rate_ = rate;
403 model.minimum_ = minimum;
404 return model.on(quantity, price);
405 }
406
407 // The stated model; unused by a scheduled policy.
408 CommissionModel flat_{};
409 // Present exactly when this policy resolves a schedule.
410 std::optional<CommissionSchedule> schedule_;
411 // The venue a caller asserted its fills happened at.
412 VenueKey asserted_;
413 // The market whose local trading day `charge`'s date is; dated rates of other markets refuse.
414 Country dating_market_{Country::KR};
415};
416
429 int ticks_{0};
430
444 std::optional<std::pair<Decimal64, Decimal64>> apply(
445 const Decimal64& price,
446 OrderSide side,
447 const PriceRule& rule
448 ) const {
449 auto moved = price;
450
451 if (basis_points_.coefficient() != 0) {
452 const auto fraction = decimal_divide(basis_points_, Decimal64::parse("10000"), 8);
453 if (!fraction) {
454 return std::nullopt;
455 }
456 const auto amount = decimal_multiply(price, *fraction);
457 if (!amount) {
458 return std::nullopt;
459 }
460 const auto shifted = side == OrderSide::BUY ?
461 decimal_add(moved, *amount) :
462 decimal_subtract(moved, *amount);
463 if (!shifted) {
464 return std::nullopt;
465 }
466 moved = *shifted;
467 }
468
469 if (ticks_ > 0) {
470 // One step at a time, each with the tick where it starts: the first tick of a band is not the
471 // last tick of the band below.
472 const auto zero = Decimal64::parse("0");
473 auto walked = price;
474 for (int step = 0; step < ticks_; ++step) {
475 const auto tick = side == OrderSide::BUY ?
476 rule.tick_at(walked) :
477 tick_below(rule, walked);
478 if (!tick) {
479 // An unknown grid, or the bottom of a known one, contributes nothing rather than a guessed step.
480 break;
481 }
482 const auto next = side == OrderSide::BUY ?
483 decimal_add(walked, *tick) :
484 decimal_subtract(walked, *tick);
485 if (!next) {
486 return std::nullopt;
487 }
488 if (!decimal_less(zero, *next)) {
489 // No price at or below zero.
490 break;
491 }
492 walked = *next;
493 }
494 const auto steps = side == OrderSide::BUY ?
495 decimal_subtract(walked, price) :
496 decimal_subtract(price, walked);
497 if (!steps) {
498 return std::nullopt;
499 }
500 const auto shifted = side == OrderSide::BUY ?
501 decimal_add(moved, *steps) :
502 decimal_subtract(moved, *steps);
503 if (!shifted) {
504 return std::nullopt;
505 }
506 moved = *shifted;
507 }
508
509 const auto given_up = side == OrderSide::BUY ?
510 decimal_subtract(moved, price) :
511 decimal_subtract(price, moved);
512 if (!given_up) {
513 return std::nullopt;
514 }
515 return std::pair<Decimal64, Decimal64>{moved, *given_up};
516 }
517
522 static SlippageModel none() { return SlippageModel{}; }
523
530 SlippageModel model;
531 model.basis_points_ = std::move(points);
532 return model;
533 }
534
540 static SlippageModel of_ticks(int count) {
541 SlippageModel model;
542 model.ticks_ = count;
543 return model;
544 }
545
546private:
547 // The tick of the band below a price: the step down to the next lower grid price, for a price on a
548 // grid like KRX's. None at or below the first band's start, or without bands.
549 static std::optional<Decimal64> tick_below(const PriceRule& rule, const Decimal64& price) {
550 std::optional<Decimal64> tick;
551 for (const auto& band : rule.bands_) {
552 if (!decimal_less(band.from_, price)) {
553 break;
554 }
555 tick = band.tick_;
556 }
557 return tick;
558 }
559};
560
578
584inline constexpr std::string_view limit_fill_price_name(LimitFillPrice choice) noexcept {
585 switch (choice) {
587 return "at_limit";
589 return "at_open_if_better";
590 }
591 return "unknown";
592}
593
594} // namespace backtest
595} // namespace algotradelib
std::string str() const
Returns the code as a string.
Definition types.hpp:192
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
Either a value or a BrokerError.
Definition types.hpp:797
Result(T value)
Constructs a successful result.
Definition types.hpp:803
Which venue a commission rate applies to: any venue, a normalized Venue, or a raw code.
Definition types.hpp:1244
const Code< 8 > & raw_code() const noexcept
Returns the raw venue code.
Definition types.hpp:1308
VenueKeyKind kind() const noexcept
Returns the key's kind.
Definition types.hpp:1290
Venue venue() const noexcept
Returns the normalized venue.
Definition types.hpp:1302
CommissionPolicy()=default
Constructs a policy that charges nothing (the report says so).
static Result< CommissionPolicy > scheduled(CommissionSchedule schedule, VenueKey asserted, Country dating_market)
Makes a policy that picks a rate from a schedule for each execution.
Definition cost.hpp:211
const VenueKey & asserted_venue() const noexcept
Returns the asserted venue.
Definition cost.hpp:247
std::string_view tariff() const noexcept
Returns the schedule's tariff name.
Definition cost.hpp:253
static CommissionPolicy stated(CommissionModel model)
Makes a policy that charges one model on every execution.
Definition cost.hpp:194
Result< CommissionCharge > charge(const Decimal64 &quantity, const Decimal64 &price, const Date &date, Country country) const
Computes the commission on one execution.
Definition cost.hpp:270
bool is_scheduled() const noexcept
Tests whether the policy resolves a schedule.
Definition cost.hpp:241
Replays a strategy over stored bars: fill and cost models, the replay ledger, and the report.
Definition clock.hpp:14
Result< CommissionModel > commission_model_from(const CommissionSchedule &schedule, Country country, const Date &date)
Builds a flat commission model from a schedule's rate for a market on a date.
Definition cost.hpp:94
LimitFillPrice
Which price a backtest credits a limit order that fills.
Definition cost.hpp:562
@ AT_OPEN_IF_BETTER
Fill at the bar's open when it opened through the limit (assumes the order was resting before the ope...
Definition cost.hpp:576
@ AT_LIMIT
The default: fill at the limit, or at the bar's worst traded price when the whole bar was better than...
Definition cost.hpp:571
constexpr std::string_view limit_fill_price_name(LimitFillPrice choice) noexcept
Returns a lowercase name for a choice, such as "at_limit", for reports.
Definition cost.hpp:584
The library: broker-neutral types (core), and the modules below.
@ NORMALIZED
A Venue the library normalizes.
Definition types.hpp:1232
@ RAW
A broker venue code the library does not normalize, kept verbatim.
Definition types.hpp:1234
@ ANY
Any venue (a venue-neutral rate).
Definition types.hpp:1230
CommissionOrigin
Where a commission schedule's numbers came from.
Definition types.hpp:1374
@ CALLER_STATED
The caller stated them, from a fee page or their own agreement.
Definition types.hpp:1378
@ BROKER_PUBLISHED
The broker published them, through an inquiry the library made.
Definition types.hpp:1376
constexpr bool commission_origin_names_tariff(CommissionOrigin origin) noexcept
Tests whether a schedule of this origin must name the tariff it was read from.
Definition types.hpp:1401
std::optional< Decimal64 > decimal_add(const Decimal64 &lhs, const Decimal64 &rhs)
Adds two decimals exactly.
Definition types.hpp:509
@ INVALID_ARGUMENT
The request was invalid, refused by the client before sending or by the broker.
Definition types.hpp:713
Country
A country (market) the library supports.
Definition types.hpp:30
constexpr bool commission_origin_is_broker(CommissionOrigin origin) noexcept
Tests whether commission rates of this origin came from the broker.
Definition types.hpp:1386
constexpr std::string_view venue_name(Venue venue) noexcept
Returns a display name for a venue, such as "KRX".
Definition types.hpp:58
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
OrderSide
The side of an order: its economic direction, buy or sell.
Definition types.hpp:1683
The error a broker or transport operation returns.
Definition types.hpp:742
std::string message_
A human-readable description, for logs; do not parse it.
Definition types.hpp:772
ErrorCategory category_
The kind of failure; the field to branch on.
Definition types.hpp:744
std::string broker_code_
The broker's own error code, verbatim (for example KIS's EGW00201).
Definition types.hpp:761
bool reached_broker_
Whether a request was sent before this error.
Definition types.hpp:753
std::optional< CommissionRate > rate_
The rate in force, if one definitely is.
Definition types.hpp:1569
The commission rates a broker published for an account, or a caller stated.
Definition types.hpp:1535
RateSelection rate_for(Country country, const VenueKey &asserted, const Date &date) const
Finds the rate in force for a market and venue on a date.
Definition types.hpp:1592
CommissionOrigin origin_
Whether a broker published the rates or a caller stated them.
Definition types.hpp:1541
std::string tariff_
Which tariff the rates are, in the source's own words.
Definition types.hpp:1549
A calendar date with no time of day or time zone.
Definition types.hpp:1142
The price grid (tick sizes) an instrument's order prices must sit on.
std::optional< Decimal64 > tick_at(const Decimal64 &price) const
Returns the tick size at a price.
std::vector< TickBand > bands_
The bands, ascending by from_; empty unless kind_ is STATED.
The commission rate that charged an execution, as the backtest report lists it.
Definition cost.hpp:124
VenueKey venue_
The venue the rate was keyed to.
Definition cost.hpp:128
std::optional< Decimal64 > institution_rate_
The institution fee rate; empty if the source gave one combined rate.
Definition cost.hpp:132
std::optional< Date > until_
The last date of the rate's window; empty if open-ended.
Definition cost.hpp:138
friend bool operator==(const AppliedRate &lhs, const AppliedRate &rhs)
Compares every field except executions_.
Definition cost.hpp:154
std::optional< Date > from_
The first date of the rate's window; empty if open-ended.
Definition cost.hpp:136
Decimal64 minimum_
The per-execution minimum on the broker's commission; zero if none.
Definition cost.hpp:134
CommissionOrigin origin_
Whether a broker published the rate or a caller stated it.
Definition cost.hpp:140
Decimal64 rate_
The broker's commission, as a fraction of traded value.
Definition cost.hpp:130
std::size_t executions_
How many executions the rate charged, counted in the report's table.
Definition cost.hpp:146
Country country_
The market the rate was selected for.
Definition cost.hpp:126
What one execution was charged: the broker's commission and the institution fee.
Definition cost.hpp:167
std::optional< Decimal64 > institution_
The institution fee (유관기관제비용), if the rate is split.
Definition cost.hpp:171
Decimal64 broker_
The broker's commission, after its minimum.
Definition cost.hpp:169
AppliedRate applied_
The rate that charged it.
Definition cost.hpp:173
A flat commission: a rate on traded value, with an optional per-execution minimum.
Definition cost.hpp:28
std::optional< Decimal64 > on(const Decimal64 &quantity, const Decimal64 &price) const
Computes the commission on one execution.
Definition cost.hpp:46
Decimal64 rate_
The fraction of traded value charged per execution (0.00015, not 0.015).
Definition cost.hpp:30
static CommissionModel of(Decimal64 rate, Decimal64 minimum=Decimal64::parse("0"))
Returns a rate-based model.
Definition cost.hpp:75
Decimal64 minimum_
The smallest commission on any execution.
Definition cost.hpp:32
bool from_broker_
Whether the rate came from a broker's published schedule; commission_model_from sets it from the sche...
Definition cost.hpp:37
static CommissionModel none()
Returns a model that charges nothing (the report says so).
Definition cost.hpp:67
Slippage: how far a fill price moves against the order (a buy pays more, a sell gets less).
Definition cost.hpp:425
Decimal64 basis_points_
The price given up per execution, in ten-thousandths of the price.
Definition cost.hpp:427
static SlippageModel of_basis_points(Decimal64 points)
Returns a basis-point slippage model.
Definition cost.hpp:529
static SlippageModel of_ticks(int count)
Returns a tick slippage model.
Definition cost.hpp:540
static SlippageModel none()
Returns a model with no slippage (the report says so).
Definition cost.hpp:522
int ticks_
Grid steps given up per execution, each a step to the next grid price against the order.
Definition cost.hpp:429
std::optional< std::pair< Decimal64, Decimal64 > > apply(const Decimal64 &price, OrderSide side, const PriceRule &rule) const
Moves a price against the order.
Definition cost.hpp:444