FastMM documentation
FastMM C++ API 0.6.0
Public headers, tier 1 and tier 2
Loading...
Searching...
No Matches
decimal.hpp
Go to the documentation of this file.
1#pragma once
2// Exact decimal helpers for venue payloads (6.2). Every price/quantity string a venue sends
3// is parsed with Fixed::from_decimal (integer loop, no double) and returned as a Result so
4// the caller can count malformed messages instead of throwing. Timestamps arrive as
5// millisecond integers (Binance `E`/`T`, Bybit `ts`/`T`) or strings (Bybit `execTime`).
9
10#include <cstddef>
11#include <cstdint>
12#include <limits>
13#include <string_view>
14
15namespace fastmm::venues {
16
17enum class DecimalError : std::uint8_t {
18 Empty = 1,
19 Malformed = 2, // non-digit, exponent, whitespace, > 8 significant fraction digits
20 Overflow = 3, // does not fit the int64 1e-8 range
21};
22[[nodiscard]] constexpr std::string_view to_string(DecimalError e) noexcept {
23 switch (e) {
25 return "empty";
27 return "malformed";
29 return "overflow";
30 }
31 return "?";
32}
33
34namespace detail {
35// Classifies why from_decimal rejected `s` (only called on the failure path).
36[[nodiscard]] constexpr DecimalError classify(std::string_view s) noexcept {
37 if (s.empty()) return DecimalError::Empty;
38 std::size_t i = (s[0] == '-' || s[0] == '+') ? 1 : 0;
39 std::size_t int_digits = 0;
40 for (; i < s.size() && s[i] != '.'; ++i) {
41 if (s[i] < '0' || s[i] > '9') return DecimalError::Malformed;
42 ++int_digits;
43 }
44 if (i < s.size()) {
45 for (++i; i < s.size(); ++i) {
46 if (s[i] < '0' || s[i] > '9') return DecimalError::Malformed;
47 }
48 }
49 // 92233720368 has 11 digits: anything longer is an overflow, anything shorter that still
50 // failed must have carried too many fraction digits.
51 return int_digits >= 11 ? DecimalError::Overflow : DecimalError::Malformed;
52}
53} // namespace detail
54
55template <class F>
56[[nodiscard]] inline Result<F, DecimalError> parse_fixed(std::string_view s) noexcept {
57 if (const auto v = F::from_decimal(s)) return *v;
58 return fail(detail::classify(s));
59}
60[[nodiscard]] inline Result<Price, DecimalError> parse_price(std::string_view s) noexcept {
61 return parse_fixed<Price>(s);
63// A value the venue computed rather than one an order carries can have more fraction digits than
64// 8: Binance USDⓈ-M's positionRisk and ACCOUNT_UPDATE give an entry price of "83954.61851851852",
65// OKX a spot commission of "-0.00000192834" BTC (fillSz times the fee rate). Rounded half away
66// from zero to 8 decimals; anything else parse_fixed refuses is refused here too.
67template <class F>
68[[nodiscard]] inline Result<F, DecimalError> parse_rounded(std::string_view s) noexcept {
69 if (const auto v = F::from_decimal(s)) return *v;
70 const std::size_t dot = s.find('.');
71 constexpr auto kKeep = static_cast<std::size_t>(kFixedDecimals);
72 if (dot == std::string_view::npos || s.size() - dot - 1 <= kKeep)
73 return fail(detail::classify(s));
74 for (std::size_t i = dot + 1; i < s.size(); ++i) {
75 if (s[i] < '0' || s[i] > '9') return fail(DecimalError::Malformed);
76 }
77 const auto v = F::from_decimal(s.substr(0, dot + 1 + kKeep));
78 if (!v) return fail(detail::classify(s));
79 if (s[dot + 1 + kKeep] < '5') return *v;
80 const std::int64_t up = v->raw < 0 || (v->raw == 0 && s[0] == '-') ? -1 : 1;
81 if (up > 0 && v->raw == std::numeric_limits<std::int64_t>::max())
83 if (up < 0 && v->raw == std::numeric_limits<std::int64_t>::min())
85 return F::from_raw(v->raw + up);
86}
87// A position's entry price, an average fill price.
88[[nodiscard]] inline Result<Price, DecimalError> parse_avg_price(std::string_view s) noexcept {
89 return parse_rounded<Price>(s);
91// A commission, which the venue computes from the fill.
92[[nodiscard]] inline Result<Notional, DecimalError> parse_fee(std::string_view s) noexcept {
93 return parse_rounded<Notional>(s);
95[[nodiscard]] inline Result<Qty, DecimalError> parse_qty(std::string_view s) noexcept {
96 return parse_fixed<Qty>(s);
98[[nodiscard]] inline Result<Notional, DecimalError> parse_notional(std::string_view s) noexcept {
99 return parse_fixed<Notional>(s);
101
102// "[-]digits" -> int64; rejects empty, signs other than a leading '-', overflow.
103[[nodiscard]] inline Result<std::int64_t, DecimalError> parse_int64(std::string_view s) noexcept {
104 if (s.empty()) return fail(DecimalError::Empty);
105 std::size_t i = 0;
106 bool neg = false;
107 if (s[0] == '-') {
108 neg = true;
109 i = 1;
110 if (s.size() == 1) return fail(DecimalError::Malformed);
111 }
112 std::uint64_t v = 0;
113 constexpr std::uint64_t kMax =
114 static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max());
115 for (; i < s.size(); ++i) {
116 const char c = s[i];
117 if (c < '0' || c > '9') return fail(DecimalError::Malformed);
118 const auto d = static_cast<std::uint64_t>(c - '0');
119 if (v > (kMax - d) / 10) return fail(DecimalError::Overflow);
120 v = v * 10 + d;
121 }
122 return neg ? -static_cast<std::int64_t>(v) : static_cast<std::int64_t>(v);
123}
124
125// Milliseconds since the Unix epoch -> Timestamp (ns).
126[[nodiscard]] constexpr Timestamp ts_from_ms(std::int64_t ms) noexcept {
127 return Timestamp{ms * 1'000'000};
129[[nodiscard]] inline Result<Timestamp, DecimalError> parse_ts_ms(std::string_view s) noexcept {
130 const auto v = parse_int64(s);
131 if (!v) return fail(v.error());
132 return ts_from_ms(*v);
133}
134
135// Stack formatting buffer: `DecimalText t(px); send(t.view());` - no heap, no locale.
136class DecimalText {
137 public:
138 template <class Tag>
139 explicit DecimalText(Fixed<Tag> v) noexcept : len_(v.to_decimal(buf_)) {
140 buf_[len_] = '\0';
142 [[nodiscard]] std::string_view view() const noexcept { return {buf_, len_}; }
143 [[nodiscard]] const char* c_str() const noexcept { return buf_; }
144 [[nodiscard]] std::size_t size() const noexcept { return len_; }
146 private:
147 char buf_[kMaxDecimalChars + 1];
148 std::size_t len_;
149};
150
151// Formats an int64 into a caller buffer (>= 21 chars); returns the length.
152[[nodiscard]] constexpr std::size_t format_int64(std::int64_t v, char* out) noexcept {
153 char tmp[20];
154 std::size_t n = 0;
155 std::uint64_t mag = v < 0 ? 0 - static_cast<std::uint64_t>(v) : static_cast<std::uint64_t>(v);
156 do {
157 tmp[n++] = static_cast<char>('0' + mag % 10);
158 mag /= 10;
159 } while (mag != 0);
160 std::size_t w = 0;
161 if (v < 0) out[w++] = '-';
162 while (n > 0) out[w++] = tmp[--n];
163 return w;
164}
165
166} // namespace fastmm::venues
Definition result.hpp:30
Stack formatting buffer: DecimalText t(px); send(t.view()); - no heap, no locale.
Definition decimal.hpp:138
std::string_view view() const noexcept
Definition decimal.hpp:144
const char * c_str() const noexcept
Definition decimal.hpp:145
std::size_t size() const noexcept
Definition decimal.hpp:146
Minimal expected<T, E>: the hot path never throws, it returns Result.
Fixed<Tag>: int64 fixed point with a global 1e-8 scale (ADR-0001).
Definition venue.hpp:38
Result< F, DecimalError > parse_rounded(std::string_view s) noexcept
A value the venue computed rather than one an order carries can have more fraction digits than 8: Bin...
Definition decimal.hpp:70
constexpr Timestamp ts_from_ms(std::int64_t ms) noexcept
Milliseconds since the Unix epoch -> Timestamp (ns).
Definition decimal.hpp:128
Result< Price, DecimalError > parse_avg_price(std::string_view s) noexcept
A position's entry price, an average fill price.
Definition decimal.hpp:90
DecimalError
Definition decimal.hpp:19
@ Malformed
non-digit, exponent, whitespace, > 8 significant fraction digits
@ Overflow
does not fit the int64 1e-8 range
Result< Price, DecimalError > parse_price(std::string_view s) noexcept
Definition decimal.hpp:62
Result< Notional, DecimalError > parse_notional(std::string_view s) noexcept
Definition decimal.hpp:100
Result< Timestamp, DecimalError > parse_ts_ms(std::string_view s) noexcept
Definition decimal.hpp:131
constexpr std::string_view to_string(ChannelState s) noexcept
Definition venue.hpp:51
Result< Qty, DecimalError > parse_qty(std::string_view s) noexcept
Definition decimal.hpp:97
constexpr std::size_t format_int64(std::int64_t v, char *out) noexcept
Formats an int64 into a caller buffer (>= 21 chars); returns the length.
Definition decimal.hpp:154
Result< std::int64_t, DecimalError > parse_int64(std::string_view s) noexcept
"[-]digits" -> int64; rejects empty, signs other than a leading '-', overflow.
Definition decimal.hpp:105
Result< F, DecimalError > parse_fixed(std::string_view s) noexcept
Definition decimal.hpp:58
Result< Notional, DecimalError > parse_fee(std::string_view s) noexcept
A commission, which the venue computes from the fill.
Definition decimal.hpp:94
@ Malformed
JSON error or missing/invalid field.
@ Overflow
ring full (market data) or output buffer too small
constexpr int kFixedDecimals
Definition fixed_point.hpp:24
constexpr Unexpected< std::decay_t< E > > fail(E &&e) noexcept
Definition result.hpp:25
constexpr std::size_t kMaxDecimalChars
sign + 11 integer digits + '.' + 8 fraction digits + NUL
Definition fixed_point.hpp:26
Definition fixed_point.hpp:150
Time primitives.