FastMM documentation
FastMM C++ API 0.6.0
Public headers, tier 1 and tier 2
Loading...
Searching...
No Matches
params.hpp
Go to the documentation of this file.
1#pragma once
2// Strategy parameter schema (8.6, ADR-0012).
3//
4// struct MyParams {
5// FASTMM_PARAMS(MyParams)
6// FASTMM_PARAM(double, gamma, 0.1, 0.0, 10.0, "risk aversion")
7// FASTMM_PARAM(int, levels, 1, 1, 8, "quote levels per side")
8// FASTMM_PARAM(bool, hedge, false, false, true, "hedge fills")
9// FASTMM_PARAM(Qty, quote_qty, 0.01_qty, 0_qty, 1000_qty, "quantity per level, base units")
10// FASTMM_PARAM_BPS(half_spread_bps, 5_bps, 0_bps, 1000_bps, "half spread around mid")
11// FASTMM_PARAM_MS(stale_ms, milliseconds(2000), milliseconds(0), milliseconds(60000), "...")
12//
13// // Optional cross-field check, run by StrategyBase::configure() after every key is applied.
14// std::optional<std::string> validate() const {
15// if (quote_qty > max_inventory) return "quote_qty must not exceed max_inventory";
16// return std::nullopt;
17// }
18// };
19//
20// FASTMM_PARAM declares the field *and* a schema entry (name, type, default, range, doc, parser
21// and formatter). Field types: bool, integers, floating point (schema types int, double, bool) and
22// Price, Qty, Notional (schema type decimal, parsed exactly). FASTMM_PARAM_BPS declares a Ratio
23// configured in basis points (type bps, up to 4 decimals) and FASTMM_PARAM_MS a Duration configured
24// in whole milliseconds (type ms). Exact values accept exponent notation ("2e-05"), because TOML
25// floats reach the parser formatted by fmt and Python floats by repr; a value is rejected only if
26// more decimals remain after applying the exponent than the type holds. Range checks compare typed
27// values; `def`, `min` and `max` in ParamDesc are doubles for display and Python only.
28//
29// The schema is collected once by default-constructing the struct with a thread-local collector
30// active, so registration is deterministic and needs no static initialisers. apply() maps string
31// values (from TOML / CLI / Python) onto the fields. All of this is startup-only code (std::string
32// and std::map are fine).
33#include "fastmm/core/containers/static_vector.hpp"
35#include "fastmm/core/time.hpp"
36
37#include <bit>
38#include <charconv>
39#include <cmath>
40#include <cstddef>
41#include <cstdint>
42#include <limits>
43#include <map>
44#include <optional>
45#include <stdexcept>
46#include <string>
47#include <string_view>
48#include <type_traits>
49#include <utility>
50
51namespace fastmm {
52
53enum class ParamType : std::uint8_t {
54 Int = 0,
55 Double = 1,
56 Bool = 2,
57 Decimal = 3, // Price, Qty, Notional
58 Bps = 4, // Ratio configured in basis points
59 Millis = 5, // Duration configured in milliseconds
60};
61[[nodiscard]] constexpr std::string_view to_string(ParamType t) noexcept {
62 switch (t) {
64 return "int";
66 return "double";
67 case ParamType::Bool:
68 return "bool";
70 return "decimal";
71 case ParamType::Bps:
72 return "bps";
74 return "ms";
75 }
76 return "?";
77}
78
79struct ParamDesc {
80 const char* name;
82 double def; // display only (listings, Python); range checks use the typed bounds
83 double min;
84 double max;
85 const char* doc;
86 // Parses `value` exactly, checks it against the typed range and assigns the field. On failure
87 // the field is unchanged and the message (without the parameter name) is returned.
88 std::optional<std::string> (*parse)(void* obj, std::string_view value);
89 // The field's current value in a form parse() accepts ("0.01", "2.5", "true", "2000").
90 std::string (*format)(const void* obj);
91 // The field as the int64 a ParamUpdateMsg carries, and back (engine thread: no checks). Integers
92 // and bool as their value, Price, Qty, Notional and Ratio as raw fixed point, a Duration in ns
93 // and a floating-point field as the bits of the double.
94 std::int64_t (*get_raw)(const void* obj) noexcept;
95 void (*set_raw)(void* obj, std::int64_t raw) noexcept;
96};
98inline constexpr std::size_t kMaxParams = 128;
99
101 public:
102 bool add(const ParamDesc& d) noexcept { return descs_.push_back(d); }
103 [[nodiscard]] const ParamDesc* find(std::string_view name) const noexcept {
104 for (const ParamDesc& d : descs_) {
105 if (name == d.name) return &d;
106 }
107 return nullptr;
108 }
109 [[nodiscard]] std::size_t size() const noexcept { return descs_.size(); }
110 [[nodiscard]] const ParamDesc* begin() const noexcept { return descs_.begin(); }
111 [[nodiscard]] const ParamDesc* end() const noexcept { return descs_.end(); }
113 private:
114 StaticVector<ParamDesc, kMaxParams> descs_;
115};
116
117using ParamMap = std::map<std::string, std::string>;
118
119namespace detail {
120
121inline thread_local ParamSchema* t_param_collector = nullptr;
122inline thread_local bool t_param_overflow = false; // a registration the schema had no room for
123
124// static_cast unless the types already match (keeps -Wuseless-cast quiet in macro expansions).
125template <class T, class U>
126constexpr T param_cast(U v) noexcept {
127 if constexpr (std::is_same_v<T, U>) {
128 return v;
129 } else {
130 return static_cast<T>(v);
131 }
132}
133
134// v / 10^decimals exactly, without trailing zeros: "2.5", "-0.0001", "3".
135inline std::string format_scaled(std::int64_t v, int decimals) {
136 const std::uint64_t mag =
137 v < 0 ? 0U - static_cast<std::uint64_t>(v) : static_cast<std::uint64_t>(v);
138 std::uint64_t scale = 1;
139 for (int k = 0; k < decimals; ++k) scale *= 10;
140 std::string out = v < 0 ? "-" : "";
141 out += std::to_string(mag / scale);
142 std::uint64_t frac = mag % scale;
143 if (frac != 0) {
144 std::string digits(static_cast<std::size_t>(decimals), '0');
145 for (std::size_t k = digits.size(); k > 0; --k) {
146 digits[k - 1] = static_cast<char>('0' + frac % 10);
147 frac /= 10;
148 }
149 while (digits.back() == '0') digits.pop_back();
150 out += '.';
151 out += digits;
152 }
153 return out;
154}
155
156struct PlainUnit {};
157struct BpsUnit {};
158struct MsUnit {};
159
160// ParamCodec<T, Unit>: schema type, exact parse, format and display value of one field type.
161template <class T, class Unit>
162struct ParamCodec {
163 static_assert(sizeof(T) == 0,
164 "fastmm: FASTMM_PARAM supports bool, integers, floating point, Price, Qty and "
165 "Notional; use FASTMM_PARAM_BPS for a Ratio and FASTMM_PARAM_MS for a Duration");
166};
167
168template <>
169struct ParamCodec<bool, PlainUnit> {
170 static constexpr ParamType kType = ParamType::Bool;
171 static constexpr std::string_view kHint = " (true or false)";
172 [[nodiscard]] static std::optional<bool> parse(std::string_view s) noexcept {
173 if (s == "true" || s == "1" || s == "yes" || s == "on") return true;
174 if (s == "false" || s == "0" || s == "no" || s == "off") return false;
175 return std::nullopt;
176 }
177 [[nodiscard]] static std::string format(bool v) { return v ? "true" : "false"; }
178 [[nodiscard]] static double display(bool v) noexcept { return v ? 1.0 : 0.0; }
179 [[nodiscard]] static std::int64_t to_raw(bool v) noexcept { return v ? 1 : 0; }
180 [[nodiscard]] static bool from_raw(std::int64_t raw) noexcept { return raw != 0; }
181};
182
183template <class T>
184 requires(std::is_integral_v<T> && !std::is_same_v<T, bool>)
185struct ParamCodec<T, PlainUnit> {
186 static constexpr ParamType kType = ParamType::Int;
187 static constexpr std::string_view kHint = " (a whole number)";
188 [[nodiscard]] static std::optional<T> parse(std::string_view s) noexcept {
189 const std::optional<std::int64_t> v = parse_scaled_decimal(s, 0); // "3", "3.0", "2e3"
190 if (!v || !std::in_range<T>(*v)) return std::nullopt;
191 return param_cast<T>(*v);
192 }
193 [[nodiscard]] static std::string format(T v) { return std::to_string(v); }
194 [[nodiscard]] static double display(T v) noexcept { return static_cast<double>(v); }
195 [[nodiscard]] static std::int64_t to_raw(T v) noexcept { return param_cast<std::int64_t>(v); }
196 [[nodiscard]] static T from_raw(std::int64_t raw) noexcept { return param_cast<T>(raw); }
197};
198
199template <class T>
200 requires std::is_floating_point_v<T>
201struct ParamCodec<T, PlainUnit> {
202 static constexpr ParamType kType = ParamType::Double;
203 static constexpr std::string_view kHint = " (a finite number)";
204 [[nodiscard]] static std::optional<T> parse(std::string_view s) noexcept {
205 if (!s.empty() && s.front() == '+') s.remove_prefix(1);
206 double d = 0.0;
207 const auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), d);
208 if (s.empty() || ec != std::errc{} || ptr != s.data() + s.size() || !std::isfinite(d))
209 return std::nullopt;
210 return param_cast<T>(d);
211 }
212 [[nodiscard]] static std::string format(T v) {
213 char buf[64];
214 const auto r = std::to_chars(buf, buf + sizeof(buf), v); // shortest round-trip form
215 return {buf, r.ptr};
216 }
217 [[nodiscard]] static double display(T v) noexcept { return param_cast<double>(v); }
218 [[nodiscard]] static std::int64_t to_raw(T v) noexcept {
219 return std::bit_cast<std::int64_t>(param_cast<double>(v));
220 }
221 [[nodiscard]] static T from_raw(std::int64_t raw) noexcept {
222 return param_cast<T>(std::bit_cast<double>(raw));
223 }
224};
225
226template <class Tag>
227 requires(std::is_same_v<Fixed<Tag>, Price> || std::is_same_v<Fixed<Tag>, Qty> ||
228 std::is_same_v<Fixed<Tag>, Notional>)
229struct ParamCodec<Fixed<Tag>, PlainUnit> {
230 static constexpr ParamType kType = ParamType::Decimal;
231 static constexpr std::string_view kHint = " (at most 8 decimals)";
232 [[nodiscard]] static std::optional<Fixed<Tag>> parse(std::string_view s) noexcept {
233 return Fixed<Tag>::parse(s);
234 }
235 [[nodiscard]] static std::string format(Fixed<Tag> v) {
236 return format_scaled(v.raw, kFixedDecimals);
237 }
238 [[nodiscard]] static double display(Fixed<Tag> v) noexcept { return v.to_double(); }
239 [[nodiscard]] static std::int64_t to_raw(Fixed<Tag> v) noexcept { return v.raw; }
240 [[nodiscard]] static Fixed<Tag> from_raw(std::int64_t raw) noexcept {
241 return Fixed<Tag>::from_raw(raw);
242 }
243};
244
245template <>
246struct ParamCodec<Ratio, BpsUnit> {
247 static constexpr ParamType kType = ParamType::Bps;
248 static constexpr std::string_view kHint = " (basis points, at most 4 decimals)";
249 [[nodiscard]] static std::optional<Ratio> parse(std::string_view s) noexcept {
250 const std::optional<std::int64_t> raw = parse_scaled_decimal(s, kBpsDecimals);
251 if (!raw) return std::nullopt;
252 return Ratio::from_raw(*raw);
253 }
254 [[nodiscard]] static std::string format(Ratio v) { return format_scaled(v.raw, kBpsDecimals); }
255 [[nodiscard]] static double display(Ratio v) noexcept { return v.to_bps(); }
256 [[nodiscard]] static std::int64_t to_raw(Ratio v) noexcept { return v.raw; }
257 [[nodiscard]] static Ratio from_raw(std::int64_t raw) noexcept { return Ratio::from_raw(raw); }
258};
259
260template <>
261struct ParamCodec<Duration, MsUnit> {
262 static constexpr ParamType kType = ParamType::Millis;
263 static constexpr std::string_view kHint = " (whole milliseconds)";
264 static constexpr std::int64_t kNsPerMs = 1'000'000;
265 [[nodiscard]] static std::optional<Duration> parse(std::string_view s) noexcept {
266 const std::optional<std::int64_t> ms = parse_scaled_decimal(s, 0);
267 constexpr std::int64_t kMaxMs = std::numeric_limits<std::int64_t>::max() / kNsPerMs;
268 if (!ms || *ms > kMaxMs || *ms < -kMaxMs) return std::nullopt;
269 return milliseconds(*ms);
270 }
271 [[nodiscard]] static std::string format(Duration v) { return format_scaled(v.ns, 6); }
272 [[nodiscard]] static double display(Duration v) noexcept {
273 return static_cast<double>(v.ns) / static_cast<double>(kNsPerMs);
274 }
275 [[nodiscard]] static std::int64_t to_raw(Duration v) noexcept { return v.ns; }
276 [[nodiscard]] static Duration from_raw(std::int64_t raw) noexcept { return Duration{raw}; }
277};
278
279template <class Codec, class T>
280std::optional<std::string> assign_param(T& field, std::string_view value, T lo, T hi) {
281 const std::optional<T> v = Codec::parse(value);
282 std::string err;
283 if (!v) {
284 return err.append("cannot parse '")
285 .append(value)
286 .append("' as ")
287 .append(to_string(Codec::kType))
288 .append(Codec::kHint);
289 }
290 if (*v < lo || hi < *v) {
291 return err.append("value ")
292 .append(value)
293 .append(" outside [")
294 .append(Codec::format(lo))
295 .append(", ")
296 .append(Codec::format(hi))
297 .append("]");
298 }
299 field = *v;
300 return std::nullopt;
301}
302
303inline void register_param(const ParamDesc& d) noexcept {
304 if (t_param_collector != nullptr && !t_param_collector->add(d)) t_param_overflow = true;
305}
306
307template <class Self>
308ParamSchema collect_schema() {
309 static_assert(std::is_default_constructible_v<Self>);
310 ParamSchema s;
311 t_param_collector = &s;
312 t_param_overflow = false;
313 Self tmp{};
314 static_cast<void>(tmp);
315 t_param_collector = nullptr;
316 // A parameter past the table would be silently left at its default: refuse the strategy.
317 if (t_param_overflow) {
318 throw std::invalid_argument("fastmm: a strategy has at most " + std::to_string(kMaxParams) +
319 " parameters");
320 }
321 return s;
322}
323
324// Applies every entry of `m`; returns the first error message, if any.
325inline std::optional<std::string> apply_params(const ParamSchema& schema,
326 void* obj,
327 const ParamMap& m) {
328 for (const auto& [key, value] : m) {
329 const ParamDesc* d = schema.find(key);
330 std::string err; // built with append: startup path, but no needless temporaries
331 if (d == nullptr) return err.append("unknown parameter '").append(key).append("'");
332 if (auto e = d->parse(obj, value)) {
333 return err.append("parameter '").append(key).append("': ").append(*e);
334 }
335 }
336 return std::nullopt;
337}
338
339inline std::string describe_params(const ParamSchema& schema, const void* obj) {
340 std::string out;
341 for (const ParamDesc& d : schema) {
342 out += d.name;
343 out += '=';
344 out += d.format(obj);
345 out += ' ';
346 }
347 if (!out.empty()) out.pop_back();
348 return out;
349}
350
351// Runs `std::optional<std::string> validate() const` when the params struct declares one.
352template <class P>
353std::optional<std::string> validate_params(const P& p) {
354 if constexpr (requires { p.validate(); }) {
355 static_assert(
356 std::is_same_v<decltype(p.validate()), std::optional<std::string>>,
357 "fastmm: validate() must be declared std::optional<std::string> validate() const");
358 return p.validate();
359 } else {
360 static_assert(
361 !requires(P& q) { q.validate(); },
362 "fastmm: validate() must be const: std::optional<std::string> validate() const");
363 return std::nullopt;
364 }
365}
366
367} // namespace detail
368} // namespace fastmm
369
370#define FASTMM_PARAMS(Self) \
371 using FastmmParamsSelf = Self; \
372 static const ::fastmm::ParamSchema& schema() { \
373 static const ::fastmm::ParamSchema s = ::fastmm::detail::collect_schema<Self>(); \
374 return s; \
375 } \
376 std::optional<std::string> apply(const ::fastmm::ParamMap& m) { \
377 return ::fastmm::detail::apply_params(schema(), this, m); \
378 } \
379 [[nodiscard]] std::string describe() const { \
380 return ::fastmm::detail::describe_params(schema(), this); \
381 }
382
383// Implementation of the FASTMM_PARAM* macros: the field, then a zero-size registrar whose
384// constructor adds the schema entry while collect_schema() runs.
385#define FASTMM_PARAM_IMPL(unit, type, name, def, lo, hi, doc) \
386 type name = def; \
387 struct FastmmReg_##name { \
388 FastmmReg_##name() noexcept { \
389 using FastmmCodec = ::fastmm::detail::ParamCodec<type, unit>; \
390 ::fastmm::detail::register_param(::fastmm::ParamDesc{ \
391 #name, \
392 FastmmCodec::kType, \
393 FastmmCodec::display(::fastmm::detail::param_cast<type>(def)), \
394 FastmmCodec::display(::fastmm::detail::param_cast<type>(lo)), \
395 FastmmCodec::display(::fastmm::detail::param_cast<type>(hi)), \
396 doc, \
397 +[](void* fastmm_obj, std::string_view fastmm_value) { \
398 return ::fastmm::detail::assign_param<FastmmCodec, type>( \
399 static_cast<FastmmParamsSelf*>(fastmm_obj)->name, \
400 fastmm_value, \
401 ::fastmm::detail::param_cast<type>(lo), \
402 ::fastmm::detail::param_cast<type>(hi)); \
403 }, \
404 +[](const void* fastmm_obj) { \
405 return FastmmCodec::format(static_cast<const FastmmParamsSelf*>(fastmm_obj)->name); \
406 }, \
407 +[](const void* fastmm_obj) noexcept -> std::int64_t { \
408 return FastmmCodec::to_raw(static_cast<const FastmmParamsSelf*>(fastmm_obj)->name); \
409 }, \
410 +[](void* fastmm_obj, std::int64_t fastmm_raw) noexcept { \
411 static_cast<FastmmParamsSelf*>(fastmm_obj)->name = FastmmCodec::from_raw(fastmm_raw); \
412 }}); \
413 } \
414 }; \
415 [[no_unique_address]] FastmmReg_##name fastmm_reg_##name{};
416
417// FASTMM_PARAM(type, name, default, min, max, doc): bool, integer, floating point, Price, Qty or
418// Notional field.
419#define FASTMM_PARAM(type, name, def, lo, hi, doc) \
420 FASTMM_PARAM_IMPL(::fastmm::detail::PlainUnit, type, name, def, lo, hi, doc)
421// FASTMM_PARAM_BPS(name, default, min, max, doc): Ratio field configured in basis points
422// (5_bps, 0.25_bps; up to 4 decimals).
423#define FASTMM_PARAM_BPS(name, def, lo, hi, doc) \
424 FASTMM_PARAM_IMPL(::fastmm::detail::BpsUnit, ::fastmm::Ratio, name, def, lo, hi, doc)
425// FASTMM_PARAM_MS(name, default, min, max, doc): Duration field configured in whole milliseconds
426// (bounds as Durations: milliseconds(2000)).
427#define FASTMM_PARAM_MS(name, def, lo, hi, doc) \
428 FASTMM_PARAM_IMPL(::fastmm::detail::MsUnit, ::fastmm::Duration, name, def, lo, hi, doc)
Definition params.hpp:102
bool add(const ParamDesc &d) noexcept
Definition params.hpp:104
const ParamDesc * begin() const noexcept
Definition params.hpp:112
const ParamDesc * end() const noexcept
Definition params.hpp:113
std::size_t size() const noexcept
Definition params.hpp:111
const ParamDesc * find(std::string_view name) const noexcept
Definition params.hpp:105
Fixed<Tag>: int64 fixed point with a global 1e-8 scale (ADR-0001).
Definition strategy.hpp:16
constexpr Duration milliseconds(std::int64_t v) noexcept
Definition time.hpp:38
constexpr int kFixedDecimals
Definition fixed_point.hpp:24
constexpr std::string_view to_string(ParamType t) noexcept
Definition params.hpp:63
ParamType
Definition params.hpp:55
@ Bps
Ratio configured in basis points.
@ Decimal
Price, Qty, Notional.
@ Millis
Duration configured in milliseconds.
constexpr std::size_t kMaxParams
Definition params.hpp:100
constexpr int kBpsDecimals
Definition fixed_point.hpp:29
std::map< std::string, std::string > ParamMap
Definition params.hpp:119
Definition time.hpp:22
std::int64_t ns
Definition time.hpp:23
double to_double() const noexcept
Definition fixed_point.hpp:297
static constexpr std::optional< Fixed > parse(std::string_view s) noexcept
Exact parse of a decimal in plain or exponent notation: "0.1", "2e-05", "1.5E3", "-3".
Definition fixed_point.hpp:220
double to_bps() const noexcept
Ratio only. Diagnostics only.
Definition fixed_point.hpp:242
static constexpr Fixed from_raw(std::int64_t r) noexcept
Definition fixed_point.hpp:155
std::int64_t raw
Definition fixed_point.hpp:152
Definition params.hpp:81
double max
Definition params.hpp:86
ParamType type
Definition params.hpp:83
std::optional< std::string >(* parse)(void *obj, std::string_view value)
Parses value exactly, checks it against the typed range and assigns the field.
Definition params.hpp:90
double min
Definition params.hpp:85
const char * doc
Definition params.hpp:87
std::string(* format)(const void *obj)
The field's current value in a form parse() accepts ("0.01", "2.5", "true", "2000").
Definition params.hpp:92
std::int64_t(* get_raw)(const void *obj) noexcept
The field as the int64 a ParamUpdateMsg carries, and back (engine thread: no checks).
Definition params.hpp:96
const char * name
Definition params.hpp:82
double def
display only (listings, Python); range checks use the typed bounds
Definition params.hpp:84
void(* set_raw)(void *obj, std::int64_t raw) noexcept
Definition params.hpp:97
Time primitives.