181 using Caps =
typename Session::Caps;
189 "SessionSource's first session is the trading session (holdings, orders), and this one's client is a "
190 "market-data source (DataOnlyTraits): pass a broker's session first and the data source as the "
191 "market-data session, SessionSource<Trading, Journal, MarketData>"
208 : session_(&session), market_data_(&session), universe_(std::move(universe)) {}
217 SessionSource(Session& session, MarketData& market_data, std::vector<InstrumentId> universe)
218 : session_(&session), market_data_(&market_data), universe_(std::move(universe)) {}
252 currencies_ = std::move(currencies);
297 interval_ = interval;
326 const std::optional<BrokerError>&
last_error() const noexcept {
return last_error_; }
346 template <
typename Runner>
350 return poll_calendar(runner_instance);
352 return poll_bars(runner_instance);
354 return poll_quotes(runner_instance);
356 return poll_holdings(runner_instance);
358 return poll_working_orders(runner_instance);
360 return poll_buying_power(runner_instance);
362 return poll_price_limits(runner_instance);
379 template <
typename Runner>
381 if constexpr (Caps::has_working_orders) {
392 return poll_working_orders(runner_instance,
false);
395 if constexpr (Caps::has_history_limit) {
397 query.
limit_ = history_limit_;
402 std::vector<OrderSnapshot> rows;
403 bool complete =
false;
404 for (
int page = 0; page < maximum_history_pages_ && !complete; ++page) {
405 auto result = session_->order_history(query);
407 last_error_ = result.error();
410 auto& listed = result.value().orders_;
411 rows.insert(rows.end(), std::make_move_iterator(listed.begin()), std::make_move_iterator(listed.end()));
412 const bool more = result.value().next_cursor_ && !result.value().next_cursor_->empty();
415 }
else if constexpr (Caps::has_history_cursor) {
416 query.
cursor_ = result.value().next_cursor_;
429 reconcile(runner_instance, rows);
449 template <
typename Runner>
451 if constexpr (session_source_detail::takes_slices<Runner>::value) {
453 throw std::logic_error(
454 "a bar poll for on_bars was interrupted by an exception (" + *interrupted_ +
"); its period may be "
455 "partly recorded and partly delivered, so this source does not continue: restart the program with a "
456 "fresh runner and source; older periods warm up the strategy, and the newest eligible period may trade"
461 if (universe_.empty()) {
471 std::size_t instrument_{0};
472 strategy::BarEvent event_;
475 std::vector<Collected> collected;
476 for (std::size_t at = 0; at < universe_.size(); ++at) {
477 const auto& instrument = universe_[at];
479 query.instrument_ = instrument;
480 query.interval_ = interval_;
481 query.count_ = count_;
483 auto result = market_data_->bars(query);
485 last_error_ = result.error();
488 const auto& series = result.value();
489 if (series.bars_.empty()) {
515 const auto instant = std::max(runner_instance.observed_at(), series.received_at_);
516 const bool knowable = series.stamp_ ==
BarStamp::CLOSE || series.interval_ == BarInterval::ONE_DAY ||
518 const bool timed = knowable && instant != std::chrono::system_clock::time_point{};
519 const auto& seen = last_bar_[instrument.symbol_];
520 const auto first = collected.size();
521 for (
const auto& bar : series.bars_) {
522 if (seen &&
bar.timestamp_ <= *seen) {
527 auto& held = held_bar_[instrument.symbol_];
528 if (!held || !(*held ==
bar.timestamp_)) {
529 held =
bar.timestamp_;
530 ++bars_not_yet_closed_;
534 collected.push_back(Collected{
542 if (collected.size() > first) {
543 collected.back().newest_ =
true;
547 std::stable_sort(collected.begin(), collected.end(), [](
const Collected& a,
const Collected& b) {
548 return a.event_.period_end_ < b.event_.period_end_;
557 std::optional<std::chrono::system_clock::time_point> latest_history;
558 for (
const auto& item : collected) {
559 if (!item.newest_ && (!latest_history || *latest_history < item.event_.period_end_)) {
560 latest_history = item.event_.period_end_;
564 session_source_detail::feeds_warmup<Runner>::value,
565 "SessionSource feeds a read's older bars through Runner::feed_warmup; a runner without it "
566 "would send decisions made on history"
569 if (collected.empty()) {
572 if constexpr (session_source_detail::takes_slices<Runner>::value) {
581 strategy::slice_detail::Bucket bucket;
582 bool bucket_is_history =
false;
584 for (
const auto& item : collected) {
585 const auto& instrument = universe_[item.instrument_];
586 const auto&
event = item.event_;
589 if (delivered_through_ && !(*delivered_through_ < event.period_end_)) {
590 runner_instance.record_history_only(event);
591 last_bar_[instrument.symbol_] =
event.bar_.timestamp_;
592 ++bars_not_delivered_;
595 if (!bucket.empty() && bucket.period_end() < event.period_end_) {
596 deliver(runner_instance, bucket, bucket_is_history);
598 if (bucket.holds(event)) {
599 last_bar_[instrument.symbol_] =
event.bar_.timestamp_;
600 ++bars_not_delivered_;
604 !item.newest_ || (latest_history && !(*latest_history <
event.period_end_));
605 runner_instance.record(event);
607 bucket_is_history = history;
608 last_bar_[instrument.symbol_] =
event.bar_.timestamp_;
610 ++bars_fed_as_warmup_;
613 deliver(runner_instance, bucket, bucket_is_history);
614 }
catch (
const std::exception& error) {
615 interrupted_ = error.what();
618 interrupted_ =
"an unknown exception";
622 for (
const auto& item : collected) {
623 const auto& instrument = universe_[item.instrument_];
624 if (!item.newest_ || (latest_history && !(*latest_history < item.event_.period_end_))) {
625 runner_instance.feed_warmup(item.event_);
626 ++bars_fed_as_warmup_;
627 last_bar_[instrument.symbol_] = item.event_.bar_.timestamp_;
630 runner_instance.context().set_last_price(instrument, item.event_.bar_.close_);
631 runner_instance.feed(item.event_);
632 last_bar_[instrument.symbol_] = item.event_.bar_.timestamp_;
640 template <
typename Runner>
641 void deliver(Runner& runner_instance, strategy::slice_detail::Bucket& bucket,
bool history) {
642 while (!bucket.empty()) {
643 auto slice = bucket.take_first();
644 delivered_through_ = slice.period_end_;
646 runner_instance.feed_warmup(slice);
648 runner_instance.feed(slice);
654 template <
typename Runner>
656 if (universe_.empty()) {
660 for (
const auto& instrument : universe_) {
661 auto result = market_data_->quote(instrument);
663 last_error_ = result.error();
666 strategy::QuoteEvent event;
667 event.quote_ = result.value();
668 runner_instance.feed(event);
680 template <
typename Runner>
682 auto result = session_->holdings();
684 last_error_ = result.error();
689 auto portfolio = runner_instance.context().portfolio();
690 portfolio.positions_ = result.value().positions_;
697 portfolio.sellable_basis_ = result.value().sellable_basis_;
702 portfolio.as_of_ = result.value().received_at_;
703 runner_instance.context().set_portfolio(std::move(portfolio));
713 template <
typename Runner>
715 if constexpr (!Caps::has_buying_power) {
716 static_cast<void>(runner_instance);
719 if (currencies_.empty()) {
722 auto portfolio = runner_instance.context().portfolio();
723 bool updated =
false;
725 std::optional<BrokerError> outright;
726 std::optional<BrokerError> throttled;
727 for (
const auto& currency : currencies_) {
728 auto result = session_->buying_power(currency);
742 outright = result.error();
744 }
else if (!throttled) {
745 throttled = result.error();
749 bool replaced =
false;
750 for (
auto& reading : portfolio.buying_power_) {
751 if (reading.cash_.currency_ == currency) {
752 reading = result.value();
758 portfolio.buying_power_.push_back(result.value());
763 runner_instance.context().set_portfolio(std::move(portfolio));
765 const auto& reason = outright ? outright : throttled;
767 last_error_ = *reason;
805 template <
typename Runner>
807 if constexpr (!MarketData::Caps::has_price_limits) {
810 static_cast<void>(runner_instance);
813 if (universe_.empty()) {
816 bool updated =
false;
817 std::optional<BrokerError> outright;
818 std::optional<BrokerError> throttled;
819 for (
const auto& instrument : universe_) {
820 auto result = market_data_->price_limits(instrument);
824 outright = result.error();
826 }
else if (!throttled) {
827 throttled = result.error();
831 runner_instance.set_price_limits(instrument, std::move(result.value()));
834 const auto& reason = outright ? outright : throttled;
836 last_error_ = *reason;
866 template <
typename Runner>
868 if constexpr (!MarketData::Caps::has_calendar) {
872 static_cast<void>(runner_instance);
875 auto result = market_data_->calendar(calendar_country_);
877 last_error_ = result.error();
881 runner_instance.context().set_calendar(std::move(result.value()));
904 void observe_charged(
const std::vector<OrderSnapshot>& rows) {
905 std::vector<std::string> present;
906 present.reserve(rows.size());
907 for (
const auto& row : rows) {
908 present.push_back(row.order_id_);
909 const auto& execution = row.execution_;
910 const auto states_cost = execution.commission_ || execution.tax_;
912 auto held = charged_seen_.find(row.order_id_);
913 if (held == charged_seen_.end()) {
916 charged_seen_.emplace(row.order_id_, std::move(fresh));
917 held = charged_seen_.find(row.order_id_);
919 ++charged_.orders_stating_cost_;
924 ++charged_.orders_stating_none_;
926 }
else if (states_cost && !held->second.counted_cost_) {
927 held->second.counted_cost_ =
true;
928 ++charged_.orders_stating_cost_;
932 charged_.commission_, held->second.commission_,
933 execution.commission_, execution.filled_quantity_
936 charged_.tax_, held->second.tax_, execution.tax_, execution.filled_quantity_
940 for (
const auto& previous : charged_listing_) {
941 bool still_there =
false;
942 for (
const auto&
id : present) {
943 if (
id == previous) {
951 ++charged_.working_list_disappearances_;
954 charged_listing_ = std::move(present);
963 std::vector<runner::ChargedCosts::StatedTotal>& totals,
965 const std::optional<Money>& now,
966 const Decimal64& quantity
974 if (charged_.add(totals, *now)) {
980 if (!(held.amount_->currency_ == now->currency_)) {
983 ++charged_.conflicting_restatements_;
987 if (!(held.amount_->amount_ == now->amount_)) {
989 ++charged_.conflicting_restatements_;
999 ++charged_.unrepresentable_sums_;
1002 if (charged_.add(totals, Money{*moved, now->currency_})) {
1004 held.at_ = quantity;
1009 template <
typename Runner>
1011 if constexpr (Caps::has_working_orders) {
1012 auto result = session_->working_orders();
1014 last_error_ = result.error();
1017 last_error_.reset();
1019 auto portfolio = runner_instance.context().portfolio();
1020 portfolio.working_ = result.value().orders_;
1023 portfolio.working_as_of_ = result.value().received_at_;
1024 runner_instance.context().set_portfolio(std::move(portfolio));
1029 std::vector<OrderSnapshot> rows;
1030 rows.reserve(result.value().orders_.size());
1031 for (
const auto& working : result.value().orders_) {
1032 rows.push_back(working.order_);
1037 observe_charged(rows);
1038 reconcile(runner_instance, rows);
1055 for (
const auto& update : updates_.changed(result.value().orders_)) {
1056 runner_instance.feed(update);
1059 updates_.adopt(result.value().orders_);
1071 const auto deferred_since = runner_instance.deferred_awaiting_evidence();
1072 const auto unconfirmed_since = runner_instance.unconfirmed_awaiting_evidence();
1073 if (deferred_since || unconfirmed_since) {
1074 const auto since = !deferred_since ? *unconfirmed_since :
1075 !unconfirmed_since ? *deferred_since :
1076 std::min(*deferred_since, *unconfirmed_since);
1077 std::vector<OrderSnapshot> evidence;
1082 const auto until = std::max({runner_instance.context().now(), runner_instance.observed_at(), since});
1083 if (!read_evidence(since, until, evidence)) {
1086 if (unconfirmed_since) {
1087 runner_instance.confirm_from_history(evidence);
1089 if (deferred_since) {
1090 runner_instance.retry_deferred(evidence);
1099 (void)runner_instance;
1113 template <
typename Runner>
1114 void reconcile(Runner& runner_instance,
const std::vector<OrderSnapshot>& rows) {
1115 if (journal_ ==
nullptr) {
1118 const auto replay = journal_->replay();
1121 reconcile_ = runner::ReconcileReport{};
1122 }
else if (!sweep(runner_instance, replay, open, rows)) {
1125 search_again(runner_instance, replay, open, rows);
1141 template <
typename Runner>
1142 static std::chrono::system_clock::time_point sweep_instant(
const Runner& runner_instance) {
1143 const auto now = std::max(runner_instance.context().now(), runner_instance.observed_at());
1144 return now == std::chrono::system_clock::time_point{} ? std::chrono::system_clock::now() : now;
1150 template <
typename Runner>
1151 bool sweep(Runner& runner_instance,
const runner::JournalReplay& replay,
1152 const std::vector<runner::OpenInstruction>& open,
const std::vector<OrderSnapshot>& rows) {
1153 auto candidates = rows;
1164 std::vector<OrderSnapshot> history;
1165 if (!read_history(open, history)) {
1169 open,
"the order history could not be read, so nothing could be concluded from absence"
1173 if constexpr (Caps::history_requires_range) {
1186 sweep_instant(runner_instance),
1187 runner_instance.ledger(),
1191 runner_instance.serialized() ?
1193 " (the order list and the order history of its send); that does not establish that no order was "
1194 "placed, and a serialized runner keeps the order owed until dated evidence accounts for it or an "
1195 "operator releases it (Runner::release_obligations)"
1198 " (the order list and the order history of its send); that does not establish that no order was "
1202 if (reconcile_.unrecorded_) {
1206 runner_instance.halt_locally(
1207 "the order journal could not record a reconciliation, so the hold stands"
1214 for (
const auto& match : reconcile_.matches_) {
1218 for (
const auto& instruction : open) {
1222 runner_instance.resolve_indeterminate_placement(
1223 instruction.entry_.instrument_, instruction.entry_.side_,
1226 instruction.entry_.id_
1243 template <
typename Runner>
1244 void search_again(Runner& runner_instance,
const runner::JournalReplay& replay,
1245 const std::vector<runner::OpenInstruction>& open,
const std::vector<OrderSnapshot>& rows) {
1246 if (!runner_instance.serialized()) {
1249 std::vector<std::string> ids;
1250 for (
const auto& owed : runner_instance.obligations()) {
1251 const bool still_open = std::any_of(open.begin(), open.end(), [&](
const runner::OpenInstruction& instruction) {
1252 return instruction.entry_.id_ == owed.journal_id_;
1255 ids.push_back(owed.journal_id_);
1262 std::vector<OrderSnapshot> history;
1263 if (!read_history(late, history)) {
1268 const auto dated = [](std::vector<OrderSnapshot> all) {
1270 std::remove_if(all.begin(), all.end(), [](
const OrderSnapshot& row) { return !row.ordered_at_; }), all.end()
1274 auto candidates = dated(rows);
1279 candidates.begin(), candidates.end(), [](
const OrderSnapshot& row) { return row.ordered_date_assumed_; }
1284 const auto now = sweep_instant(runner_instance);
1285 for (
const auto& instruction : late) {
1292 runner_instance.halt_locally(
"the order journal could not record a reconciliation, so the hold stands");
1295 runner_instance.resolve_indeterminate_placement(
1296 instruction.entry_.instrument_, instruction.entry_.side_, std::optional<std::string>(match.order_id_),
1297 instruction.entry_.id_
1302 const auto& withheld = reconcile_.withheld_;
1303 if (std::find(withheld.begin(), withheld.end(), match.instrument_) == withheld.end() &&
1305 runner_instance.ledger().resolve_after_reading_orders(match.instrument_);
1319 bool read_history(
const std::vector<runner::OpenInstruction>& all,
1320 std::vector<OrderSnapshot>& into) {
1323 std::vector<runner::OpenInstruction> open;
1324 for (
const auto& instruction : all) {
1325 if (!instruction.entry_.requires_operator_) {
1326 open.push_back(instruction);
1332 if constexpr (Caps::history_requires_range) {
1339 auto earliest = open.front().opened_at_;
1341 for (
const auto& instruction : open) {
1342 if (instruction.opened_at_ < earliest) {
1343 earliest = instruction.opened_at_;
1346 if (sent_by > latest) {
1350 return read_history_days(earliest, latest, into);
1356 bool read_history_days(
1357 std::chrono::system_clock::time_point from,
1358 std::chrono::system_clock::time_point to,
1359 std::vector<OrderSnapshot>& into
1361 std::vector<Date> days;
1362 const auto add = [&days](
const Date& day) {
1363 for (
const auto& seen : days) {
1364 if (seen.year_ == day.year_ && seen.month_ == day.month_ && seen.day_ == day.day_) {
1368 days.push_back(day);
1370 const auto until = to + std::chrono::hours(24);
1371 for (
auto at = from - std::chrono::hours(24); at < until; at += std::chrono::hours(24)) {
1379 for (
const auto& day : days) {
1380 std::vector<OrderSnapshot> page;
1381 if (!read_history_in({day, day}, page,
true)) {
1384 for (
const auto& row : page) {
1386 for (
const auto& held : into) {
1387 if (same_history_row(held, row)) {
1393 into.push_back(row);
1411 std::chrono::system_clock::time_point since,
1412 std::chrono::system_clock::time_point now,
1413 std::vector<OrderSnapshot>& into
1415 if constexpr (Caps::history_requires_range) {
1416 return read_history_in(
1424 return read_history_days(since, now, into);
1430 bool read_history_in(
1431 const std::pair<Date, Date>& range,
1432 std::vector<OrderSnapshot>& into,
1433 bool dated = Caps::history_requires_range
1435 OrderHistoryQuery query;
1436 if constexpr (Caps::has_history_limit) {
1440 query.limit_ = history_limit_;
1443 query.from_ = range.first;
1444 query.to_ = range.second;
1447 for (
int page = 0; page < maximum_history_pages_; ++page) {
1448 auto result = session_->order_history(query);
1450 last_error_ = result.error();
1455 for (
const auto& row : result.value().orders_) {
1457 for (
const auto& held : into) {
1458 if (same_history_row(held, row)) {
1464 into.push_back(row);
1467 const bool more = result.value().next_cursor_ && !result.value().next_cursor_->empty();
1471 if constexpr (Caps::has_history_cursor) {
1472 query.cursor_ = result.value().next_cursor_;
1488 static bool same_history_row(
const OrderSnapshot& a,
const OrderSnapshot& b) {
1490 !
decimal_less(a.execution_.filled_quantity_, b.execution_.filled_quantity_) &&
1491 !
decimal_less(b.execution_.filled_quantity_, a.execution_.filled_quantity_);
1495 MarketData* market_data_;
1496 std::vector<InstrumentId> universe_;
1497 std::vector<Currency> currencies_;
1499 runner::ChargedCosts charged_;
1501 std::map<std::string, ObservedCharge> charged_seen_;
1503 std::vector<std::string> charged_listing_;
1504 Country calendar_country_{Country::KR};
1505 Journal* journal_{
nullptr};
1506 int maximum_history_pages_{20};
1507 runner::MatchWindow window_;
1508 runner::ReconcileReport reconcile_;
1510 runner::OrderUpdateWatch<BrokerTag> updates_;
1513 std::size_t bars_not_yet_closed_{0};
1514 std::size_t bars_fed_as_warmup_{0};
1517 std::size_t bars_not_delivered_{0};
1518 std::optional<std::chrono::system_clock::time_point> delivered_through_;
1520 std::optional<std::string> interrupted_;
1521 std::map<std::string, std::optional<std::chrono::system_clock::time_point>> held_bar_;
1522 int history_limit_{100};
1523 std::map<std::string, std::optional<std::chrono::system_clock::time_point>> last_bar_;
1524 std::optional<BrokerError> last_error_;