146class TossClient :
public std::enable_shared_from_this<TossClient> {
193 using TokenHandler = net::any_completion_handler<void(std::optional<BrokerError>)>;
203 net::any_io_executor executor,
204 ssl::context& context,
208 : strand_(net::make_strand(std::move(executor))), config_(std::move(config)),
210 std::make_shared<
network::AsyncHttpClient>(strand_, context,
"openapi.tossinvest.com",
"443", options)
224 template <
typename CompletionToken>
227 [self = shared_from_this(), query = std::move(query)](
auto&& handler)
mutable {
228 auto completion =
BarsHandler(std::forward<
decltype(handler)>(handler));
231 [self, query = std::move(query), completion = std::move(completion)]()
mutable {
232 self->start_bars(std::move(query), std::move(completion));
250 template <
typename CompletionToken>
253 [self = shared_from_this(), country, from](
auto&& handler)
mutable {
254 auto completion =
CalendarHandler(std::forward<
decltype(handler)>(handler));
257 [self, country, from, completion = std::move(completion)]()
mutable {
258 self->start_calendar(country, from, std::move(completion));
273 template <
typename CompletionToken>
276 [self = shared_from_this(), instrument = std::move(instrument)](
auto&& handler)
mutable {
280 [self, instrument = std::move(instrument), completion = std::move(completion)]()
mutable {
281 self->start_market_read(
282 std::move(instrument),
283 "/api/v1/stocks?symbols=",
284 std::move(completion),
301 template <
typename CompletionToken>
304 [self = shared_from_this(), instrument = std::move(instrument)](
auto&& handler)
mutable {
308 [self, instrument = std::move(instrument), completion = std::move(completion)]()
mutable {
309 self->start_market_read(
310 std::move(instrument),
311 "/api/v1/price-limits?symbol=",
312 std::move(completion),
330 template <
typename CompletionToken>
332 return net::async_initiate<CompletionToken, void(
Result<std::vector<TradePrint>>)>(
333 [self = shared_from_this(), instrument = std::move(instrument), count](
336 auto completion =
TradesHandler(std::forward<
decltype(handler)>(handler));
339 [self, instrument = std::move(instrument), count, completion = std::move(completion)]()
344 const int wanted = count < 1 ? 1 : (count > trade_limit_ ? trade_limit_ : count);
345 self->start_market_read(
346 std::move(instrument),
347 "/api/v1/trades?count=" + std::to_string(wanted) +
"&symbol=",
348 std::move(completion),
365 template <
typename CompletionToken>
366 auto async_prices(std::vector<InstrumentId> instruments, CompletionToken&& token) {
367 return net::async_initiate<CompletionToken, void(
Result<std::vector<QuoteSnapshot>>)>(
368 [self = shared_from_this(), instruments = std::move(instruments)](
auto&& handler)
mutable {
369 auto completion =
PricesHandler(std::forward<
decltype(handler)>(handler));
372 [self, instruments = std::move(instruments), completion = std::move(completion)]()
374 self->start_prices(std::move(instruments), std::move(completion));
389 template <
typename CompletionToken>
392 [self = shared_from_this(), instrument = std::move(instrument)](
auto&& handler)
mutable {
393 auto completion =
DepthHandler(std::forward<
decltype(handler)>(handler));
396 [self, instrument = std::move(instrument), completion = std::move(completion)]()
mutable {
397 self->start_depth(std::move(instrument), std::move(completion));
413 template <
typename CompletionToken>
416 [self = shared_from_this(), instrument = std::move(instrument)](
auto&& handler)
mutable {
417 auto completion =
QuoteHandler(std::forward<
decltype(handler)>(handler));
420 [self, instrument = std::move(instrument), completion = std::move(completion)]()
mutable {
421 self->start_quote(std::move(instrument), std::move(completion));
435 template <
typename CompletionToken>
437 return net::async_initiate<CompletionToken, void(
Result<std::vector<TossAccount>>)>(
438 [self = shared_from_this()](
auto&& handler)
mutable {
439 auto completion =
AccountsHandler(std::forward<
decltype(handler)>(handler));
442 [self, completion = std::move(completion)]()
mutable {
443 self->start_accounts(std::move(completion));
459 template <
typename CompletionToken>
462 [self = shared_from_this(), currency, account_sequence](
auto&& handler)
mutable {
466 [self, currency, account_sequence, completion = std::move(completion)]()
mutable {
467 self->start_buying_power(currency, account_sequence, std::move(completion));
483 template <
typename CompletionToken>
486 std::int64_t account_sequence,
487 CompletionToken&& token
490 [self = shared_from_this(), instrument = std::move(instrument), account_sequence](
496 [self, instrument = std::move(instrument), account_sequence,
497 completion = std::move(completion)]()
mutable {
498 self->start_sellable_quantity(
499 std::move(instrument), account_sequence, std::move(completion)
515 template <
typename CompletionToken>
518 [self = shared_from_this(), account_sequence](
auto&& handler)
mutable {
522 [self, account_sequence, completion = std::move(completion)]()
mutable {
523 self->start_commissions(account_sequence, std::move(completion));
538 template <
typename CompletionToken>
541 [self = shared_from_this(), account_sequence](
auto&& handler)
mutable {
542 auto completion =
HoldingsHandler(std::forward<
decltype(handler)>(handler));
545 [self, account_sequence, completion = std::move(completion)]()
mutable {
546 self->start_holdings(account_sequence, std::move(completion));
577 template <
typename CompletionToken>
579 std::int64_t account_sequence,
581 CompletionToken&& token
584 [self = shared_from_this(), account_sequence, order = std::move(order)](
587 auto completion =
OrderAckHandler(std::forward<
decltype(handler)>(handler));
593 order = std::move(order),
594 completion = std::move(completion)
596 self->start_place_order(account_sequence, std::move(order), std::move(completion));
620 template <
typename CompletionToken>
622 std::int64_t account_sequence,
624 CompletionToken&& token
627 [self = shared_from_this(), account_sequence, amendment = std::move(amendment)](
630 auto completion =
OrderAckHandler(std::forward<
decltype(handler)>(handler));
636 amendment = std::move(amendment),
637 completion = std::move(completion)
639 self->start_replace_order(
640 account_sequence, std::move(amendment), std::move(completion)
663 template <
typename CompletionToken>
665 std::int64_t account_sequence,
666 std::string order_id,
667 CompletionToken&& token
670 [self = shared_from_this(), account_sequence, order_id = std::move(order_id)](
673 auto completion =
OrderAckHandler(std::forward<
decltype(handler)>(handler));
679 order_id = std::move(order_id),
680 completion = std::move(completion)
682 self->start_cancel_order(account_sequence, std::move(order_id), std::move(completion));
705 template <
typename CompletionToken>
708 [self = shared_from_this(), query = std::move(query)](
auto&& handler)
mutable {
709 auto completion =
OrderPageHandler(std::forward<
decltype(handler)>(handler));
712 [self, query = std::move(query), completion = std::move(completion)]()
mutable {
713 self->start_orders(std::move(query), std::move(completion));
734 template <
typename CompletionToken>
735 auto async_order(std::int64_t account_sequence, std::string order_id, CompletionToken&& token) {
737 [self = shared_from_this(), account_sequence, order_id = std::move(order_id)](
740 auto completion =
OrderHandler(std::forward<
decltype(handler)>(handler));
746 order_id = std::move(order_id),
747 completion = std::move(completion)
749 self->start_order(account_sequence, std::move(order_id), std::move(completion));
766 const auto* limiter = http_->rate_limits();
767 return limiter ? &limiter->stats() :
nullptr;
774 friend struct TossClientTestAccess;
776 net::strand<net::any_io_executor> strand_;
778 std::shared_ptr<network::AsyncHttpClient> http_;
780 bool refreshing_{
false};
781 std::deque<TokenHandler> token_waiters_;
789 struct BarOperation {
792 std::vector<Bar> descending_;
793 std::optional<std::string> before_;
794 std::optional<std::string> last_cursor_;
798 BarOperation(BarQuery query,
BarsHandler completion)
799 : query_(std::move(query)), completion_(std::move(completion)) {
801 before_ = format_iso8601_utc(*query_.before_);
807 static constexpr int bar_page_limit_ = 200;
815 static constexpr int bar_page_budget_ = 50;
818 void start_bars(BarQuery query,
BarsHandler completion) {
819 if (
auto invalid = validate_bar_query(query)) {
820 deliver(std::move(completion), Result<BarSeries>(std::move(*invalid)));
824 auto operation = std::make_shared<BarOperation>(std::move(query), std::move(completion));
827 [self = shared_from_this(), operation](std::optional<BrokerError> token_error)
mutable {
830 std::move(operation->completion_), Result<BarSeries>(std::move(*token_error))
834 self->request_bars(operation);
841 static std::optional<BrokerError> validate_bar_query(
const BarQuery& query) {
844 error.reached_broker_ =
false;
845 if (!broker_detail::valid_symbol(query.instrument_.symbol_)) {
846 error.broker_code_ =
"invalid_instrument";
847 error.message_ =
"bar request requires a symbol";
850 if (query.count_ < 1) {
851 error.broker_code_ =
"invalid_count";
852 error.message_ =
"bar request must ask for at least one bar";
859 static const char* bar_interval_code(
BarInterval interval) {
860 return interval == BarInterval::ONE_MINUTE ?
"1m" :
"1d";
864 void request_bars(
const std::shared_ptr<BarOperation>& operation) {
865 const int outstanding = operation->query_.count_ -
866 static_cast<int>(operation->descending_.size());
867 const int wanted = outstanding > bar_page_limit_ ? bar_page_limit_ : outstanding;
869 std::string target =
"/api/v1/candles?symbol=" +
870 broker_detail::query_encode(operation->query_.instrument_.symbol_) +
871 "&interval=" + bar_interval_code(operation->query_.interval_) +
872 "&count=" + std::to_string(wanted) +
873 "&adjusted=" + (operation->query_.adjusted_ ?
"true" :
"false");
874 if (operation->before_) {
875 target +=
"&before=" + broker_detail::query_encode(*operation->before_);
878 network::HttpRequest request;
881 request.method_ = http::verb::get;
882 request.target_ = std::move(target);
883 request.headers_.emplace_back(
"Authorization",
"Bearer " + token_.access_token_);
888 [self = shared_from_this(), operation](Result<network::HttpResponse> result)
mutable {
889 self->handle_bars_page(operation, std::move(result));
895 void handle_bars_page(
896 const std::shared_ptr<BarOperation>& operation,
897 Result<network::HttpResponse> result
899 std::optional<std::string> cursor;
900 auto appended = parse_bars_page(
901 std::move(result), operation->query_, operation->descending_, cursor, operation->currency_
904 deliver(std::move(operation->completion_), Result<BarSeries>(std::move(*appended)));
908 const int collected =
static_cast<int>(operation->descending_.size());
909 const bool satisfied = collected >= operation->query_.count_;
910 const bool exhausted = !cursor || cursor->empty();
911 const bool repeated = cursor && operation->last_cursor_ && *cursor == *operation->last_cursor_;
912 const bool spent = operation->pages_ >= bar_page_budget_;
913 if (satisfied || exhausted || repeated || spent) {
914 finish_bars(operation, cursor);
918 operation->last_cursor_ = cursor;
919 operation->before_ = cursor;
920 request_bars(operation);
931 static std::optional<BrokerError> parse_bars_page(
932 Result<network::HttpResponse> result,
933 const BarQuery& query,
934 std::vector<Bar>& into,
935 std::optional<std::string>& cursor,
939 return std::move(result.error());
941 BrokerError parse_error;
942 auto document = broker_detail::parse_object(result.value(), parse_error);
946 if (result.value().result_int() < 200 || result.value().result_int() >= 300) {
947 return toss_error(result.value(), *document);
951 const auto& payload = required_object(*document,
"result");
952 const auto rows = payload.if_contains(
"candles");
953 if (!rows || !rows->is_array()) {
954 throw std::runtime_error(
"missing array field: candles");
956 const auto before = into.size();
957 for (
const auto& value : rows->as_array()) {
958 if (!value.is_object()) {
959 throw std::runtime_error(
"candle entry is not an object");
961 const auto& row = value.as_object();
964 const auto stated =
Currency(broker_detail::json_string(row,
"currency"));
965 if (into.empty() && before == 0) {
967 }
else if (!(stated == currency)) {
968 throw std::runtime_error(
"candle currency changed within one series");
970 into.push_back(parse_bar(row, query));
972 if (into.size() == before) {
976 if (
const auto next = payload.if_contains(
"nextBefore"); next && next->is_string()) {
977 cursor = std::string(next->as_string());
982 }
catch (
const std::exception& exception) {
985 error.broker_code_ =
"invalid_candle_response";
986 error.message_ = exception.what();
997 static Bar parse_bar(
const json::object& row,
const BarQuery& query) {
998 static_cast<void>(query);
1002 throw std::runtime_error(
"invalid candle timestamp");
1004 bar.timestamp_ = *timestamp;
1018 const std::shared_ptr<BarOperation>& operation,
1019 const std::optional<std::string>& cursor
1022 series.instrument_ = operation->query_.instrument_;
1023 series.interval_ = operation->query_.interval_;
1026 series.stamp_ = operation->query_.interval_ == BarInterval::ONE_MINUTE ?
1029 series.currency_ = operation->currency_;
1030 series.adjusted_ = operation->query_.adjusted_;
1031 series.received_at_ = std::chrono::system_clock::now();
1033 auto&
bars = operation->descending_;
1034 if (
static_cast<int>(
bars.size()) > operation->query_.count_) {
1035 bars.resize(
static_cast<std::size_t
>(operation->query_.count_));
1037 series.bars_.assign(
bars.rbegin(),
bars.rend());
1038 if (cursor && !cursor->empty()) {
1039 series.next_cursor_ = *cursor;
1041 deliver(std::move(operation->completion_), Result<BarSeries>(std::move(series)));
1049 static Result<json::value> market_result(Result<network::HttpResponse> response) {
1051 return Result<json::value>(std::move(response.error()));
1053 BrokerError parse_error;
1054 auto document = broker_detail::parse_object(response.value(), parse_error);
1056 return Result<json::value>(std::move(parse_error));
1058 if (response.value().result_int() < 200 || response.value().result_int() >= 300) {
1059 return Result<json::value>(toss_error(response.value(), *document));
1061 const auto payload = document->if_contains(
"result");
1065 error.broker_code_ =
"invalid_market_response";
1066 error.message_ =
"response carries no result";
1067 return Result<json::value>(std::move(error));
1069 return Result<json::value>(std::move(*payload));
1079 static Result<Instrument> parse_instrument(
1080 InstrumentId requested,
1081 Result<network::HttpResponse> result
1083 auto payload = market_result(std::move(result));
1088 if (!payload.value().is_array() || payload.value().as_array().empty()) {
1089 throw std::runtime_error(
"reference data result is not a non-empty array");
1091 const auto& row = payload.value().as_array().front().as_object();
1092 if (broker_detail::json_string(row,
"symbol") != requested.symbol_) {
1093 throw std::runtime_error(
"reference data response symbol does not match request");
1096 Instrument instrument;
1097 instrument.id_ = std::move(requested);
1098 instrument.name_ = broker_detail::json_string(row,
"name");
1099 instrument.english_name_ = broker_detail::json_string(row,
"englishName");
1100 instrument.isin_ = broker_detail::json_string(row,
"isinCode");
1101 instrument.segment_ =
market_segment(broker_detail::json_string(row,
"market"));
1102 instrument.type_ = normalized_security_type(broker_detail::json_string(row,
"securityType"));
1103 instrument.status_ =
1104 normalized_listing_status(broker_detail::json_string(row,
"status"));
1105 instrument.currency_ =
Currency(broker_detail::json_string(row,
"currency"));
1106 if (
const auto value = row.if_contains(
"isCommonShare"); value && value->is_bool()) {
1107 instrument.common_share_ = value->as_bool();
1114 instrument.listed_on_ = stated_calendar_date(row,
"listDate");
1115 instrument.delisted_on_ = stated_calendar_date(row,
"delistDate");
1116 instrument.shares_outstanding_ = nullable_decimal(row,
"sharesOutstanding");
1117 instrument.price_rule_ =
1120 }
catch (
const std::exception& exception) {
1126 static SecurityType normalized_security_type(std::string_view code) {
1127 if (code ==
"STOCK") {
1128 return SecurityType::STOCK;
1130 if (code ==
"FOREIGN_STOCK") {
1131 return SecurityType::FOREIGN_STOCK;
1133 if (code ==
"DEPOSITARY_RECEIPT") {
1134 return SecurityType::DEPOSITARY_RECEIPT;
1136 if (code ==
"INFRASTRUCTURE_FUND") {
1137 return SecurityType::INFRASTRUCTURE_FUND;
1139 if (code ==
"REIT") {
1140 return SecurityType::REIT;
1142 if (code ==
"ETF") {
1143 return SecurityType::ETF;
1145 if (code ==
"FOREIGN_ETF") {
1146 return SecurityType::FOREIGN_ETF;
1148 if (code ==
"ETN") {
1149 return SecurityType::ETN;
1151 if (code ==
"STOCK_WARRANTS") {
1152 return SecurityType::STOCK_WARRANT;
1154 return SecurityType::UNKNOWN;
1163 if (code ==
"SCHEDULED") {
1164 return InstrumentStatus::SCHEDULED;
1166 if (code ==
"ACTIVE") {
1167 return InstrumentStatus::ACTIVE;
1169 if (code ==
"DELISTED") {
1170 return InstrumentStatus::DELISTED;
1172 return InstrumentStatus::UNKNOWN;
1181 static StatedDate stated_calendar_date(
const json::object& row, std::string_view key) {
1182 const auto bound = calendar_bound(row, key);
1183 if (bound.unreadable_) {
1194 error.reached_broker_ =
false;
1195 error.broker_code_ =
"invalid_date";
1196 error.message_ =
"calendar request requires a valid Gregorian date";
1197 deliver(std::move(completion), Result<TradingCalendar>(std::move(error)));
1203 [self = shared_from_this(), country, from, completion = std::move(completion)](
1204 std::optional<BrokerError> token_error
1208 std::move(completion), Result<TradingCalendar>(std::move(*token_error))
1212 network::HttpRequest request;
1215 request.method_ = http::verb::get;
1216 request.target_ = std::string(
"/api/v1/market-calendar/") + std::string(market_code(country));
1218 request.target_ +=
"?date=" + iso_date(*from);
1220 request.headers_.emplace_back(
1221 "Authorization",
"Bearer " + self->token_.access_token_
1225 [self, country, completion = std::move(completion)](
1226 Result<network::HttpResponse> result
1229 std::move(completion), parse_calendar(country, std::move(result))
1253 static Result<TradingCalendar> parse_calendar(
1255 Result<network::HttpResponse> result
1257 auto payload = market_result(std::move(result));
1262 if (!payload.value().is_object()) {
1263 throw std::runtime_error(
"calendar result is not an object");
1265 const auto&
object = payload.value().as_object();
1266 TradingCalendar calendar;
1267 calendar.country_ = country;
1268 const std::string_view hours = calendar_hours_field(country);
1269 append_calendar_day(
object,
"previousBusinessDay", hours,
true, calendar);
1270 append_calendar_day(
object,
"today", hours,
false, calendar);
1271 append_calendar_day(
object,
"nextBusinessDay", hours,
true, calendar);
1273 calendar.days_.begin(),
1274 calendar.days_.end(),
1275 [](
const TradingDay& lhs,
const TradingDay& rhs) { return lhs.date_ < rhs.date_; }
1277 calendar.received_at_ = std::chrono::system_clock::now();
1279 }
catch (
const std::exception& exception) {
1285 static void append_calendar_day(
1286 const json::object&
object,
1287 std::string_view key,
1288 std::string_view hours_key,
1290 TradingCalendar& calendar
1292 const auto value =
object.if_contains(key);
1293 if (!value || !value->is_object()) {
1296 const auto& row = value->as_object();
1297 const auto date = parse_calendar_date(broker_detail::json_string(row,
"date"));
1302 ++calendar.unreadable_days_;
1310 const auto hours = row.if_contains(hours_key);
1312 throw std::runtime_error(
"calendar day has no " + std::string(hours_key) +
" field");
1314 day.open_ = !hours->is_null();
1316 calendar.days_.push_back(day);
1327 static std::optional<Date> parse_calendar_date(std::string_view text) {
1328 if (text.size() != 10 || text[4] !=
'-' || text[7] !=
'-') {
1329 return std::nullopt;
1331 const auto digits = [&](std::size_t at, std::size_t count) -> std::optional<int> {
1333 for (std::size_t index = at; index < at + count; ++index) {
1334 const auto character = text[index];
1335 if (character <
'0' || character >
'9') {
1336 return std::nullopt;
1338 value = value * 10 + (character -
'0');
1342 const auto year = digits(0, 4);
1343 const auto month = digits(5, 2);
1344 const auto day = digits(8, 2);
1345 if (!year || !month || !day) {
1346 return std::nullopt;
1350 date.month_ =
static_cast<unsigned>(*month);
1351 date.day_ =
static_cast<unsigned>(*day);
1352 return is_valid_date(date) ? std::optional<Date>(date) : std::nullopt;
1356 struct CalendarBound {
1357 std::optional<Date> date_;
1358 bool unreadable_{
false};
1369 static CalendarBound calendar_bound(
const json::object& row, std::string_view key) {
1370 CalendarBound bound;
1371 const auto value = row.if_contains(key);
1372 if (!value || value->is_null()) {
1375 if (!value->is_string()) {
1376 bound.unreadable_ =
true;
1379 bound.date_ = parse_calendar_date(value->as_string().c_str());
1380 bound.unreadable_ = !bound.date_;
1385 static constexpr int trade_limit_ = 50;
1388 static constexpr int price_batch_limit_ = 200;
1394 template <
typename Handler,
typename Parse>
1395 void start_market_read(
1396 InstrumentId instrument,
1401 using ResultType =
decltype(parse(InstrumentId{}, Result<network::HttpResponse>(BrokerError{})));
1402 static_assert(!std::is_void<ResultType>::value,
"a market-data parser must return a Result");
1403 if (!broker_detail::valid_symbol(instrument.symbol_)) {
1406 error.reached_broker_ =
false;
1407 error.broker_code_ =
"invalid_instrument";
1408 error.message_ =
"market data request requires a symbol";
1409 deliver(std::move(completion), ResultType(std::move(error)));
1416 self = shared_from_this(),
1417 instrument = std::move(instrument),
1418 prefix = std::move(prefix),
1419 completion = std::move(completion),
1421 ](std::optional<BrokerError> token_error)
mutable {
1423 self->deliver(std::move(completion), ResultType(std::move(*token_error)));
1426 network::HttpRequest request;
1429 request.method_ = http::verb::get;
1430 request.target_ = prefix + broker_detail::query_encode(instrument.symbol_);
1431 request.headers_.emplace_back(
1432 "Authorization",
"Bearer " + self->token_.access_token_
1436 [self, instrument = std::move(instrument), completion = std::move(completion), parse](
1437 Result<network::HttpResponse> result
1440 std::move(completion), parse(std::move(instrument), std::move(result))
1454 static Result<PriceLimits> parse_price_limits(
1455 InstrumentId instrument,
1456 Result<network::HttpResponse> result
1458 static_cast<void>(instrument);
1459 auto payload = market_result(std::move(result));
1464 if (!payload.value().is_object()) {
1465 throw std::runtime_error(
"price limit result is not an object");
1467 const auto&
object = payload.value().as_object();
1469 limits.currency_ =
Currency(broker_detail::json_string(
object,
"currency"));
1470 limits.upper_ = nullable_decimal(
object,
"upperLimitPrice");
1471 limits.lower_ = nullable_decimal(
object,
"lowerLimitPrice");
1472 if (
const auto stamp =
object.if_contains(
"timestamp"); stamp && stamp->is_string()) {
1476 }
catch (
const std::exception& exception) {
1482 static Result<std::vector<TradePrint>> parse_trades(
1483 InstrumentId instrument,
1484 Result<network::HttpResponse> result
1486 static_cast<void>(instrument);
1487 auto payload = market_result(std::move(result));
1489 return Result<std::vector<TradePrint>>(std::move(payload.error()));
1492 if (!payload.value().is_array()) {
1493 throw std::runtime_error(
"trade result is not an array");
1495 const auto& rows = payload.value().as_array();
1496 std::vector<TradePrint> prints;
1497 prints.reserve(rows.size());
1498 for (
const auto& value : rows) {
1499 if (!value.is_object()) {
1500 throw std::runtime_error(
"trade entry is not an object");
1502 const auto& row = value.as_object();
1505 print.quantity_ =
Decimal64::parse(broker_detail::json_string(row,
"volume"));
1506 print.currency_ =
Currency(broker_detail::json_string(row,
"currency"));
1507 print.exchange_time_ =
1509 if (!print.exchange_time_) {
1510 throw std::runtime_error(
"invalid trade timestamp");
1512 prints.push_back(std::move(print));
1514 return Result<std::vector<TradePrint>>(std::move(prints));
1515 }
catch (
const std::exception& exception) {
1516 return Result<std::vector<TradePrint>>(market_error(
"invalid_trade_response", exception));
1521 void start_prices(std::vector<InstrumentId> instruments,
PricesHandler completion) {
1524 error.reached_broker_ =
false;
1525 if (instruments.empty()) {
1526 error.broker_code_ =
"empty_batch";
1527 error.message_ =
"a batch price request needs at least one instrument";
1528 deliver(std::move(completion), Result<std::vector<QuoteSnapshot>>(std::move(error)));
1531 if (
static_cast<int>(instruments.size()) > price_batch_limit_) {
1532 error.broker_code_ =
"batch_too_large";
1534 "Toss quotes at most two hundred instruments per request; a longer list is "
1535 "refused rather than split, because splitting would spend several times the "
1536 "request budget a batch call exists to save";
1537 deliver(std::move(completion), Result<std::vector<QuoteSnapshot>>(std::move(error)));
1541 std::string symbols;
1542 for (
const auto& instrument : instruments) {
1543 if (!broker_detail::valid_symbol(instrument.symbol_)) {
1544 error.broker_code_ =
"invalid_instrument";
1545 error.message_ =
"batch price request requires a symbol for every instrument";
1546 deliver(std::move(completion), Result<std::vector<QuoteSnapshot>>(std::move(error)));
1549 if (!symbols.empty()) {
1552 symbols += instrument.symbol_;
1558 self = shared_from_this(),
1559 instruments = std::move(instruments),
1560 symbols = std::move(symbols),
1561 completion = std::move(completion)
1562 ](std::optional<BrokerError> token_error)
mutable {
1565 std::move(completion),
1566 Result<std::vector<QuoteSnapshot>>(std::move(*token_error))
1570 network::HttpRequest request;
1572 request.method_ = http::verb::get;
1573 request.target_ =
"/api/v1/prices?symbols=" + broker_detail::query_encode(symbols);
1574 request.headers_.emplace_back(
1575 "Authorization",
"Bearer " + self->token_.access_token_
1579 [self, instruments = std::move(instruments), completion = std::move(completion)](
1580 Result<network::HttpResponse> result
1583 std::move(completion), parse_prices(instruments, std::move(result))
1598 static Result<std::vector<QuoteSnapshot>> parse_prices(
1599 const std::vector<InstrumentId>& requested,
1600 Result<network::HttpResponse> result
1602 auto payload = market_result(std::move(result));
1604 return Result<std::vector<QuoteSnapshot>>(std::move(payload.error()));
1607 if (!payload.value().is_array()) {
1608 throw std::runtime_error(
"price result is not an array");
1610 const auto& rows = payload.value().as_array();
1611 std::vector<QuoteSnapshot> quotes;
1612 quotes.reserve(rows.size());
1613 for (
const auto& value : rows) {
1614 if (!value.is_object()) {
1615 throw std::runtime_error(
"price entry is not an object");
1617 const auto& row = value.as_object();
1618 const auto symbol = broker_detail::json_string(row,
"symbol");
1619 const auto match = std::find_if(
1622 [&symbol](
const InstrumentId& candidate) { return candidate.symbol_ == symbol; }
1624 if (match == requested.end()) {
1625 throw std::runtime_error(
"price response carries an unrequested symbol");
1627 QuoteSnapshot
quote;
1628 quote.instrument_ = *match;
1630 quote.currency_ =
Currency(broker_detail::json_string(row,
"currency"));
1631 if (
const auto stamp = row.if_contains(
"timestamp"); stamp && stamp->is_string()) {
1634 quote.received_at_ = std::chrono::system_clock::now();
1635 quotes.push_back(std::move(quote));
1637 return Result<std::vector<QuoteSnapshot>>(std::move(quotes));
1638 }
catch (
const std::exception& exception) {
1639 return Result<std::vector<QuoteSnapshot>>(market_error(
"invalid_price_response", exception));
1644 static BrokerError market_error(
const char* code,
const std::exception& cause) {
1647 error.broker_code_ = code;
1648 error.message_ = cause.what();
1653 void start_depth(InstrumentId instrument,
DepthHandler completion) {
1654 if (!broker_detail::valid_symbol(instrument.symbol_)) {
1657 error.reached_broker_ =
false;
1658 error.broker_code_ =
"invalid_instrument";
1659 error.message_ =
"order book request requires a symbol";
1660 deliver(std::move(completion), Result<DepthSnapshot>(std::move(error)));
1667 self = shared_from_this(),
1668 instrument = std::move(instrument),
1669 completion = std::move(completion)
1670 ](std::optional<BrokerError> token_error)
mutable {
1673 std::move(completion), Result<DepthSnapshot>(std::move(*token_error))
1677 network::HttpRequest request;
1680 request.method_ = http::verb::get;
1681 request.target_ =
"/api/v1/orderbook?symbol=" +
1682 broker_detail::query_encode(instrument.symbol_);
1683 request.headers_.emplace_back(
1684 "Authorization",
"Bearer " + self->token_.access_token_
1688 [self, instrument = std::move(instrument), completion = std::move(completion)](
1689 Result<network::HttpResponse> result
1692 std::move(completion), parse_depth(std::move(instrument), std::move(result))
1707 static Result<DepthSnapshot> parse_depth(
1708 InstrumentId instrument,
1709 Result<network::HttpResponse> result
1714 BrokerError parse_error;
1715 auto document = broker_detail::parse_object(result.value(), parse_error);
1719 if (result.value().result_int() < 200 || result.value().result_int() >= 300) {
1723 const auto& payload = required_object(*document,
"result");
1725 book.instrument_ = std::move(instrument);
1726 book.currency_ =
Currency(broker_detail::json_string(payload,
"currency"));
1727 append_levels(payload,
"bids", book.bids_);
1728 append_levels(payload,
"asks", book.asks_);
1730 if (
const auto stamp = payload.if_contains(
"timestamp"); stamp && stamp->is_string()) {
1732 if (!book.exchange_time_) {
1733 throw std::runtime_error(
"invalid order book timestamp");
1736 book.received_at_ = std::chrono::system_clock::now();
1738 }
catch (
const std::exception& exception) {
1741 error.broker_code_ =
"invalid_orderbook_response";
1742 error.message_ = exception.what();
1748 static void append_levels(
1749 const json::object& payload,
1750 std::string_view key,
1751 std::vector<DepthLevel>& levels
1753 const auto rows = payload.if_contains(key);
1754 if (!rows || !rows->is_array()) {
1755 throw std::runtime_error(
"missing array field: " + std::string(key));
1757 levels.reserve(rows->as_array().size());
1758 for (
const auto& value : rows->as_array()) {
1759 if (!value.is_object()) {
1760 throw std::runtime_error(
"order book entry is not an object");
1762 const auto& row = value.as_object();
1772 void start_quote(InstrumentId instrument,
QuoteHandler completion) {
1773 if (instrument.country_ != Country::KR || !broker_detail::valid_symbol(instrument.symbol_)) {
1776 error.reached_broker_ =
false;
1777 error.broker_code_ =
"invalid_instrument";
1778 error.message_ =
"Toss quote currently supports Korean equity symbols only";
1779 complete(std::move(completion), Result<QuoteSnapshot>(std::move(error)));
1784 [self = shared_from_this(),
1785 instrument = std::move(instrument),
1786 completion = std::move(completion)](std::optional<BrokerError> token_error)
mutable {
1789 std::move(completion), Result<QuoteSnapshot>(std::move(*token_error))
1793 self->request_quote(std::move(instrument), std::move(completion));
1802 [self = shared_from_this(), completion = std::move(completion)](
1803 std::optional<BrokerError> token_error
1806 self->complete_accounts(
1807 std::move(completion),
1808 Result<std::vector<TossAccount>>(std::move(*token_error))
1812 self->request_accounts(std::move(completion));
1818 void start_buying_power(
1820 std::int64_t account_sequence,
1823 auto invalid = validate_account_sequence(account_sequence);
1824 if (!invalid && !priced_currency(currency)) {
1829 error.reached_broker_ =
false;
1830 error.broker_code_ =
"unsupported_currency";
1831 error.message_ =
"Toss prices buying power in KRW and USD only, not " + currency.str();
1832 invalid = std::move(error);
1835 complete_buying_power(std::move(completion), Result<BuyingPower>(std::move(*invalid)));
1840 [self = shared_from_this(), currency, account_sequence, completion = std::move(completion)](
1841 std::optional<BrokerError> token_error
1844 self->complete_buying_power(
1845 std::move(completion), Result<BuyingPower>(std::move(*token_error))
1849 self->request_buying_power(currency, account_sequence, std::move(completion));
1855 void request_buying_power(
1857 std::int64_t account_sequence,
1860 network::HttpRequest request;
1863 request.method_ = http::verb::get;
1864 request.target_ =
"/api/v1/buying-power?currency=" + currency.str();
1865 request.headers_ = {
1866 {
"Authorization",
"Bearer " + token_.access_token_},
1867 {
"X-Tossinvest-Account", std::to_string(account_sequence)},
1871 [self = shared_from_this(), completion = std::move(completion)](
1872 Result<network::HttpResponse> result
1876 [self, completion = std::move(completion), result = std::move(result)]()
mutable {
1877 self->complete_buying_power(
1878 std::move(completion), parse_buying_power(std::move(result))
1886 void start_sellable_quantity(
1887 InstrumentId instrument,
1888 std::int64_t account_sequence,
1891 if (
auto invalid = validate_account_sequence(account_sequence)) {
1892 complete_sellable_quantity(std::move(completion), Result<Decimal64>(std::move(*invalid)));
1895 if (!broker_detail::valid_symbol(instrument.symbol_)) {
1898 error.reached_broker_ =
false;
1899 error.broker_code_ =
"invalid_instrument";
1900 error.message_ =
"sellable-quantity request requires a symbol";
1901 complete_sellable_quantity(std::move(completion), Result<Decimal64>(std::move(error)));
1906 [self = shared_from_this(), instrument = std::move(instrument), account_sequence,
1907 completion = std::move(completion)](std::optional<BrokerError> token_error)
mutable {
1909 self->complete_sellable_quantity(
1910 std::move(completion), Result<Decimal64>(std::move(*token_error))
1914 self->request_sellable_quantity(
1915 std::move(instrument), account_sequence, std::move(completion)
1922 void request_sellable_quantity(
1923 InstrumentId instrument,
1924 std::int64_t account_sequence,
1927 network::HttpRequest request;
1929 request.method_ = http::verb::get;
1931 "/api/v1/sellable-quantity?symbol=" + broker_detail::query_encode(instrument.symbol_);
1932 request.headers_ = {
1933 {
"Authorization",
"Bearer " + token_.access_token_},
1934 {
"X-Tossinvest-Account", std::to_string(account_sequence)},
1938 [self = shared_from_this(), completion = std::move(completion)](
1939 Result<network::HttpResponse> result
1943 [self, completion = std::move(completion), result = std::move(result)]()
mutable {
1944 self->complete_sellable_quantity(
1945 std::move(completion), parse_sellable_quantity(std::move(result))
1954 if (
auto invalid = validate_account_sequence(account_sequence)) {
1955 complete_commissions(
1956 std::move(completion), Result<CommissionSchedule>(std::move(*invalid))
1962 [self = shared_from_this(), account_sequence, completion = std::move(completion)](
1963 std::optional<BrokerError> token_error
1966 self->complete_commissions(
1967 std::move(completion), Result<CommissionSchedule>(std::move(*token_error))
1971 self->request_commissions(account_sequence, std::move(completion));
1978 network::HttpRequest request;
1980 request.method_ = http::verb::get;
1981 request.target_ =
"/api/v1/commissions";
1982 request.headers_ = {
1983 {
"Authorization",
"Bearer " + token_.access_token_},
1984 {
"X-Tossinvest-Account", std::to_string(account_sequence)},
1988 [self = shared_from_this(), completion = std::move(completion)](
1989 Result<network::HttpResponse> result
1993 [self, completion = std::move(completion), result = std::move(result)]()
mutable {
1994 self->complete_commissions(
1995 std::move(completion), parse_commissions(std::move(result))
2003 void start_holdings(std::int64_t account_sequence,
HoldingsHandler completion) {
2004 if (
auto invalid = validate_account_sequence(account_sequence)) {
2006 std::move(completion), Result<HoldingsSnapshot>(std::move(*invalid))
2012 [self = shared_from_this(), account_sequence, completion = std::move(completion)](
2013 std::optional<BrokerError> token_error
2016 self->complete_holdings(
2017 std::move(completion), Result<HoldingsSnapshot>(std::move(*token_error))
2021 self->request_holdings(account_sequence, std::move(completion));
2028 if (token_.valid()) {
2029 std::move(handler)(std::nullopt);
2032 token_waiters_.push_back(std::move(handler));
2037 network::HttpRequest request;
2040 request.method_ = http::verb::post;
2041 request.target_ =
"/oauth2/token";
2042 request.body_ = config_.to_form_urlencoded_body();
2043 request.headers_.emplace_back(
"content-type",
"application/x-www-form-urlencoded");
2044 http_->async_request(
2046 [self = shared_from_this()](Result<network::HttpResponse> result)
mutable {
2047 net::post(self->strand_, [self, result = std::move(result)]()
mutable {
2048 self->finish_token(std::move(result));
2054 void finish_token(Result<network::HttpResponse> result) {
2055 refreshing_ =
false;
2056 std::optional<BrokerError> failure;
2058 failure = std::move(result.error());
2060 BrokerError parse_error;
2061 auto document = broker_detail::parse_object(result.value(), parse_error);
2063 failure = std::move(parse_error);
2064 }
else if (result.value().result_int() < 200 || result.value().result_int() >= 300) {
2065 failure = toss_error(result.value(), *document);
2069 token_.from_json(*document);
2070 }
catch (
const std::exception& exception) {
2073 error.broker_code_ =
"invalid_token_response";
2074 error.message_ = exception.what();
2075 failure = std::move(error);
2086 failure->reached_broker_ =
false;
2088 auto waiters = std::move(token_waiters_);
2089 token_waiters_.clear();
2090 for (
auto& waiter : waiters) {
2092 std::move(waiter)(*failure);
2094 std::move(waiter)(std::nullopt);
2099 void request_quote(InstrumentId instrument,
QuoteHandler completion) {
2100 network::HttpRequest request;
2103 request.method_ = http::verb::get;
2104 request.target_ =
"/api/v1/prices?symbols=" + broker_detail::query_encode(instrument.symbol_);
2105 request.headers_.emplace_back(
"Authorization",
"Bearer " + token_.access_token_);
2109 self = shared_from_this(),
2110 instrument = std::move(instrument),
2111 completion = std::move(completion)
2112 ](Result<network::HttpResponse> result)
mutable {
2117 instrument = std::move(instrument),
2118 completion = std::move(completion),
2119 result = std::move(result)
2122 std::move(completion), parse_quote(std::move(instrument), std::move(result))
2131 network::HttpRequest request;
2134 request.method_ = http::verb::get;
2135 request.target_ =
"/api/v1/accounts";
2136 request.headers_.emplace_back(
"Authorization",
"Bearer " + token_.access_token_);
2139 [self = shared_from_this(), completion = std::move(completion)](
2140 Result<network::HttpResponse> result
2144 [self, completion = std::move(completion), result = std::move(result)]()
mutable {
2145 self->complete_accounts(
2146 std::move(completion), parse_accounts(std::move(result))
2154 void request_holdings(std::int64_t account_sequence,
HoldingsHandler completion) {
2155 network::HttpRequest request;
2158 request.method_ = http::verb::get;
2159 request.target_ =
"/api/v1/holdings";
2160 request.headers_ = {
2161 {
"Authorization",
"Bearer " + token_.access_token_},
2162 {
"X-Tossinvest-Account", std::to_string(account_sequence)},
2166 [self = shared_from_this(), completion = std::move(completion)](
2167 Result<network::HttpResponse> result
2171 [self, completion = std::move(completion), result = std::move(result)]()
mutable {
2172 self->complete_holdings(
2173 std::move(completion), parse_holdings(std::move(result))
2181 static Result<QuoteSnapshot> parse_quote(InstrumentId instrument, Result<network::HttpResponse> result) {
2185 BrokerError parse_error;
2186 auto document = broker_detail::parse_object(result.value(), parse_error);
2190 if (result.value().result_int() < 200 || result.value().result_int() >= 300) {
2191 auto error = toss_error(result.value(), *document);
2195 const auto payload = document->if_contains(
"result");
2197 !payload || !payload->is_array() || payload->as_array().empty() ||
2198 !payload->as_array().front().is_object()
2200 throw std::runtime_error(
"missing non-empty result array");
2202 const auto& output = payload->as_array().front().as_object();
2203 if (broker_detail::json_string(output,
"symbol") != instrument.symbol_) {
2204 throw std::runtime_error(
"quote response symbol does not match request");
2206 QuoteSnapshot
quote;
2207 quote.instrument_ = std::move(instrument);
2209 quote.currency_ =
Currency(broker_detail::json_string(output,
"currency"));
2211 const auto timestamp = output.if_contains(
"timestamp"); timestamp && timestamp->is_string()
2214 if (!
quote.exchange_time_) {
2215 throw std::runtime_error(
"invalid timestamp");
2218 quote.received_at_ = std::chrono::system_clock::now();
2220 }
catch (
const std::exception& exception) {
2223 error.broker_code_ =
"invalid_quote_response";
2224 error.message_ = exception.what();
2229 static Result<std::vector<TossAccount>> parse_accounts(Result<network::HttpResponse> result) {
2231 return Result<std::vector<TossAccount>>(std::move(result.error()));
2233 BrokerError parse_error;
2234 auto document = broker_detail::parse_object(result.value(), parse_error);
2236 return Result<std::vector<TossAccount>>(std::move(parse_error));
2238 if (result.value().result_int() < 200 || result.value().result_int() >= 300) {
2239 auto error = toss_error(result.value(), *document);
2240 return Result<std::vector<TossAccount>>(std::move(error));
2243 const auto payload = document->if_contains(
"result");
2244 if (!payload || !payload->is_array()) {
2245 throw std::runtime_error(
"missing array field: result");
2247 std::vector<TossAccount> accounts;
2248 accounts.reserve(payload->as_array().size());
2249 for (
const auto& value : payload->as_array()) {
2250 if (!value.is_object()) {
2251 throw std::runtime_error(
"account item is not an object");
2253 const auto& account = value.as_object();
2254 const auto sequence = broker_detail::json_int64(account,
"accountSeq");
2255 if (sequence <= 0) {
2256 throw std::runtime_error(
"accountSeq must be positive");
2260 broker_detail::json_string(account,
"accountNo"),
2262 broker_detail::json_string(account,
"accountType"),
2266 return Result<std::vector<TossAccount>>(std::move(accounts));
2267 }
catch (
const std::exception& exception) {
2270 error.broker_code_ =
"invalid_accounts_response";
2271 error.message_ = exception.what();
2272 return Result<std::vector<TossAccount>>(std::move(error));
2277 static bool priced_currency(
const Currency& currency) {
2285 static Result<BuyingPower> parse_buying_power(Result<network::HttpResponse> result) {
2286 auto payload = account_payload(result);
2291 if (!payload.value().is_object()) {
2292 throw std::runtime_error(
"buying power result is not an object");
2294 const auto& output = payload.value().as_object();
2296 power.cash_ = Money{
2298 Currency(broker_detail::json_string(output,
"currency")),
2301 power.received_at_ = std::chrono::system_clock::now();
2303 }
catch (
const std::exception& exception) {
2309 static Result<Decimal64> parse_sellable_quantity(Result<network::HttpResponse> result) {
2310 auto payload = account_payload(result);
2315 if (!payload.value().is_object()) {
2316 throw std::runtime_error(
"sellable quantity result is not an object");
2318 const auto& output = payload.value().as_object();
2322 }
catch (
const std::exception& exception) {
2323 return Result<Decimal64>(account_error(
"invalid_sellable_quantity_response", exception));
2333 static Result<CommissionSchedule> parse_commissions(Result<network::HttpResponse> result) {
2334 auto payload = account_payload(result);
2339 if (!payload.value().is_array()) {
2340 throw std::runtime_error(
"commission result is not an array");
2342 CommissionSchedule schedule;
2343 schedule.rates_.reserve(payload.value().as_array().size());
2344 for (
const auto& value : payload.value().as_array()) {
2345 if (!value.is_object()) {
2346 throw std::runtime_error(
"commission item is not an object");
2348 const auto& row = value.as_object();
2349 const auto market = broker_detail::json_string(row,
"marketCountry");
2350 CommissionRate rate;
2351 if (market ==
"KR") {
2352 rate.country_ = Country::KR;
2353 }
else if (market ==
"US") {
2354 rate.country_ = Country::US;
2355 }
else if (
const auto preserved = market.empty() ? std::nullopt :
Code<16>::try_make(market)) {
2366 rate.other_ = *preserved;
2372 ++schedule.dropped_;
2375 rate.rate_ =
Decimal64::parse(broker_detail::json_string(row,
"commissionRate"));
2384 const auto from = calendar_bound(row,
"startDate");
2385 const auto until = calendar_bound(row,
"endDate");
2386 rate.from_ = from.date_;
2387 rate.until_ = until.date_;
2388 rate.from_unreadable_ = from.unreadable_;
2389 rate.until_unreadable_ = until.unreadable_;
2390 if (rate.from_unreadable_ || rate.until_unreadable_) {
2393 ++schedule.unreadable_bounds_;
2395 schedule.rates_.push_back(std::move(rate));
2397 schedule.received_at_ = std::chrono::system_clock::now();
2401 schedule.tariff_ =
"tossinvest published commission schedule";
2403 }
catch (
const std::exception& exception) {
2413 static Result<json::value> account_payload(Result<network::HttpResponse>& result) {
2415 return Result<json::value>(std::move(result.error()));
2417 BrokerError parse_error;
2418 auto document = broker_detail::parse_object(result.value(), parse_error);
2420 return Result<json::value>(std::move(parse_error));
2422 if (result.value().result_int() < 200 || result.value().result_int() >= 300) {
2423 return Result<json::value>(toss_error(result.value(), *document));
2425 const auto payload = document->if_contains(
"result");
2429 error.broker_code_ =
"missing_result";
2430 error.message_ =
"response carries no result field";
2431 return Result<json::value>(std::move(error));
2433 return Result<json::value>(std::move(*payload));
2437 static BrokerError account_error(std::string code,
const std::exception& exception) {
2440 error.broker_code_ = std::move(code);
2441 error.message_ = exception.what();
2445 static Result<HoldingsSnapshot> parse_holdings(Result<network::HttpResponse> result) {
2449 BrokerError parse_error;
2450 auto document = broker_detail::parse_object(result.value(), parse_error);
2454 if (result.value().result_int() < 200 || result.value().result_int() >= 300) {
2455 auto error = toss_error(result.value(), *document);
2459 const auto payload = document->if_contains(
"result");
2460 if (!payload || !payload->is_object()) {
2461 throw std::runtime_error(
"missing object field: result");
2463 const auto& output = payload->as_object();
2464 const auto items = output.if_contains(
"items");
2465 if (!items || !items->is_array()) {
2466 throw std::runtime_error(
"missing array field: items");
2469 HoldingsSnapshot snapshot;
2470 snapshot.positions_.reserve(items->as_array().size());
2471 for (
const auto& value : items->as_array()) {
2472 if (!value.is_object()) {
2473 throw std::runtime_error(
"holdings item is not an object");
2475 snapshot.positions_.push_back(parse_position(value.as_object()));
2477 append_totals(output, snapshot.totals_);
2478 snapshot.received_at_ = std::chrono::system_clock::now();
2483 snapshot.sellable_basis_ = SellableBasis{};
2485 }
catch (
const std::exception& exception) {
2488 error.broker_code_ =
"invalid_holdings_response";
2489 error.message_ = exception.what();
2502 return Venue::COMBINED;
2504 return Venue::UNSPECIFIED;
2506 return Venue::UNSPECIFIED;
2510 static constexpr bool trades_country(
Country country)
noexcept {
2520 static constexpr std::string_view market_code(
Country country)
noexcept {
2531 static constexpr std::string_view calendar_hours_field(
Country country)
noexcept {
2534 return "integrated";
2536 return "regularMarket";
2542 static constexpr bool prices_in_whole_units(
Country country)
noexcept {
2552 static HoldingPosition parse_position(
const json::object& output) {
2553 const auto market_country = broker_detail::json_string(output,
"marketCountry");
2554 const auto currency =
Currency(broker_detail::json_string(output,
"currency"));
2556 Venue venue = Venue::UNSPECIFIED;
2557 if (market_country ==
"KR") {
2558 country = Country::KR;
2559 venue = instrument_venue(Country::KR);
2561 throw std::runtime_error(
"KR holding currency is not KRW");
2563 }
else if (market_country ==
"US") {
2564 country = Country::US;
2566 throw std::runtime_error(
"US holding currency is not USD");
2569 throw std::runtime_error(
"unsupported marketCountry: " + market_country);
2572 const auto& market_value = required_object(output,
"marketValue");
2573 const auto& profit_loss = required_object(output,
"profitLoss");
2574 HoldingPosition position;
2575 position.instrument_ = InstrumentId{
2576 country, venue, broker_detail::json_string(output,
"symbol")
2578 position.name_ = broker_detail::json_string(output,
"name");
2579 position.quantity_ =
Decimal64::parse(broker_detail::json_string(output,
"quantity"));
2580 position.average_purchase_price_ =
2581 Decimal64::parse(broker_detail::json_string(output,
"averagePurchasePrice"));
2582 position.last_price_ =
Decimal64::parse(broker_detail::json_string(output,
"lastPrice"));
2583 position.purchase_value_ = Money{
2584 Decimal64::parse(broker_detail::json_string(market_value,
"purchaseAmount")), currency
2586 position.market_value_ = Money{
2587 Decimal64::parse(broker_detail::json_string(market_value,
"amount")), currency
2589 position.unrealized_profit_loss_ = Money{
2590 Decimal64::parse(broker_detail::json_string(profit_loss,
"amount")), currency
2592 position.unrealized_return_ =
2597 static const json::object& required_object(
const json::object& parent, std::string_view key) {
2598 const auto value = parent.if_contains(key);
2599 if (!value || !value->is_object()) {
2600 throw std::runtime_error(
"missing object field: " + std::string(key));
2602 return value->as_object();
2605 static std::optional<Decimal64> optional_decimal(
const json::object&
object, std::string_view key) {
2606 const auto value =
object.if_contains(key);
2608 throw std::runtime_error(
"missing decimal field: " + std::string(key));
2610 if (value->is_null()) {
2611 return std::nullopt;
2613 if (!value->is_string()) {
2614 throw std::runtime_error(
"invalid decimal field: " + std::string(key));
2619 static void append_totals(
const json::object& output, std::vector<HoldingsTotals>& totals) {
2620 const auto& purchase = required_object(output,
"totalPurchaseAmount");
2621 const auto& market = required_object(required_object(output,
"marketValue"),
"amount");
2622 const auto& profit_loss = required_object(required_object(output,
"profitLoss"),
"amount");
2632 const auto usd_purchase = optional_decimal(purchase,
"usd");
2633 const auto usd_market = optional_decimal(market,
"usd");
2634 const auto usd_profit_loss = optional_decimal(profit_loss,
"usd");
2635 const bool any_usd = usd_purchase || usd_market || usd_profit_loss;
2636 const bool all_usd = usd_purchase && usd_market && usd_profit_loss;
2637 if (any_usd != all_usd) {
2638 throw std::runtime_error(
"USD overview fields have inconsistent nullability");
2643 Currency::usd(), *usd_purchase, *usd_market, *usd_profit_loss,
2649 static BrokerError toss_error(
const network::HttpResponse& response,
const json::object& document) {
2650 const int status = response.result_int();
2652 result.http_status_ = status;
2660 const auto error = document.if_contains(
"error");
2661 if (!error || !error->is_object()) {
2662 result.message_ =
"Toss response has no error object";
2665 const auto&
object = error->as_object();
2666 if (
const auto value =
object.if_contains(
"code"); value && value->is_string()) {
2667 result.broker_code_ = value->as_string().c_str();
2669 if (
const auto value =
object.if_contains(
"requestId"); value && value->is_string()) {
2670 result.request_id_ = value->as_string().c_str();
2672 if (
const auto value =
object.if_contains(
"message"); value && value->is_string()) {
2673 result.message_ = value->as_string().c_str();
2679 static std::optional<BrokerError> validate_account_sequence(std::int64_t account_sequence) {
2680 if (account_sequence > 0) {
2681 return std::nullopt;
2685 error.reached_broker_ =
false;
2686 error.broker_code_ =
"invalid_account";
2687 error.message_ =
"Toss account sequence must be positive";
2692 static std::optional<BrokerError> validate_order_id(std::string_view order_id) {
2693 if (!order_id.empty()) {
2694 return std::nullopt;
2698 error.reached_broker_ =
false;
2699 error.broker_code_ =
"invalid_order_reference";
2700 error.message_ =
"Toss order identifier must not be empty";
2710 static std::optional<BrokerError> validate_client_order_id(std::string_view client_order_id) {
2711 bool valid = !client_order_id.empty() && client_order_id.size() <= 36;
2712 for (
const char ch : client_order_id) {
2713 const bool allowed = (ch >=
'a' && ch <=
'z') || (ch >=
'A' && ch <=
'Z') ||
2714 (ch >=
'0' && ch <=
'9') || ch ==
'-' || ch ==
'_';
2715 valid = valid && allowed;
2718 return std::nullopt;
2722 error.reached_broker_ =
false;
2723 error.broker_code_ =
"invalid_client_order_id";
2725 "Toss client order id must be 1-36 characters of letters, digits, hyphen, or underscore";
2734 static std::optional<BrokerError> validate_quantity(
2735 const Decimal64& quantity,
2736 bool fraction_allowed =
false
2738 const auto max_scale = fraction_allowed ? 6 : 0;
2739 if (quantity.coefficient() > 0 && quantity.scale() <= max_scale) {
2740 return std::nullopt;
2744 error.reached_broker_ =
false;
2745 error.broker_code_ =
"invalid_quantity";
2747 "Toss order quantity must be a positive whole number of shares; Toss accepts a fraction, "
2748 "to six decimal places, only on a US market sell";
2753 static std::optional<BrokerError> validate_price(
2755 const std::optional<Decimal64>& price
2759 error.reached_broker_ =
false;
2760 error.broker_code_ =
"invalid_price";
2763 error.message_ =
"Toss limit order requires a price";
2766 if (price->coefficient() <= 0) {
2767 error.message_ =
"Toss limit order price must be positive";
2770 return std::nullopt;
2774 error.message_ =
"Toss market order must not carry a price";
2777 return std::nullopt;
2783 error.broker_code_ =
"unsupported_order";
2784 error.message_ =
"Toss orders currently support limit and market orders only";
2799 static std::optional<BrokerError> validate_order_shape(
2807 error.reached_broker_ =
false;
2810 error.broker_code_ =
"unsupported_order_type";
2811 error.message_ =
"Toss accepts a limit or market order only";
2815 error.broker_code_ =
"unsupported_time_in_force";
2817 "Toss orders are day-valid; it publishes no immediate-or-cancel or "
2818 "fill-or-kill condition";
2822 error.broker_code_ =
"unsupported_session";
2825 return std::nullopt;
2828 return std::nullopt;
2831 "Toss accepts a closing-auction order only for a US limit order";
2834 if (country == Country::KR) {
2835 return std::nullopt;
2837 error.message_ =
"Toss accepts an opening-auction order only for a Korean order";
2840 error.message_ =
"Toss publishes no order code for this trading session";
2846 static std::optional<BrokerError> validate_order(
const OrderRequest& order) {
2847 if (!trades_country(order.instrument_.country_)) {
2850 error.reached_broker_ =
false;
2851 error.broker_code_ =
"unsupported_market";
2852 error.message_ =
"Toss trades Korean and US listings only";
2855 if (!broker_detail::valid_symbol(order.instrument_.symbol_)) {
2858 error.reached_broker_ =
false;
2859 error.broker_code_ =
"invalid_instrument";
2860 error.message_ =
"Toss order symbol contains unsupported characters";
2866 if (order.instrument_.venue_ != instrument_venue(order.instrument_.country_)) {
2869 error.reached_broker_ =
false;
2870 error.broker_code_ =
"invalid_instrument";
2872 "Toss identifies an instrument by the venue its own listings report; it offers no venue "
2873 "choice, so an order may not name one";
2876 if (order.venue_ != Venue::UNSPECIFIED) {
2879 error.reached_broker_ =
false;
2880 error.broker_code_ =
"venue_unavailable";
2882 "Toss publishes no venue choice on an order; leave OrderRequest::venue_ unspecified";
2885 if (order.client_order_id_) {
2886 if (
auto error = validate_client_order_id(*order.client_order_id_)) {
2894 if (order.trigger_price_) {
2897 error.reached_broker_ =
false;
2898 error.broker_code_ =
"unsupported_trigger_price";
2899 error.message_ =
"this Toss client does not place conditional orders, so it takes no trigger price";
2907 if (
auto error = validate_order_shape(
2908 order.type_, order.time_in_force_, order.session_, order.instrument_.country_
2914 if (session_code(order.session_)[0] ==
'\0') {
2917 error.reached_broker_ =
false;
2918 error.broker_code_ =
"unsupported_session";
2919 error.message_ =
"Toss publishes no order code for this trading session";
2924 const bool fraction_allowed = order.instrument_.country_ == Country::US &&
2926 if (
auto error = validate_quantity(order.quantity_, fraction_allowed)) {
2929 if (
auto error = validate_price(order.type_, order.price_)) {
2932 if (prices_in_whole_units(order.instrument_.country_) && order.price_ && order.price_->scale() != 0) {
2935 error.reached_broker_ =
false;
2936 error.broker_code_ =
"invalid_price";
2937 error.message_ =
"Toss Korean limit order price must be a whole number of won";
2940 return std::nullopt;
2953 static const char* order_type_code(
OrderType type) {
2962 throw std::logic_error(
"Toss order body built from an unvalidated price basis");
2970 static std::optional<BrokerError> validate_amendment_shape(
OrderType type) {
2972 return std::nullopt;
2976 error.reached_broker_ =
false;
2977 error.broker_code_ =
"unsupported_order_type";
2978 error.message_ =
"Toss modifies an order to a limit or market basis only";
2993 static const char* session_code(
OrderSession session) {
3001 case OrderSession::UNKNOWN:
3016 case OrderSession::UNKNOWN:
3017 return TimeInForce::UNKNOWN;
3028 return TimeInForce::UNKNOWN;
3032 static network::HttpRequest make_place_order_request(
3033 std::int64_t account_sequence,
3034 std::string_view access_token,
3035 const OrderRequest& order
3038 {
"symbol", order.instrument_.symbol_},
3039 {
"side", order.side_ == OrderSide::SELL ?
"SELL" :
"BUY"},
3040 {
"orderType", order_type_code(order.type_)},
3041 {
"timeInForce", session_code(order.session_)},
3042 {
"quantity", order.quantity_.str()},
3044 if (order.client_order_id_) {
3045 body.emplace(
"clientOrderId", *order.client_order_id_);
3048 body.emplace(
"price", order.price_->str());
3050 if (order.confirm_high_value_) {
3051 body.emplace(
"confirmHighValueOrder",
true);
3054 network::HttpRequest request;
3057 request.method_ = http::verb::post;
3058 request.target_ =
"/api/v1/orders";
3059 request.body_ = json::serialize(body);
3060 request.headers_ = {
3061 {
"Authorization",
"Bearer " + std::string(access_token)},
3062 {
"X-Tossinvest-Account", std::to_string(account_sequence)},
3063 {
"Content-Type",
"application/json"},
3069 static network::HttpRequest make_replace_order_request(
3070 std::int64_t account_sequence,
3071 std::string_view access_token,
3072 const TossAmendment& amendment
3074 json::object body{{
"orderType", order_type_code(amendment.type_)}};
3075 if (amendment.quantity_) {
3076 body.emplace(
"quantity", amendment.quantity_->str());
3078 if (amendment.price_) {
3079 body.emplace(
"price", amendment.price_->str());
3081 if (amendment.confirm_high_value_) {
3082 body.emplace(
"confirmHighValueOrder",
true);
3085 network::HttpRequest request;
3088 request.method_ = http::verb::post;
3090 "/api/v1/orders/" + broker_detail::query_encode(amendment.order_id_) +
"/modify";
3091 request.body_ = json::serialize(body);
3092 request.headers_ = {
3093 {
"Authorization",
"Bearer " + std::string(access_token)},
3094 {
"X-Tossinvest-Account", std::to_string(account_sequence)},
3095 {
"Content-Type",
"application/json"},
3101 static network::HttpRequest make_cancel_order_request(
3102 std::int64_t account_sequence,
3103 std::string_view access_token,
3104 std::string_view order_id
3106 network::HttpRequest request;
3109 request.method_ = http::verb::post;
3110 request.target_ =
"/api/v1/orders/" + broker_detail::query_encode(order_id) +
"/cancel";
3111 request.body_ =
"{}";
3112 request.headers_ = {
3113 {
"Authorization",
"Bearer " + std::string(access_token)},
3114 {
"X-Tossinvest-Account", std::to_string(account_sequence)},
3115 {
"Content-Type",
"application/json"},
3121 static network::HttpRequest make_orders_request(
3122 std::string_view access_token,
3123 const TossOrderQuery& query
3126 switch (query.scope_) {
3133 std::string target =
"/api/v1/orders?status=";
3134 target += open ?
"OPEN" :
"CLOSED";
3135 if (query.symbol_) {
3136 target +=
"&symbol=" + broker_detail::query_encode(*query.symbol_);
3139 target +=
"&from=" + iso_date(*query.from_);
3142 target +=
"&to=" + iso_date(*query.to_);
3148 if (query.cursor_) {
3149 target +=
"&cursor=" + broker_detail::query_encode(*query.cursor_);
3152 target +=
"&limit=" + std::to_string(*query.limit_);
3156 network::HttpRequest request;
3159 request.method_ = http::verb::get;
3160 request.target_ = std::move(target);
3161 request.headers_ = {
3162 {
"Authorization",
"Bearer " + std::string(access_token)},
3163 {
"X-Tossinvest-Account", std::to_string(query.account_sequence_)},
3169 static network::HttpRequest make_order_request(
3170 std::int64_t account_sequence,
3171 std::string_view access_token,
3172 std::string_view order_id
3174 network::HttpRequest request;
3177 request.method_ = http::verb::get;
3178 request.target_ =
"/api/v1/orders/" + broker_detail::query_encode(order_id);
3179 request.headers_ = {
3180 {
"Authorization",
"Bearer " + std::string(access_token)},
3181 {
"X-Tossinvest-Account", std::to_string(account_sequence)},
3187 void start_place_order(
3188 std::int64_t account_sequence,
3192 auto invalid = validate_account_sequence(account_sequence);
3194 invalid = validate_order(order);
3197 complete_order_ack(std::move(completion), Result<OrderAck>(std::move(*invalid)));
3204 self = shared_from_this(),
3206 order = std::move(order),
3207 completion = std::move(completion)
3208 ](std::optional<BrokerError> token_error)
mutable {
3210 self->complete_order_ack(
3211 std::move(completion), Result<OrderAck>(std::move(*token_error))
3215 auto request = make_place_order_request(
3216 account_sequence, self->token_.access_token_, order
3218 self->request_order_ack(std::move(request), std::move(completion));
3225 void start_replace_order(
3226 std::int64_t account_sequence,
3227 TossAmendment amendment,
3230 auto invalid = validate_account_sequence(account_sequence);
3232 invalid = validate_order_id(amendment.order_id_);
3234 if (!invalid && amendment.quantity_) {
3235 invalid = validate_quantity(*amendment.quantity_);
3238 invalid = validate_amendment_shape(amendment.type_);
3241 invalid = validate_price(amendment.type_, amendment.price_);
3244 complete_order_ack(std::move(completion), Result<OrderAck>(std::move(*invalid)));
3251 self = shared_from_this(),
3253 amendment = std::move(amendment),
3254 completion = std::move(completion)
3255 ](std::optional<BrokerError> token_error)
mutable {
3257 self->complete_order_ack(
3258 std::move(completion), Result<OrderAck>(std::move(*token_error))
3262 auto request = make_replace_order_request(
3263 account_sequence, self->token_.access_token_, amendment
3265 self->request_order_ack(std::move(request), std::move(completion));
3272 void start_cancel_order(
3273 std::int64_t account_sequence,
3274 std::string order_id,
3277 auto invalid = validate_account_sequence(account_sequence);
3279 invalid = validate_order_id(order_id);
3282 complete_order_ack(std::move(completion), Result<OrderAck>(std::move(*invalid)));
3289 self = shared_from_this(),
3291 order_id = std::move(order_id),
3292 completion = std::move(completion)
3293 ](std::optional<BrokerError> token_error)
mutable {
3295 self->complete_order_ack(
3296 std::move(completion), Result<OrderAck>(std::move(*token_error))
3300 auto request = make_cancel_order_request(
3301 account_sequence, self->token_.access_token_, order_id
3303 self->request_order_ack(std::move(request), std::move(completion));
3310 void request_order_ack(network::HttpRequest request,
OrderAckHandler completion) {
3313 [self = shared_from_this(), completion = std::move(completion)](
3314 Result<network::HttpResponse> result
3318 [self, completion = std::move(completion), result = std::move(result)]()
mutable {
3319 self->complete_order_ack(std::move(completion), parse_order_ack(std::move(result)));
3328 if (
auto invalid = validate_account_sequence(query.account_sequence_)) {
3329 complete_order_page(std::move(completion), Result<OrderPage>(std::move(*invalid)));
3332 if (query.symbol_ && !broker_detail::valid_symbol(*query.symbol_)) {
3335 error.reached_broker_ =
false;
3336 error.broker_code_ =
"invalid_instrument";
3337 error.message_ =
"Toss order filter symbol contains unsupported characters";
3338 complete_order_page(std::move(completion), Result<OrderPage>(std::move(error)));
3341 if (query.limit_ && (*query.limit_ < 1 || *query.limit_ > 100)) {
3344 error.reached_broker_ =
false;
3345 error.broker_code_ =
"invalid_limit";
3346 error.message_ =
"Toss order listing accepts a page size between 1 and 100";
3347 complete_order_page(std::move(completion), Result<OrderPage>(std::move(error)));
3353 (query.from_ && query.to_ && *query.to_ < *query.from_)
3357 error.reached_broker_ =
false;
3358 error.broker_code_ =
"invalid_date_range";
3359 error.message_ =
"Toss order listing requires valid inclusive calendar bounds";
3360 complete_order_page(std::move(completion), Result<OrderPage>(std::move(error)));
3366 [self = shared_from_this(), query = std::move(query), completion = std::move(completion)](
3367 std::optional<BrokerError> token_error
3370 self->complete_order_page(
3371 std::move(completion), Result<OrderPage>(std::move(*token_error))
3375 auto request = make_orders_request(self->token_.access_token_, query);
3378 [self, completion = std::move(completion)](
3379 Result<network::HttpResponse> result
3383 [self, completion = std::move(completion), result = std::move(result)]()
mutable {
3384 self->complete_order_page(
3385 std::move(completion), parse_orders(std::move(result))
3397 void start_order(std::int64_t account_sequence, std::string order_id,
OrderHandler completion) {
3398 auto invalid = validate_account_sequence(account_sequence);
3400 invalid = validate_order_id(order_id);
3403 complete_order(std::move(completion), Result<OrderSnapshot>(std::move(*invalid)));
3410 self = shared_from_this(),
3412 order_id = std::move(order_id),
3413 completion = std::move(completion)
3414 ](std::optional<BrokerError> token_error)
mutable {
3416 self->complete_order(
3417 std::move(completion), Result<OrderSnapshot>(std::move(*token_error))
3421 auto request = make_order_request(
3422 account_sequence, self->token_.access_token_, order_id
3426 [self, completion = std::move(completion)](
3427 Result<network::HttpResponse> result
3431 [self, completion = std::move(completion), result = std::move(result)]()
mutable {
3432 self->complete_order(std::move(completion), parse_order(std::move(result)));
3443 static Result<OrderAck> parse_order_ack(Result<network::HttpResponse> result) {
3447 BrokerError parse_error;
3448 auto document = broker_detail::parse_object(result.value(), parse_error);
3452 if (result.value().result_int() < 200 || result.value().result_int() >= 300) {
3453 auto error = toss_error(result.value(), *document);
3457 const auto payload = document->if_contains(
"result");
3458 if (!payload || !payload->is_object()) {
3459 throw std::runtime_error(
"missing object field: result");
3461 const auto& fields = payload->as_object();
3464 ack.order_id_ = broker_detail::json_string(fields,
"orderId");
3465 if (
const auto value = fields.if_contains(
"clientOrderId"); value && value->is_string()) {
3466 ack.client_order_id_ = std::string(value->as_string());
3468 ack.accepted_at_ = std::chrono::system_clock::now();
3470 }
catch (
const std::exception& exception) {
3473 error.broker_code_ =
"invalid_order_response";
3474 error.message_ = exception.what();
3480 static Result<OrderPage> parse_orders(Result<network::HttpResponse> result) {
3484 BrokerError parse_error;
3485 auto document = broker_detail::parse_object(result.value(), parse_error);
3489 if (result.value().result_int() < 200 || result.value().result_int() >= 300) {
3490 auto error = toss_error(result.value(), *document);
3494 const auto payload = document->if_contains(
"result");
3495 if (!payload || !payload->is_object()) {
3496 throw std::runtime_error(
"missing object field: result");
3498 const auto& output = payload->as_object();
3499 const auto orders = output.if_contains(
"orders");
3500 if (!orders || !orders->is_array()) {
3501 throw std::runtime_error(
"missing array field: orders");
3505 page.orders_.reserve(orders->as_array().size());
3506 for (
const auto& value : orders->as_array()) {
3507 if (!value.is_object()) {
3508 throw std::runtime_error(
"order item is not an object");
3510 page.orders_.push_back(parse_order_object(value.as_object()));
3512 if (
const auto cursor = output.if_contains(
"nextCursor"); cursor && cursor->is_string()) {
3513 page.next_cursor_ = std::string(cursor->as_string());
3515 page.received_at_ = std::chrono::system_clock::now();
3517 }
catch (
const std::exception& exception) {
3520 error.broker_code_ =
"invalid_orders_response";
3521 error.message_ = exception.what();
3527 static Result<OrderSnapshot> parse_order(Result<network::HttpResponse> result) {
3531 BrokerError parse_error;
3532 auto document = broker_detail::parse_object(result.value(), parse_error);
3536 if (result.value().result_int() < 200 || result.value().result_int() >= 300) {
3537 auto error = toss_error(result.value(), *document);
3541 const auto payload = document->if_contains(
"result");
3542 if (!payload || !payload->is_object()) {
3543 throw std::runtime_error(
"missing object field: result");
3546 }
catch (
const std::exception& exception) {
3549 error.broker_code_ =
"invalid_order_response";
3550 error.message_ = exception.what();
3564 static OrderSnapshot parse_order_object(
const json::object& row) {
3565 const auto currency =
Currency(broker_detail::json_string(row,
"currency"));
3569 country = Country::KR;
3571 country = Country::US;
3573 throw std::runtime_error(
"unsupported Toss order currency: " + currency.str());
3576 OrderSnapshot order;
3577 order.order_id_ = broker_detail::json_string(row,
"orderId");
3582 order.instrument_ = InstrumentId{
3584 instrument_venue(country),
3585 broker_detail::json_string(row,
"symbol"),
3587 order.side_ = normalized_order_side(broker_detail::json_string(row,
"side"));
3588 order.type_ = normalized_order_type(broker_detail::json_string(row,
"orderType"));
3589 const auto time_in_force = broker_detail::json_string(row,
"timeInForce");
3590 order.session_ = normalized_session(time_in_force);
3595 order.time_in_force_ = read_time_in_force(order.session_);
3597 order.shape_code_ = *code;
3599 order.status_ = normalized_status(broker_detail::json_string(row,
"status"));
3600 order.price_ = nullable_decimal(row,
"price");
3601 order.quantity_ =
Decimal64::parse(broker_detail::json_string(row,
"quantity"));
3607 order.order_amount_ = nullable_rounded_decimal(row,
"orderAmount");
3608 order.currency_ = currency;
3609 order.ordered_at_ = optional_timestamp(row,
"orderedAt");
3610 order.canceled_at_ = optional_timestamp(row,
"canceledAt");
3612 const auto execution = row.if_contains(
"execution");
3613 if (!execution || !execution->is_object()) {
3614 throw std::runtime_error(
"missing object field: execution");
3616 const auto& fills = execution->as_object();
3617 order.execution_.filled_quantity_ =
3619 order.execution_.average_filled_price_ = optional_decimal(fills,
"averageFilledPrice");
3620 order.execution_.filled_amount_ = optional_money(fills,
"filledAmount", currency);
3621 order.execution_.commission_ = optional_money(fills,
"commission", currency);
3622 order.execution_.tax_ = optional_money(fills,
"tax", currency);
3623 order.execution_.filled_at_ = optional_timestamp(fills,
"filledAt");
3624 order.execution_.settlement_date_ = optional_date(fills,
"settlementDate");
3629 static OrderType normalized_order_type(std::string_view code) {
3630 if (code ==
"LIMIT") {
3633 if (code ==
"MARKET") {
3636 return OrderType::UNKNOWN;
3640 static OrderSession normalized_session(std::string_view code) {
3641 if (code ==
"DAY") {
3644 if (code ==
"CLS") {
3647 if (code ==
"OPG") {
3650 return OrderSession::UNKNOWN;
3654 static OrderStatus normalized_status(std::string_view code) {
3655 if (code ==
"PENDING") {
3656 return OrderStatus::PENDING;
3658 if (code ==
"PENDING_CANCEL") {
3659 return OrderStatus::PENDING_CANCEL;
3661 if (code ==
"PENDING_REPLACE") {
3662 return OrderStatus::PENDING_REPLACE;
3664 if (code ==
"PARTIAL_FILLED") {
3665 return OrderStatus::PARTIAL_FILLED;
3667 if (code ==
"FILLED") {
3668 return OrderStatus::FILLED;
3670 if (code ==
"CANCELED") {
3671 return OrderStatus::CANCELED;
3673 if (code ==
"REJECTED") {
3674 return OrderStatus::REJECTED;
3676 if (code ==
"CANCEL_REJECTED") {
3677 return OrderStatus::CANCEL_REJECTED;
3679 if (code ==
"REPLACE_REJECTED") {
3680 return OrderStatus::REPLACE_REJECTED;
3682 if (code ==
"REPLACED") {
3683 return OrderStatus::REPLACED;
3685 return OrderStatus::UNKNOWN;
3689 static OrderSide normalized_order_side(std::string_view code) {
3690 if (code ==
"BUY") {
3691 return OrderSide::BUY;
3693 if (code ==
"SELL") {
3694 return OrderSide::SELL;
3696 throw std::runtime_error(
"invalid Toss order side code");
3705 static std::optional<Decimal64> nullable_decimal(
3706 const json::object&
object,
3707 std::string_view key
3709 const auto value =
object.if_contains(key);
3710 if (!value || value->is_null()) {
3711 return std::nullopt;
3713 if (!value->is_string()) {
3714 throw std::runtime_error(
"invalid decimal field: " + std::string(key));
3726 static std::optional<Decimal64> nullable_rounded_decimal(
3727 const json::object&
object,
3728 std::string_view key
3730 const auto value =
object.if_contains(key);
3731 if (!value || value->is_null()) {
3732 return std::nullopt;
3734 if (!value->is_string()) {
3735 throw std::runtime_error(
"invalid decimal field: " + std::string(key));
3737 return rounded_decimal(std::string_view(value->as_string()));
3746 static Decimal64 rounded_decimal(std::string_view text) {
3749 }
catch (
const std::out_of_range&) {
3750 const auto sign = !text.empty() && (text.front() ==
'-' || text.front() ==
'+') ? 1U : 0U;
3751 const auto point = text.find(
'.');
3752 if (point == std::string_view::npos) {
3755 auto whole = text.substr(sign, point - sign);
3756 const auto fraction = text.substr(point + 1);
3757 for (
const auto part : {whole, fraction}) {
3758 if (part.find_first_not_of(
"0123456789") != std::string_view::npos) {
3759 throw std::invalid_argument(
"decimal value contains an invalid character");
3762 const auto significant = whole.find_first_not_of(
'0');
3763 whole = significant == std::string_view::npos ? std::string_view() : whole.substr(significant);
3764 constexpr std::size_t digits = 18;
3765 if (whole.size() > digits) {
3768 std::size_t kept = 0;
3769 if (!whole.empty()) {
3770 kept = std::min(fraction.size(), digits - whole.size());
3772 const auto leading = fraction.find_first_not_of(
'0');
3773 if (leading == std::string_view::npos) {
3776 kept = std::min(fraction.size(), leading + digits);
3778 if (kept > std::numeric_limits<std::uint8_t>::max()) {
3779 throw std::out_of_range(
"decimal scale exceeds Decimal64 range");
3781 std::uint64_t magnitude = 0;
3782 for (
const auto ch : whole) {
3783 magnitude = magnitude * 10U +
static_cast<std::uint64_t
>(ch -
'0');
3785 for (
const auto ch : fraction.substr(0, kept)) {
3786 magnitude = magnitude * 10U +
static_cast<std::uint64_t
>(ch -
'0');
3788 const auto dropped = fraction.substr(kept);
3789 if (!dropped.empty()) {
3790 const bool beyond_half = dropped.find_first_not_of(
'0', 1) != std::string_view::npos;
3791 if (dropped.front() >
'5' || (dropped.front() ==
'5' && (beyond_half || magnitude % 2U == 1U))) {
3795 const auto coefficient =
static_cast<std::int64_t
>(magnitude);
3796 const Decimal64 rounded(
3797 sign == 1U && text.front() ==
'-' ? -coefficient : coefficient,
static_cast<std::uint8_t
>(kept)
3804 static std::optional<Money> optional_money(
3805 const json::object&
object,
3806 std::string_view key,
3807 const Currency& currency
3809 const auto amount = optional_decimal(
object, key);
3811 return std::nullopt;
3813 return Money{*amount, currency};
3817 static std::optional<std::chrono::system_clock::time_point> optional_timestamp(
3818 const json::object&
object,
3819 std::string_view key
3821 const auto value =
object.if_contains(key);
3822 if (!value || value->is_null()) {
3823 return std::nullopt;
3825 if (!value->is_string()) {
3826 throw std::runtime_error(
"invalid timestamp field: " + std::string(key));
3830 throw std::runtime_error(
"invalid timestamp field: " + std::string(key));
3836 static std::optional<Date> optional_date(
const json::object&
object, std::string_view key) {
3837 const auto value =
object.if_contains(key);
3838 if (!value || value->is_null()) {
3839 return std::nullopt;
3841 if (!value->is_string()) {
3842 throw std::runtime_error(
"invalid date field: " + std::string(key));
3844 const std::string_view text(value->as_string());
3849 text.size() != 10 || text[4] !=
'-' || text[7] !=
'-' ||
3850 std::sscanf(std::string(text).c_str(),
"%4d-%2u-%2u", &year, &month, &day) != 3
3852 throw std::runtime_error(
"invalid date field: " + std::string(key));
3854 Date date{year, month, day};
3856 throw std::runtime_error(
"invalid date field: " + std::string(key));
3862 static std::string iso_date(
const Date& date) {
3864 std::snprintf(buffer,
sizeof(buffer),
"%04d-%02u-%02u", date.year_, date.month_, date.day_);
3865 return std::string(buffer);
3877 template <
typename Handler>
3878 void send(network::HttpRequest request, Handler handler) {
3879 std::string authorization;
3880 for (
const auto& [name, value] : request.headers_) {
3881 if (name ==
"Authorization") {
3882 authorization = value;
3885 http_->async_request(
3888 self = shared_from_this(),
3889 authorization = std::move(authorization),
3890 handler = std::move(handler)
3891 ](Result<network::HttpResponse> result)
mutable {
3892 if (result && result.value().result_int() == 401) {
3895 net::post(self->strand_, [self, authorization = std::move(authorization)] {
3896 self->forget_refused_token(authorization);
3899 std::move(handler)(std::move(result));
3905 void forget_refused_token(
const std::string& authorization) {
3916 template <
typename Handler,
typename T>
3917 void deliver(Handler handler, Result<T> result) {
3919 note_rate_limit(result.error());
3921 auto executor = net::get_associated_executor(handler, strand_);
3922 net::dispatch(executor, [handler = std::move(handler), result = std::move(result)]()
mutable {
3923 std::move(handler)(std::move(result));
3933 void note_rate_limit(
const BrokerError& error) {
3937 auto* limiter = http_->rate_limits();
3941 const auto retry = error.retry_after_ ?
3942 std::chrono::duration_cast<network::RateLimitController::Duration>(*error.retry_after_) :
3943 std::chrono::duration_cast<network::RateLimitController::Duration>(std::chrono::seconds(1));
3944 if (error.rate_limit_) {
3945 limiter->shrink(*error.rate_limit_, *error.rate_limit_, retry);
3951 limiter->hold(retry);
3954 void complete_order_ack(OrderAckHandler handler, Result<OrderAck> result) {
3955 deliver(std::move(handler), std::move(result));
3959 void complete_order_page(OrderPageHandler handler, Result<OrderPage> result) {
3960 deliver(std::move(handler), std::move(result));
3964 void complete_order(OrderHandler handler, Result<OrderSnapshot> result) {
3965 deliver(std::move(handler), std::move(result));
3968 void complete(QuoteHandler handler, Result<QuoteSnapshot> result) {
3969 deliver(std::move(handler), std::move(result));
3972 void complete_accounts(AccountsHandler handler, Result<std::vector<TossAccount>> result) {
3973 deliver(std::move(handler), std::move(result));
3976 void complete_holdings(HoldingsHandler handler, Result<HoldingsSnapshot> result) {
3977 deliver(std::move(handler), std::move(result));
3980 void complete_buying_power(BuyingPowerHandler handler, Result<BuyingPower> result) {
3981 deliver(std::move(handler), std::move(result));
3984 void complete_sellable_quantity(SellableQuantityHandler handler, Result<Decimal64> result) {
3985 deliver(std::move(handler), std::move(result));
3988 void complete_commissions(CommissionScheduleHandler handler, Result<CommissionSchedule> result) {
3989 deliver(std::move(handler), std::move(result));