algotradelib 0.0.0.9000
C++ header-only library for algorithmic trading
Loading...
Searching...
No Matches
common.hpp
1#pragma once
2
8#define ATL_BROKER_ADAPTER_PRESENT 1
9
10#include <algotradelib/network>
11
12#include <boost/json.hpp>
13
14#include <charconv>
15#include <cstddef>
16#include <cstdint>
17#include <limits>
18#include <optional>
19#include <string>
20#include <string_view>
21#include <utility>
22
23namespace algotradelib::broker_detail {
24
25// Reads a whole-number header, or nothing when it is absent or malformed. Which headers carry meaning
26// is a broker's own contract, so each adapter reads the ones its documentation promises; only the
27// parsing lives here.
28inline std::optional<std::int64_t> integer_header(
29 const network::HttpResponse& response,
30 std::string_view name
31) {
32 const auto value = response.base().find(name);
33 if (value == response.base().end()) {
34 return std::nullopt;
35 }
36 const auto text = value->value();
37 std::int64_t number{0};
38 const auto parsed = std::from_chars(text.data(), text.data() + text.size(), number);
39 if (parsed.ec != std::errc{} || parsed.ptr != text.data() + text.size() || number < 0) {
40 return std::nullopt;
41 }
42 return number;
43}
44
45// The most of a reply body an error message carries. The error envelopes seen so far (a code, a
46// message, a request id) fit; whatever else arrives is not copied whole into logs and journals. It is
47// an excerpt, not redaction.
48inline constexpr std::size_t reply_excerpt_limit = 512;
49
50// Returns `body` whole if it fits in `limit` bytes; otherwise its first `limit` bytes, cut back so no
51// UTF-8 character is split (KIS and Kiwoom write Korean), then how many bytes were left out. A UTF-8
52// character is at most four bytes, so the cut moves back at most three: a body that is not UTF-8
53// keeps its prefix rather than losing it to a run of continuation bytes.
54inline std::string reply_excerpt(std::string_view body, std::size_t limit = reply_excerpt_limit) {
55 if (body.size() <= limit) {
56 return std::string(body);
57 }
58 std::size_t cut = limit;
59 while (cut > 0 && limit - cut < 3 && (static_cast<unsigned char>(body[cut]) & 0xC0U) == 0x80U) {
60 --cut;
61 }
62 return std::string(body.substr(0, cut)) + "... (" + std::to_string(body.size() - cut) + " more bytes)";
63}
64
65inline BrokerError http_error(const network::HttpResponse& response) {
66 BrokerError error;
67 error.http_status_ = response.result_int();
68 error.category_ = response.result() == http::status::too_many_requests ?
71 error.message_ = reply_excerpt(response.body());
72 return error;
73}
74
75// Reads a response's body as a JSON object. A body that is not one is `PROTOCOL` `invalid_json`, with the
76// response's HTTP status kept and the parser's message, never the body.
77inline std::optional<json::object> parse_object(const network::HttpResponse& response, BrokerError& error) {
78 boost::system::error_code ec;
79 auto value = json::parse(response.body(), ec);
80 if (ec || !value.is_object()) {
81 error.category_ = ErrorCategory::PROTOCOL;
82 error.http_status_ = response.result_int();
83 error.broker_code_ = "invalid_json";
84 error.message_ = ec ? ec.message() : "JSON response is not an object";
85 return std::nullopt;
86 }
87 return std::move(value.as_object());
88}
89
90inline std::string json_string(const json::object& object, std::string_view key) {
91 const auto value = object.if_contains(key);
92 if (!value || !value->is_string()) {
93 throw std::runtime_error("missing string field: " + std::string(key));
94 }
95 return std::string(value->as_string());
96}
97
98inline std::int64_t json_int64(const json::object& object, std::string_view key) {
99 const auto value = object.if_contains(key);
100 if (!value) {
101 throw std::runtime_error("missing integer field: " + std::string(key));
102 }
103 if (value->is_int64()) {
104 return value->as_int64();
105 }
106 if (
107 value->is_uint64() &&
108 value->as_uint64() <= static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max())
109 ) {
110 return static_cast<std::int64_t>(value->as_uint64());
111 }
112 throw std::runtime_error("invalid integer field: " + std::string(key));
113}
114
115inline bool valid_symbol(std::string_view symbol) {
116 if (symbol.empty()) {
117 return false;
118 }
119 for (const char ch : symbol) {
120 const bool valid = (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') ||
121 (ch >= 'a' && ch <= 'z') || ch == '.' || ch == '-';
122 if (!valid) {
123 return false;
124 }
125 }
126 return true;
127}
128
129inline std::string query_encode(std::string_view value) {
130 static constexpr char hex[] = "0123456789ABCDEF";
131 std::string result;
132 result.reserve(value.size());
133 for (const unsigned char ch : value) {
134 const bool unreserved = (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') ||
135 (ch >= '0' && ch <= '9') || ch == '-' || ch == '.' || ch == '_' || ch == '~';
136 if (unreserved) {
137 result.push_back(static_cast<char>(ch));
138 } else {
139 result.push_back('%');
140 result.push_back(hex[ch >> 4]);
141 result.push_back(hex[ch & 0x0F]);
142 }
143 }
144 return result;
145}
146
147inline bool decimal_digits(std::string_view value, std::size_t length) {
148 if (value.size() != length) {
149 return false;
150 }
151 for (const char ch : value) {
152 if (ch < '0' || ch > '9') {
153 return false;
154 }
155 }
156 return true;
157}
158
159// Reads `YYYYMMDD` as a calendar date, or nothing when it is not one. Every character is checked as
160// a digit before any arithmetic, and the result must be a real date: `std::stoi` per component would
161// read `"2026092x"` as 2026-09-02, and unchecked digit arithmetic would read `"2026090:"` as
162// 2026-09-10, since `':'` follows `'9'`.
163inline std::optional<Date> compact_date(std::string_view text) {
164 if (!decimal_digits(text, 8)) {
165 return std::nullopt;
166 }
167 const auto number = [text](std::size_t offset, std::size_t length) {
168 int value = 0;
169 for (std::size_t index = offset; index < offset + length; ++index) {
170 value = value * 10 + (text[index] - '0');
171 }
172 return value;
173 };
174 Date date{
175 number(0, 4), static_cast<unsigned>(number(4, 2)), static_cast<unsigned>(number(6, 2))
176 };
177 return is_valid_date(date) ? std::optional<Date>(date) : std::nullopt;
178}
179
180inline Decimal64 percent_to_ratio(std::string_view value) {
181 const auto percent = Decimal64::parse(value);
182 if (percent.scale() > std::numeric_limits<std::uint8_t>::max() - 2) {
183 throw std::out_of_range("percentage scale exceeds Decimal64 range");
184 }
185
186 auto coefficient = percent.coefficient();
187 auto scale = static_cast<std::uint8_t>(percent.scale() + 2);
188 while (scale > 0 && coefficient % 10 == 0) {
189 coefficient /= 10;
190 --scale;
191 }
192 return Decimal64(coefficient, scale);
193}
194
195} // namespace algotradelib::broker_detail
Decimal64()=default
Constructs zero.
static Decimal64 parse(std::string_view input)
Parses a decimal string exactly (no binary floating point).
Definition types.hpp:323
http::response< http::string_body > HttpResponse
The HTTP response type the transport clients return.
@ RATE_LIMIT
The broker refused the request for its rate limit.
Definition types.hpp:697
@ HTTP
The broker answered with an HTTP error status (http_status_).
Definition types.hpp:693
@ PROTOCOL
The reply could not be read, or the broker declared a failure this client does not classify further: ...
Definition types.hpp:704
bool is_valid_date(const Date &date)
Tests whether a date exists in the Gregorian calendar, in years 1 to 9999.
Definition types.hpp:1212