algotradelib 0.0.0.9000
C++ header-only library for algorithmic trading
Loading...
Searching...
No Matches
file_journal.hpp
1#pragma once
2
3#include "./journal.hpp"
4
5#include <algotradelib/core>
6
7#include <boost/crc.hpp>
8#include <boost/json.hpp>
9
10#include <cerrno>
11#include <chrono>
12#include <cstdint>
13#include <cstdio>
14#include <memory>
15#include <optional>
16#include <string>
17#include <string_view>
18#include <utility>
19#include <vector>
20
21#if defined(_WIN32)
22#include <io.h>
23#else
24#include <fcntl.h>
25#include <sys/file.h>
26#include <unistd.h>
27
28#include <cstring>
29#endif
30
31namespace algotradelib {
32namespace runner {
33
34namespace json = boost::json;
35
36namespace journal_detail {
37
54inline bool flush_to_disk(std::FILE* handle) noexcept {
55 if (std::fflush(handle) != 0) {
56 return false;
57 }
58#if defined(_WIN32)
59 return ::_commit(::_fileno(handle)) == 0;
60#else
61 return ::fsync(::fileno(handle)) == 0;
62#endif
63}
64
95inline bool plausible_instant(std::int64_t micros) noexcept {
96 constexpr std::int64_t kYear2020 = 1577836800LL * 1000000LL;
97 constexpr std::int64_t kCenturyOut = 4102444800LL * 1000000LL;
98 return micros >= kYear2020 && micros <= kCenturyOut;
99}
100
101inline bool has_field(const json::object& row, std::string_view key) {
102 return row.if_contains(key) != nullptr;
103}
104
106inline std::optional<std::string> readable_string(
107 const json::object& row, std::string_view key
108) {
109 const auto value = row.if_contains(key);
110 if (value == nullptr || !value->is_string() || value->as_string().empty()) {
111 return std::nullopt;
112 }
113 return std::string(value->as_string());
114}
115
116inline std::string string_field(const json::object& row, std::string_view key) {
117 const auto value = row.if_contains(key);
118 if (value == nullptr || !value->is_string()) {
119 return {};
120 }
121 return std::string(value->as_string());
122}
123
131inline constexpr std::string_view country_name(Country country) noexcept {
132 switch (country) {
133 case Country::KR:
134 return "KR";
135 case Country::US:
136 return "US";
137 }
138 return "";
139}
140
141static_assert(
142 lists_every_value(all_countries, [](Country country) { return !country_name(country).empty(); }),
143 "all_countries must list every Country"
144);
145
147inline constexpr std::string_view venue_name(Venue venue) noexcept {
148 switch (venue) {
149 case Venue::UNSPECIFIED:
150 return "UNSPECIFIED";
151 case Venue::KRX:
152 return "KRX";
153 case Venue::NXT:
154 return "NXT";
155 case Venue::COMBINED:
156 return "COMBINED";
157 }
158 return "";
159}
160
171inline std::optional<Venue> known_venue_from(std::string_view text) noexcept {
172 return value_named(all_venues, venue_name, text);
173}
174
175
177inline constexpr std::string_view side_name(OrderSide side) noexcept {
178 switch (side) {
179 case OrderSide::BUY:
180 return "BUY";
181 case OrderSide::SELL:
182 return "SELL";
183 }
184 return "";
185}
186
187static_assert(
188 lists_every_value(all_order_sides, [](OrderSide side) { return !side_name(side).empty(); }),
189 "all_order_sides must list every OrderSide"
190);
191
192
201inline constexpr std::string_view order_type_name(OrderType type) noexcept {
202 switch (type) {
203 case OrderType::LIMIT:
204 return "LIMIT";
206 return "MARKET";
208 return "CONDITIONAL_LIMIT";
210 return "BEST_LIMIT";
212 return "PRIMARY_LIMIT";
214 return "MID_PRICE";
216 return "STOP_LIMIT";
218 return "CLOSING_PRICE";
219 case OrderType::UNKNOWN:
220 return "UNKNOWN";
221 }
222 return "";
223}
224
225static_assert(
226 lists_every_value(all_order_types, [](OrderType type) { return !order_type_name(type).empty(); }),
227 "all_order_types must list every OrderType"
228);
229
230
232inline constexpr std::string_view time_in_force_name(TimeInForce condition) noexcept {
233 switch (condition) {
234 case TimeInForce::DAY:
235 return "DAY";
237 return "IOC";
239 return "FOK";
240 case TimeInForce::UNKNOWN:
241 return "UNKNOWN";
242 }
243 return "";
244}
245
246static_assert(
247 lists_every_value(all_times_in_force, [](TimeInForce condition) { return !time_in_force_name(condition).empty(); }),
248 "all_times_in_force must list every TimeInForce"
249);
250
252inline constexpr std::string_view order_session_name(OrderSession session) noexcept {
253 switch (session) {
255 return "REGULAR";
257 return "OPENING_AUCTION";
259 return "CLOSING_AUCTION";
261 return "PRE_MARKET";
263 return "AFTER_HOURS";
265 return "AFTER_HOURS_AUCTION";
267 return "AFTER_MARKET";
269 return "GTP";
270 case OrderSession::UNKNOWN:
271 return "UNKNOWN";
272 }
273 return "";
274}
275
276static_assert(
277 lists_every_value(all_order_sessions, [](OrderSession session) { return !order_session_name(session).empty(); }),
278 "all_order_sessions must list every OrderSession"
279);
280
281
314inline std::optional<Decimal64> known_decimal_from(std::string_view text) noexcept {
315 try {
316 return Decimal64::parse(text);
317 } catch (const std::exception&) {
318 return std::nullopt;
319 }
320}
321
322inline std::optional<Country> known_country_from(std::string_view text) noexcept {
323 return value_named(all_countries, country_name, text);
324}
325
326inline std::optional<OrderSide> known_side_from(std::string_view text) noexcept {
327 return value_named(all_order_sides, side_name, text);
328}
329
330inline std::optional<OrderType> known_order_type_from(std::string_view text) noexcept {
331 return value_named(all_order_types, order_type_name, text);
332}
333
334inline std::optional<TimeInForce> known_time_in_force_from(std::string_view text) noexcept {
335 return value_named(all_times_in_force, time_in_force_name, text);
336}
337
338inline std::optional<OrderSession> known_order_session_from(std::string_view text) noexcept {
339 return value_named(all_order_sessions, order_session_name, text);
340}
341
342
344inline std::chrono::system_clock::time_point instant_from(std::int64_t micros) noexcept {
345 return std::chrono::system_clock::time_point{} + std::chrono::microseconds(micros);
346}
347
349inline std::int64_t micros_of(std::chrono::system_clock::time_point value) noexcept {
350 return std::chrono::duration_cast<std::chrono::microseconds>(value.time_since_epoch()).count();
351}
352
365inline std::optional<std::string> salvage_string(std::string_view line, std::string_view key) {
366 std::string needle;
367 needle.reserve(key.size() + 4);
368 needle += '"';
369 needle.append(key);
370 needle += "\":\"";
371 const auto at = line.find(needle);
372 if (at == std::string_view::npos) {
373 return std::nullopt;
374 }
375 const auto begin = at + needle.size();
376 // **The closing quote is the first one not escaped**, not the first one. A value holding `\"`
377 // was cut short at the escape, and a symbol cut short is another symbol's key.
378 auto end = begin;
379 while (end < line.size() && line[end] != '"') {
380 end += line[end] == '\\' ? 2 : 1;
381 }
382 if (end >= line.size()) {
383 // The value itself was cut off mid-write. Reporting what is there would invent a symbol.
384 return std::nullopt;
385 }
386 // Decoded as the JSON string it is, so an escape reads as the character it stands for. A value
387 // whose escapes do not decode is not salvaged, for the same reason a cut-off one is not.
388 boost::system::error_code error;
389 const auto quoted = json::parse(line.substr(begin - 1, end - begin + 2), error);
390 if (error || !quoted.is_string()) {
391 return std::nullopt;
392 }
393 return std::string(quoted.as_string());
394}
395
404inline std::uint32_t line_checksum(std::string_view bytes) {
405 boost::crc_32_type crc;
406 crc.process_bytes(bytes.data(), bytes.size());
407 return crc.checksum();
408}
409
411inline std::string checksum_text(std::uint32_t value) {
412 static constexpr char digits[] = "0123456789abcdef";
413 std::string text(8, '0');
414 for (int i = 7; i >= 0; --i) {
415 text[static_cast<std::size_t>(i)] = digits[value & 0xFu];
416 value >>= 4;
417 }
418 return text;
419}
420
422inline constexpr std::string_view kChecksumMember = ",\"crc\":\"";
423
425inline constexpr std::size_t kChecksumSuffixSize = kChecksumMember.size() + 8 + 2;
426
428inline bool ends_in_checksum(std::string_view line) {
429 if (line.size() <= kChecksumSuffixSize) {
430 return false;
431 }
432 const auto suffix = line.substr(line.size() - kChecksumSuffixSize);
433 if (suffix.substr(0, kChecksumMember.size()) != kChecksumMember ||
434 suffix.substr(kChecksumSuffixSize - 2) != "\"}") {
435 return false;
436 }
437 for (const char digit : suffix.substr(kChecksumMember.size(), 8)) {
438 if (!((digit >= '0' && digit <= '9') || (digit >= 'a' && digit <= 'f'))) {
439 return false;
440 }
441 }
442 return true;
443}
444
451inline bool checksum_matches(std::string_view line) {
452 std::string body(line.substr(0, line.size() - kChecksumSuffixSize));
453 body += '}';
454 const auto stated = line.substr(line.size() - kChecksumSuffixSize + kChecksumMember.size(), 8);
455 return checksum_text(line_checksum(body)) == stated;
456}
457
467inline bool known_entry_key(std::string_view key) {
468 static constexpr std::string_view known[] = {
469 "id", "action", "outcome", "symbol", "country", "venue", "side", "type", "time_in_force",
470 "session", "route", "quantity", "price", "trigger", "at", "at_text", "order_id", "detail",
471 "requires_operator", "scope", "crc", "baseline",
472 };
473 for (const auto candidate : known) {
474 if (candidate == key) {
475 return true;
476 }
477 }
478 return false;
479}
480
481} // namespace journal_detail
482
487inline constexpr std::string_view journal_format_name() noexcept {
488 return "algotradelib-order-journal";
489}
490
496inline std::string journal_header_line(std::string_view scope) {
497 json::object row;
498 row["journal"] = std::string(journal_format_name());
499 row["version"] = 1;
500 row["scope"] = std::string(scope);
501 // Declares that every entry after the header carries a checksum (older readers ignore the key).
502 row["integrity"] = "crc32";
503 return json::serialize(row);
504}
505
511inline bool journal_header_declares_checksums(std::string_view line) {
512 boost::system::error_code error;
513 const auto value = json::parse(line, error);
514 if (error || !value.is_object()) {
515 return false;
516 }
517 const auto* integrity = value.as_object().if_contains("integrity");
518 return integrity != nullptr && integrity->is_string() &&
519 std::string_view(integrity->as_string()) == "crc32";
520}
521
527inline std::optional<std::string> journal_header_scope(std::string_view line) {
528 boost::system::error_code error;
529 const auto value = json::parse(line, error);
530 if (error || !value.is_object()) {
531 return std::nullopt;
532 }
533 const auto& row = value.as_object();
534 const auto marker = row.if_contains("journal");
535 if (marker == nullptr || !marker->is_string() ||
536 std::string_view(marker->as_string()) != journal_format_name()) {
537 return std::nullopt;
538 }
539 return journal_detail::string_field(row, "scope");
540}
541
551
557inline std::string journal_entry_to_line(const JournalEntry& entry) {
558 json::object row;
559 // Ordered so that the fields a damaged line most needs to yield come first. A truncated write
560 // keeps its prefix, so putting the instrument ahead of the detail text is what makes salvage
561 // work in practice rather than only in principle.
562 row["id"] = entry.id_;
563 row["action"] = std::string(journal_action_name(entry.action_));
564 row["outcome"] = std::string(journal_outcome_name(entry.outcome_));
565 row["country"] = std::string(journal_detail::country_name(entry.instrument_.country_));
566 row["venue"] = std::string(journal_detail::venue_name(entry.instrument_.venue_));
567 row["symbol"] = entry.instrument_.symbol_;
568 row["side"] = std::string(journal_detail::side_name(entry.side_));
569 row["type"] = std::string(journal_detail::order_type_name(entry.type_));
570 row["time_in_force"] = std::string(journal_detail::time_in_force_name(entry.time_in_force_));
571 row["session"] = std::string(journal_detail::order_session_name(entry.session_));
572 // Distinct from `venue`, which is the instrument's identity venue. This is
573 // where the order actually goes -- the binding resolves a request that named
574 // no venue into the one the broker's default reaches -- and a line that omits
575 // the key entirely is
576 // one written before the field existed -- which is not the same as one that
577 // asked for no particular venue, and recovery treats the two differently.
578 if (entry.venue_) {
579 row["route"] = std::string(journal_detail::venue_name(*entry.venue_));
580 }
581 row["quantity"] = entry.quantity_.str();
582 if (entry.price_) {
583 row["price"] = entry.price_->str();
584 }
585 // Only when present, so a line that is not a stop carries no `trigger` key and every old journal
586 // reads unchanged. (Every new line ends in a `crc` member, added below; the fields before it are
587 // what an earlier writer produced.)
588 if (entry.trigger_price_) {
589 row["trigger"] = entry.trigger_price_->str();
590 }
591 // Only on a serialized runner's lines, so an overlapping runner's journal reads as before.
592 if (entry.baseline_) {
593 row["baseline"] = entry.baseline_->str();
594 }
595 // The authoritative instant, exact on every platform. `at_text` beside it is for the operator
596 // reading the file with `tail`, is only second-resolution, and is never read back: one field is
597 // the record and the other is the caption.
598 row["at"] = journal_detail::micros_of(entry.recorded_at_);
599 row["at_text"] = format_iso8601_utc(entry.recorded_at_);
600 if (!entry.order_id_.empty()) {
601 row["order_id"] = entry.order_id_;
602 }
603 if (!entry.detail_.empty()) {
604 row["detail"] = entry.detail_;
605 }
606 if (entry.requires_operator_) {
607 row["requires_operator"] = true;
608 }
609 if (!entry.scope_.empty()) {
610 row["scope"] = entry.scope_;
611 }
612 // **Last, and over the line as it reads without it** -- the whole object, closing brace included,
613 // which the member then replaces. See `journal_detail::line_checksum` for what it detects
614 // and `journal_entry_from_line` for what a line that fails it becomes.
615 auto line = json::serialize(row);
616 const auto checksum = journal_detail::checksum_text(journal_detail::line_checksum(line));
617 line.pop_back();
618 line.append(journal_detail::kChecksumMember);
619 line += checksum;
620 line += "\"}";
621 return line;
622}
623
632inline JournalEntry unattributable_journal_entry(std::string_view line) {
633 JournalEntry entry;
634 entry.id_ = "unknown~truncated";
636 entry.requires_operator_ = true;
637 entry.detail_ = "a truncated journal line named no instrument; something was in flight: " +
638 std::string(line.substr(0, line.size() < 120 ? line.size() : 120));
639 return entry;
640}
641
652inline std::optional<JournalEntry> journal_entry_from_line(std::string_view line) {
653 boost::system::error_code error;
654 const auto value = json::parse(line, error);
655 if (error || !value.is_object()) {
656 return std::nullopt;
657 }
658 const auto& row = value.as_object();
659
660 const auto action = journal_action_from(journal_detail::string_field(row, "action"));
661 const auto outcome = journal_outcome_from(journal_detail::string_field(row, "outcome"));
662 // An operator-only line is exempt from the bound below, and deliberately: a
663 // salvaged fragment records the epoch precisely *because* it has no
664 // timestamp, and `requires_operator_` is what stops that epoch window
665 // resolving it. Repairing such a line writes it back, so rejecting the epoch
666 // here would make every repair unreadable on the next open. The bound is for
667 // a line that claims to be ordinary and carries an instant it cannot have.
668 const auto operator_only = row.if_contains("requires_operator");
669 const bool marked = operator_only != nullptr && operator_only->is_bool() &&
670 operator_only->as_bool();
671
672 const auto at = row.if_contains("at");
673 if (!action || !outcome || at == nullptr || !at->is_int64() ||
674 (!marked && !journal_detail::plausible_instant(at->as_int64()))) {
675 // A well-formed JSON object that is not a journal line. Treated as damage rather than as an
676 // empty entry, because an entry with a default instrument would be a claim about an order.
677 return std::nullopt;
678 }
679
680 // **A present value this reader cannot parse is damage, not a default.**
681 // Each of these has a documented default for an *absent* key -- a line that
682 // predates the field -- and applying that default to a token the line does
683 // carry turns damage into evidence: a damaged `"SELL"` reads as a buy, a
684 // damaged `"IOC"` as a day order, and a damaged `"COMBINED"` moves the entry
685 // to an instrument the runner is not trading, so the ledger marks the wrong
686 // key and the right one is never withheld at all.
687 //
688 // An unreadable value therefore produces an entry with **no instrument**,
689 // which `unattributable_journal_entry` documents and recovery already turns
690 // into a global halt. Marking a guessed instrument would not protect the real
691 // one, because they are different keys.
692 // Each field is asked two questions, because they have different answers.
693 // *Is the key there?* -- if not, the line predates the field and its
694 // documented default is a fact about such a line. *Is what it holds
695 // readable?* -- if not, the line says something this release cannot use, and
696 // guessing turns damage into evidence. `string_field` answers neither: it
697 // flattens absent, non-string and empty into the same `""`.
698 const auto read = [&row](std::string_view key) {
699 return journal_detail::readable_string(row, key);
700 };
701 // Present but unusable, for any reason: not a string, an empty string, or a
702 // token the reader could not parse. The first version of this asked only
703 // whether the *string* was readable, which let `"IOC"` damaged to `"IO?"`
704 // through -- that is a perfectly good string, and a perfectly bad condition.
705 const auto unusable = [&row](std::string_view key, bool parsed) {
706 return journal_detail::has_field(row, key) && !parsed;
707 };
708
709 // `std::optional::and_then` is C++23 and this library is C++17.
710 const auto parsed = [&read](std::string_view key, auto reader) {
711 using Result = decltype(reader(std::string_view{}));
712 const auto text = read(key);
713 return text ? reader(*text) : Result{};
714 };
715
716 // A `crc` member anywhere but last, or not eight lowercase hex digits, is not this writer's; one
717 // that is must match the line without it -- the object with its own closing brace. See
718 // `journal_detail::line_checksum`.
719 const bool checksum_holds = !journal_detail::has_field(row, "crc") ||
720 (journal_detail::ends_in_checksum(line) && journal_detail::checksum_matches(line));
721 bool unknown_key = false;
722 for (const auto& member : row) {
723 if (!journal_detail::known_entry_key(member.key())) {
724 unknown_key = true;
725 }
726 }
727 // **A line whose bytes changed is not read at all**, rather than read as a damaged entry. A damaged
728 // entry carries its own outcome and an id suffixed `~damaged`, which is right for a line whose
729 // *value* no longer parses -- but a damaged *terminal* line with an intact pending partner then
730 // pairs with nothing, is dropped as an orphan, and the pending line stays open as an ordinary one:
731 // the change was detected and then discarded before recovery decided anything. An unreadable line
732 // is counted by `parse_journal_bytes` and halts recovery outright, whatever it was.
733 if (!checksum_holds || unknown_key) {
734 return std::nullopt;
735 }
736
737 const auto symbol = read("symbol");
738 const auto country = parsed("country", journal_detail::known_country_from);
739 const auto venue = parsed("venue", journal_detail::known_venue_from);
740 const auto side = parsed("side", journal_detail::known_side_from);
741 const auto type = parsed("type", journal_detail::known_order_type_from);
742 const auto condition = parsed("time_in_force", journal_detail::known_time_in_force_from);
743 const auto session = parsed("session", journal_detail::known_order_session_from);
744 const auto quantity = parsed("quantity", journal_detail::known_decimal_from);
745 const auto price = parsed("price", journal_detail::known_decimal_from);
746 const auto trigger = parsed("trigger", journal_detail::known_decimal_from);
747 const auto baseline = parsed("baseline", journal_detail::known_decimal_from);
748
749 const bool damaged =
750 // Identity and the fields every complete line carries: required outright.
751 // A line this reader cannot attribute to an instrument must not be
752 // attributed to a guess, and `quantity` at a silent zero is a claim about
753 // an order's size.
754 !symbol || !country || !venue || !side || !type || !quantity ||
755 // Legacy-defaulted fields: absent is fine, present and unreadable is not.
756 unusable("time_in_force", condition.has_value()) ||
757 unusable("session", session.has_value()) ||
758 // A price is absent for a market order and required for a basis that
759 // carries one, so its presence is judged against the basis rather than
760 // assumed either way.
761 unusable("price", price.has_value()) ||
762 // The same rule for a stop's trigger: absent on every other order and on every line written
763 // before the field existed, and a present value that does not parse is damage.
764 unusable("trigger", trigger.has_value()) ||
765 unusable("baseline", baseline.has_value()) ||
766 // Checked above, where a changed line becomes unreadable rather than damaged; restated so the
767 // condition reads whole.
768 !checksum_holds ||
769 unknown_key;
770 // **A price missing from a priced basis, and a cancellation with no target
771 // id, are deliberately *not* damage here.** Both are lines this library's own
772 // writer emits: `JournalingExecutor` appends the pending line *before* the
773 // executor runs, so an order the binding then refuses locally -- a limit with
774 // no price, a cancellation of a reservation whose broker id is not known --
775 // has already been written by the time anyone could object. Rejecting them
776 // turned an ordinary local refusal into a global halt on the next restart,
777 // which is worse than the hazard the rejection was meant to close.
778 //
779 // The hazard is real and it lives one step later: such a line must not be
780 // *resolved* on evidence it does not have. `runner::match_against` refuses to
781 // conclude anything from one, which leaves it withheld for an operator while
782 // its own terminal line still closes it when the refusal was ordinary.
783 if (damaged) {
784 auto unattributable = unattributable_journal_entry(line);
785 const auto id = journal_detail::string_field(row, "id");
786 unattributable.id_ = (id.empty() ? std::string("unknown") : id) + "~damaged";
787 unattributable.action_ = *action;
788 // Its own outcome, not `PENDING`. Both lines of a pair carry the same id, so
789 // a damaged *terminal* line still closes its damaged pending partner --
790 // forcing `PENDING` left both open and halted a runner over an instruction
791 // that had already resolved.
792 unattributable.outcome_ = *outcome;
793 unattributable.recorded_at_ = journal_detail::instant_from(at->as_int64());
794 unattributable.detail_ =
795 "a journal line carries a value this release cannot read, so the order it describes cannot "
796 "be identified; nothing may be traded until an operator resolves it: " +
797 std::string(line.substr(0, line.size() < 160 ? line.size() : 160));
798 return unattributable;
799 }
800
801 JournalEntry entry;
802 entry.id_ = journal_detail::string_field(row, "id");
803 entry.action_ = *action;
804 entry.outcome_ = *outcome;
805 entry.instrument_ = InstrumentId{*country, *venue, *symbol};
806 entry.side_ = *side;
807 entry.type_ = *type;
808 // These two keep their documented defaults for an *absent* key only; a
809 // present value that failed to parse was caught above.
810 entry.time_in_force_ = condition ? *condition : TimeInForce::DAY;
811 entry.session_ = session ? *session : OrderSession::REGULAR;
812 // Absent key -> absent value, which is "this line predates the field" rather
813 // than "no route was asked for". Recovery will not attribute a row to an
814 // instruction whose route nobody recorded. The field defaults to a recorded
815 // `UNSPECIFIED`, so this clears it rather than leaving the default standing.
816 // An absent key means the line predates the field. An unrecognized value means
817 // the line says something this reader cannot use. Both are "unknown", and
818 // neither may become the `UNSPECIFIED` that recovery treats as a recorded
819 // request for no venue.
820 entry.venue_ = journal_detail::known_venue_from(journal_detail::string_field(row, "route"));
821 // Damaged numbers land in the same place as damaged enumerations, for the
822 // same reason -- and they used to land somewhere far worse, throwing out of
823 // recovery entirely. See `known_decimal_from`.
824 entry.quantity_ = *quantity;
825 if (price) {
826 entry.price_ = *price;
827 }
828 if (trigger) {
829 entry.trigger_price_ = *trigger;
830 }
831 entry.baseline_ = baseline;
832 entry.recorded_at_ = journal_detail::instant_from(at->as_int64());
833 entry.order_id_ = journal_detail::string_field(row, "order_id");
834 entry.detail_ = journal_detail::string_field(row, "detail");
835 entry.requires_operator_ = marked;
836 entry.scope_ = journal_detail::string_field(row, "scope");
837 return entry;
838}
839
850inline bool salvage_damaged(std::string_view line, std::string_view key) {
851 const std::string needle = "\"" + std::string(key) + "\":";
852 const auto at = line.find(needle);
853 if (at == std::string_view::npos) {
854 return false;
855 }
856 return line.find(',', at + needle.size()) != std::string_view::npos;
857}
858
870inline std::optional<JournalEntry> salvage_journal_line(std::string_view line) {
871 const auto symbol = journal_detail::salvage_string(line, "symbol");
872 if (!symbol || symbol->empty()) {
873 return std::nullopt;
874 }
875 JournalEntry entry;
876 // The flag that makes the promise in this comment block true. Without it the entry's missing
877 // timestamp reads as the epoch, every window test passes immediately, no real row falls inside
878 // the window, and the first sweep resolves it `NOT_PLACED`.
879 entry.requires_operator_ = true;
880 const auto id = journal_detail::salvage_string(line, "id");
881 entry.id_ = (id ? *id : std::string("unknown")) + "~truncated";
882 entry.action_ =
883 journal_action_from(journal_detail::salvage_string(line, "action").value_or("place"))
884 .value_or(JournalAction::PLACE);
886 // **Absent is a default; present and unreadable is damage** -- the same rule
887 // the strict reader follows, and for the same reason. A truncated line is
888 // missing its *tail*, so an absent field is expected and its default is a fact
889 // about what the line could have said. A field that is there and cannot be
890 // read is corruption in the middle, and guessing it hands this entry a
891 // different instrument than the order had: `requires_operator_` would then
892 // withhold a key the runner is not trading under, while the real one stays
893 // open. Such a line becomes unattributable instead, carrying no instrument at
894 // all, which recovery turns into a global halt.
895 // A key the truncation removed takes its default; a key that is *there* and
896 // unreadable is damage, and `salvage_string` cannot tell those apart on its
897 // own -- it recognizes `"key":"value"` and nothing else, so `null` and `7`
898 // both read as absent.
899 const auto salvaged = [&line](std::string_view key, const char* fallback) {
900 const auto text = journal_detail::salvage_string(line, key);
901 if (text) {
902 return std::optional<std::string>(*text);
903 }
904 return salvage_damaged(line, key) ? std::optional<std::string>()
905 : std::optional<std::string>(fallback);
906 };
907 const auto parsed = [&salvaged](std::string_view key, const char* fallback, auto reader) {
908 using Result = decltype(reader(std::string_view{}));
909 const auto text = salvaged(key, fallback);
910 return text ? reader(*text) : Result{};
911 };
912
913 const auto country = parsed("country", "KR", journal_detail::known_country_from);
914 const auto venue = parsed("venue", "UNSPECIFIED", journal_detail::known_venue_from);
915 const auto side = parsed("side", "BUY", journal_detail::known_side_from);
916 const auto type = parsed("type", "UNKNOWN", journal_detail::known_order_type_from);
917 const auto condition =
918 parsed("time_in_force", "DAY", journal_detail::known_time_in_force_from);
919 const auto session = parsed("session", "REGULAR", journal_detail::known_order_session_from);
920 if (!country || !venue || !side || !type || !condition || !session) {
921 auto unattributable = unattributable_journal_entry(line);
922 unattributable.id_ = entry.id_;
923 unattributable.detail_ =
924 "a truncated journal line carries a value this release cannot read, so the order it "
925 "describes cannot be identified; nothing may be traded until an operator resolves it: " +
926 std::string(line.substr(0, line.size() < 160 ? line.size() : 160));
927 return unattributable;
928 }
929 entry.instrument_ = InstrumentId{*country, *venue, *symbol};
930 entry.side_ = *side;
931 entry.type_ = *type;
932 entry.time_in_force_ = *condition;
933 entry.session_ = *session;
934 entry.venue_ = journal_detail::known_venue_from(
935 journal_detail::salvage_string(line, "route").value_or("")
936 );
937 if (const auto quantity = journal_detail::salvage_string(line, "quantity")) {
938 // A truncated line's last field is routinely half-written, so an unparseable
939 // number here is expected rather than exceptional -- and it must not throw
940 // out of a salvage path that exists to survive exactly this.
941 if (const auto value = journal_detail::known_decimal_from(*quantity)) {
942 entry.quantity_ = *value;
943 }
944 }
945 if (const auto price = journal_detail::salvage_string(line, "price")) {
946 if (const auto value = journal_detail::known_decimal_from(*price)) {
947 entry.price_ = *value;
948 }
949 }
950 if (const auto trigger = journal_detail::salvage_string(line, "trigger")) {
951 if (const auto value = journal_detail::known_decimal_from(*trigger)) {
952 entry.trigger_price_ = *value;
953 }
954 }
955 entry.detail_ = "recovered from a truncated journal line; only an operator can resolve it";
956 return entry;
957}
958
959
966inline JournalBytes read_journal_bytes(const std::string& path) {
967 JournalBytes bytes;
968 errno = 0;
969 std::unique_ptr<std::FILE, int (*)(std::FILE*)> handle(
970 std::fopen(path.c_str(), "rb"), [](std::FILE* file) { return file ? std::fclose(file) : 0; }
971 );
972 if (handle == nullptr) {
973 if (errno == ENOENT) {
975 return bytes;
976 }
977 // Every other reason -- a permission the process does not have, a directory where a file was
978 // expected, a device error -- means the file exists and its contents are unknown. Reporting
979 // that as "no journal" is how a process claims a path holding its predecessor's in-flight
980 // orders and then trades on an empty ledger.
982 bytes.detail_ = "the order journal at " + path + " exists and could not be opened for reading";
983 return bytes;
984 }
985
986 char buffer[4096];
987 while (const auto read = std::fread(buffer, 1, sizeof(buffer), handle.get())) {
988 bytes.text_.append(buffer, read);
989 }
990 if (std::ferror(handle.get()) != 0) {
991 // A read that stopped partway looks exactly like an ordinary end of file to the loop above, so
992 // the truncated tail it produces would be a fiction about where the journal ends.
994 bytes.detail_ = "the order journal at " + path + " could not be read to the end";
995 bytes.text_.clear();
996 return bytes;
997 }
999 return bytes;
1000}
1001
1014 JournalReplay replay;
1015 replay.status_ = bytes.status_;
1016 if (bytes.status_ != JournalReadStatus::READ) {
1017 replay.detail_ = bytes.detail_;
1018 return replay;
1019 }
1020
1021 const auto& text = bytes.text_;
1022 std::size_t begin = 0;
1023 // Set by the first checksummed entry line; see the check below.
1024 bool seen_checksum = false;
1025 while (begin < text.size()) {
1026 const auto end = text.find('\n', begin);
1027 if (end == std::string::npos) {
1028 // Everything after the last newline is a write that never finished. This is the whole reason
1029 // the format is line-oriented, and dropping it silently would defeat the feature.
1030 replay.truncated_tail_ = true;
1031 const std::string_view tail(text.data() + begin, text.size() - begin);
1032 if (auto salvaged = salvage_journal_line(tail)) {
1033 replay.detail_ = "the final journal line was truncated; it named " +
1034 salvaged->instrument_.symbol_ + " and is treated as unresolved";
1035 replay.entries_.push_back(std::move(*salvaged));
1036 } else {
1037 replay.detail_ =
1038 "the final journal line was truncated and named no instrument; something was in flight";
1039 // The same entry a writer's repair appends for this tail, so recovery halts on it whether or
1040 // not the file was repaired, and whatever else is open beside it.
1041 replay.entries_.push_back(unattributable_journal_entry(tail));
1042 }
1043 break;
1044 }
1045 const std::string_view line(text.data() + begin, end - begin);
1046 begin = end + 1;
1047 // **An empty line, or a header anywhere but first, is damage.** This writer produces neither: every
1048 // append is one non-empty line and its newline, and the header is written once, before anything
1049 // else. Skipping them was how an entry overwritten by a bare newline, or by a copy of the header,
1050 // vanished from recovery with nothing counted -- a changed line removed rather than detected.
1051 const auto unreadable = [&replay](const char* why) {
1052 ++replay.unreadable_lines_;
1053 if (replay.detail_.empty()) {
1054 replay.detail_ = why;
1055 }
1056 };
1057 if (line.empty()) {
1058 unreadable("the journal contains an empty line, which this library never writes");
1059 continue;
1060 }
1061 if (auto scope = journal_header_scope(line)) {
1062 if (replay.has_header_ || !replay.entries_.empty() || replay.unreadable_lines_ > 0) {
1063 unreadable("the journal contains a header after its first line, which this library never "
1064 "writes");
1065 continue;
1066 }
1067 // Not an instruction and not damage. Recorded so an opener can check ownership before it
1068 // writes anything at all.
1069 replay.has_header_ = true;
1070 replay.scope_ = std::move(*scope);
1071 // A header that declares checksums protects every entry after it, the first one included.
1072 seen_checksum = seen_checksum || journal_header_declares_checksums(line);
1073 continue;
1074 }
1075 const bool checksummed = journal_detail::ends_in_checksum(line);
1076 if (seen_checksum && !checksummed) {
1077 // **This writer never puts a checksum-less line after a checksummed one, or after a header that
1078 // declares checksums**, so this is a protected line that lost its checksum -- damage that removed
1079 // the whole member, which the line's own reader cannot tell from a line written before checksums
1080 // existed. Only the first checksummed line of a file begun by an earlier release is left without
1081 // this protection; see `journal_header_line`.
1082 ++replay.unreadable_lines_;
1083 if (replay.detail_.empty()) {
1084 replay.detail_ =
1085 "a journal line without a checksum follows a checksummed one, so it was changed after it "
1086 "was written";
1087 }
1088 continue;
1089 }
1090 seen_checksum = seen_checksum || checksummed;
1091 if (auto entry = journal_entry_from_line(line)) {
1092 replay.entries_.push_back(std::move(*entry));
1093 } else {
1094 // A complete line that is not a journal entry. Not expected from this writer, so it means
1095 // the file was edited or written by something else, or a repair was interrupted; counted
1096 // rather than ignored, and recovery halts on it.
1097 ++replay.unreadable_lines_;
1098 if (replay.detail_.empty()) {
1099 replay.detail_ = "the journal contains a line this library did not write";
1100 }
1101 }
1102 }
1103 return replay;
1104}
1105
1111inline JournalReplay read_journal_file(const std::string& path) {
1113}
1114
1133public:
1143 FileJournal(std::string path, std::string scope)
1144 : path_(std::move(path)), scope_(std::move(scope)), handle_(nullptr, &closer) {
1145 if (scope_.empty()) {
1146 // An unnamed journal could not tell another account's file from its own.
1147 rejection_ = "an order journal must name the broker and account it belongs to";
1148 return;
1149 }
1150#if defined(_WIN32)
1151 if (!claim_file()) {
1152 return;
1153 }
1154 repair_torn_tail();
1155 handle_.reset(std::fopen(path_.c_str(), "ab"));
1156#else
1157 // Locked before anything reads or repairs the file, and on the descriptor that becomes the
1158 // write handle: the lock lives exactly as long as `open()` is true, every refusal below closes
1159 // it, and moving the journal moves it with the handle.
1160 const int descriptor = ::open(path_.c_str(), O_WRONLY | O_APPEND | O_CREAT | O_CLOEXEC, 0666);
1161 if (descriptor < 0) {
1162 const int error = errno;
1163 rejection_ = "the journal at " + path_ + " could not be opened (" + std::strerror(error) + ")";
1164 return;
1165 }
1166 if (!lock(descriptor)) {
1167 ::close(descriptor);
1168 return;
1169 }
1170 try {
1171 if (!claim_file()) {
1172 ::close(descriptor);
1173 return;
1174 }
1175 repair_torn_tail();
1176 } catch (...) {
1177 // A held lock outliving a failed open would refuse this program's own retry.
1178 ::close(descriptor);
1179 throw;
1180 }
1181 handle_.reset(::fdopen(descriptor, "ab"));
1182 if (!open()) {
1183 const int error = errno;
1184 rejection_ =
1185 "the journal at " + path_ + " could not be opened for appending (" + std::strerror(error) + ")";
1186 ::close(descriptor);
1187 }
1188#endif
1189 if (!open()) {
1190 return;
1191 }
1192 if (needs_header_ && !write_header()) {
1193 return;
1194 }
1195 if (!pending_repair_.empty()) {
1196 finish_repair();
1197 }
1198 }
1199
1204 bool open() const noexcept { return handle_ != nullptr; }
1205
1211 const std::string& repaired() const noexcept { return repaired_; }
1212
1217 const std::string& path() const noexcept { return path_; }
1218
1223 const std::string& scope() const noexcept { return scope_; }
1224
1234 const std::string& rejection() const noexcept { return rejection_; }
1235
1244 bool append(const JournalEntry& entry) {
1245 if (!open()) {
1246 return false;
1247 }
1248 auto stamped = entry;
1249 // Overwritten: the journal owns the scope.
1250 stamped.scope_ = scope_;
1251 auto line = journal_entry_to_line(stamped);
1252 line += '\n';
1253 const auto written = std::fwrite(line.data(), 1, line.size(), handle_.get());
1254 if (written != line.size()) {
1255 return false;
1256 }
1257 return journal_detail::flush_to_disk(handle_.get());
1258 }
1259
1264 JournalReplay replay() const { return read_journal_file(path_); }
1265
1266private:
1267 // Returns whether this journal may write to the file, deciding ownership from the header alone.
1268 //
1269 // **The header, not the records, and that distinction is the fix.** Reading ownership from the
1270 // records leaves a hole exactly where it matters most: the scope is one field among many on a
1271 // record, so a process killed partway through writing the *first* instruction leaves a file whose
1272 // only line is torn and unscoped. Another account could then open the path, repair that line into
1273 // its own journal, and reconcile the first account's instruction against its own rows.
1274 //
1275 // A header is written before any instruction is contemplated, so it is complete or the file is
1276 // empty. Three cases, and the third is the strict one:
1277 //
1278 // - **Empty or absent.** Ours to claim; the header is written before the first append.
1279 // - **Header present.** The scopes must agree.
1280 // - **Non-empty with no header.** Refused. Something else wrote this file, or it predates the
1281 // format, and either way its ownership cannot be established -- which is not a question to
1282 // guess at while holding a file that may name another account's live orders.
1283 bool claim_file() {
1284 const auto replay = read_journal_file(path_);
1285 if (!replay.readable()) {
1286 // Opening for *append* can succeed on a file that cannot be read -- a write-only file is the
1287 // plain case -- so without this the journal would claim a path whose contents it has never
1288 // seen, write a header into it, and report nothing owed.
1289 rejection_ = replay.detail_.empty() ?
1290 "the journal at " + path_ + " exists and cannot be read" :
1292 return false;
1293 }
1294 const bool empty = !replay.has_header_ && replay.entries_.empty() &&
1296 if (empty) {
1297 needs_header_ = true;
1298 return true;
1299 }
1300 if (!replay.has_header_) {
1301 rejection_ = "the journal at " + path_ +
1302 " has no header, so the account it belongs to cannot be established";
1303 return false;
1304 }
1305 if (replay.scope_ != scope_) {
1306 rejection_ = "the journal at " + path_ + " belongs to " + replay.scope_ + ", not to " +
1307 scope_;
1308 return false;
1309 }
1310 return true;
1311 }
1312
1313 // Writes and flushes the header, before any instruction can be recorded.
1314 bool write_header() {
1315 auto line = journal_header_line(scope_);
1316 line += '\n';
1317 const auto written = std::fwrite(line.data(), 1, line.size(), handle_.get());
1318 if (written != line.size() || !journal_detail::flush_to_disk(handle_.get())) {
1319 handle_.reset();
1320 rejection_ = "the journal header at " + path_ + " could not be written";
1321 return false;
1322 }
1323 needs_header_ = false;
1324 return true;
1325 }
1326
1327 // Closes a stream, tolerating the null a failed open leaves behind.
1328 static void closer(std::FILE* handle) noexcept {
1329 if (handle != nullptr) {
1330 std::fclose(handle);
1331 }
1332 }
1333
1334 // Reads the file and works out what a torn final line needs, without writing anything yet.
1335 //
1336 // Split from the writing half because the file must be inspected *before* it is opened for
1337 // appending: once a stream is in append mode, everything written goes to the end, which is
1338 // precisely what must not happen until the fragment is terminated.
1339 void repair_torn_tail() {
1340 const auto replay = read_journal_file(path_);
1341 if (!replay.truncated_tail_) {
1342 return;
1343 }
1344 const auto tail = read_torn_tail(path_);
1345 auto salvaged = salvage_journal_line(tail);
1346 pending_repair_ = salvaged ?
1347 journal_entry_to_line(*salvaged) :
1349 repaired_ = salvaged ?
1350 "a torn final line was terminated and salvaged as " + salvaged->instrument_.symbol_ :
1351 "a torn final line was terminated; it named no instrument";
1352 }
1353
1354 // Terminates the fragment and appends the record that stands for it, durably.
1355 void finish_repair() {
1356 // The newline first and on its own: it is what turns the fragment into a complete -- if
1357 // unreadable -- line, so that the record written next is a line of its own rather than a
1358 // continuation of a half-written one.
1359 std::string repair = "\n";
1360 repair += pending_repair_;
1361 repair += '\n';
1362 const auto written = std::fwrite(repair.data(), 1, repair.size(), handle_.get());
1363 if (written != repair.size() || !journal_detail::flush_to_disk(handle_.get())) {
1364 // The repair itself failed, so the file is still torn. Refusing to be open is the only safe
1365 // answer: a caller that cannot journal must not trade, and `open()` is how it finds out.
1366 handle_.reset();
1367 repaired_ = "a torn final line could not be repaired";
1368 rejection_ = "the journal at " + path_ + " ends in a torn line that could not be repaired";
1369 return;
1370 }
1371 pending_repair_.clear();
1372 }
1373
1374#if !defined(_WIN32)
1375 // Takes the writer's lock on `descriptor`, or says in `rejection_` why it could not.
1376 bool lock(int descriptor) {
1377 if (::flock(descriptor, LOCK_EX | LOCK_NB) == 0) {
1378 return true;
1379 }
1380 const int error = errno;
1381 rejection_ = error == EWOULDBLOCK ?
1382 "the journal at " + path_ + " is already open for writing, in another process or another "
1383 "FileJournal (one journal, one writer)" :
1384 "the journal at " + path_ + " could not be locked (" + std::strerror(error) +
1385 "), so a second writer could not be refused";
1386 return false;
1387 }
1388#endif
1389
1390 // Returns the text after the file's last newline, which is the fragment a kill left behind.
1391 static std::string read_torn_tail(const std::string& path) {
1392 std::unique_ptr<std::FILE, void (*)(std::FILE*)> reader(
1393 std::fopen(path.c_str(), "rb"), &closer
1394 );
1395 if (reader == nullptr) {
1396 return {};
1397 }
1398 std::string text;
1399 char buffer[4096];
1400 while (const auto read = std::fread(buffer, 1, sizeof(buffer), reader.get())) {
1401 text.append(buffer, read);
1402 }
1403 const auto last = text.rfind('\n');
1404 return last == std::string::npos ? text : text.substr(last + 1);
1405 }
1406
1407 std::string path_;
1408 std::string scope_;
1409 std::unique_ptr<std::FILE, void (*)(std::FILE*)> handle_;
1410 std::string pending_repair_;
1411 std::string repaired_;
1412 std::string rejection_;
1413 bool needs_header_{false};
1414};
1415
1416} // namespace runner
1417} // namespace algotradelib
std::string str() const
Formats the value as a decimal string.
Definition types.hpp:411
static Decimal64 parse(std::string_view input)
Parses a decimal string exactly (no binary floating point).
Definition types.hpp:323
Either a value or a BrokerError.
Definition types.hpp:797
bool open() const noexcept
Tests whether the journal opened.
const std::string & repaired() const noexcept
Returns what opening had to repair: a torn last line is terminated and a salvaged entry written besid...
bool append(const JournalEntry &entry)
Appends one entry and flushes it to disk.
JournalReplay replay() const
Reads the whole journal back.
const std::string & scope() const noexcept
Returns the journal's broker and account.
FileJournal(std::string path, std::string scope)
Opens a journal file for appending, creating it if absent.
const std::string & path() const noexcept
Returns the file's path.
const std::string & rejection() const noexcept
Returns why the journal refused to open.
Runs a strategy: the runner, the polling loop, the order journal, and stopping.
Definition clock.hpp:7
std::optional< JournalOutcome > journal_outcome_from(std::string_view text) noexcept
Parses an outcome's name.
Definition journal.hpp:178
constexpr std::string_view journal_outcome_name(JournalOutcome outcome) noexcept
Returns an outcome's name as written to the journal ("pending", ...).
Definition journal.hpp:148
JournalBytes read_journal_bytes(const std::string &path)
Reads a journal file's bytes, without opening it for appending.
JournalReadStatus
Whether a journal file could be read.
Definition journal.hpp:433
@ MISSING
No such file: a first start, nothing owed.
Definition journal.hpp:435
@ READ
The file was read in full.
Definition journal.hpp:437
@ UNREADABLE
The file exists and could not be read; its contents are unknown.
Definition journal.hpp:439
std::optional< std::string > journal_header_scope(std::string_view line)
Returns the scope a header line declares.
JournalReplay read_journal_file(const std::string &path)
Reads and parses a journal file.
bool salvage_damaged(std::string_view line, std::string_view key)
Tests whether a journal line has a field that was written completely but cannot be read.
constexpr std::string_view journal_action_name(JournalAction action) noexcept
Returns an action's name as written to the journal ("place", "cancel").
Definition journal.hpp:45
constexpr std::string_view journal_format_name() noexcept
Returns the format name in a journal header's journal field.
@ PENDING
Written before the send; whether the instruction exists is not yet known.
Definition journal.hpp:95
std::string journal_header_line(std::string_view scope)
Renders the header line a journal file starts with, naming its broker and account.
std::optional< JournalAction > journal_action_from(std::string_view text) noexcept
Parses an action's name.
Definition journal.hpp:82
std::string journal_entry_to_line(const JournalEntry &entry)
Renders one entry as a journal line: a JSON object ending in a CRC-32 checksum member.
std::optional< JournalEntry > journal_entry_from_line(std::string_view line)
Parses one journal line.
JournalEntry unattributable_journal_entry(std::string_view line)
Makes the entry that stands for a torn line naming no instrument.
bool journal_header_declares_checksums(std::string_view line)
Tests whether a header line declares that every entry after it carries a checksum.
std::optional< JournalEntry > salvage_journal_line(std::string_view line)
Salvages a torn final line into an entry nothing can resolve automatically.
JournalReplay parse_journal_bytes(const JournalBytes &bytes)
Parses a journal's bytes, salvaging a torn final line.
The library: broker-neutral types (core), and the modules below.
constexpr std::array< Venue, 4 > all_venues
Every Venue, in declaration order.
Definition types.hpp:51
constexpr std::optional< Enum > value_named(const std::array< Enum, N > &values, Name name, std::string_view text) noexcept
Returns the value of a list whose name is text, so a parser reads exactly what its writer writes.
constexpr bool lists_every_value(const std::array< Enum, N > &values, IsEnumerator is_enumerator) noexcept
Tests whether a list holds every value of an enum exactly once, in any order.
OrderType
How an order's price is decided (its price basis).
Definition types.hpp:1707
@ LIMIT
지정가: fills at the stated price or better.
Definition types.hpp:1710
@ BEST_LIMIT
최유리지정가: priced at the best quote on the opposite side.
Definition types.hpp:1716
@ PRIMARY_LIMIT
최우선지정가: priced at the best quote on the order's own side.
Definition types.hpp:1718
@ CONDITIONAL_LIMIT
조건부지정가: a limit that becomes a market order at the close.
Definition types.hpp:1714
@ MID_PRICE
중간가: priced at the midpoint of the spread.
Definition types.hpp:1720
@ STOP_LIMIT
스톱지정가: a limit released when a trigger price is reached.
Definition types.hpp:1722
@ CLOSING_PRICE
시간외종가: priced at a session's closing price.
Definition types.hpp:1724
@ MARKET
시장가: fills at whatever the book offers.
Definition types.hpp:1712
constexpr std::array< OrderType, 9 > all_order_types
Every OrderType, in declaration order.
Definition types.hpp:1728
Country
A country (market) the library supports.
Definition types.hpp:30
OrderSession
The trading session an order is submitted into.
Definition types.hpp:1839
@ OPENING_AUCTION
The opening single-price auction (시가단일가; Toss OPG).
Definition types.hpp:1844
@ AFTER_HOURS
After the close, at that session's close (장후 시간외).
Definition types.hpp:1850
@ CLOSING_AUCTION
The closing auction (Toss CLS, a US limit-on-close).
Definition types.hpp:1846
@ REGULAR
The continuous regular session.
Definition types.hpp:1842
@ AFTER_MARKET
KRX's after-market session (애프터마켓).
Definition types.hpp:1854
@ AFTER_HOURS_AUCTION
The after-hours single-price auction (시간외 단일가).
Definition types.hpp:1852
@ PRE_MARKET
Before the open, at the previous session's close (장전 시간외).
Definition types.hpp:1848
@ GTP
NXT's GTP session.
Definition types.hpp:1856
constexpr std::array< OrderSide, 2 > all_order_sides
Every OrderSide, in declaration order.
Definition types.hpp:1689
constexpr std::array< OrderSession, 9 > all_order_sessions
Every OrderSession, in declaration order.
Definition types.hpp:1860
Venue
A trading venue: an exchange, or the combined view across exchanges.
Definition types.hpp:43
constexpr std::array< TimeInForce, 4 > all_times_in_force
Every TimeInForce, in declaration order.
Definition types.hpp:1808
TimeInForce
How long an order stays valid and what happens to an unfilled remainder.
Definition types.hpp:1797
@ FILL_OR_KILL
Fills entirely and immediately, or not at all.
Definition types.hpp:1804
@ DAY
Rests until the session ends; an unfilled remainder is cancelled then.
Definition types.hpp:1800
@ IMMEDIATE_OR_CANCEL
Fills what it can immediately and cancels the remainder.
Definition types.hpp:1802
OrderSide
The side of an order: its economic direction, buy or sell.
Definition types.hpp:1683
std::string format_iso8601_utc(std::chrono::system_clock::time_point value)
Formats an instant as an ISO 8601 timestamp in UTC, such as 2026-10-06T00:30:00Z.
Definition types.hpp:2453
constexpr std::array< Country, 2 > all_countries
Every Country, in declaration order.
Definition types.hpp:36
Identifies an instrument: its country, venue and symbol.
Definition types.hpp:78
Country country_
The listing country, which selects the symbol convention and the price grid.
Definition types.hpp:80
Venue venue_
The venue that is part of the identity (two venues are two instruments).
Definition types.hpp:86
std::string symbol_
The symbol as the venue spells it, such as "005930".
Definition types.hpp:88
A journal file's bytes, and whether they could be read.
JournalReadStatus status_
Whether the file was read.
std::string text_
The contents; empty unless status_ is READ.
std::string detail_
What went wrong, for the operator; empty on success.
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
Decimal64 quantity_
The quantity, which recovery matches against.
Definition journal.hpp:392
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
std::optional< Decimal64 > baseline_
On a serialized runner's placement, the instrument's net holding the order was decided against; on a ...
Definition journal.hpp:403
bool requires_operator_
Whether only an operator may resolve this instruction.
Definition journal.hpp:417
std::string scope_
The broker and account the instruction belongs to, such as "kis:live:12345678-01".
Definition journal.hpp:424
JournalAction action_
Whether the instruction is a placement or a cancellation.
Definition journal.hpp:368
OrderSession session_
The session.
Definition journal.hpp:382
TimeInForce time_in_force_
The time in force.
Definition journal.hpp:380
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
bool has_header_
Whether a header was found; a non-empty file without one is not written to.
Definition journal.hpp:476
std::string scope_
The broker and account the file's header declares, if it has one.
Definition journal.hpp:474
std::vector< JournalEntry > entries_
Every line that parsed, in file order.
Definition journal.hpp:466
JournalReadStatus status_
Whether the file was read; READ for an in-memory journal.
Definition journal.hpp:478
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