3#include "./journal.hpp"
5#include <algotradelib/core>
7#include <boost/crc.hpp>
8#include <boost/json.hpp>
34namespace json = boost::json;
36namespace journal_detail {
54inline bool flush_to_disk(std::FILE* handle)
noexcept {
55 if (std::fflush(handle) != 0) {
59 return ::_commit(::_fileno(handle)) == 0;
61 return ::fsync(::fileno(handle)) == 0;
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;
101inline bool has_field(
const json::object& row, std::string_view key) {
102 return row.if_contains(key) !=
nullptr;
106inline std::optional<std::string> readable_string(
107 const json::object& row, std::string_view key
109 const auto value = row.if_contains(key);
110 if (value ==
nullptr || !value->is_string() || value->as_string().empty()) {
113 return std::string(value->as_string());
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()) {
121 return std::string(value->as_string());
131inline constexpr std::string_view country_name(
Country country)
noexcept {
143 "all_countries must list every Country"
147inline constexpr std::string_view venue_name(
Venue venue)
noexcept {
149 case Venue::UNSPECIFIED:
150 return "UNSPECIFIED";
155 case Venue::COMBINED:
171inline std::optional<Venue> known_venue_from(std::string_view text)
noexcept {
177inline constexpr std::string_view side_name(
OrderSide side)
noexcept {
181 case OrderSide::SELL:
189 "all_order_sides must list every OrderSide"
201inline constexpr std::string_view order_type_name(
OrderType type)
noexcept {
208 return "CONDITIONAL_LIMIT";
212 return "PRIMARY_LIMIT";
218 return "CLOSING_PRICE";
219 case OrderType::UNKNOWN:
227 "all_order_types must list every OrderType"
232inline constexpr std::string_view time_in_force_name(
TimeInForce condition)
noexcept {
240 case TimeInForce::UNKNOWN:
248 "all_times_in_force must list every TimeInForce"
252inline constexpr std::string_view order_session_name(
OrderSession session)
noexcept {
257 return "OPENING_AUCTION";
259 return "CLOSING_AUCTION";
263 return "AFTER_HOURS";
265 return "AFTER_HOURS_AUCTION";
267 return "AFTER_MARKET";
270 case OrderSession::UNKNOWN:
278 "all_order_sessions must list every OrderSession"
314inline std::optional<Decimal64> known_decimal_from(std::string_view text)
noexcept {
317 }
catch (
const std::exception&) {
322inline std::optional<Country> known_country_from(std::string_view text)
noexcept {
326inline std::optional<OrderSide> known_side_from(std::string_view text)
noexcept {
330inline std::optional<OrderType> known_order_type_from(std::string_view text)
noexcept {
334inline std::optional<TimeInForce> known_time_in_force_from(std::string_view text)
noexcept {
338inline std::optional<OrderSession> known_order_session_from(std::string_view text)
noexcept {
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);
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();
365inline std::optional<std::string> salvage_string(std::string_view line, std::string_view key) {
367 needle.reserve(key.size() + 4);
371 const auto at = line.find(needle);
372 if (at == std::string_view::npos) {
375 const auto begin = at + needle.size();
379 while (end < line.size() && line[end] !=
'"') {
380 end += line[end] ==
'\\' ? 2 : 1;
382 if (end >= line.size()) {
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()) {
393 return std::string(quoted.as_string());
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();
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];
422inline constexpr std::string_view kChecksumMember =
",\"crc\":\"";
425inline constexpr std::size_t kChecksumSuffixSize = kChecksumMember.size() + 8 + 2;
428inline bool ends_in_checksum(std::string_view line) {
429 if (line.size() <= kChecksumSuffixSize) {
432 const auto suffix = line.substr(line.size() - kChecksumSuffixSize);
433 if (suffix.substr(0, kChecksumMember.size()) != kChecksumMember ||
434 suffix.substr(kChecksumSuffixSize - 2) !=
"\"}") {
437 for (
const char digit : suffix.substr(kChecksumMember.size(), 8)) {
438 if (!((digit >=
'0' && digit <=
'9') || (digit >=
'a' && digit <=
'f'))) {
451inline bool checksum_matches(std::string_view line) {
452 std::string body(line.substr(0, line.size() - kChecksumSuffixSize));
454 const auto stated = line.substr(line.size() - kChecksumSuffixSize + kChecksumMember.size(), 8);
455 return checksum_text(line_checksum(body)) == stated;
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",
473 for (
const auto candidate : known) {
474 if (candidate == key) {
488 return "algotradelib-order-journal";
500 row[
"scope"] = std::string(scope);
502 row[
"integrity"] =
"crc32";
503 return json::serialize(row);
512 boost::system::error_code error;
513 const auto value = json::parse(line, error);
514 if (error || !value.is_object()) {
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";
528 boost::system::error_code error;
529 const auto value = json::parse(line, error);
530 if (error || !value.is_object()) {
533 const auto& row = value.as_object();
534 const auto marker = row.if_contains(
"journal");
535 if (marker ==
nullptr || !marker->is_string() ||
539 return journal_detail::string_field(row,
"scope");
562 row[
"id"] = entry.
id_;
566 row[
"venue"] = std::string(journal_detail::venue_name(entry.
instrument_.
venue_));
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_));
579 row[
"route"] = std::string(journal_detail::venue_name(*entry.
venue_));
583 row[
"price"] = entry.
price_->str();
593 row[
"baseline"] = entry.
baseline_->str();
598 row[
"at"] = journal_detail::micros_of(entry.
recorded_at_);
607 row[
"requires_operator"] =
true;
609 if (!entry.
scope_.empty()) {
610 row[
"scope"] = entry.
scope_;
615 auto line = json::serialize(row);
616 const auto checksum = journal_detail::checksum_text(journal_detail::line_checksum(line));
618 line.append(journal_detail::kChecksumMember);
634 entry.
id_ =
"unknown~truncated";
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));
653 boost::system::error_code error;
654 const auto value = json::parse(line, error);
655 if (error || !value.is_object()) {
658 const auto& row = value.as_object();
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();
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()))) {
698 const auto read = [&row](std::string_view key) {
699 return journal_detail::readable_string(row, key);
705 const auto unusable = [&row](std::string_view key,
bool parsed) {
706 return journal_detail::has_field(row, key) && !parsed;
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{};
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())) {
733 if (!checksum_holds || unknown_key) {
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);
754 !symbol || !country || !venue || !side || !type || !quantity ||
756 unusable(
"time_in_force", condition.has_value()) ||
757 unusable(
"session", session.has_value()) ||
761 unusable(
"price", price.has_value()) ||
764 unusable(
"trigger", trigger.has_value()) ||
765 unusable(
"baseline", baseline.has_value()) ||
785 const auto id = journal_detail::string_field(row,
"id");
786 unattributable.id_ = (
id.empty() ? std::string(
"unknown") :
id) +
"~damaged";
787 unattributable.action_ = *action;
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;
802 entry.
id_ = journal_detail::string_field(row,
"id");
820 entry.
venue_ = journal_detail::known_venue_from(journal_detail::string_field(row,
"route"));
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");
836 entry.
scope_ = journal_detail::string_field(row,
"scope");
851 const std::string needle =
"\"" + std::string(key) +
"\":";
852 const auto at = line.find(needle);
853 if (at == std::string_view::npos) {
856 return line.find(
',', at + needle.size()) != std::string_view::npos;
871 const auto symbol = journal_detail::salvage_string(line,
"symbol");
872 if (!symbol || symbol->empty()) {
880 const auto id = journal_detail::salvage_string(line,
"id");
881 entry.
id_ = (
id ? *id : std::string(
"unknown")) +
"~truncated";
899 const auto salvaged = [&line](std::string_view key,
const char* fallback) {
900 const auto text = journal_detail::salvage_string(line, key);
902 return std::optional<std::string>(*text);
905 : std::optional<std::string>(fallback);
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{};
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) {
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;
934 entry.
venue_ = journal_detail::known_venue_from(
935 journal_detail::salvage_string(line,
"route").value_or(
"")
937 if (
const auto quantity = journal_detail::salvage_string(line,
"quantity")) {
941 if (
const auto value = journal_detail::known_decimal_from(*quantity)) {
945 if (
const auto price = journal_detail::salvage_string(line,
"price")) {
946 if (
const auto value = journal_detail::known_decimal_from(*price)) {
950 if (
const auto trigger = journal_detail::salvage_string(line,
"trigger")) {
951 if (
const auto value = journal_detail::known_decimal_from(*trigger)) {
955 entry.
detail_ =
"recovered from a truncated journal line; only an operator can resolve it";
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; }
972 if (handle ==
nullptr) {
973 if (errno == ENOENT) {
982 bytes.
detail_ =
"the order journal at " + path +
" exists and could not be opened for reading";
987 while (
const auto read = std::fread(buffer, 1,
sizeof(buffer), handle.get())) {
988 bytes.
text_.append(buffer, read);
990 if (std::ferror(handle.get()) != 0) {
994 bytes.
detail_ =
"the order journal at " + path +
" could not be read to the end";
1021 const auto& text = bytes.
text_;
1022 std::size_t begin = 0;
1024 bool seen_checksum =
false;
1025 while (begin < text.size()) {
1026 const auto end = text.find(
'\n', begin);
1027 if (end == std::string::npos) {
1031 const std::string_view tail(text.data() + begin, text.size() - begin);
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));
1038 "the final journal line was truncated and named no instrument; something was in flight";
1045 const std::string_view line(text.data() + begin, end - begin);
1051 const auto unreadable = [&replay](
const char* why) {
1058 unreadable(
"the journal contains an empty line, which this library never writes");
1063 unreadable(
"the journal contains a header after its first line, which this library never "
1070 replay.
scope_ = std::move(*scope);
1075 const bool checksummed = journal_detail::ends_in_checksum(line);
1076 if (seen_checksum && !checksummed) {
1085 "a journal line without a checksum follows a checksummed one, so it was changed after it "
1090 seen_checksum = seen_checksum || checksummed;
1092 replay.
entries_.push_back(std::move(*entry));
1099 replay.
detail_ =
"the journal contains a line this library did not write";
1144 : path_(std::move(
path)), scope_(std::move(
scope)), handle_(nullptr, &closer) {
1145 if (scope_.empty()) {
1147 rejection_ =
"an order journal must name the broker and account it belongs to";
1151 if (!claim_file()) {
1155 handle_.reset(std::fopen(path_.c_str(),
"ab"));
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) +
")";
1166 if (!lock(descriptor)) {
1167 ::close(descriptor);
1171 if (!claim_file()) {
1172 ::close(descriptor);
1178 ::close(descriptor);
1181 handle_.reset(::fdopen(descriptor,
"ab"));
1183 const int error = errno;
1185 "the journal at " + path_ +
" could not be opened for appending (" + std::strerror(error) +
")";
1186 ::close(descriptor);
1192 if (needs_header_ && !write_header()) {
1195 if (!pending_repair_.empty()) {
1204 bool open() const noexcept {
return handle_ !=
nullptr; }
1211 const std::string&
repaired() const noexcept {
return repaired_; }
1217 const std::string&
path() const noexcept {
return path_; }
1223 const std::string&
scope() const noexcept {
return scope_; }
1234 const std::string&
rejection() const noexcept {
return rejection_; }
1248 auto stamped = entry;
1253 const auto written = std::fwrite(line.data(), 1, line.size(), handle_.get());
1254 if (written != line.size()) {
1257 return journal_detail::flush_to_disk(handle_.get());
1290 "the journal at " + path_ +
" exists and cannot be read" :
1297 needs_header_ =
true;
1301 rejection_ =
"the journal at " + path_ +
1302 " has no header, so the account it belongs to cannot be established";
1306 rejection_ =
"the journal at " + path_ +
" belongs to " +
replay.
scope_ +
", not to " +
1314 bool write_header() {
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())) {
1320 rejection_ =
"the journal header at " + path_ +
" could not be written";
1323 needs_header_ =
false;
1328 static void closer(std::FILE* handle)
noexcept {
1329 if (handle !=
nullptr) {
1330 std::fclose(handle);
1339 void repair_torn_tail() {
1344 const auto tail = read_torn_tail(path_);
1346 pending_repair_ = 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";
1355 void finish_repair() {
1359 std::string repair =
"\n";
1360 repair += pending_repair_;
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())) {
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";
1371 pending_repair_.clear();
1376 bool lock(
int descriptor) {
1377 if (::flock(descriptor, LOCK_EX | LOCK_NB) == 0) {
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";
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
1395 if (reader ==
nullptr) {
1400 while (
const auto read = std::fread(buffer, 1,
sizeof(buffer), reader.get())) {
1401 text.append(buffer, read);
1403 const auto last = text.rfind(
'\n');
1404 return last == std::string::npos ? text : text.substr(last + 1);
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};
std::string str() const
Formats the value as a decimal string.
static Decimal64 parse(std::string_view input)
Parses a decimal string exactly (no binary floating point).
Either a value or a BrokerError.
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.
std::optional< JournalOutcome > journal_outcome_from(std::string_view text) noexcept
Parses an outcome's name.
constexpr std::string_view journal_outcome_name(JournalOutcome outcome) noexcept
Returns an outcome's name as written to the journal ("pending", ...).
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.
@ MISSING
No such file: a first start, nothing owed.
@ READ
The file was read in full.
@ UNREADABLE
The file exists and could not be read; its contents are unknown.
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").
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.
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.
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.
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).
@ LIMIT
지정가: fills at the stated price or better.
@ BEST_LIMIT
최유리지정가: priced at the best quote on the opposite side.
@ PRIMARY_LIMIT
최우선지정가: priced at the best quote on the order's own side.
@ CONDITIONAL_LIMIT
조건부지정가: a limit that becomes a market order at the close.
@ MID_PRICE
중간가: priced at the midpoint of the spread.
@ STOP_LIMIT
스톱지정가: a limit released when a trigger price is reached.
@ CLOSING_PRICE
시간외종가: priced at a session's closing price.
@ MARKET
시장가: fills at whatever the book offers.
constexpr std::array< OrderType, 9 > all_order_types
Every OrderType, in declaration order.
Country
A country (market) the library supports.
OrderSession
The trading session an order is submitted into.
@ OPENING_AUCTION
The opening single-price auction (시가단일가; Toss OPG).
@ AFTER_HOURS
After the close, at that session's close (장후 시간외).
@ CLOSING_AUCTION
The closing auction (Toss CLS, a US limit-on-close).
@ REGULAR
The continuous regular session.
@ AFTER_MARKET
KRX's after-market session (애프터마켓).
@ AFTER_HOURS_AUCTION
The after-hours single-price auction (시간외 단일가).
@ PRE_MARKET
Before the open, at the previous session's close (장전 시간외).
constexpr std::array< OrderSide, 2 > all_order_sides
Every OrderSide, in declaration order.
constexpr std::array< OrderSession, 9 > all_order_sessions
Every OrderSession, in declaration order.
Venue
A trading venue: an exchange, or the combined view across exchanges.
constexpr std::array< TimeInForce, 4 > all_times_in_force
Every TimeInForce, in declaration order.
TimeInForce
How long an order stays valid and what happens to an unfilled remainder.
@ FILL_OR_KILL
Fills entirely and immediately, or not at all.
@ DAY
Rests until the session ends; an unfilled remainder is cancelled then.
@ IMMEDIATE_OR_CANCEL
Fills what it can immediately and cancels the remainder.
OrderSide
The side of an order: its economic direction, buy or sell.
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.
constexpr std::array< Country, 2 > all_countries
Every Country, in declaration order.
Identifies an instrument: its country, venue and symbol.
Country country_
The listing country, which selects the symbol convention and the price grid.
Venue venue_
The venue that is part of the identity (two venues are two instruments).
std::string symbol_
The symbol as the venue spells it, such as "005930".
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.
std::string id_
Links an answering line to its PENDING line.
std::chrono::system_clock::time_point recorded_at_
When the line was written (for PENDING, before the request went out).
OrderType type_
The order type; recovery matches on type, time in force and session together.
Decimal64 quantity_
The quantity, which recovery matches against.
std::optional< Decimal64 > price_
The price, if the order has one.
JournalOutcome outcome_
What the line says about the instruction.
std::string detail_
Why an answering line says what it says, for the operator.
std::optional< Venue > venue_
The venue the order actually goes to, as the binding resolves it.
InstrumentId instrument_
The instrument (identity; the routing venue is venue_).
std::optional< Decimal64 > trigger_price_
The trigger price of a stop-limit order; empty otherwise (and on lines that predate it).
std::optional< Decimal64 > baseline_
On a serialized runner's placement, the instrument's net holding the order was decided against; on a ...
bool requires_operator_
Whether only an operator may resolve this instruction.
std::string scope_
The broker and account the instruction belongs to, such as "kis:live:12345678-01".
JournalAction action_
Whether the instruction is a placement or a cancellation.
OrderSession session_
The session.
TimeInForce time_in_force_
The time in force.
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...
The contents of a journal, including what could not be read.
bool readable() const noexcept
Tests whether the file could be read.
std::size_t unreadable_lines_
How many lines did not parse (on an append-only file, writes that did not finish).
bool has_header_
Whether a header was found; a non-empty file without one is not written to.
std::string scope_
The broker and account the file's header declares, if it has one.
std::vector< JournalEntry > entries_
Every line that parsed, in file order.
JournalReadStatus status_
Whether the file was read; READ for an in-memory journal.
std::string detail_
What was wrong, for the operator.
bool truncated_tail_
Whether the last line was incomplete, as after a kill during a write.