algotradelib 0.0.0.9000
C++ header-only library for algorithmic trading
Loading...
Searching...
No Matches
reconcile_orders.hpp
1#pragma once
2
3#include "./execution.hpp"
4#include "./journal.hpp"
5
6#include <algotradelib/core>
7
8#include <algorithm>
9#include <chrono>
10#include <cstddef>
11#include <ctime>
12#include <iterator>
13#include <optional>
14#include <string>
15#include <string_view>
16#include <unordered_map>
17#include <utility>
18#include <vector>
19
20namespace algotradelib {
21namespace runner {
22
58
64inline constexpr std::string_view match_verdict_name(MatchVerdict verdict) noexcept {
65 switch (verdict) {
67 return "matched";
69 return "not_placed";
71 return "ambiguous";
73 return "undecided";
75 return "needs_operator";
76 }
77 return "unknown";
78}
79
85inline constexpr bool match_verdict_resolves(MatchVerdict verdict) noexcept {
86 return verdict == MatchVerdict::MATCHED || verdict == MatchVerdict::NOT_PLACED;
87}
88
97 std::chrono::seconds clock_skew_{30};
99 std::chrono::seconds maximum_request_{60};
101 std::chrono::seconds listing_delay_{30};
102};
103
119
120namespace reconcile_detail {
121
129inline bool same_value(const Decimal64& lhs, const Decimal64& rhs) {
130 return !decimal_less(lhs, rhs) && !decimal_less(rhs, lhs);
131}
132
134inline bool same_value(
135 const std::optional<Decimal64>& lhs, const std::optional<Decimal64>& rhs
136) {
137 if (!lhs.has_value() || !rhs.has_value()) {
138 return lhs.has_value() == rhs.has_value();
139 }
140 return same_value(*lhs, *rhs);
141}
142
143} // namespace reconcile_detail
144
151inline std::chrono::system_clock::time_point instruction_sent_by(
152 const OpenInstruction& instruction, const MatchWindow& window
153) {
154 return instruction.closed_at_ ?
155 *instruction.closed_at_ :
156 instruction.opened_at_ + window.maximum_request_;
157}
158
167 const OpenInstruction& instruction,
168 std::chrono::system_clock::time_point now,
169 const MatchWindow& window
170) {
171 return now >= instruction_sent_by(instruction, window) + window.clock_skew_ +
172 window.listing_delay_;
173}
174
175namespace shape_detail {
176
178template <typename Enum>
179inline constexpr bool both_known(Enum left, Enum right) noexcept {
180 return left != Enum::UNKNOWN && right != Enum::UNKNOWN;
181}
182
183} // namespace shape_detail
184
198
204inline constexpr bool evidence_confirms(Evidence evidence) noexcept {
205 switch (evidence) {
207 return true;
209 case Evidence::ABSENT:
210 return false;
211 }
212 return false;
213}
214
220inline constexpr bool evidence_contradicts(Evidence evidence) noexcept {
221 switch (evidence) {
223 return true;
225 case Evidence::ABSENT:
226 return false;
227 }
228 return false;
229}
230
237inline constexpr bool evidence_leaves_open(Evidence evidence) noexcept {
238 switch (evidence) {
239 case Evidence::ABSENT:
240 return true;
243 return false;
244 }
245 return true;
246}
247
250
258template <typename Enum>
259inline Evidence attribute_evidence(Enum row_value, Enum entry_value) noexcept {
260 if (row_value == Enum::UNKNOWN || entry_value == Enum::UNKNOWN) {
261 return Evidence::ABSENT;
262 }
263 return row_value == entry_value ? Evidence::CONFIRMS : Evidence::CONTRADICTS;
264}
265
282 const std::optional<Decimal64>& row_trigger,
283 const std::optional<Decimal64>& entry_trigger
284) {
285 if (!row_trigger) {
286 return entry_trigger ? Evidence::ABSENT : Evidence::CONFIRMS;
287 }
288 if (!entry_trigger) {
289 return Evidence::ABSENT;
290 }
291 return reconcile_detail::same_value(*row_trigger, *entry_trigger) ?
294}
295
313 const OpenInstruction& instruction, const OrderSnapshot& row, const MatchWindow& window
314) {
315 if (row.ordered_date_assumed_) {
316 return Evidence::ABSENT;
317 }
318 if (!row.ordered_at_) {
319 return Evidence::CONFIRMS;
320 }
321 const auto begin = instruction.opened_at_ - window.clock_skew_;
322 const auto end = instruction_sent_by(instruction, window) + window.clock_skew_;
323 return (*row.ordered_at_ >= begin && *row.ordered_at_ <= end) ? Evidence::CONFIRMS
325}
326
338 const OpenInstruction& instruction, const OrderSnapshot& row
339) {
340 const auto& asked = instruction.entry_.venue_;
341 if (!asked) {
342 // The line predates the field, so what it asked for is unknowable.
344 }
345 if (*asked == Venue::UNSPECIFIED) {
346 // It asked for nothing, so nothing about the row's venue can contradict it.
348 }
349 if (row.venue_ == Venue::UNSPECIFIED) {
350 // It asked for a venue and the broker did not say where the order went.
352 }
354}
355
368inline bool row_corresponds(
369 const OpenInstruction& instruction, const OrderSnapshot& row, const MatchWindow& window
370) {
371 const auto& entry = instruction.entry_;
372 if (!(row.instrument_ == entry.instrument_)) {
373 return false;
374 }
375 if (row.side_ != entry.side_) {
376 return false;
377 }
378 // A known basis on both sides must agree. An `UNKNOWN` on either side is not
379 // a disagreement and is not a correspondence either: it is a row this
380 // library could not classify, and `row_might_correspond` below is what stops
381 // it being read as absence.
382 if (!shape_detail::both_known(row.type_, entry.type_)) {
383 return false;
384 }
385 if (row.type_ != entry.type_) {
386 return false;
387 }
388 if (!reconcile_detail::same_value(row.quantity_, entry.quantity_)) {
389 return false;
390 }
391 if (!reconcile_detail::same_value(row.price_, entry.price_)) {
392 return false;
393 }
394 // A match needs every attribute established, not merely not contradicted.
395 for (const auto evidence : {
396 attribute_evidence(row.time_in_force_, entry.time_in_force_),
397 attribute_evidence(row.session_, entry.session_),
398 trigger_evidence(row.trigger_price_, entry.trigger_price_),
399 route_evidence(instruction, row),
400 time_evidence(instruction, row, window),
401 }) {
402 if (!evidence_confirms(evidence)) {
403 return false;
404 }
405 }
406 return true;
407}
408
424 const OpenInstruction& instruction, const OrderSnapshot& row, const MatchWindow& window
425) {
426 const auto& entry = instruction.entry_;
427 // Anything that positively disagrees rules the row out however much else is
428 // silent -- an unknown-code row at a venue the instruction did not use is not
429 // this order, and letting it force an ambiguity would stall recovery for
430 // nothing.
431 for (const auto evidence : {
432 attribute_evidence(row.type_, entry.type_),
433 attribute_evidence(row.time_in_force_, entry.time_in_force_),
434 attribute_evidence(row.session_, entry.session_),
435 trigger_evidence(row.trigger_price_, entry.trigger_price_),
436 route_evidence(instruction, row),
437 time_evidence(instruction, row, window),
438 }) {
439 if (evidence_contradicts(evidence)) {
440 return false;
441 }
442 }
443
444 const bool anything_silent =
445 evidence_leaves_open(attribute_evidence(row.type_, entry.type_)) ||
446 evidence_leaves_open(attribute_evidence(row.time_in_force_, entry.time_in_force_)) ||
447 evidence_leaves_open(attribute_evidence(row.session_, entry.session_)) ||
448 evidence_leaves_open(trigger_evidence(row.trigger_price_, entry.trigger_price_)) ||
449 evidence_leaves_open(route_evidence(instruction, row)) ||
450 evidence_leaves_open(time_evidence(instruction, row, window));
451 if (!anything_silent) {
452 // Everything comparable was compared, so `row_corresponds` has already
453 // given the definitive answer and this predicate has nothing to add.
454 return false;
455 }
456
457 if (!(row.instrument_ == entry.instrument_) || row.side_ != entry.side_) {
458 return false;
459 }
460 if (!reconcile_detail::same_value(row.quantity_, entry.quantity_)) {
461 return false;
462 }
463 // Compare what survived. A row whose *basis* is unreadable has no price
464 // either, because the parser discards it -- but one whose only silence is a
465 // route or a timestamp still has both, and a row differing there is plainly
466 // another order rather than an ambiguity worth withholding an instrument for.
467 if (!evidence_leaves_open(attribute_evidence(row.type_, entry.type_))) {
468 if (!reconcile_detail::same_value(row.price_, entry.price_)) {
469 return false;
470 }
471 }
472 return true;
473}
474
485inline std::string unresolvable_reason(const JournalEntry& entry) {
486 const bool by_order_id = journal_action_matches_by_order_id(entry.action_);
487 if (by_order_id && entry.order_id_.empty()) {
488 return "a cancellation was journalled without the order id it names, so no row can be "
489 "identified as its target; an operator must say what became of it";
490 }
491 // **Placements only.** A cancellation is matched on the broker id alone --
492 // `row.order_id_ == entry.order_id_` and nothing else -- so its copy of the
493 // order's shape is never compared and an inconsistency in it cannot mislead
494 // anything. Applying the check to every action withheld an instrument whose
495 // identity the journal *had* recorded, for a cancellation that resolves
496 // without trouble: the shape is copied from the working order, so a
497 // reservation carrying an unpriced `LIMIT` or a reported `MARKET` price puts
498 // it there without the cancellation caring.
499 if (!by_order_id && entry.type_ != OrderType::UNKNOWN) {
500 const bool priced = carries_caller_price(entry.type_);
501 if (priced && !entry.price_) {
502 return "a price-bearing order was journalled without its price, so no row can be matched "
503 "on it; an operator must say what became of it";
504 }
505 if (!priced && entry.price_) {
506 return "an order whose price the market sets was journalled carrying one, so no row can "
507 "be matched on it; an operator must say what became of it";
508 }
509 // The trigger's twin of the price rule, for the same reason: a line the writer emits before
510 // the binding could refuse it, which no row can be matched against.
511 const bool triggered = carries_trigger_price(entry.type_);
512 if (triggered && !entry.trigger_price_) {
513 return "a stop-limit order was journalled without its trigger price, so no row can be "
514 "matched on it; an operator must say what became of it";
515 }
516 if (!triggered && entry.trigger_price_) {
517 return "an order that takes no trigger price was journalled carrying one, so no row can be "
518 "matched on it; an operator must say what became of it";
519 }
520 }
521 return {};
522}
523
535inline std::vector<OpenInstruction> recorded_placements(const JournalReplay& replay) {
536 std::vector<OpenInstruction> found;
537 std::unordered_map<std::string, std::size_t> found_at;
538 std::vector<std::string> refused;
539 for (const auto& entry : replay.entries_) {
540 if (entry.action_ != JournalAction::PLACE || entry.requires_operator_) {
541 continue;
542 }
543 auto known = found_at.find(entry.id_);
544 if (known == found_at.end()) {
545 OpenInstruction placement;
546 placement.entry_ = entry;
547 placement.entry_.order_id_.clear();
548 placement.opened_at_ = entry.recorded_at_;
549 known = found_at.emplace(entry.id_, found.size()).first;
550 found.push_back(std::move(placement));
551 }
552 auto& placement = found[known->second];
553 if (journal_outcome_refuses(entry.outcome_)) {
554 refused.push_back(entry.id_);
555 }
556 if (journal_outcome_answers_send(entry.outcome_) && !placement.closed_at_) {
557 placement.closed_at_ = entry.recorded_at_;
558 }
559 if (journal_outcome_names_order(entry.outcome_) && !entry.order_id_.empty() && placement.entry_.order_id_.empty()) {
560 placement.entry_.order_id_ = entry.order_id_;
561 }
562 }
563 found.erase(
564 std::remove_if(
565 found.begin(), found.end(),
566 [&](const OpenInstruction& placement) {
567 return placement.entry_.order_id_.empty() ||
568 std::find(refused.begin(), refused.end(), placement.entry_.id_) != refused.end();
569 }
570 ),
571 found.end()
572 );
573 return found;
574}
575
589 const OpenInstruction& instruction,
590 const OrderSnapshot& row,
591 const std::vector<OpenInstruction>& recorded,
592 const MatchWindow& window
593) {
594 if (row.order_id_.empty()) {
595 return nullptr;
596 }
597 for (const auto& other : recorded) {
598 if (other.entry_.id_ == instruction.entry_.id_ || other.entry_.order_id_ != row.order_id_) {
599 continue;
600 }
601 if (row_corresponds(other, row, window) || row_might_correspond(other, row, window)) {
602 return &other;
603 }
604 }
605 return nullptr;
606}
607
627 const OpenInstruction& instruction,
628 const std::vector<OrderSnapshot>& rows,
629 const std::vector<OpenInstruction>& recorded,
630 std::chrono::system_clock::time_point now,
631 const MatchWindow& window = {}
632) {
633 OrderMatch match;
634 match.instrument_ = instruction.entry_.instrument_;
635 match.entry_id_ = instruction.entry_.id_;
636
637 // **An instruction whose own line cannot support a conclusion.** These are
638 // written by this library -- `JournalingExecutor` appends the pending line
639 // before the binding runs, so a locally refused order is on disk before
640 // anything could object -- which is why the *reader* accepts them. What must
641 // not happen is resolving one: each lacks precisely the field the matcher
642 // would decide on, so a sweep would conclude from silence.
643 //
644 // An ordinary local refusal is unaffected: its own terminal line closes the
645 // pending one before any of this is reached. Only an instruction still open
646 // at recovery arrives here, and for those the honest answer is an operator.
647 if (const auto reason = unresolvable_reason(instruction.entry_); !reason.empty()) {
648 match.verdict_ = MatchVerdict::NEEDS_OPERATOR;
649 match.detail_ = reason;
650 return match;
651 }
652 if (instruction.entry_.requires_operator_) {
653 // Checked before anything else, so no window arithmetic runs on a timestamp that was never
654 // recorded. This is the entry the file-journal documentation calls "something nothing can ever
655 // resolve", and this check is what makes that true rather than aspirational.
656 match.verdict_ = MatchVerdict::NEEDS_OPERATOR;
657 match.detail_ = instruction.entry_.detail_.empty() ?
658 "recovered from a damaged journal line; only an operator can resolve it" :
659 instruction.entry_.detail_;
660 return match;
661 }
662
663 const OrderSnapshot* found = nullptr;
664 std::size_t unreadable = 0;
665 std::size_t claimed = 0;
666 const OrderSnapshot* claimed_row = nullptr;
667 const OpenInstruction* claimant = nullptr;
668 const bool by_order_id = journal_action_matches_by_order_id(instruction.entry_.action_);
669 if (by_order_id) {
670 for (const auto& row : rows) {
671 if (!instruction.entry_.order_id_.empty() && row.order_id_ == instruction.entry_.order_id_) {
672 ++match.candidates_;
673 found = &row;
674 }
675 }
676 } else {
677 for (const auto& row : rows) {
678 const bool fits = row_corresponds(instruction, row, window);
679 // Counted separately, because these are not candidates for a *match* -- the
680 // library could not read their shape, so it cannot claim one is this order.
681 // They are candidates against concluding the order does not exist.
682 const bool might_fit = row_might_correspond(instruction, row, window);
683 if (!fits && !might_fit) {
684 continue;
685 }
686 // Another placement's order by the journal's own record: neither this order nor evidence
687 // that this order is missing.
688 if (const auto* owner = recorded_owner(instruction, row, recorded, window)) {
689 ++claimed;
690 claimed_row = &row;
691 claimant = owner;
692 continue;
693 }
694 if (fits) {
695 ++match.candidates_;
696 found = &row;
697 }
698 if (might_fit) {
699 ++unreadable;
700 }
701 }
702 }
703
704 if (claimed > 0) {
705 // Not `MATCHED`: the row is another placement's. Not `NOT_PLACED` either, even when nothing else
706 // fits, and not `MATCHED` on another row that fits: the record may be a sweep's mistaken name,
707 // and then the claimed row is this order and the other row is the claimant's.
708 match.verdict_ = MatchVerdict::AMBIGUOUS;
709 const auto others = match.candidates_ + unreadable;
710 match.candidates_ = others + claimed;
711 match.detail_ = "order " + claimed_row->order_id_ + " in the broker's listing fits this instruction, and the "
712 "journal records it for another placement (" + claimant->entry_.id_ + ")" +
713 (claimed > 1 ? ", as it does " + std::to_string(claimed - 1) + " more such row(s)" : std::string()) +
714 (others > 0 ? "; " + std::to_string(others) + " other row(s) fit as well" : std::string()) +
715 "; nothing was resolved";
716 return match;
717 }
718 if (match.candidates_ > 1) {
719 match.verdict_ = MatchVerdict::AMBIGUOUS;
720 match.detail_ = std::to_string(match.candidates_) +
721 " orders in the broker's listing could be this instruction; nothing was resolved";
722 return match;
723 }
724 if (unreadable > 0) {
725 // Report, never guess -- and **whether or not a readable row also matched**.
726 // A row whose order code this release does not publish is not evidence that
727 // the order is missing, so `NOT_PLACED` would clear the hold and send it
728 // again beside the one already resting. But it is equally not evidence that
729 // some *other* row is the order: with one readable row matching every
730 // attribute and one unreadable row matching every attribute that survives
731 // an unreadable shape, the unreadable one may be the instruction and the
732 // readable one another order on the same account. Resolving to the readable
733 // one would attribute the wrong order and clear the hold on it, which is
734 // the same harm by a different route.
735 match.verdict_ = MatchVerdict::AMBIGUOUS;
736 match.candidates_ += unreadable;
737 match.order_id_.clear();
738 // Says which it is. An operator reading "unrecognized order code" about a
739 // row whose code was perfectly readable, and whose *route* was the unknown,
740 // is being sent to look at the wrong field.
741 match.detail_ = std::to_string(unreadable) +
742 " order(s) in the broker's listing match this instruction on instrument, side, quantity "
743 "and time, but this library cannot confirm which order they are: an order code it does "
744 "not recognize, a route nobody recorded, or both" +
745 (found != nullptr ? ", and another row matches it fully" : "") +
746 "; nothing was resolved. Each row carries the broker's own code as shape_code_ and its "
747 "venue as venue_";
748 return match;
749 }
750 if (match.candidates_ == 1) {
751 match.verdict_ = MatchVerdict::MATCHED;
752 match.order_id_ = found->order_id_;
753 match.detail_ = by_order_id ?
754 "the order named by the cancellation is still in the broker's listing" :
755 "the broker's listing carries this order";
756 return match;
757 }
758 if (!match_window_closed(instruction, now, window)) {
759 match.verdict_ = MatchVerdict::UNDECIDED;
760 match.detail_ =
761 "no matching order yet, and not enough time has passed for the listing to show one";
762 return match;
763 }
764 match.verdict_ = MatchVerdict::NOT_PLACED;
765 match.detail_ = by_order_id ?
766 "the order named by the cancellation is no longer in the broker's listing" :
767 "no matching order was found among the rows read, within the matching window";
768 return match;
769}
770
785 const OpenInstruction& instruction,
786 const std::vector<OrderSnapshot>& rows,
787 std::chrono::system_clock::time_point now,
788 const MatchWindow& window = {}
789) {
790 return match_against(instruction, rows, {}, now, window);
791}
792
816 std::vector<OrderSnapshot>& rows,
817 const JournalReplay& replay,
818 const std::vector<OpenInstruction>& matching,
819 const MatchWindow& window = {}
820) {
821 // The other placements on the instruments being matched, each with the span of its send.
822 std::vector<OpenInstruction> others;
823 std::unordered_map<std::string, std::size_t> other_of;
824 std::vector<std::string> refused;
825 for (const auto& entry : replay.entries_) {
826 if (entry.action_ != JournalAction::PLACE) {
827 continue;
828 }
829 const bool on_an_instrument_matched = std::any_of(
830 matching.begin(), matching.end(),
831 [&](const OpenInstruction& instruction) { return instruction.entry_.instrument_ == entry.instrument_; }
832 );
833 const bool being_matched = std::any_of(matching.begin(), matching.end(), [&](const OpenInstruction& instruction) {
834 return instruction.entry_.id_ == entry.id_;
835 });
836 if (!on_an_instrument_matched || being_matched) {
837 continue;
838 }
839 if (journal_outcome_refuses(entry.outcome_)) {
840 refused.push_back(entry.id_);
841 }
842 auto known = other_of.find(entry.id_);
843 if (known == other_of.end()) {
844 OpenInstruction other;
845 other.entry_ = entry;
846 other.opened_at_ = entry.recorded_at_;
847 known = other_of.emplace(entry.id_, others.size()).first;
848 others.push_back(std::move(other));
849 }
850 auto& other = others[known->second];
851 if (journal_outcome_answers_send(entry.outcome_) && !other.closed_at_) {
852 other.closed_at_ = entry.recorded_at_;
853 }
854 }
855 rows.erase(
856 std::remove_if(
857 rows.begin(), rows.end(),
858 [&](const OrderSnapshot& row) {
859 return std::any_of(others.begin(), others.end(), [&](const OpenInstruction& other) {
860 return std::find(refused.begin(), refused.end(), other.entry_.id_) == refused.end() &&
861 (row_corresponds(other, row, window) || row_might_correspond(other, row, window));
862 });
863 }
864 ),
865 rows.end()
866 );
867}
868
872 std::vector<OrderMatch> matches_;
874 std::vector<InstrumentId> resolved_;
876 std::vector<InstrumentId> withheld_;
878 bool ambiguous_{false};
880 std::size_t recorded_{0};
885 bool unrecorded_{false};
886
891 bool settled() const noexcept { return withheld_.empty(); }
892};
893
902 const OpenInstruction& instruction,
903 const OrderMatch& match,
904 std::chrono::system_clock::time_point now
905) {
906 auto entry = instruction.entry_;
907 // RESOLVED records what the listing showed, not the broker's answer to the instruction.
909 entry.recorded_at_ = now;
910 entry.requires_operator_ = false;
911 entry.order_id_ = match.order_id_;
912 entry.detail_ = "resolved by reading the broker's order list: " + match.detail_;
913 return entry;
914}
915
936template <typename Journal>
938 const std::vector<OpenInstruction>& open,
939 const std::vector<OrderSnapshot>& rows,
940 const JournalReplay& replay,
941 std::chrono::system_clock::time_point now,
942 IndeterminacyLedger& ledger,
943 Journal& journal,
944 const MatchWindow& window = {},
945 std::string_view unplaced_note = {}
946) {
947 ReconcileReport report;
948 report.matches_.reserve(open.size());
949 const auto recorded = recorded_placements(replay);
950
951 std::vector<InstrumentId> undecided;
952 std::vector<InstrumentId> seen;
953 for (const auto& instruction : open) {
954 auto match = match_against(instruction, rows, recorded, now, window);
955 const auto instrument = match.instrument_;
956 if (match.verdict_ == MatchVerdict::AMBIGUOUS) {
957 report.ambiguous_ = true;
958 }
959 if (match.verdict_ == MatchVerdict::NOT_PLACED && instruction.entry_.action_ == JournalAction::PLACE) {
960 match.detail_ += unplaced_note;
961 }
962 if (match_verdict_resolves(match.verdict_)) {
963 // Written before the ledger is cleared, for the same reason a placement is written before it
964 // is sent: the durable record has to be the conservative one. A sweep that cleared memory and
965 // failed to record why would leave the instruction owed on disk, and the next restart would
966 // withhold the instrument again for a question that had already been answered.
967 if (journal.append(resolution_entry(instruction, match, now))) {
968 ++report.recorded_;
969 } else {
970 report.unrecorded_ = true;
971 undecided.push_back(instrument);
972 match.detail_ += " (not recorded: the journal could not be written, so the hold stands)";
973 }
974 } else {
975 undecided.push_back(instrument);
976 }
977 report.matches_.push_back(std::move(match));
978
979 bool known = false;
980 for (const auto& id : seen) {
981 if (id == instrument) {
982 known = true;
983 break;
984 }
985 }
986 if (!known) {
987 seen.push_back(instrument);
988 }
989 }
990
991 for (const auto& instrument : seen) {
992 bool blocked = false;
993 for (const auto& id : undecided) {
994 if (id == instrument) {
995 blocked = true;
996 break;
997 }
998 }
999 if (blocked) {
1000 report.withheld_.push_back(instrument);
1001 continue;
1002 }
1003 ledger.resolve_after_reading_orders(instrument);
1004 report.resolved_.push_back(instrument);
1005 }
1006 return report;
1007}
1008
1022template <typename Journal>
1024 const std::vector<OpenInstruction>& open,
1025 const std::vector<OrderSnapshot>& rows,
1026 std::chrono::system_clock::time_point now,
1027 IndeterminacyLedger& ledger,
1028 Journal& journal,
1029 const MatchWindow& window = {},
1030 std::string_view unplaced_note = {}
1031) {
1032 return reconcile_against(open, rows, journal.replay(), now, ledger, journal, window, unplaced_note);
1033}
1034
1045 const std::vector<OpenInstruction>& open, std::string detail
1046) {
1047 ReconcileReport report;
1048 report.matches_.reserve(open.size());
1049 for (const auto& instruction : open) {
1050 OrderMatch match;
1052 match.instrument_ = instruction.entry_.instrument_;
1053 match.entry_id_ = instruction.entry_.id_;
1054 match.detail_ = detail;
1055 report.matches_.push_back(std::move(match));
1056
1057 bool known = false;
1058 for (const auto& id : report.withheld_) {
1059 if (id == instruction.entry_.instrument_) {
1060 known = true;
1061 break;
1062 }
1063 }
1064 if (!known) {
1065 report.withheld_.push_back(instruction.entry_.instrument_);
1066 }
1067 }
1068 return report;
1069}
1070
1077inline bool any_placement(const std::vector<OpenInstruction>& open) {
1078 for (const auto& instruction : open) {
1079 // Operator-only entries are excluded, because no listing can decide them: `match_against`
1080 // returns `NEEDS_OPERATOR` before it looks at a single row. Including them bought pointless
1081 // pagination on Toss and a guaranteed rejected inquiry on KIS, on every sweep, forever.
1082 if (instruction.entry_.action_ == JournalAction::PLACE &&
1083 !instruction.entry_.requires_operator_) {
1084 return true;
1085 }
1086 }
1087 return false;
1088}
1089
1097inline bool exact_copy(const OrderSnapshot& a, const OrderSnapshot& b) {
1098 return !a.order_id_.empty() && a.order_id_ == b.order_id_ && a.instrument_ == b.instrument_ &&
1100}
1101
1111inline bool dates_listed_order(const OrderSnapshot& assumed, const OrderSnapshot& row) {
1112 if (row.ordered_date_assumed_ || !row.ordered_at_ || !assumed.ordered_at_ || assumed.order_id_.empty() ||
1113 row.order_id_ != assumed.order_id_ || !(row.instrument_ == assumed.instrument_)) {
1114 return false;
1115 }
1116 // Both stamped from a date and the same `HHmmss` in one zone, so one order's two rows differ by whole days.
1117 if ((*assumed.ordered_at_ - *row.ordered_at_) % std::chrono::hours(24) != std::chrono::hours(0)) {
1118 return false;
1119 }
1120 // A number reused on another day can belong to a different order at the same time of day.
1121 const bool venues_differ = row.venue_ != Venue::UNSPECIFIED && assumed.venue_ != Venue::UNSPECIFIED &&
1122 row.venue_ != assumed.venue_;
1123 return row.side_ == assumed.side_ &&
1124 reconcile_detail::same_value(row.quantity_, assumed.quantity_) &&
1125 reconcile_detail::same_value(row.price_, assumed.price_) &&
1130 !venues_differ;
1131}
1132
1147 std::vector<OrderSnapshot>& into,
1148 const std::vector<OrderSnapshot>& rows
1149) {
1150 std::vector<OrderSnapshot> listing;
1151 listing.reserve(into.size() + rows.size());
1152 for (auto& listed : into) {
1153 const bool dated_by_history = listed.ordered_date_assumed_ &&
1154 std::any_of(rows.begin(), rows.end(), [&](const OrderSnapshot& row) { return dates_listed_order(listed, row); });
1155 if (!dated_by_history) {
1156 listing.push_back(std::move(listed));
1157 }
1158 }
1159 into = std::move(listing);
1160 const auto listed = into.size();
1161 for (const auto& row : rows) {
1162 bool known = false;
1163 for (std::size_t at = 0; at < listed; ++at) {
1164 // A listing row still assumed is no copy of anything: the dated row is the evidence, and both stay.
1165 if (!into[at].ordered_date_assumed_ && exact_copy(into[at], row)) {
1166 known = true;
1167 break;
1168 }
1169 }
1170 if (!known) {
1171 into.push_back(row);
1172 }
1173 }
1174}
1175
1181inline void merge_rows(std::vector<OrderSnapshot>& into, const std::vector<OrderSnapshot>& rows) {
1182 for (const auto& row : rows) {
1183 bool known = false;
1184 for (const auto& held : into) {
1185 // Deduplicated by the broker's own id, because an order that is still open appears in *both*
1186 // listings. Two copies of one row would read as two candidates, and the matcher would report
1187 // `AMBIGUOUS` for an instruction that is not ambiguous at all.
1188 if (!row.order_id_.empty() && held.order_id_ == row.order_id_) {
1189 known = true;
1190 break;
1191 }
1192 }
1193 if (!known) {
1194 into.push_back(row);
1195 }
1196 }
1197}
1198
1204inline Date date_of(std::chrono::system_clock::time_point at) {
1205 const auto seconds = std::chrono::time_point_cast<std::chrono::seconds>(at);
1206 const std::time_t raw = std::chrono::system_clock::to_time_t(seconds);
1207 std::tm utc{};
1208#ifdef _WIN32
1209 gmtime_s(&utc, &raw);
1210#else
1211 gmtime_r(&raw, &utc);
1212#endif
1213 Date date;
1214 date.year_ = utc.tm_year + 1900;
1215 date.month_ = static_cast<unsigned>(utc.tm_mon + 1);
1216 date.day_ = static_cast<unsigned>(utc.tm_mday);
1217 return date;
1218}
1219
1230inline std::pair<Date, Date> history_range_for(
1231 const std::vector<OpenInstruction>& open, const MatchWindow& window
1232) {
1233 auto earliest = open.front().opened_at_;
1234 auto latest = instruction_sent_by(open.front(), window);
1235 for (const auto& instruction : open) {
1236 if (instruction.opened_at_ < earliest) {
1237 earliest = instruction.opened_at_;
1238 }
1239 const auto sent_by = instruction_sent_by(instruction, window);
1240 if (sent_by > latest) {
1241 latest = sent_by;
1242 }
1243 }
1244 return {
1245 date_of(earliest - std::chrono::hours(24)),
1246 date_of(latest + std::chrono::hours(24)),
1247 };
1248}
1249
1253 std::vector<InstrumentId> marked_;
1255 std::vector<OpenInstruction> open_;
1257 bool truncated_{false};
1262 bool unattributable_{false};
1264 std::size_t unreadable_lines_{0};
1266 bool unreadable_file_{false};
1268 std::string detail_;
1269
1274 bool clean() const noexcept {
1275 return marked_.empty() && !truncated_ && !unattributable_ && unreadable_lines_ == 0 &&
1277 }
1278};
1279
1292 RecoveryReport report;
1293 report.truncated_ = replay.truncated_tail_;
1294 report.unreadable_lines_ = replay.unreadable_lines_;
1295 report.unreadable_file_ = !replay.readable();
1296 report.detail_ = replay.detail_;
1297 report.open_ = unresolved_instructions(replay);
1298
1299 for (const auto& instruction : report.open_) {
1300 const auto& instrument = instruction.entry_.instrument_;
1301 if (instrument.symbol_.empty()) {
1302 report.unattributable_ = true;
1303 continue;
1304 }
1305 const auto marked = ledger.requires_reconcile(instrument);
1306 ledger.mark(
1307 instrument,
1308 instruction.detail_.empty() ?
1309 "a previous process left this instruction unresolved in the order journal" :
1310 instruction.detail_
1311 );
1312 if (!marked) {
1313 report.marked_.push_back(instrument);
1314 }
1315 }
1316 if (report.truncated_ && report.open_.empty()) {
1317 report.unattributable_ = true;
1318 }
1319 return report;
1320}
1321
1329template <typename Journal>
1330RecoveryReport recover_into(const Journal& journal, IndeterminacyLedger& ledger) {
1331 return recover_into(journal.replay(), ledger);
1332}
1333
1349template <typename Runner, typename Journal>
1350RecoveryReport recover_runner(Runner& runner_instance, const Journal& journal) {
1351 const auto replay = journal.replay();
1352 auto report = recover_into(replay, runner_instance.ledger());
1353 // A serialized runner also gets back every order it may still owe (`Runner::restore_obligations`),
1354 // and places nothing until this has run.
1355 runner_instance.restore_obligations(replay);
1356 if (report.unattributable_) {
1357 runner_instance.halt_locally(
1358 report.detail_.empty() ?
1359 "the order journal was damaged in a way that names no instrument" :
1360 report.detail_
1361 );
1362 }
1363 if (report.unreadable_lines_ > 0) {
1364 runner_instance.halt_locally(
1365 "the order journal holds " + std::to_string(report.unreadable_lines_) +
1366 " line(s) this library did not write, so it cannot account for what was in flight"
1367 );
1368 }
1369 if (report.unreadable_file_) {
1370 // Nothing about the file's contents is known, and unlike a missing journal this one may hold
1371 // every order a predecessor left in flight. There is nothing to withhold specifically.
1372 runner_instance.halt_locally(
1373 report.detail_.empty() ?
1374 "the order journal exists and could not be read" :
1375 report.detail_
1376 );
1377 }
1378 return report;
1379}
1380
1381} // namespace runner
1382} // namespace algotradelib
An exact signed decimal: an int64 coefficient times 10^-scale.
Definition types.hpp:297
The instruments with an order of unknown outcome, where nothing may be placed until the broker's orde...
bool requires_reconcile(const InstrumentId &id) const
Tests whether an instrument is waiting for reconciliation.
void mark(const InstrumentId &id, std::string detail)
Records an instrument's order of unknown outcome; an existing entry keeps its first detail.
void resolve_after_reading_orders(const InstrumentId &id)
Clears an instrument; call it only after reading the broker's order list for it.
Runs one strategy: feeds it events, reconciles what it asks for, checks the risk limits,...
Definition runner.hpp:297
void halt_locally(std::string detail)
Stops this runner sending, with the LOCAL_FAILURE hold, until resume_after_local_failure.
Definition runner.hpp:472
void restore_obligations(const JournalReplay &replay)
Restores the orders a previous process left unaccounted for; runner::recover_runner calls it,...
Definition runner.hpp:1017
const IndeterminacyLedger & ledger() const noexcept
Returns the ledger of instruments withheld pending reconciliation or an operator: an order of unknown...
Definition runner.hpp:380
Runs a strategy: the runner, the polling loop, the order journal, and stopping.
Definition clock.hpp:7
MatchVerdict
What the broker's order list says about one instruction of unknown outcome.
@ AMBIGUOUS
Several rows could correspond, a row that fits is another placement's, or a row that fits cannot be c...
@ NOT_PLACED
No row corresponds and the window has closed: no order was found.
@ UNDECIDED
No row yet, and the window is still open; wait for the next sweep.
@ MATCHED
Exactly one row corresponds; the order exists.
@ NEEDS_OPERATOR
The instruction came from a damaged journal line, or its line cannot be matched (unresolvable_reason)...
void merge_rows(std::vector< OrderSnapshot > &into, const std::vector< OrderSnapshot > &rows)
Adds rows to a listing, skipping any whose order id is already there.
Date date_of(std::chrono::system_clock::time_point at)
Returns the UTC calendar date of an instant.
constexpr bool journal_outcome_answers_send(JournalOutcome outcome) noexcept
Tests whether a line with this outcome answers the send itself (the broker's reply,...
Definition journal.hpp:234
bool any_placement(const std::vector< OpenInstruction > &open)
Tests whether any instruction is a placement a listing could decide (which needs history,...
void merge_dated_history(std::vector< OrderSnapshot > &into, const std::vector< OrderSnapshot > &rows)
Adds history rows to a working-order listing, dropping established copies (exact_copy) and listing ro...
bool row_might_correspond(const OpenInstruction &instruction, const OrderSnapshot &row, const MatchWindow &window)
Tests whether a row might be an instruction's order although some attributes could not be read (such ...
ReconcileReport decide_nothing(const std::vector< OpenInstruction > &open, std::string detail)
Makes a report that decides nothing, for a sweep that could not see a complete listing.
std::pair< Date, Date > history_range_for(const std::vector< OpenInstruction > &open, const MatchWindow &window)
Returns the date range an order-history inquiry must cover for some instructions.
std::chrono::system_clock::time_point instruction_sent_by(const OpenInstruction &instruction, const MatchWindow &window)
Returns the last instant an instruction could have reached the broker.
Evidence time_evidence(const OpenInstruction &instruction, const OrderSnapshot &row, const MatchWindow &window)
Compares a row's order time with the window in which the instruction was sent.
std::vector< OpenInstruction > recorded_placements(const JournalReplay &replay)
Returns the placements a journal records an order id for, each with the span of its send.
const OpenInstruction * recorded_owner(const OpenInstruction &instruction, const OrderSnapshot &row, const std::vector< OpenInstruction > &recorded, const MatchWindow &window)
Returns the recorded placement whose order a row is: the row carries the order id the journal records...
bool match_window_closed(const OpenInstruction &instruction, std::chrono::system_clock::time_point now, const MatchWindow &window)
Tests whether a missing row may now be read as "not placed".
constexpr bool journal_action_matches_by_order_id(JournalAction action) noexcept
Tests whether recovery matches an instruction of this action by the broker's order id it names,...
Definition journal.hpp:67
constexpr bool journal_outcome_refuses(JournalOutcome outcome) noexcept
Tests whether a line with this outcome says the instruction was refused, so it does not exist.
Definition journal.hpp:274
RecoveryReport recover_into(const JournalReplay &replay, IndeterminacyLedger &ledger)
Marks the ledger for every instrument a journal left an instruction of unknown outcome for.
bool dates_listed_order(const OrderSnapshot &assumed, const OrderSnapshot &row)
Tests whether a dated history row is the order a listing row whose date was assumed shows: the same o...
RecoveryReport recover_runner(Runner &runner_instance, const Journal &journal)
Recovers a journal into a runner at startup: marks each instrument left owed, restores a serialized r...
constexpr bool match_verdict_resolves(MatchVerdict verdict) noexcept
Tests whether a verdict settles the question.
@ RESOLVED
A recovery sweep read the broker's order list and closed the question.
Definition journal.hpp:116
Evidence trigger_evidence(const std::optional< Decimal64 > &row_trigger, const std::optional< Decimal64 > &entry_trigger)
Compares a row's trigger price with the instruction's.
void drop_rows_fitting_another_placement(std::vector< OrderSnapshot > &rows, const JournalReplay &replay, const std::vector< OpenInstruction > &matching, const MatchWindow &window={})
Removes the rows that could be another placement's order as well.
OrderMatch match_against(const OpenInstruction &instruction, const std::vector< OrderSnapshot > &rows, const std::vector< OpenInstruction > &recorded, std::chrono::system_clock::time_point now, const MatchWindow &window={})
Decides what a broker listing says about one instruction of unknown outcome.
std::string unresolvable_reason(const JournalEntry &entry)
Returns why an instruction cannot be resolved from a broker listing.
ReconcileReport reconcile_against(const std::vector< OpenInstruction > &open, const std::vector< OrderSnapshot > &rows, const JournalReplay &replay, std::chrono::system_clock::time_point now, IndeterminacyLedger &ledger, Journal &journal, const MatchWindow &window={}, std::string_view unplaced_note={})
Resolves what a broker listing settles, records it in the journal, and clears the ledger for each ins...
constexpr std::string_view match_verdict_name(MatchVerdict verdict) noexcept
Returns a verdict's lowercase name, such as "not_placed", for logs.
constexpr bool evidence_contradicts(Evidence evidence) noexcept
Tests whether a piece of evidence rules the row out.
Evidence RouteEvidence
Evidence, under the name used for the route comparison.
JournalEntry resolution_entry(const OpenInstruction &instruction, const OrderMatch &match, std::chrono::system_clock::time_point now)
Makes the RESOLVED journal line that records a sweep's verdict.
std::vector< OpenInstruction > unresolved_instructions(const JournalReplay &replay)
Lists the instructions whose outcome is unknown, in the order they were opened.
Definition journal.hpp:537
constexpr bool evidence_leaves_open(Evidence evidence) noexcept
Tests whether a piece of evidence leaves the question open, so the row might still be the order and c...
bool exact_copy(const OrderSnapshot &a, const OrderSnapshot &b)
Tests whether two rows are established copies of one order: the same order id and instrument,...
Evidence
What one attribute of a broker row says about an instruction.
@ CONTRADICTS
The row disagrees; it is not this order.
@ ABSENT
Not enough was recorded to say.
constexpr bool evidence_confirms(Evidence evidence) noexcept
Tests whether a piece of evidence confirms the row is the instruction's order.
constexpr bool journal_outcome_names_order(JournalOutcome outcome) noexcept
Tests whether a line with this outcome may carry the broker's id for the order.
Definition journal.hpp:254
Evidence attribute_evidence(Enum row_value, Enum entry_value) noexcept
Compares an enumerated attribute of a row with the instruction's.
bool row_corresponds(const OpenInstruction &instruction, const OrderSnapshot &row, const MatchWindow &window)
Tests whether a row is established to be an instruction's order.
RouteEvidence route_evidence(const OpenInstruction &instruction, const OrderSnapshot &row)
Compares a row's venue with the route the instruction recorded.
The library: broker-neutral types (core), and the modules below.
constexpr bool carries_trigger_price(OrderType type) noexcept
Tests whether an order type takes a trigger price.
Definition types.hpp:1774
constexpr bool carries_caller_price(OrderType type) noexcept
Tests whether an order type takes a price from the caller.
Definition types.hpp:1748
bool decimal_less(const Decimal64 &lhs, const Decimal64 &rhs)
Compares two decimals numerically, whatever their scales.
Definition types.hpp:665
A calendar date with no time of day or time zone.
Definition types.hpp:1142
unsigned month_
The month, 1-12; zero means unset.
Definition types.hpp:1146
int year_
The year.
Definition types.hpp:1144
unsigned day_
The day of the month, 1-31; zero means unset.
Definition types.hpp:1148
Identifies an instrument: its country, venue and symbol.
Definition types.hpp:78
The state of one order, as a broker listing reported it.
Definition types.hpp:2008
Venue venue_
Where the broker says the order was routed; UNSPECIFIED if it does not say.
Definition types.hpp:2029
OrderSide side_
Buy or sell.
Definition types.hpp:2016
OrderSession session_
The session; UNKNOWN if the broker's code was not recognized.
Definition types.hpp:2022
OrderType type_
The price basis; UNKNOWN if the broker's code was not recognized.
Definition types.hpp:2018
std::optional< Decimal64 > trigger_price_
The trigger price, whenever the broker reports a positive one.
Definition types.hpp:2048
bool ordered_date_assumed_
Whether the broker stated only the time of day, and the date in ordered_at_ is the day the adapter re...
Definition types.hpp:2070
Decimal64 quantity_
The quantity ordered; the remainder is this minus execution_.filled_quantity_.
Definition types.hpp:2050
std::string order_id_
The broker's order number.
Definition types.hpp:2010
std::optional< std::chrono::system_clock::time_point > ordered_at_
When the broker recorded the order, if it gives a time. KIS's real-server working orders have none; o...
Definition types.hpp:2065
InstrumentId instrument_
The instrument (identity venue; the routing venue is venue_).
Definition types.hpp:2012
TimeInForce time_in_force_
The time in force; UNKNOWN if the broker's code was not recognized.
Definition types.hpp:2020
std::optional< Decimal64 > price_
The limit price, if the order has one.
Definition types.hpp:2040
One journal line: an instruction and what was known about it when the line was written.
Definition journal.hpp:360
std::string id_
Links an answering line to its PENDING line.
Definition journal.hpp:366
std::chrono::system_clock::time_point recorded_at_
When the line was written (for PENDING, before the request went out).
Definition journal.hpp:372
OrderType type_
The order type; recovery matches on type, time in force and session together.
Definition journal.hpp:378
std::optional< Decimal64 > price_
The price, if the order has one.
Definition journal.hpp:394
JournalOutcome outcome_
What the line says about the instruction.
Definition journal.hpp:370
std::string detail_
Why an answering line says what it says, for the operator.
Definition journal.hpp:410
std::optional< Venue > venue_
The venue the order actually goes to, as the binding resolves it.
Definition journal.hpp:390
InstrumentId instrument_
The instrument (identity; the routing venue is venue_).
Definition journal.hpp:374
std::optional< Decimal64 > trigger_price_
The trigger price of a stop-limit order; empty otherwise (and on lines that predate it).
Definition journal.hpp:396
bool requires_operator_
Whether only an operator may resolve this instruction.
Definition journal.hpp:417
JournalAction action_
Whether the instruction is a placement or a cancellation.
Definition journal.hpp:368
std::string order_id_
The broker's order id, if known: from the start on a CANCEL line, and only on an answering line for a...
Definition journal.hpp:408
The contents of a journal, including what could not be read.
Definition journal.hpp:464
bool readable() const noexcept
Tests whether the file could be read.
Definition journal.hpp:484
std::size_t unreadable_lines_
How many lines did not parse (on an append-only file, writes that did not finish).
Definition journal.hpp:468
std::vector< JournalEntry > entries_
Every line that parsed, in file order.
Definition journal.hpp:466
std::string detail_
What was wrong, for the operator.
Definition journal.hpp:472
bool truncated_tail_
Whether the last line was incomplete, as after a kill during a write.
Definition journal.hpp:470
The time allowances used when matching journalled instructions against a broker listing.
std::chrono::seconds clock_skew_
How far the broker's clock may differ from this machine's, either way.
std::chrono::seconds listing_delay_
How long the broker's listing may lag an order it accepted.
std::chrono::seconds maximum_request_
How long a request could have been in flight when no answering line records its end.
One instruction of unknown outcome, and the span in which it may have reached the broker.
Definition journal.hpp:510
JournalEntry entry_
The instruction, as its first line recorded it (its order_id_ may come from a later line).
Definition journal.hpp:512
std::string detail_
What the journal says went wrong.
Definition journal.hpp:523
std::optional< std::chrono::system_clock::time_point > closed_at_
When the send was answered: by an INDETERMINATE line in unresolved_instructions, by any answer in rec...
Definition journal.hpp:519
std::chrono::system_clock::time_point opened_at_
When the send began.
Definition journal.hpp:514
One instruction's verdict, with context for the operator.
InstrumentId instrument_
The instruction's instrument.
std::string entry_id_
The journal line id.
std::string detail_
What was decided and why, for the operator.
std::string order_id_
The broker's order id, on MATCHED.
MatchVerdict verdict_
The verdict.
std::size_t candidates_
How many rows could correspond (one or more on AMBIGUOUS).
What one sweep of the broker's order list settled, and what is still withheld.
std::vector< OrderMatch > matches_
One verdict per instruction, in journal order.
bool settled() const noexcept
Tests whether the sweep settled everything.
bool unrecorded_
Whether a resolution could not be written to the journal; its instrument then stays withheld.
std::size_t recorded_
How many resolutions were written to the journal.
std::vector< InstrumentId > resolved_
Instruments cleared; they may trade again.
bool ambiguous_
Whether any instruction was AMBIGUOUS.
std::vector< InstrumentId > withheld_
Instruments still withheld (their reasons are in matches_).
What a journal showed was left owed by the process that wrote it.
bool unattributable_
Whether something was in flight that names no instrument (a torn line too short to name one); recover...
std::vector< InstrumentId > marked_
Instruments marked in the ledger, withheld until the order list resolves them.
bool truncated_
Whether the last line was torn.
std::string detail_
What was found, for the operator.
std::vector< OpenInstruction > open_
The instructions of unknown outcome.
std::size_t unreadable_lines_
Complete lines that did not parse.
bool clean() const noexcept
Tests whether nothing was owed or damaged.
bool unreadable_file_
Whether the journal exists and could not be read; it may hold orders left in flight.