885class KiwoomClient :
public std::enable_shared_from_this<KiwoomClient> {
916 using TokenHandler = net::any_completion_handler<void(std::optional<BrokerError>)>;
926 net::any_io_executor executor,
927 ssl::context& context,
931 : strand_(net::make_strand(std::move(executor))), config_(std::move(
config)),
933 std::make_shared<
network::AsyncHttpClient>(
934 strand_, context, config_.rest_host(), config_.rest_port(), options
945 template <
typename CompletionToken>
948 [self = shared_from_this()](
auto handler,
InstrumentId subject)
mutable {
951 [self, subject = std::move(subject), handler = std::move(handler)]()
mutable {
952 self->start_market_read<QuoteSnapshot>(
953 std::move(subject),
"ka10007",
"/api/dostk/mrkcond", QuoteHandler(std::move(handler)),
954 &KiwoomClient::parse_quote
960 std::move(instrument)
971 template <
typename CompletionToken>
974 [self = shared_from_this()](
auto handler,
InstrumentId subject)
mutable {
977 [self, subject = std::move(subject), handler = std::move(handler)]()
mutable {
978 self->start_market_read<PriceLimits>(
979 std::move(subject),
"ka10007",
"/api/dostk/mrkcond",
980 PriceLimitsHandler(std::move(handler)), &KiwoomClient::parse_price_limits
986 std::move(instrument)
997 template <
typename CompletionToken>
1000 [self = shared_from_this()](
auto handler,
InstrumentId subject)
mutable {
1003 [self, subject = std::move(subject), handler = std::move(handler)]()
mutable {
1004 self->start_market_read<DepthSnapshot>(
1005 std::move(subject),
"ka10004",
"/api/dostk/mrkcond", DepthHandler(std::move(handler)),
1006 &KiwoomClient::parse_depth
1012 std::move(instrument)
1024 template <
typename CompletionToken>
1026 return net::async_initiate<CompletionToken, void(
Result<std::vector<TradePrint>>)>(
1027 [self = shared_from_this()](
auto handler,
InstrumentId subject,
int wanted)
mutable {
1030 [self, subject = std::move(subject), wanted, handler = std::move(handler)]()
mutable {
1031 self->start_trades(std::move(subject), wanted, TradesHandler(std::move(handler)));
1036 std::move(instrument),
1048 template <
typename CompletionToken>
1051 [self = shared_from_this()](
auto handler,
InstrumentId subject)
mutable {
1054 [self, subject = std::move(subject), handler = std::move(handler)]()
mutable {
1055 self->start_instrument(std::move(subject), InstrumentHandler(std::move(handler)));
1060 std::move(instrument)
1079 template <
typename CompletionToken>
1082 [self = shared_from_this()](
auto handler,
BarQuery wanted)
mutable {
1085 [self, wanted = std::move(wanted), handler = std::move(handler)]()
mutable {
1086 self->start_bars(std::move(wanted), BarsHandler(std::move(handler)));
1105 net::strand<net::any_io_executor>
get_executor() const noexcept {
return strand_; }
1120 template <
typename CompletionToken>
1123 [self = shared_from_this()](
auto handler,
KiwoomAccount)
mutable {
1126 [self, handler = std::move(handler)]()
mutable {
1127 self->start_holdings(HoldingsHandler(std::move(handler)));
1144 template <
typename CompletionToken>
1150 [self, wanted, handler = std::move(handler)]()
mutable {
1151 self->start_buying_power(wanted, BuyingPowerHandler(std::move(handler)));
1167 template <
typename CompletionToken>
1170 [self = shared_from_this()](
auto handler)
mutable {
1173 [self, handler = std::move(handler)]()
mutable {
1174 self->start_deposit(DepositHandler(std::move(handler)));
1191 template <
typename CompletionToken>
1195 std::optional<Decimal64> price,
1196 CompletionToken&& token
1199 [self = shared_from_this()](
1205 self, subject = std::move(subject), direction, limit = std::move(limit),
1206 handler = std::move(handler)
1208 self->start_orderable_amount(
1209 std::move(subject), direction, std::move(limit), OrderableHandler(std::move(handler))
1215 std::move(instrument),
1227 template <
typename CompletionToken>
1229 return net::async_initiate<CompletionToken, void(
Result<std::vector<KiwoomAccountInfo>>)>(
1230 [self = shared_from_this()](
auto handler)
mutable {
1233 [self, handler = std::move(handler)]()
mutable {
1234 self->start_accounts(AccountsHandler(std::move(handler)));
1273 template <
typename CompletionToken>
1279 [self, placed = std::move(placed), handler = std::move(handler)]()
mutable {
1280 self->start_place_order(std::move(placed), OrderAckHandler(std::move(handler)));
1304 template <
typename CompletionToken>
1308 CompletionToken&& token
1314 [self, change = std::move(change), handler = std::move(handler)]()
mutable {
1315 self->start_amend_order(
1316 std::move(change), false, OrderAckHandler(std::move(handler))
1323 std::move(amendment)
1346 template <
typename CompletionToken>
1350 CompletionToken&& token
1356 [self, change = std::move(change), handler = std::move(handler)]()
mutable {
1357 self->start_amend_order(std::move(change), true, OrderAckHandler(std::move(handler)));
1363 std::move(amendment)
1374 template <
typename CompletionToken>
1377 [self = shared_from_this()](
auto handler,
KiwoomAccount)
mutable {
1380 [self, handler = std::move(handler)]()
mutable {
1381 self->start_open_orders(OpenOrderHandler(std::move(handler)));
1398 template <
typename CompletionToken>
1402 CompletionToken&& token
1408 [self, wanted = std::move(wanted), handler = std::move(handler)]()
mutable {
1409 self->start_order_history(std::move(wanted), OrderPageHandler(std::move(handler)));
1428 const auto* limiter = http_->rate_limits();
1429 return limiter ? &limiter->stats() :
nullptr;
1440 friend struct KiwoomClientTestAccess;
1442 net::strand<net::any_io_executor> strand_;
1444 std::shared_ptr<network::AsyncHttpClient> http_;
1446 bool refreshing_{
false};
1447 std::deque<TokenHandler> token_waiters_;
1450 static constexpr int page_budget_ = 50;
1459 std::string_view api_id,
1460 std::string_view path,
1463 std::string_view next_key = {}
1465 network::HttpRequest request;
1466 request.group_ = group;
1469 request.method_ = http::verb::post;
1470 request.target_ = std::string(path);
1471 request.body_ = std::move(body);
1472 request.headers_ = {
1474 {
"api-id", std::string(api_id)},
1475 {
"content-type",
"application/json;charset=UTF-8"},
1477 if (!next_key.empty()) {
1478 request.headers_.emplace_back(
"cont-yn",
"Y");
1479 request.headers_.emplace_back(
"next-key", std::string(next_key));
1496 const auto flag = response.base().find(
"cont-yn");
1497 if (flag == response.base().end() || flag->value() !=
"Y") {
1498 return std::nullopt;
1500 const auto key = response.base().find(
"next-key");
1501 if (key == response.base().end()) {
1502 return std::string();
1504 return std::string(key->value());
1512 if (token_.valid()) {
1513 std::move(handler)(std::nullopt);
1516 token_waiters_.push_back(std::move(handler));
1522 network::HttpRequest request;
1524 request.method_ = http::verb::post;
1525 request.target_ =
"/oauth2/token";
1526 request.body_ = config_.to_jsonbody();
1527 request.headers_.emplace_back(
"content-type",
"application/json;charset=UTF-8");
1528 http_->async_request(
1530 [self = shared_from_this()](Result<network::HttpResponse> result)
mutable {
1531 net::post(self->strand_, [self, result = std::move(result)]()
mutable {
1532 self->finish_token(std::move(result));
1539 void finish_token(Result<network::HttpResponse> result) {
1540 refreshing_ =
false;
1541 std::optional<BrokerError> failure;
1543 failure = std::move(result.error());
1544 }
else if (result.value().result_int() < 200 || result.value().result_int() >= 300) {
1545 failure = kiwoom_http_error(result.value());
1548 BrokerError parse_error;
1549 auto document = broker_detail::parse_object(result.value(), parse_error);
1551 failure = std::move(parse_error);
1555 failure = std::move(declared);
1558 token_.from_json(*document);
1559 }
catch (
const std::exception& exception) {
1562 error.broker_code_ =
"invalid_token_response";
1563 error.message_ = exception.what();
1564 failure = std::move(error);
1573 failure->reached_broker_ =
false;
1575 auto waiters = std::move(token_waiters_);
1576 token_waiters_.clear();
1577 for (
auto& waiter : waiters) {
1579 std::move(waiter)(*failure);
1581 std::move(waiter)(std::nullopt);
1593 auto error = broker_detail::http_error(response);
1594 boost::system::error_code ec;
1595 const auto document = json::parse(response.body(), ec);
1596 if (ec || !document.is_object()) {
1599 if (
const auto* code = document.as_object().if_contains(
"return_code")) {
1600 if (code->is_int64()) {
1601 error.broker_code_ = std::to_string(code->as_int64());
1602 }
else if (code->is_uint64()) {
1603 error.broker_code_ = std::to_string(code->as_uint64());
1604 }
else if (code->is_string()) {
1605 error.broker_code_ = std::string(code->as_string());
1616 static Result<json::object> document(
1617 Result<network::HttpResponse> response,
1622 return Result<json::object>(std::move(response.error()));
1624 if (response.value().result_int() < 200 || response.value().result_int() >= 300) {
1625 return Result<json::object>(kiwoom_http_error(response.value()));
1627 BrokerError parse_error;
1628 auto parsed = broker_detail::parse_object(response.value(), parse_error);
1630 return Result<json::object>(std::move(parse_error));
1633 return Result<json::object>(std::move(*refused));
1636 return Result<json::object>(std::move(*declared));
1638 return Result<json::object>(std::move(*parsed));
1642 static std::optional<BrokerError> validate_instrument(
const InstrumentId& instrument) {
1643 if (instrument.country_ == Country::KR && broker_detail::valid_symbol(instrument.symbol_) &&
1644 (instrument.symbol_.size() == 6 || instrument.symbol_.size() == 7)) {
1645 return std::nullopt;
1649 error.reached_broker_ =
false;
1650 error.broker_code_ =
"invalid_instrument";
1652 "this adapter covers Kiwoom's domestic market, whose instruments are addressed by a six- or "
1653 "seven-character code";
1658 std::string symbol_body(
const InstrumentId& instrument)
const {
1660 body[
"stk_cd"] = config_.tape_symbol(instrument.symbol_);
1661 return json::serialize(body);
1665 template <
typename T,
typename Handler,
typename Parse>
1666 void start_market_read(
1667 InstrumentId instrument,
1673 if (
auto invalid = validate_instrument(instrument)) {
1674 deliver(std::move(completion), Result<T>(std::move(*invalid)));
1680 self = shared_from_this(), instrument = std::move(instrument),
1681 api_id = std::move(api_id), path = std::move(path), completion = std::move(completion),
1683 ](std::optional<BrokerError> token_error)
mutable {
1685 self->deliver(std::move(completion), Result<T>(std::move(*token_error)));
1688 auto request = self->make_request(
1691 self->http_->async_request(
1693 [self, instrument = std::move(instrument), completion = std::move(completion), parse](
1694 Result<network::HttpResponse> result
1697 std::move(completion), parse(std::move(instrument), std::move(result))
1711 static Result<QuoteSnapshot> parse_quote(
1712 InstrumentId instrument,
1713 Result<network::HttpResponse> result
1715 auto body = document(std::move(result));
1720 const auto& row = body.value();
1721 QuoteSnapshot
quote;
1722 quote.instrument_ = std::move(instrument);
1724 const auto last = detail::magnitude(row,
"cur_prc");
1726 throw std::runtime_error(
"missing or unreadable field: cur_prc");
1728 quote.last_price_ = *last;
1729 quote.volume_ = detail::magnitude(row,
"trde_qty");
1730 quote.received_at_ = std::chrono::system_clock::now();
1731 const auto date = detail::compact_date(detail::text_field(row,
"date"));
1733 quote.exchange_time_ = detail::seoul_timestamp(*date, detail::text_field(row,
"tm"));
1736 top.ask_price_ = detail::magnitude(row,
"sel_1bid");
1737 top.ask_quantity_ = detail::magnitude(row,
"sel_1bid_req");
1738 top.bid_price_ = detail::magnitude(row,
"buy_1bid");
1739 top.bid_quantity_ = detail::magnitude(row,
"buy_1bid_req");
1740 if (top.ask_price_ || top.bid_price_) {
1741 quote.top_of_book_ = top;
1744 }
catch (
const std::exception& exception) {
1753 static Result<PriceLimits> parse_price_limits(
1754 InstrumentId instrument,
1755 Result<network::HttpResponse> result
1757 static_cast<void>(instrument);
1758 auto body = document(std::move(result));
1763 const auto& row = body.value();
1766 limits.upper_ = positive(detail::magnitude(row,
"upl_pric"));
1767 limits.lower_ = positive(detail::magnitude(row,
"lst_pric"));
1768 const auto date = detail::compact_date(detail::text_field(row,
"date"));
1770 limits.exchange_time_ = detail::seoul_timestamp(*date, detail::text_field(row,
"tm"));
1773 }
catch (
const std::exception& exception) {
1783 static Result<DepthSnapshot> parse_depth(
1784 InstrumentId instrument,
1785 Result<network::HttpResponse> result
1787 auto body = document(std::move(result));
1792 const auto& row = body.value();
1794 book.instrument_ = std::move(instrument);
1796 book.received_at_ = std::chrono::system_clock::now();
1797 book.exchange_time_ =
1798 detail::seoul_timestamp(
today_in_seoul(), detail::text_field(row,
"bid_req_base_tm"));
1799 append_level(book.asks_, row,
"sel_fpr_bid",
"sel_fpr_req");
1800 append_level(book.bids_, row,
"buy_fpr_bid",
"buy_fpr_req");
1801 for (
int level = 2; level <= 10; ++level) {
1802 const std::string suffix = std::to_string(level) +
"th_pre_";
1803 append_level(book.asks_, row,
"sel_" + suffix +
"bid",
"sel_" + suffix +
"req");
1804 append_level(book.bids_, row,
"buy_" + suffix +
"bid",
"buy_" + suffix +
"req");
1808 }
catch (
const std::exception& exception) {
1814 static void append_level(
1815 std::vector<DepthLevel>& side,
1816 const json::object& row,
1817 const std::string& price_key,
1818 const std::string& quantity_key
1820 const auto price = detail::magnitude(row, price_key);
1821 const auto quantity = detail::magnitude(row, quantity_key);
1822 if (!price || price->coefficient() == 0) {
1826 level.price_ = *price;
1827 level.quantity_ = quantity ? *quantity :
Decimal64();
1828 side.push_back(level);
1832 static std::optional<Decimal64> positive(std::optional<Decimal64> value) {
1833 if (!value || value->coefficient() <= 0) {
1834 return std::nullopt;
1847 static std::optional<Decimal64> reported_trigger(
const json::object& row, std::string_view key) {
1849 return positive(detail::magnitude(row, key));
1850 }
catch (
const std::exception&) {
1851 return std::nullopt;
1856 static BrokerError protocol_error(
const char* code,
const std::exception& exception) {
1859 error.broker_code_ = code;
1860 error.message_ = exception.what();
1865 template <
typename Handler,
typename T>
1866 void deliver(Handler handler, Result<T> result) {
1868 note_rate_limit(result.error());
1870 auto executor = net::get_associated_executor(handler, strand_);
1871 net::dispatch(executor, [handler = std::move(handler), result = std::move(result)]()
mutable {
1872 std::move(handler)(std::move(result));
1885 void note_rate_limit(
const BrokerError& error) {
1889 auto* limiter = http_->rate_limits();
1890 if (limiter ==
nullptr) {
1894 error.retry_after_ ?
1895 std::chrono::duration_cast<network::RateLimitController::Duration>(*error.retry_after_) :
1896 std::chrono::duration_cast<network::RateLimitController::Duration>(std::chrono::seconds(1))
1901 void start_trades(InstrumentId instrument,
int count,
TradesHandler completion) {
1902 using Prints = std::vector<TradePrint>;
1906 error.reached_broker_ =
false;
1907 error.broker_code_ =
"invalid_count";
1908 error.message_ =
"a recent-execution request must ask for at least one print";
1909 deliver(std::move(completion), Result<Prints>(std::move(error)));
1912 if (
auto invalid = validate_instrument(instrument)) {
1913 deliver(std::move(completion), Result<Prints>(std::move(*invalid)));
1919 self = shared_from_this(), instrument = std::move(instrument), count,
1920 completion = std::move(completion)
1921 ](std::optional<BrokerError> token_error)
mutable {
1923 self->deliver(std::move(completion), Result<Prints>(std::move(*token_error)));
1926 auto request = self->make_request(
1927 "ka10003",
"/api/dostk/stkinfo", self->symbol_body(instrument),
1930 self->http_->async_request(
1932 [self, count, completion = std::move(completion)](
1933 Result<network::HttpResponse> result
1935 self->deliver(std::move(completion), parse_trades(std::move(result), count));
1948 static Result<std::vector<TradePrint>> parse_trades(
1949 Result<network::HttpResponse> result,
1952 using Prints = std::vector<TradePrint>;
1953 auto body = document(std::move(result));
1958 const auto rows = body.value().if_contains(
"cntr_infr");
1959 if (!rows || !rows->is_array()) {
1960 throw std::runtime_error(
"missing array field: cntr_infr");
1964 for (
const auto& value : rows->as_array()) {
1965 if (!value.is_object()) {
1966 throw std::runtime_error(
"cntr_infr row is not an object");
1968 if (
static_cast<int>(prints.size()) >= count) {
1971 const auto& row = value.as_object();
1972 const auto price = detail::magnitude(row,
"cur_prc");
1973 const auto quantity = detail::magnitude(row,
"cntr_trde_qty");
1974 if (!price || !quantity) {
1978 print.price_ = *price;
1979 print.quantity_ = *quantity;
1981 print.exchange_time_ = detail::seoul_timestamp(today, detail::text_field(row,
"tm"));
1982 prints.push_back(print);
1985 }
catch (
const std::exception& exception) {
1986 return Result<Prints>(protocol_error(
"invalid_trades_response", exception));
1992 if (
auto invalid = validate_instrument(instrument)) {
1993 deliver(std::move(completion), Result<Instrument>(std::move(*invalid)));
1999 self = shared_from_this(), instrument = std::move(instrument),
2000 completion = std::move(completion)
2001 ](std::optional<BrokerError> token_error)
mutable {
2003 self->deliver(std::move(completion), Result<Instrument>(std::move(*token_error)));
2009 body[
"stk_cd"] = instrument.symbol_;
2010 auto request = self->make_request(
2011 "ka10100",
"/api/dostk/stkinfo", json::serialize(body),
2014 self->http_->async_request(
2016 [self, instrument = std::move(instrument), completion = std::move(completion)](
2017 Result<network::HttpResponse> result
2020 std::move(completion), parse_instrument(std::move(instrument), std::move(result))
2030 static Result<Instrument> parse_instrument(
2031 InstrumentId instrument,
2032 Result<network::HttpResponse> result
2034 auto body = document(std::move(result));
2039 const auto& row = body.value();
2041 subject.id_ = std::move(instrument);
2042 subject.name_ = detail::text_field(row,
"name");
2044 subject.segment_ = kiwoom_market_segment(detail::text_field(row,
"marketCode"));
2045 subject.type_ = SecurityType::STOCK;
2046 subject.status_ = kiwoom_status(detail::text_field(row,
"auditInfo"));
2047 subject.shares_outstanding_ = positive(detail::magnitude(row,
"listCount"));
2048 subject.listed_on_ = detail::stated_compact_date(row,
"regDay");
2049 subject.price_rule_ =
price_rule_for(subject.id_.country_, subject.type_);
2051 }
catch (
const std::exception& exception) {
2062 static MarketSegment kiwoom_market_segment(std::string_view code) {
2081 for (
const std::string_view listed :
2082 {
"정상",
"관리종목",
"거래정지",
"단기과열",
"투자주의",
"투자경고",
"투자주의환기종목"}) {
2083 if (audit == listed) {
2084 return InstrumentStatus::ACTIVE;
2087 return InstrumentStatus::UNKNOWN;
2091 enum class PageStep {
2101 struct BarOperation {
2103 std::vector<Bar> descending_;
2106 std::string next_key_;
2112 BarOperation(BarQuery query,
BarsHandler completion)
2113 : query_(std::move(query)), completion_(std::move(completion)) {}
2117 void start_bars(BarQuery query,
BarsHandler completion) {
2118 if (
auto invalid = validate_bar_query(query)) {
2119 deliver(std::move(completion), Result<BarSeries>(std::move(*invalid)));
2122 auto operation = std::make_shared<BarOperation>(std::move(query), std::move(completion));
2123 const auto bound = operation->query_.before_ ?
2124 *operation->query_.before_ :
2125 std::chrono::system_clock::now();
2126 operation->date_ = seoul_date_of(bound);
2127 request_bars(operation);
2131 static std::optional<BrokerError> validate_bar_query(
const BarQuery& query) {
2134 error.reached_broker_ =
false;
2135 if (
auto invalid = validate_instrument(query.instrument_)) {
2138 if (query.count_ <= 0) {
2139 error.broker_code_ =
"invalid_count";
2140 error.message_ =
"a bar request must ask for at least one bar";
2147 void request_bars(std::shared_ptr<BarOperation> operation) {
2150 [self = shared_from_this(), operation](std::optional<BrokerError> token_error)
mutable {
2153 std::move(operation->completion_), Result<BarSeries>(std::move(*token_error))
2158 body[
"stk_cd"] = self->config_.tape_symbol(operation->query_.instrument_.symbol_);
2159 if (operation->query_.interval_ == BarInterval::ONE_MINUTE) {
2160 body[
"tic_scope"] =
"1";
2165 body[
"upd_stkpc_tp"] = operation->query_.adjusted_ ?
"1" :
"0";
2166 body[
"base_dt"] = detail::compact_text(operation->date_);
2167 auto request = self->make_request(
2168 chart_api_id(operation->query_.interval_),
"/api/dostk/chart",
2171 self->http_->async_request(
2173 [self, operation](Result<network::HttpResponse> result)
mutable {
2174 net::post(self->strand_, [self, operation, result = std::move(result)]()
mutable {
2175 self->handle_bars_page(operation, std::move(result));
2185 void handle_bars_page(
2186 const std::shared_ptr<BarOperation>& operation,
2187 Result<network::HttpResponse> result
2190 deliver(std::move(operation->completion_), Result<BarSeries>(std::move(result.error())));
2193 const auto next_key = continuation_key(result.value());
2194 auto body = document(std::move(result));
2196 deliver(std::move(operation->completion_), Result<BarSeries>(std::move(body.error())));
2199 const std::size_t before = operation->descending_.size();
2201 const auto rows = body.value().if_contains(
chart_rows_field(operation->query_.interval_));
2202 if (!rows || !rows->is_array()) {
2203 throw std::runtime_error(
2204 std::string(
"missing array field: ") +
chart_rows_field(operation->query_.interval_)
2207 for (
const auto& value : rows->as_array()) {
2208 if (!value.is_object()) {
2209 throw std::runtime_error(
"chart row is not an object");
2211 operation->descending_.push_back(
2212 parse_bar(value.as_object(), operation->query_.interval_)
2215 }
catch (
const std::exception& exception) {
2217 std::move(operation->completion_),
2218 Result<BarSeries>(protocol_error(
"invalid_bars_response", exception))
2223 switch (advance_bars(*operation, std::move(next_key), before)) {
2224 case PageStep::FINISH:
2225 finish_bars(operation);
2227 case PageStep::INCOMPLETE:
2229 std::move(operation->completion_),
2230 Result<BarSeries>(incomplete_listing(
"bars", operation->pages_))
2233 case PageStep::CONTINUE:
2234 request_bars(operation);
2250 static PageStep advance_bars(
2251 BarOperation& operation,
2252 std::optional<std::string> next_key,
2255 operation.more_ = next_key.has_value();
2256 operation.next_key_ = next_key ? std::move(*next_key) : std::string();
2258 const bool grew = operation.descending_.size() > before;
2259 const bool satisfied =
2260 static_cast<int>(operation.descending_.size()) >= operation.query_.count_;
2261 if (satisfied || !operation.more_) {
2262 return PageStep::FINISH;
2271 if (operation.next_key_.empty() || !grew || operation.pages_ >= page_budget_) {
2272 return PageStep::INCOMPLETE;
2274 return PageStep::CONTINUE;
2282 static Bar parse_bar(
const json::object& row,
BarInterval interval) {
2284 if (interval == BarInterval::ONE_MINUTE) {
2285 const auto stamp = detail::text_field(row,
"cntr_tm");
2286 if (stamp.size() != 14) {
2287 throw std::runtime_error(
"invalid minute bar timestamp");
2289 const auto date = detail::compact_date(std::string_view(stamp).substr(0, 8));
2290 const auto timestamp =
2291 date ? detail::seoul_timestamp(*date, std::string_view(stamp).substr(8)) : std::nullopt;
2293 throw std::runtime_error(
"invalid minute bar timestamp");
2295 bar.timestamp_ = *timestamp;
2297 const auto date = detail::compact_date(detail::text_field(row,
"dt"));
2298 const auto timestamp = date ? detail::seoul_timestamp(*date,
"000000") : std::nullopt;
2300 throw std::runtime_error(
"invalid bar date");
2302 bar.timestamp_ = *timestamp;
2304 const auto high = detail::magnitude(row,
"high_pric");
2305 const auto low = detail::magnitude(row,
"low_pric");
2306 const auto close = detail::magnitude(row,
"cur_prc");
2307 if (!high || !low || !
close) {
2308 throw std::runtime_error(
"bar row is missing a price");
2313 bar.volume_ = detail::required_magnitude(row,
"trde_qty");
2314 bar.open_ = detail::magnitude(row,
"open_pric");
2315 bar.traded_value_ = detail::magnitude(row,
"trde_prica");
2323 static BarSeries compose_bars(BarOperation& operation) {
2325 series.instrument_ = operation.query_.instrument_;
2326 series.interval_ = operation.query_.interval_;
2328 series.received_at_ = std::chrono::system_clock::now();
2332 series.adjusted_ = operation.query_.adjusted_;
2338 series.stamp_ = operation.query_.interval_ == BarInterval::ONE_MINUTE ?
2342 auto& rows = operation.descending_;
2343 if (
static_cast<int>(rows.size()) > operation.query_.count_) {
2344 rows.resize(
static_cast<std::size_t
>(operation.query_.count_));
2346 series.bars_.assign(rows.rbegin(), rows.rend());
2352 const bool resumable = operation.query_.interval_ != BarInterval::ONE_MINUTE;
2353 if (!series.bars_.empty() && operation.more_ && resumable) {
2360 void finish_bars(
const std::shared_ptr<BarOperation>& operation) {
2361 deliver(std::move(operation->completion_), Result<BarSeries>(compose_bars(*operation)));
2365 static Date seoul_date_of(std::chrono::system_clock::time_point instant) {
2366 const std::time_t raw = std::chrono::system_clock::to_time_t(
2367 std::chrono::time_point_cast<std::chrono::seconds>(instant + std::chrono::hours(9))
2371 gmtime_s(&parts, &raw);
2373 gmtime_r(&raw, &parts);
2376 date.year_ = parts.tm_year + 1900;
2377 date.month_ = parts.tm_mon + 1;
2378 date.day_ = parts.tm_mday;
2387 static PageStep advance_page(
2388 std::string& next_key_slot,
2391 std::optional<std::string> next_key
2395 return PageStep::FINISH;
2399 if (next_key->empty() || !grew || pages >= page_budget_) {
2400 return PageStep::INCOMPLETE;
2402 next_key_slot = std::move(*next_key);
2403 return PageStep::CONTINUE;
2418 static BrokerError incomplete_listing(std::string_view what,
int pages) {
2421 error.broker_code_ =
"incomplete_listing";
2422 error.reached_broker_ =
true;
2423 error.message_ = std::string(
"Kiwoom still offered another page of ") + std::string(what) +
2424 " after " + std::to_string(pages) +
2425 " page(s); the listing is incomplete and is refused rather than returned in part";
2430 struct HoldingsOperation {
2431 HoldingsSnapshot snapshot_;
2432 std::string next_key_;
2434 bool totals_seen_{
false};
2437 explicit HoldingsOperation(
HoldingsHandler completion) : completion_(std::move(completion)) {}
2442 if (
auto invalid = validate_holdings_venue()) {
2443 deliver(std::move(completion), Result<HoldingsSnapshot>(std::move(*invalid)));
2446 auto operation = std::make_shared<HoldingsOperation>(std::move(completion));
2447 request_holdings(operation);
2477 return SellableBasis{
2489 return std::nullopt;
2492 std::optional<BrokerError> validate_holdings_venue()
const {
2493 if (config_.holdings_venue_ == Venue::KRX || config_.holdings_venue_ == Venue::NXT) {
2494 return std::nullopt;
2498 error.reached_broker_ =
false;
2499 error.broker_code_ =
"holdings_venue_required";
2501 "Kiwoom's balance inquiry names one venue and publishes no combined value, and whether that "
2502 "selects which positions are listed or only how they are valued is not stated; set "
2503 "KiwoomConfig::holdings_venue_ to Venue::KRX or Venue::NXT deliberately, having read its "
2509 void request_holdings(std::shared_ptr<HoldingsOperation> operation) {
2512 [self = shared_from_this(), operation](std::optional<BrokerError> token_error)
mutable {
2515 std::move(operation->completion_), Result<HoldingsSnapshot>(std::move(*token_error))
2519 auto request = self->make_request(
2520 "kt00018",
"/api/dostk/acnt", self->holdings_body(),
2523 self->http_->async_request(
2525 [self, operation](Result<network::HttpResponse> result)
mutable {
2526 net::post(self->strand_, [self, operation, result = std::move(result)]()
mutable {
2527 self->handle_holdings_page(operation, std::move(result));
2537 std::string holdings_body()
const {
2544 body[
"qry_tp"] =
"1";
2545 body[
"dmst_stex_tp"] =
exchange_code(config_.holdings_venue_);
2546 return json::serialize(body);
2550 void handle_holdings_page(
2551 const std::shared_ptr<HoldingsOperation>& operation,
2552 Result<network::HttpResponse> result
2556 std::move(operation->completion_), Result<HoldingsSnapshot>(std::move(result.error()))
2560 const auto next_key = continuation_key(result.value());
2561 auto body = document(std::move(result));
2564 std::move(operation->completion_), Result<HoldingsSnapshot>(std::move(body.error()))
2568 const std::size_t before = operation->snapshot_.positions_.size();
2570 append_holdings_page(operation->snapshot_, operation->totals_seen_, body.value());
2571 }
catch (
const std::exception& exception) {
2573 std::move(operation->completion_),
2574 Result<HoldingsSnapshot>(protocol_error(
"invalid_holdings_response", exception))
2578 const bool grew = operation->snapshot_.positions_.size() > before;
2579 switch (advance_page(operation->next_key_, operation->pages_, grew, std::move(next_key))) {
2580 case PageStep::FINISH:
2581 operation->snapshot_.received_at_ = std::chrono::system_clock::now();
2589 operation->snapshot_.sellable_basis_ =
2590 holdings_sellable_basis(config_.holdings_sellable_scope_);
2592 std::move(operation->completion_),
2593 Result<HoldingsSnapshot>(std::move(operation->snapshot_))
2596 case PageStep::INCOMPLETE:
2598 std::move(operation->completion_),
2599 Result<HoldingsSnapshot>(incomplete_listing(
"holdings", operation->pages_))
2602 case PageStep::CONTINUE:
2603 request_holdings(operation);
2624 static void append_holdings_page(
2625 HoldingsSnapshot& snapshot,
2627 const json::object& page
2630 HoldingsTotals totals;
2632 totals.purchase_value_ = detail::magnitude_or_zero(page,
"tot_pur_amt");
2633 totals.market_value_ = detail::magnitude_or_zero(page,
"tot_evlt_amt");
2634 totals.unrealized_profit_loss_ = detail::signed_or_zero(page,
"tot_evlt_pl");
2635 snapshot.totals_.push_back(totals);
2638 const auto rows = page.if_contains(
"acnt_evlt_remn_indv_tot");
2642 if (!rows->is_array()) {
2643 throw std::runtime_error(
"acnt_evlt_remn_indv_tot is not an array");
2645 for (
const auto& value : rows->as_array()) {
2646 if (!value.is_object()) {
2647 throw std::runtime_error(
"holdings row is not an object");
2649 auto position = parse_holding(value.as_object());
2650 if (duplicate_symbol(snapshot, position.instrument_.symbol_)) {
2651 throw std::runtime_error(
2652 "holdings listing reports " + position.instrument_.symbol_ +
2653 " on more than one row although it was requested summed (qry_tp=1)"
2656 snapshot.positions_.push_back(std::move(position));
2661 static bool duplicate_symbol(
const HoldingsSnapshot& snapshot,
const std::string& symbol) {
2662 for (
const auto& position : snapshot.positions_) {
2663 if (position.instrument_.symbol_ == symbol) {
2671 static HoldingPosition parse_holding(
const json::object& row) {
2672 HoldingPosition position;
2673 position.instrument_.country_ = Country::KR;
2677 position.instrument_.venue_ = Venue::COMBINED;
2678 position.instrument_.symbol_ = detail::bare_symbol(detail::text_field(row,
"stk_cd"));
2679 position.name_ = detail::text_field(row,
"stk_nm");
2680 position.quantity_ = detail::required_magnitude(row,
"rmnd_qty");
2681 position.sellable_quantity_ = detail::magnitude(row,
"trde_able_qty");
2682 position.average_purchase_price_ = detail::magnitude_or_zero(row,
"pur_pric");
2683 position.last_price_ = detail::magnitude_or_zero(row,
"cur_prc");
2684 position.purchase_value_ = Money{detail::magnitude_or_zero(row,
"pur_amt"),
Currency::krw()};
2685 position.market_value_ = Money{detail::magnitude_or_zero(row,
"evlt_amt"),
Currency::krw()};
2686 position.unrealized_profit_loss_ =
2687 Money{detail::signed_or_zero(row,
"evltv_prft"),
Currency::krw()};
2689 if (
const auto percent = detail::signed_value(row,
"prft_rt")) {
2690 position.unrealized_return_ = broker_detail::percent_to_ratio(
2691 detail::text_field(row,
"prft_rt")
2702 error.reached_broker_ =
false;
2703 error.broker_code_ =
"unsupported_currency";
2704 error.message_ =
"Kiwoom's domestic deposit inquiry answers in won";
2705 deliver(std::move(completion), Result<BuyingPower>(std::move(error)));
2710 [self = shared_from_this(), completion = std::move(completion)](
2711 Result<KiwoomDeposit> result
2714 self->deliver(std::move(completion), Result<BuyingPower>(std::move(result.error())));
2718 power.cash_ = Money{result.value().spendable_, result.value().currency_};
2719 power.received_at_ = result.value().received_at_;
2720 self->deliver(std::move(completion), Result<BuyingPower>(std::move(power)));
2730 [self = shared_from_this(), completion = std::move(completion)](
2731 std::optional<BrokerError> token_error
2734 self->deliver(std::move(completion), Result<KiwoomDeposit>(std::move(*token_error)));
2740 body[
"qry_tp"] =
"3";
2741 auto request = self->make_request(
2742 "kt00001",
"/api/dostk/acnt", json::serialize(body),
2745 self->http_->async_request(
2747 [self, completion = std::move(completion)](
2748 Result<network::HttpResponse> result
2750 self->deliver(std::move(completion), parse_deposit(std::move(result)));
2759 static Result<KiwoomDeposit> parse_deposit(Result<network::HttpResponse> result) {
2760 auto body = document(std::move(result));
2765 const auto& row = body.value();
2766 KiwoomDeposit deposit;
2768 deposit.received_at_ = std::chrono::system_clock::now();
2769 const auto spendable = detail::signed_value(row,
"100stk_ord_alow_amt");
2771 throw std::runtime_error(
"missing or unreadable field: 100stk_ord_alow_amt");
2773 deposit.spendable_ = *spendable;
2774 deposit.orderable_ = detail::signed_or_zero(row,
"ord_alow_amt");
2775 deposit.deposit_ = detail::signed_or_zero(row,
"entr");
2776 deposit.settled_deposit_ = detail::signed_or_zero(row,
"d2_entra");
2777 deposit.substitute_amount_ = detail::signed_or_zero(row,
"repl_amt");
2778 deposit.withdrawable_ = detail::signed_or_zero(row,
"pymn_alow_amt");
2780 }
catch (
const std::exception& exception) {
2786 void start_orderable_amount(
2787 InstrumentId instrument,
2789 std::optional<Decimal64> price,
2792 if (
auto invalid = validate_instrument(instrument)) {
2793 deliver(std::move(completion), Result<KiwoomOrderableAmount>(std::move(*invalid)));
2799 self = shared_from_this(), instrument = std::move(instrument), side,
2800 price = std::move(price), completion = std::move(completion)
2801 ](std::optional<BrokerError> token_error)
mutable {
2804 std::move(completion), Result<KiwoomOrderableAmount>(std::move(*token_error))
2809 body[
"stk_cd"] = instrument.symbol_;
2810 body[
"trde_tp"] = side == OrderSide::SELL ?
"1" :
"2";
2811 body[
"uv"] = price ? price->str() :
"";
2812 auto request = self->make_request(
2813 "kt00010",
"/api/dostk/acnt", json::serialize(body),
2816 self->http_->async_request(
2818 [self, completion = std::move(completion)](
2819 Result<network::HttpResponse> result
2821 self->deliver(std::move(completion), parse_orderable_amount(std::move(result)));
2830 static Result<KiwoomOrderableAmount> parse_orderable_amount(
2831 Result<network::HttpResponse> result
2833 auto body = document(std::move(result));
2838 const auto& row = body.value();
2839 KiwoomOrderableAmount orderable;
2841 orderable.received_at_ = std::chrono::system_clock::now();
2845 orderable.orderable_cash_ = detail::signed_or_zero(row,
"ord_alowa");
2846 orderable.withdrawable_ = detail::signed_or_zero(row,
"wthd_alowa");
2847 orderable.settled_deposit_ = detail::signed_or_zero(row,
"d2entra");
2848 orderable.margin_reduction_applies_ =
2849 detail::text_field(row,
"profa_rdex_aplc_tp") ==
"1";
2850 for (
const int percent : {20, 30, 40, 50, 60, 100}) {
2851 KiwoomMarginTier tier;
2852 tier.margin_percent_ = percent;
2853 const std::string prefix =
"profa_" + std::to_string(percent) +
"ord_alow";
2854 tier.amount_ = detail::signed_value(row, prefix +
"_amt");
2855 tier.quantity_ = detail::magnitude(row, prefix +
"q");
2856 orderable.tiers_.push_back(tier);
2858 KiwoomMarginTier reduced;
2859 reduced.margin_percent_ = 60;
2860 reduced.reduced_ =
true;
2861 reduced.amount_ = detail::signed_value(row,
"profa_rdex_60ord_alow_amt");
2862 reduced.quantity_ = detail::magnitude(row,
"profa_rdex_60ord_alowq");
2863 orderable.tiers_.push_back(reduced);
2865 }
catch (
const std::exception& exception) {
2867 protocol_error(
"invalid_orderable_amount_response", exception)
2874 using Accounts = std::vector<KiwoomAccountInfo>;
2877 [self = shared_from_this(), completion = std::move(completion)](
2878 std::optional<BrokerError> token_error
2881 self->deliver(std::move(completion), Result<Accounts>(std::move(*token_error)));
2884 auto request = self->make_request(
2887 self->http_->async_request(
2889 [self, completion = std::move(completion)](
2890 Result<network::HttpResponse> result
2892 self->deliver(std::move(completion), parse_accounts(std::move(result)));
2905 static Result<std::vector<KiwoomAccountInfo>> parse_accounts(
2906 Result<network::HttpResponse> result
2908 using Accounts = std::vector<KiwoomAccountInfo>;
2909 auto body = document(std::move(result));
2914 const auto number = detail::text_field(body.value(),
"acctNo");
2915 if (number.empty()) {
2916 throw std::runtime_error(
"missing string field: acctNo");
2919 accounts.push_back(KiwoomAccountInfo{number});
2921 }
catch (
const std::exception& exception) {
2922 return Result<Accounts>(protocol_error(
"invalid_accounts_response", exception));
2930 static std::optional<BrokerError> validate_quantity(
2931 const Decimal64& quantity,
2932 bool allow_zero =
false
2934 const bool positive = quantity.coefficient() > 0;
2935 if ((positive || (allow_zero && quantity.coefficient() == 0)) && quantity.scale() == 0) {
2936 return std::nullopt;
2940 error.reached_broker_ =
false;
2941 error.broker_code_ =
"invalid_quantity";
2942 error.message_ = allow_zero ?
2943 "a Kiwoom amendment quantity must be a whole number of shares, or zero for all remaining" :
2944 "a Kiwoom order quantity must be a positive whole number of shares";
2953 static std::optional<BrokerError> validate_price(
2955 const std::optional<Decimal64>& price
2959 error.reached_broker_ =
false;
2960 error.broker_code_ =
"invalid_price";
2963 error.message_ =
"a Kiwoom order of this price basis requires a price";
2966 if (price->coefficient() <= 0) {
2967 error.message_ =
"a Kiwoom order price must be positive";
2970 if (price->scale() != 0) {
2971 error.message_ =
"a Kiwoom domestic order price must be a whole number of won";
2974 return std::nullopt;
2978 "Kiwoom takes no price for this basis; a market, best-limit, primary-limit, mid-price or "
2979 "closing-price order is priced by the book";
2982 return std::nullopt;
2991 static std::optional<BrokerError> validate_venue(
Venue venue) {
2992 if (venue == Venue::KRX || venue == Venue::NXT || venue == Venue::COMBINED) {
2993 return std::nullopt;
2997 error.reached_broker_ =
false;
2998 error.broker_code_ =
"venue_required";
3000 "Kiwoom requires dmst_stex_tp on every order and documents no default; set "
3001 "OrderRequest::venue_ to KRX, NXT or COMBINED (Kiwoom's SOR), or, for a strategy runner, "
3002 "bind it with a default route (live::bind / bind_journaled, SessionExecutor)";
3007 static constexpr Venue instrument_venue() {
return Venue::COMBINED; }
3010 static std::optional<BrokerError> validate_order(
3012 const OrderRequest& order,
3013 std::string& division
3018 if (
auto error = validate_instrument(order.instrument_)) {
3025 if (order.instrument_.venue_ != instrument_venue()) {
3028 error.reached_broker_ =
false;
3029 error.broker_code_ =
"invalid_instrument";
3031 "this adapter identifies a Korean instrument by its combined venue; set "
3032 "OrderRequest::venue_ to route the order to a particular exchange instead of naming one on "
3036 if (
auto error = validate_quantity(order.quantity_)) {
3039 if (
auto error = validate_price(order.type_, order.price_)) {
3042 if (
auto error =
invalid_trigger(order.type_, order.trigger_price_)) {
3045 if (
auto error = validate_venue(order.venue_)) {
3048 if (
auto error =
order_division(order.type_, order.time_in_force_, order.session_, division)) {
3054 return holdings_scope_refusal(scope);
3061 static std::optional<BrokerError> validate_amendment(
3063 const KiwoomAmendment& amendment,
3066 if (
auto error = validate_quantity(amendment.quantity_,
true)) {
3069 if (
auto error = validate_venue(amendment.venue_)) {
3074 error.reached_broker_ =
false;
3075 if (amendment.order_.order_number_.empty()) {
3076 error.broker_code_ =
"invalid_order_reference";
3077 error.message_ =
"a Kiwoom amendment requires the original order number";
3080 if (amendment.order_.symbol_.empty()) {
3083 error.broker_code_ =
"invalid_order_reference";
3085 "a Kiwoom amendment requires the order's symbol beside its number; both are part of the "
3086 "reference this adapter issues";
3089 if (cancel && amendment.price_) {
3090 error.broker_code_ =
"invalid_price";
3091 error.message_ =
"Kiwoom's cancellation carries a quantity and no price";
3094 if (cancel && amendment.condition_price_) {
3099 error.broker_code_ =
"invalid_condition_price";
3101 "Kiwoom's cancellation carries a quantity and no trigger price, so it cannot change an "
3102 "order's stop condition on the way out; cancel the order and place a new one instead";
3105 if (!cancel && amendment.price_ && amendment.price_->scale() != 0) {
3106 error.broker_code_ =
"invalid_price";
3107 error.message_ =
"a Kiwoom domestic order price must be a whole number of won";
3112 if (!cancel && amendment.condition_price_ && !
valid_trigger_value(*amendment.condition_price_)) {
3113 error.broker_code_ =
"invalid_trigger_price";
3114 error.message_ =
"a Kiwoom trigger price must be a positive whole number of won";
3121 return holdings_scope_refusal(scope);
3123 return std::nullopt;
3127 static network::HttpRequest place_order_request(
3129 const OrderRequest& order,
3130 std::string_view division
3134 body[
"stk_cd"] = order.instrument_.symbol_;
3135 body[
"ord_qty"] = order.quantity_.str();
3138 body[
"ord_uv"] = order.price_ ? order.price_->str() :
"";
3139 body[
"trde_tp"] = std::string(division);
3142 body[
"cond_uv"] = order.trigger_price_ ? order.trigger_price_->str() :
"";
3143 return client.make_request(
3144 order.side_ == OrderSide::BUY ?
"kt10000" :
"kt10001",
3146 json::serialize(body),
3152 static network::HttpRequest amend_order_request(
3154 const KiwoomAmendment& amendment,
3159 body[
"orig_ord_no"] = amendment.order_.order_number_;
3160 body[
"stk_cd"] = amendment.order_.symbol_;
3162 body[
"cncl_qty"] = amendment.quantity_.str();
3164 body[
"mdfy_qty"] = amendment.quantity_.str();
3165 body[
"mdfy_uv"] = amendment.price_ ? amendment.price_->str() :
"";
3166 body[
"mdfy_cond_uv"] =
3167 amendment.condition_price_ ? amendment.condition_price_->str() :
"";
3169 return client.make_request(
3170 cancel ?
"kt10003" :
"kt10002",
3172 json::serialize(body),
3178 void start_place_order(OrderRequest order,
OrderAckHandler completion) {
3179 std::string division;
3180 if (
auto invalid = validate_order(config_.holdings_position_scope_, order, division)) {
3181 deliver(std::move(completion), Result<KiwoomOrderAck>(std::move(*invalid)));
3187 self = shared_from_this(), order = std::move(order), division = std::move(division),
3188 completion = std::move(completion)
3189 ](std::optional<BrokerError> token_error)
mutable {
3192 std::move(completion), Result<KiwoomOrderAck>(std::move(*token_error))
3196 auto request = place_order_request(*self, order, division);
3197 const std::string symbol = order.instrument_.symbol_;
3198 self->http_->async_request(
3200 [self, symbol, demo = self->config_.is_demo_, completion = std::move(completion)](
3201 Result<network::HttpResponse> result
3204 std::move(completion),
3205 parse_order_ack(symbol, std::move(result), KiwoomOrderOperation::PLACE, demo)
3215 void start_amend_order(KiwoomAmendment amendment,
bool cancel,
OrderAckHandler completion) {
3216 if (
auto invalid = validate_amendment(config_.holdings_position_scope_, amendment, cancel)) {
3217 deliver(std::move(completion), Result<KiwoomOrderAck>(std::move(*invalid)));
3223 self = shared_from_this(), amendment = std::move(amendment), cancel,
3224 completion = std::move(completion)
3225 ](std::optional<BrokerError> token_error)
mutable {
3228 std::move(completion), Result<KiwoomOrderAck>(std::move(*token_error))
3232 auto request = amend_order_request(*self, amendment, cancel);
3233 const std::string symbol = amendment.order_.symbol_;
3235 self->http_->async_request(
3237 [self, symbol, operation, demo = self->config_.is_demo_, completion = std::move(completion)](
3238 Result<network::HttpResponse> result
3241 std::move(completion), parse_order_ack(symbol, std::move(result), operation, demo)
3256 static Result<KiwoomOrderAck> parse_order_ack(
3257 const std::string& symbol,
3258 Result<network::HttpResponse> result,
3262 auto body = document(std::move(result), operation, demo);
3267 const auto& row = body.value();
3268 const auto number = detail::text_field(row,
"ord_no");
3269 if (number.empty()) {
3270 throw std::runtime_error(
"missing string field: ord_no");
3273 ack.ack_.order_id_ = number;
3274 ack.ack_.accepted_at_ = std::chrono::system_clock::now();
3275 ack.order_ = KiwoomOrderRef{number, symbol};
3276 ack.base_order_number_ = detail::text_field(row,
"base_orig_ord_no");
3277 if (
auto quantity = detail::magnitude(row,
"mdfy_qty")) {
3278 ack.amended_quantity_ = *quantity;
3279 }
else if (
auto canceled = detail::magnitude(row,
"cncl_qty")) {
3280 ack.amended_quantity_ = *canceled;
3283 }
catch (
const std::exception& exception) {
3289 struct OpenOrderOperation {
3290 KiwoomOpenOrderPage page_;
3291 std::string next_key_;
3295 explicit OpenOrderOperation(
OpenOrderHandler completion) : completion_(std::move(completion)) {}
3300 auto operation = std::make_shared<OpenOrderOperation>(std::move(completion));
3301 request_open_orders(operation);
3305 void request_open_orders(std::shared_ptr<OpenOrderOperation> operation) {
3308 [self = shared_from_this(), operation](std::optional<BrokerError> token_error)
mutable {
3311 std::move(operation->completion_),
3312 Result<KiwoomOpenOrderPage>(std::move(*token_error))
3317 body[
"all_stk_tp"] =
"0";
3318 body[
"trde_tp"] =
"0";
3319 body[
"stk_cd"] =
"";
3323 body[
"stex_tp"] =
"0";
3324 auto request = self->make_request(
3325 "ka10075",
"/api/dostk/acnt", json::serialize(body),
3328 self->http_->async_request(
3330 [self, operation](Result<network::HttpResponse> result)
mutable {
3331 net::post(self->strand_, [self, operation, result = std::move(result)]()
mutable {
3332 self->handle_open_orders_page(operation, std::move(result));
3342 void handle_open_orders_page(
3343 const std::shared_ptr<OpenOrderOperation>& operation,
3344 Result<network::HttpResponse> result
3348 std::move(operation->completion_), Result<KiwoomOpenOrderPage>(std::move(result.error()))
3352 const auto next_key = continuation_key(result.value());
3353 auto body = document(std::move(result));
3356 std::move(operation->completion_), Result<KiwoomOpenOrderPage>(std::move(body.error()))
3360 const std::size_t before = operation->page_.orders_.size();
3362 const auto rows = body.value().if_contains(
"oso");
3363 if (rows !=
nullptr) {
3364 if (!rows->is_array()) {
3365 throw std::runtime_error(
"oso is not an array");
3367 for (
const auto& value : rows->as_array()) {
3368 if (!value.is_object()) {
3369 throw std::runtime_error(
"oso row is not an object");
3371 operation->page_.orders_.push_back(parse_open_order(value.as_object()));
3374 }
catch (
const std::exception& exception) {
3376 std::move(operation->completion_),
3377 Result<KiwoomOpenOrderPage>(protocol_error(
"invalid_open_orders_response", exception))
3381 const bool grew = operation->page_.orders_.size() > before;
3382 switch (advance_page(operation->next_key_, operation->pages_, grew, std::move(next_key))) {
3383 case PageStep::FINISH:
3384 operation->page_.received_at_ = std::chrono::system_clock::now();
3386 std::move(operation->completion_),
3387 Result<KiwoomOpenOrderPage>(std::move(operation->page_))
3390 case PageStep::INCOMPLETE:
3392 std::move(operation->completion_),
3393 Result<KiwoomOpenOrderPage>(incomplete_listing(
"working orders", operation->pages_))
3396 case PageStep::CONTINUE:
3397 request_open_orders(operation);
3411 static OrderSide read_side(
const std::string& label) {
3415 throw std::runtime_error(
3416 "order row states a side this adapter cannot read: '" + label +
3417 "'; it names neither 매수 nor 매도"
3422 static KiwoomOpenOrder parse_open_order(
const json::object& row) {
3423 KiwoomOpenOrder open_order;
3424 const auto number = detail::text_field(row,
"ord_no");
3425 const auto symbol = detail::bare_symbol(detail::text_field(row,
"stk_cd"));
3426 open_order.order_ref_ = KiwoomOrderRef{number, symbol};
3427 open_order.original_order_number_ = detail::text_field(row,
"orig_ord_no");
3428 open_order.amendable_quantity_ = detail::magnitude(row,
"oso_qty");
3429 open_order.shape_name_ = detail::text_field(row,
"trde_tp");
3431 OrderSnapshot& order = open_order.order_;
3432 order.order_id_ = number;
3433 order.instrument_.country_ = Country::KR;
3434 order.instrument_.venue_ = instrument_venue();
3435 order.instrument_.symbol_ = symbol;
3436 order.name_ = detail::text_field(row,
"stk_nm");
3438 order.side_ = read_side(detail::text_field(row,
"io_tp_nm"));
3439 apply_shape(open_order.shape_name_, order);
3442 detail::text_field(row,
"stex_tp"), detail::text_field(row,
"sor_yn")
3444 order.quantity_ = detail::required_magnitude(row,
"ord_qty");
3445 order.price_ = positive(detail::magnitude(row,
"ord_pric"));
3446 order.trigger_price_ = reported_trigger(row,
"stop_pric");
3450 order.ordered_at_ = detail::seoul_timestamp(
today_in_seoul(), detail::text_field(row,
"tm"));
3451 order.ordered_date_assumed_ =
true;
3452 order.execution_.filled_quantity_ = detail::required_magnitude(row,
"cntr_qty");
3453 resolve_open_status(open_order.amendable_quantity_, order);
3494 static void resolve_open_status(
3495 const std::optional<Decimal64>& amendable, OrderSnapshot& order
3497 const bool reported_over =
3498 order.status_ == OrderStatus::FILLED || order.status_ == OrderStatus::CANCELED;
3499 if (!reported_over) {
3502 if (!amendable || amendable->coefficient() <= 0) {
3505 order.status_ = order.execution_.filled_quantity_.coefficient() > 0 ?
3506 OrderStatus::PARTIAL_FILLED :
3507 OrderStatus::PENDING;
3521 static void apply_shape(std::string_view label, OrderSnapshot& order) {
3523 if (row ==
nullptr) {
3526 order.type_ = row->type_;
3527 order.time_in_force_ = row->time_in_force_;
3528 order.session_ = row->session_;
3532 struct HistoryLink {
3534 std::string cancels_;
3538 std::optional<Decimal64> remaining_;
3542 struct HistoryOperation {
3543 KiwoomHistoryQuery query_;
3546 std::vector<HistoryLink> links_;
3547 std::string next_key_;
3552 : query_(std::move(query)), completion_(std::move(completion)) {}
3556 void start_order_history(KiwoomHistoryQuery query,
OrderPageHandler completion) {
3560 error.reached_broker_ =
false;
3561 error.broker_code_ =
"invalid_date";
3562 error.message_ =
"a Kiwoom order-history inquiry requires a valid Gregorian date";
3563 deliver(std::move(completion), Result<OrderPage>(std::move(error)));
3566 auto operation = std::make_shared<HistoryOperation>(std::move(query), std::move(completion));
3567 request_order_history(operation);
3571 void request_order_history(std::shared_ptr<HistoryOperation> operation) {
3574 [self = shared_from_this(), operation](std::optional<BrokerError> token_error)
mutable {
3577 std::move(operation->completion_), Result<OrderPage>(std::move(*token_error))
3582 body[
"ord_dt"] = detail::compact_text(operation->query_.date_);
3584 body[
"qry_tp"] =
"1";
3585 body[
"stk_bond_tp"] =
"1";
3586 body[
"sell_tp"] =
"0";
3587 body[
"stk_cd"] = operation->query_.symbol_;
3588 body[
"fr_ord_no"] =
"";
3591 body[
"dmst_stex_tp"] =
"%";
3592 auto request = self->make_request(
3593 "kt00007",
"/api/dostk/acnt", json::serialize(body),
3596 self->http_->async_request(
3598 [self, operation](Result<network::HttpResponse> result)
mutable {
3599 net::post(self->strand_, [self, operation, result = std::move(result)]()
mutable {
3600 self->handle_order_history_page(operation, std::move(result));
3610 void handle_order_history_page(
3611 const std::shared_ptr<HistoryOperation>& operation,
3612 Result<network::HttpResponse> result
3615 deliver(std::move(operation->completion_), Result<OrderPage>(std::move(result.error())));
3618 const auto next_key = continuation_key(result.value());
3619 auto body = document(std::move(result));
3621 deliver(std::move(operation->completion_), Result<OrderPage>(std::move(body.error())));
3624 const std::size_t before = operation->page_.orders_.size();
3626 const auto rows = body.value().if_contains(
"acnt_ord_cntr_prps_dtl");
3627 if (rows !=
nullptr) {
3628 if (!rows->is_array()) {
3629 throw std::runtime_error(
"acnt_ord_cntr_prps_dtl is not an array");
3631 for (
const auto& value : rows->as_array()) {
3632 if (!value.is_object()) {
3633 throw std::runtime_error(
"order history row is not an object");
3635 operation->page_.orders_.push_back(
3636 parse_history_row(value.as_object(), operation->query_.date_, config_.is_demo_)
3638 operation->links_.push_back(history_link(value.as_object(), config_.is_demo_));
3641 }
catch (
const std::exception& exception) {
3643 std::move(operation->completion_),
3644 Result<OrderPage>(protocol_error(
"invalid_order_history_response", exception))
3648 const bool grew = operation->page_.orders_.size() > before;
3649 switch (advance_page(operation->next_key_, operation->pages_, grew, std::move(next_key))) {
3650 case PageStep::FINISH:
3651 operation->page_.received_at_ = std::chrono::system_clock::now();
3654 operation->page_.next_cursor_.reset();
3656 close_cancelled_originals(operation->page_.orders_, operation->links_);
3657 deliver(std::move(operation->completion_), Result<OrderPage>(std::move(operation->page_)));
3659 case PageStep::INCOMPLETE:
3661 std::move(operation->completion_),
3662 Result<OrderPage>(incomplete_listing(
"order history", operation->pages_))
3665 case PageStep::CONTINUE:
3666 request_order_history(operation);
3676 static HistoryLink history_link(
const json::object& row,
bool demo) {
3678 const auto amendment = detail::text_field(row,
"mdfy_cncl");
3679 link.plain_ = amendment.empty() || amendment ==
"일반";
3680 link.remaining_ = detail::magnitude(row,
"ord_remnq");
3681 const bool cancellation = amendment ==
"취소확인" ||
3682 (demo && amendment ==
"취소" && detail::text_field(row,
"acpt_tp") ==
"주문완료");
3684 const auto original = detail::text_field(row,
"ori_ord");
3685 if (original.find_first_not_of(
'0') != std::string::npos) {
3686 link.cancels_ = original;
3699 static void close_cancelled_originals(std::vector<OrderSnapshot>& orders,
const std::vector<HistoryLink>& links) {
3700 for (std::size_t cancel = 0; cancel < orders.size() && cancel < links.size(); ++cancel) {
3701 if (links[cancel].cancels_.empty()) {
3704 std::optional<std::size_t> found;
3705 bool ambiguous =
false;
3706 for (std::size_t at = 0; at < orders.size() && at < links.size(); ++at) {
3707 if (orders[at].order_id_ != links[cancel].cancels_) {
3710 ambiguous = ambiguous || found.has_value();
3713 if (!found || ambiguous) {
3716 auto& original = orders[*found];
3717 const auto& link = links[*found];
3718 if (!link.plain_ || !link.remaining_ || link.remaining_->coefficient() != 0 ||
3719 original.instrument_.symbol_ != orders[cancel].instrument_.symbol_ ||
3720 original.side_ != orders[cancel].side_ ||
3721 !
decimal_less(original.execution_.filled_quantity_, original.quantity_)) {
3724 original.status_ = OrderStatus::CANCELED;
3729 static OrderSnapshot parse_history_row(
const json::object& row,
const Date& date,
bool demo =
false) {
3730 OrderSnapshot order;
3731 order.order_id_ = detail::text_field(row,
"ord_no");
3732 order.instrument_.country_ = Country::KR;
3733 order.instrument_.venue_ = instrument_venue();
3737 order.instrument_.symbol_ = detail::bare_symbol(detail::text_field(row,
"stk_cd"));
3738 order.name_ = detail::text_field(row,
"stk_nm");
3740 order.side_ = read_side(detail::text_field(row,
"io_tp_nm"));
3745 const auto label = detail::text_field(row,
"trde_tp");
3746 apply_shape(demo && label ==
"보통가" ? std::string_view(
"보통") : std::string_view(label), order);
3747 order.status_ = history_status(row);
3749 order.quantity_ = detail::required_magnitude(row,
"ord_qty");
3750 order.price_ = positive(detail::magnitude(row,
"ord_uv"));
3751 order.trigger_price_ = reported_trigger(row,
"cond_uv");
3752 order.ordered_at_ = detail::seoul_timestamp(date, detail::text_field(row,
"ord_tm"));
3753 order.execution_.filled_quantity_ = detail::required_magnitude(row,
"cntr_qty");
3754 if (
const auto average = positive(detail::magnitude(row,
"cntr_uv"))) {
3755 order.execution_.average_filled_price_ = average;
3768 static OrderStatus history_status(
const json::object& row) {
3769 const auto amendment = detail::text_field(row,
"mdfy_cncl");
3770 if (amendment ==
"취소확인") {
3771 return OrderStatus::CANCELED;
3773 if (amendment ==
"정정확인") {
3774 return OrderStatus::REPLACED;
3776 const auto filled = detail::required_magnitude(row,
"cntr_qty");
3777 const auto remaining = detail::magnitude(row,
"ord_remnq");
3778 if (filled.coefficient() > 0) {
3779 if (remaining && remaining->coefficient() == 0) {
3780 return OrderStatus::FILLED;
3782 return OrderStatus::PARTIAL_FILLED;