From fb3fcc597c24e5db0ccc7756f0deaf9d2b02c26e Mon Sep 17 00:00:00 2001 From: David Steiner Date: Thu, 16 Apr 2026 13:23:44 +0200 Subject: [PATCH 01/11] Scaffold new crate for custom fields example --- Cargo.lock | 16 ++++++++++++++++ examples/custom-fields/Cargo.toml | 22 ++++++++++++++++++++++ examples/custom-fields/src/main.rs | 3 +++ 3 files changed, 41 insertions(+) create mode 100644 examples/custom-fields/Cargo.toml create mode 100644 examples/custom-fields/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 8f15809..c661514 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -748,6 +748,22 @@ dependencies = [ "typenum", ] +[[package]] +name = "custom-fields" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "clap", + "hotfix", + "hotfix-codegen", + "hotfix-dictionary", + "hotfix-message", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "darling" version = "0.21.3" diff --git a/examples/custom-fields/Cargo.toml b/examples/custom-fields/Cargo.toml new file mode 100644 index 0000000..51ee4d8 --- /dev/null +++ b/examples/custom-fields/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "custom-fields" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +publish = false + +[dependencies] +hotfix = { path = "../../crates/hotfix", features = ["fix44"] } +hotfix-message = { path = "../../crates/hotfix-message", features = ["fix44"] } + +anyhow.workspace = true +async-trait.workspace = true +clap = { workspace = true, features = ["derive"] } +tokio = { workspace = true, features = ["full"] } +tracing.workspace = true +tracing-subscriber = { workspace = true, features = ["env-filter"] } + +[build-dependencies] +hotfix-codegen = { path = "../../crates/hotfix-codegen" } +hotfix-dictionary = { path = "../../crates/hotfix-dictionary", features = ["fix44"] } diff --git a/examples/custom-fields/src/main.rs b/examples/custom-fields/src/main.rs new file mode 100644 index 0000000..f64b074 --- /dev/null +++ b/examples/custom-fields/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("custom-fields example placeholder"); +} From 4fa3b44be43a5568fb6252f2379e1dc70beef142 Mon Sep 17 00:00:00 2001 From: David Steiner Date: Thu, 16 Apr 2026 15:32:51 +0200 Subject: [PATCH 02/11] Add custom XML spec for custom fields example --- examples/custom-fields/spec/FIX44-custom.xml | 6596 ++++++++++++++++++ 1 file changed, 6596 insertions(+) create mode 100644 examples/custom-fields/spec/FIX44-custom.xml diff --git a/examples/custom-fields/spec/FIX44-custom.xml b/examples/custom-fields/spec/FIX44-custom.xml new file mode 100644 index 0000000..e28f95f --- /dev/null +++ b/examples/custom-fields/spec/FIX44-custom.xml @@ -0,0 +1,6596 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
From 1dcecc8a58ae299022c39806baaed1be7f1531a1 Mon Sep 17 00:00:00 2001 From: David Steiner Date: Thu, 16 Apr 2026 15:46:15 +0200 Subject: [PATCH 03/11] Add build.rs to generate code from custom XML --- examples/custom-fields/build.rs | 29 ++++++++++++++++++++++++ examples/custom-fields/src/custom_fix.rs | 8 +++++++ examples/custom-fields/src/main.rs | 7 +++++- 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 examples/custom-fields/build.rs create mode 100644 examples/custom-fields/src/custom_fix.rs diff --git a/examples/custom-fields/build.rs b/examples/custom-fields/build.rs new file mode 100644 index 0000000..f2e776e --- /dev/null +++ b/examples/custom-fields/build.rs @@ -0,0 +1,29 @@ +use hotfix_codegen as codegen; +use hotfix_dictionary::Dictionary; +use std::env::var; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +fn main() -> std::io::Result<()> { + let spec_path = "spec/FIX44-custom.xml"; + println!("cargo:rerun-if-changed={spec_path}"); + + let dict = Dictionary::load_from_file(spec_path) + .expect("failed to load custom FIX 4.4 dictionary"); + + let mut settings = codegen::Settings::default(); + // The generated code uses `::dict::FieldLocation`, `::FieldType`, + // and `::HardCodedFixFieldDefinition` — re-exported by `hotfix-message` + // but not by `hotfix`, so we point codegen at `hotfix_message`. + settings.hotfix_crate_name = "hotfix_message".to_string(); + + let code = codegen::gen_definitions(&dict, &settings); + + let out_dir = PathBuf::from(var("OUT_DIR").expect("OUT_DIR not set by cargo")); + let out_path = out_dir.join("custom_fix.rs"); + let mut file = File::create(&out_path)?; + file.write_all(code.as_bytes())?; + + Ok(()) +} \ No newline at end of file diff --git a/examples/custom-fields/src/custom_fix.rs b/examples/custom-fields/src/custom_fix.rs new file mode 100644 index 0000000..e4ff710 --- /dev/null +++ b/examples/custom-fields/src/custom_fix.rs @@ -0,0 +1,8 @@ +//! Field constants generated at build time from `spec/FIX44-custom.xml`. +//! +//! See `build.rs` and the README for how this is produced. + +#![allow(dead_code)] +#![allow(clippy::all)] + +include!(concat!(env!("OUT_DIR"), "/custom_fix.rs")); diff --git a/examples/custom-fields/src/main.rs b/examples/custom-fields/src/main.rs index f64b074..b87e571 100644 --- a/examples/custom-fields/src/main.rs +++ b/examples/custom-fields/src/main.rs @@ -1,3 +1,8 @@ +mod custom_fix; + fn main() { - println!("custom-fields example placeholder"); + println!( + "ClientStrategyId tag = {}", + custom_fix::CLIENT_STRATEGY_ID.tag, + ); } From 5435d660733142eff2dd9ca8509bc8aad63db80b Mon Sep 17 00:00:00 2001 From: David Steiner Date: Thu, 16 Apr 2026 16:12:31 +0200 Subject: [PATCH 04/11] Add outbound message type --- examples/custom-fields/src/main.rs | 17 +++++++++- examples/custom-fields/src/messages.rs | 43 ++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 examples/custom-fields/src/messages.rs diff --git a/examples/custom-fields/src/main.rs b/examples/custom-fields/src/main.rs index b87e571..f03f345 100644 --- a/examples/custom-fields/src/main.rs +++ b/examples/custom-fields/src/main.rs @@ -1,8 +1,23 @@ mod custom_fix; +mod messages; + +use hotfix::field_types::Timestamp; +use hotfix::fix44; + +use crate::messages::{NewOrderSingle, OutboundMsg}; fn main() { + let _order = OutboundMsg::NewOrderSingle(NewOrderSingle { + cl_ord_id: "demo-1".to_string(), + symbol: "EUR/USD".to_string(), + side: fix44::Side::Buy, + order_qty: 100, + transact_time: Timestamp::utc_now(), + client_strategy_id: 42, + }); + println!( - "ClientStrategyId tag = {}", + "constructed NewOrderSingle (custom tag {} = 42)", custom_fix::CLIENT_STRATEGY_ID.tag, ); } diff --git a/examples/custom-fields/src/messages.rs b/examples/custom-fields/src/messages.rs new file mode 100644 index 0000000..89edda7 --- /dev/null +++ b/examples/custom-fields/src/messages.rs @@ -0,0 +1,43 @@ +use hotfix::Message as HotfixMessage; +use hotfix::field_types::Timestamp; +use hotfix::fix44; +use hotfix::message::{OutboundMessage, Part}; + +use crate::custom_fix; + +#[derive(Debug, Clone)] +pub struct NewOrderSingle { + pub cl_ord_id: String, + pub symbol: String, + pub side: fix44::Side, + pub order_qty: u32, + pub transact_time: Timestamp, + pub client_strategy_id: i32, +} + +#[derive(Debug, Clone)] +pub enum OutboundMsg { + NewOrderSingle(NewOrderSingle), +} + +impl OutboundMessage for OutboundMsg { + fn write(&self, msg: &mut HotfixMessage) { + match self { + OutboundMsg::NewOrderSingle(order) => { + msg.set(fix44::CL_ORD_ID, order.cl_ord_id.as_str()); + msg.set(fix44::SYMBOL, order.symbol.as_str()); + msg.set(fix44::SIDE, order.side); + msg.set(fix44::ORDER_QTY, order.order_qty); + msg.set(fix44::TRANSACT_TIME, order.transact_time.clone()); + msg.set(fix44::ORD_TYPE, fix44::OrdType::Market); + msg.set(custom_fix::CLIENT_STRATEGY_ID, order.client_strategy_id); + } + } + } + + fn message_type(&self) -> &str { + match self { + OutboundMsg::NewOrderSingle(_) => "D", + } + } +} From 1bada90a4585786c8ff6a797ac831619ffb4ab48 Mon Sep 17 00:00:00 2001 From: David Steiner Date: Thu, 16 Apr 2026 16:25:13 +0200 Subject: [PATCH 05/11] Add inbound message type and application --- examples/custom-fields/build.rs | 6 +- examples/custom-fields/src/application.rs | 67 +++++++++++++++++++++++ examples/custom-fields/src/main.rs | 31 +++++------ examples/custom-fields/src/messages.rs | 9 +++ 4 files changed, 94 insertions(+), 19 deletions(-) create mode 100644 examples/custom-fields/src/application.rs diff --git a/examples/custom-fields/build.rs b/examples/custom-fields/build.rs index f2e776e..660260e 100644 --- a/examples/custom-fields/build.rs +++ b/examples/custom-fields/build.rs @@ -9,8 +9,8 @@ fn main() -> std::io::Result<()> { let spec_path = "spec/FIX44-custom.xml"; println!("cargo:rerun-if-changed={spec_path}"); - let dict = Dictionary::load_from_file(spec_path) - .expect("failed to load custom FIX 4.4 dictionary"); + let dict = + Dictionary::load_from_file(spec_path).expect("failed to load custom FIX 4.4 dictionary"); let mut settings = codegen::Settings::default(); // The generated code uses `::dict::FieldLocation`, `::FieldType`, @@ -26,4 +26,4 @@ fn main() -> std::io::Result<()> { file.write_all(code.as_bytes())?; Ok(()) -} \ No newline at end of file +} diff --git a/examples/custom-fields/src/application.rs b/examples/custom-fields/src/application.rs new file mode 100644 index 0000000..caafcb6 --- /dev/null +++ b/examples/custom-fields/src/application.rs @@ -0,0 +1,67 @@ +use std::sync::Arc; + +use hotfix::Application; +use hotfix::Message; +use hotfix::application::{InboundDecision, OutboundDecision}; +use hotfix::fix44; +use hotfix::session::Status; +use hotfix_message::Part; +use tokio::sync::{Notify, mpsc}; +use tracing::{info, warn}; + +use crate::custom_fix; +use crate::messages::{ExecReportSummary, OutboundMsg}; + +pub struct TestApplication { + pub logon_signal: Arc, + pub exec_tx: mpsc::UnboundedSender, +} + +#[async_trait::async_trait] +impl Application for TestApplication { + type Outbound = OutboundMsg; + + async fn on_outbound_message(&self, _msg: &OutboundMsg) -> OutboundDecision { + OutboundDecision::Send + } + + async fn on_inbound_message(&self, msg: &Message) -> InboundDecision { + let msg_type: Result<&str, _> = msg.header().get(hotfix_message::session_fields::MSG_TYPE); + if !matches!(msg_type, Ok("8")) { + return InboundDecision::Accept; + } + + let cl_ord_id: Result<&str, _> = msg.get(fix44::CL_ORD_ID); + let ord_status: Result = msg.get(fix44::ORD_STATUS); + let client_strategy_id: Option = msg.get(custom_fix::CLIENT_STRATEGY_ID).ok(); + + match (cl_ord_id, ord_status) { + (Ok(cl_ord_id), Ok(ord_status)) => { + let summary = ExecReportSummary { + cl_ord_id: cl_ord_id.to_string(), + ord_status, + client_strategy_id, + }; + if let Err(err) = self.exec_tx.send(summary) { + warn!("failed to forward execution report: {err}"); + } + } + _ => warn!("execution report missing ClOrdID or OrdStatus"), + } + + InboundDecision::Accept + } + + async fn on_logout(&mut self, reason: &str) { + info!("logged out: {reason}"); + } + + async fn on_logon(&mut self) { + info!("logged on"); + self.logon_signal.notify_one(); + } + + async fn on_state_change(&self, from: &Status, to: &Status) { + info!("session state changed: {from:?} -> {to:?}"); + } +} diff --git a/examples/custom-fields/src/main.rs b/examples/custom-fields/src/main.rs index f03f345..20702bd 100644 --- a/examples/custom-fields/src/main.rs +++ b/examples/custom-fields/src/main.rs @@ -1,23 +1,22 @@ +mod application; mod custom_fix; mod messages; -use hotfix::field_types::Timestamp; -use hotfix::fix44; +use std::sync::Arc; -use crate::messages::{NewOrderSingle, OutboundMsg}; +use tokio::sync::{Notify, mpsc}; -fn main() { - let _order = OutboundMsg::NewOrderSingle(NewOrderSingle { - cl_ord_id: "demo-1".to_string(), - symbol: "EUR/USD".to_string(), - side: fix44::Side::Buy, - order_qty: 100, - transact_time: Timestamp::utc_now(), - client_strategy_id: 42, - }); +use crate::application::TestApplication; - println!( - "constructed NewOrderSingle (custom tag {} = 42)", - custom_fix::CLIENT_STRATEGY_ID.tag, - ); +#[tokio::main] +async fn main() { + let logon_signal = Arc::new(Notify::new()); + let (exec_tx, _exec_rx) = mpsc::unbounded_channel(); + + let _app = TestApplication { + logon_signal: logon_signal.clone(), + exec_tx, + }; + + println!("custom-fields example: application wired up"); } diff --git a/examples/custom-fields/src/messages.rs b/examples/custom-fields/src/messages.rs index 89edda7..e57e384 100644 --- a/examples/custom-fields/src/messages.rs +++ b/examples/custom-fields/src/messages.rs @@ -16,10 +16,19 @@ pub struct NewOrderSingle { } #[derive(Debug, Clone)] +#[allow(dead_code)] pub enum OutboundMsg { NewOrderSingle(NewOrderSingle), } +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub struct ExecReportSummary { + pub cl_ord_id: String, + pub ord_status: fix44::OrdStatus, + pub client_strategy_id: Option, +} + impl OutboundMessage for OutboundMsg { fn write(&self, msg: &mut HotfixMessage) { match self { From 1877a0e29d95bef15fa92a7ba6587008885d0da3 Mon Sep 17 00:00:00 2001 From: David Steiner Date: Thu, 16 Apr 2026 16:29:38 +0200 Subject: [PATCH 06/11] Add test config --- examples/custom-fields/config/test-config.toml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 examples/custom-fields/config/test-config.toml diff --git a/examples/custom-fields/config/test-config.toml b/examples/custom-fields/config/test-config.toml new file mode 100644 index 0000000..de42b8f --- /dev/null +++ b/examples/custom-fields/config/test-config.toml @@ -0,0 +1,12 @@ +[[sessions]] +begin_string = "FIX.4.4" +sender_comp_id = "dummy-initiator" +target_comp_id = "dummy-acceptor" + +connection_port = 9880 +connection_host = "127.0.0.1" + +heartbeat_interval = 30 +reset_on_logon = true + +data_dictionary_path = "examples/custom-fields/spec/FIX44-custom.xml" From 28f46a472ed9127ad77bd23466700e71606312e8 Mon Sep 17 00:00:00 2001 From: David Steiner Date: Thu, 16 Apr 2026 20:40:42 +0200 Subject: [PATCH 07/11] Add end-to-end flow for submitting an order in custom-fields example --- examples/custom-fields/src/main.rs | 122 ++++++++++++++++++++++++- examples/custom-fields/src/messages.rs | 2 - 2 files changed, 118 insertions(+), 6 deletions(-) diff --git a/examples/custom-fields/src/main.rs b/examples/custom-fields/src/main.rs index 20702bd..e0acaec 100644 --- a/examples/custom-fields/src/main.rs +++ b/examples/custom-fields/src/main.rs @@ -3,20 +3,134 @@ mod custom_fix; mod messages; use std::sync::Arc; +use std::time::Duration; +use anyhow::{Context, Result, anyhow}; +use clap::Parser; +use hotfix::config::Config; +use hotfix::field_types::Timestamp; +use hotfix::fix44; +use hotfix::initiator::Initiator; +use hotfix::store::in_memory::InMemoryMessageStore; use tokio::sync::{Notify, mpsc}; +use tokio::time::timeout; +use tracing::{error, info}; +use tracing_subscriber::EnvFilter; use crate::application::TestApplication; +use crate::messages::{ExecReportSummary, NewOrderSingle, OutboundMsg}; + +const LOGON_TIMEOUT: Duration = Duration::from_secs(10); +const FILL_TIMEOUT: Duration = Duration::from_secs(10); +const STRATEGY_ID: i32 = 42; +const CL_ORD_ID: &str = "demo-1"; + +#[derive(Parser, Debug)] +#[command(author, version, about)] +struct Args { + #[arg(short, long)] + config: String, +} #[tokio::main] -async fn main() { +async fn main() -> Result<()> { + let args = Args::parse(); + + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"))) + .init(); + + let mut config = Config::load_from_path(&args.config) + .context("failed to load config")?; + let session_config = config + .sessions + .pop() + .context("config must include a session")?; + let logon_signal = Arc::new(Notify::new()); - let (exec_tx, _exec_rx) = mpsc::unbounded_channel(); + let (exec_tx, mut exec_rx) = mpsc::unbounded_channel::(); - let _app = TestApplication { + let app = TestApplication { logon_signal: logon_signal.clone(), exec_tx, }; - println!("custom-fields example: application wired up"); + let initiator: Initiator = Initiator::start( + session_config, + app, + InMemoryMessageStore::default(), + ) + .await + .context("failed to start initiator")?; + + info!("waiting for logon (up to {:?})", LOGON_TIMEOUT); + timeout(LOGON_TIMEOUT, logon_signal.notified()) + .await + .map_err(|_| anyhow!("session did not log on within {LOGON_TIMEOUT:?}"))?; + + let order = NewOrderSingle { + cl_ord_id: CL_ORD_ID.to_string(), + symbol: "EUR/USD".to_string(), + side: fix44::Side::Buy, + order_qty: 100, + transact_time: Timestamp::utc_now(), + client_strategy_id: STRATEGY_ID, + }; + info!( + "sending NewOrderSingle ClOrdID={CL_ORD_ID} ClientStrategyId={STRATEGY_ID}" + ); + initiator + .send(OutboundMsg::NewOrderSingle(order)) + .await + .context("failed to send NewOrderSingle")?; + + let result = wait_for_fill(&mut exec_rx).await; + + info!("shutting down"); + if let Err(err) = initiator.shutdown(false).await { + error!("graceful shutdown failed: {err}"); + } + + result +} + +async fn wait_for_fill(exec_rx: &mut mpsc::UnboundedReceiver) -> Result<()> { + let deadline = tokio::time::Instant::now() + FILL_TIMEOUT; + + loop { + let remaining = deadline.saturating_duration_since(tokio::time::Instant::now()); + if remaining.is_zero() { + return Err(anyhow!("did not receive a Filled ExecutionReport within {FILL_TIMEOUT:?}")); + } + + let summary = match timeout(remaining, exec_rx.recv()).await { + Ok(Some(s)) => s, + Ok(None) => return Err(anyhow!("execution-report channel closed unexpectedly")), + Err(_) => return Err(anyhow!("did not receive a Filled ExecutionReport within {FILL_TIMEOUT:?}")), + }; + + info!( + "received ExecutionReport ClOrdID={} OrdStatus={:?} ClientStrategyId={:?}", + summary.cl_ord_id, summary.ord_status, summary.client_strategy_id, + ); + + let echoed = summary + .client_strategy_id + .ok_or_else(|| anyhow!( + "ExecutionReport for ClOrdID={} did not echo ClientStrategyId — \ + the acceptor likely doesn't know about tag 6001", + summary.cl_ord_id, + ))?; + + if echoed != STRATEGY_ID { + return Err(anyhow!( + "ExecutionReport ClientStrategyId mismatch: expected {STRATEGY_ID}, got {echoed}", + )); + } + + if matches!(summary.ord_status, fix44::OrdStatus::Filled) { + info!("order filled, custom field round-tripped successfully"); + return Ok(()); + } + } } diff --git a/examples/custom-fields/src/messages.rs b/examples/custom-fields/src/messages.rs index e57e384..1c5dd88 100644 --- a/examples/custom-fields/src/messages.rs +++ b/examples/custom-fields/src/messages.rs @@ -16,13 +16,11 @@ pub struct NewOrderSingle { } #[derive(Debug, Clone)] -#[allow(dead_code)] pub enum OutboundMsg { NewOrderSingle(NewOrderSingle), } #[derive(Debug, Clone)] -#[allow(dead_code)] pub struct ExecReportSummary { pub cl_ord_id: String, pub ord_status: fix44::OrdStatus, From 1719ea5c6b9e67a89dd9b7d60f9a1fbdfba23c58 Mon Sep 17 00:00:00 2001 From: David Steiner Date: Thu, 16 Apr 2026 21:56:23 +0200 Subject: [PATCH 08/11] Update dummy executor to make it work with new custom tag --- dummy-executor/cmd/executor.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/dummy-executor/cmd/executor.go b/dummy-executor/cmd/executor.go index 91a798b..4587d92 100644 --- a/dummy-executor/cmd/executor.go +++ b/dummy-executor/cmd/executor.go @@ -109,6 +109,14 @@ func (e *Executor) onNewOrderSingle(msg newordersingle.NewOrderSingle, sessionID log.Printf("Received NewOrderSingle: ClOrdID=%s Symbol=%s Side=%s Qty=%s", clOrdID, symbol, string(side), orderQty.String()) + // Read the optional custom tag (6001 = ClientStrategyId). + var clientStrategyID quickfix.FIXInt + hasClientStrategyID := false + if err := msg.Body.GetField(quickfix.Tag(6001), &clientStrategyID); err == nil { + hasClientStrategyID = true + log.Printf(" ClientStrategyId=%d", int(clientStrategyID)) + } + // Look up FX rate; default to 1.0000 for unknown pairs. price, ok := fxRates[symbol] if !ok { @@ -133,6 +141,9 @@ func (e *Executor) onNewOrderSingle(msg newordersingle.NewOrderSingle, sessionID ack.Set(field.NewClOrdID(clOrdID)) ack.Set(field.NewSymbol(symbol)) ack.Set(field.NewOrderQty(orderQty, 2)) + if hasClientStrategyID { + ack.Body.SetField(quickfix.Tag(6001), clientStrategyID) + } if sendErr := quickfix.SendToTarget(ack.ToMessage(), sessionID); sendErr != nil { log.Printf("Error sending ACK: %v", sendErr) @@ -156,6 +167,9 @@ func (e *Executor) onNewOrderSingle(msg newordersingle.NewOrderSingle, sessionID fill.Set(field.NewOrderQty(orderQty, 2)) fill.Set(field.NewLastQty(orderQty, 2)) fill.Set(field.NewLastPx(price, 4)) + if hasClientStrategyID { + fill.Body.SetField(quickfix.Tag(6001), clientStrategyID) + } if sendErr := quickfix.SendToTarget(fill.ToMessage(), sessionID); sendErr != nil { log.Printf("Error sending FILL: %v", sendErr) From ae33e204f63f8273e3df191600551aa7afe277de Mon Sep 17 00:00:00 2001 From: David Steiner Date: Thu, 16 Apr 2026 22:05:11 +0200 Subject: [PATCH 09/11] Add README for custom fields example --- examples/custom-fields/README.md | 62 ++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 examples/custom-fields/README.md diff --git a/examples/custom-fields/README.md b/examples/custom-fields/README.md new file mode 100644 index 0000000..3bdb02d --- /dev/null +++ b/examples/custom-fields/README.md @@ -0,0 +1,62 @@ +# Custom Fields — using a custom XML dictionary + +This example demonstrates how to use a QuickFIX-style XML dictionary that +extends the bundled FIX 4.4 spec with your own custom fields, exercising both +sides of HotFIX's custom-XML support: + +- **Build-time codegen** — `build.rs` runs `hotfix-codegen` against + `spec/FIX44-custom.xml` to produce typed field constants under a + `custom_fix` module (e.g. `custom_fix::CLIENT_STRATEGY_ID`). +- **Runtime dictionary validation** — the session loads the same XML at + startup via `data_dictionary_path` and uses it to validate inbound and + outbound messages. + +The example sends a `NewOrderSingle (D)` carrying `ClientStrategyId=42` +and expects the dummy executor to echo the field on the resulting +`ExecutionReport`s. If the field doesn't round-trip, the example exits +non-zero with a descriptive error. + +## The custom XML + +`spec/FIX44-custom.xml` is a verbatim copy of the bundled +`crates/hotfix-dictionary/src/resources/quickfix/FIX-4.4.xml` with one +addition: a `` +in the `` block, plus an optional reference to it on +`NewOrderSingle` and `ExecutionReport`. + +## Mixing `hotfix::fix44` and `custom_fix` + +The example uses stock field constants from `hotfix::fix44::*` (e.g. +`fix44::CL_ORD_ID`) and the new constant from `custom_fix::CLIENT_STRATEGY_ID`. +This is safe because the custom XML didn't change any FIX 4.4 tag — the +`custom_fix` constants for stock tags are bit-for-bit identical to the ones +in `hotfix::fix44`. If you'd prefer a single source of truth, switch your +imports to `custom_fix::*` everywhere. + +## Running the example + +In one terminal, start the dummy executor: + +```shell +cd dummy-executor && go run . +``` + +In another, from the repo root, run the example: + +```shell +cargo run -p custom-fields -- --config examples/custom-fields/config/test-config.toml +``` + +Expected log output: + +``` +INFO custom_fields: waiting for logon (up to 10s) +INFO custom_fields::application: logged on +INFO custom_fields: sending NewOrderSingle ClOrdID=demo-1 ClientStrategyId=42 +INFO custom_fields: received ExecutionReport ClOrdID=demo-1 OrdStatus=New ClientStrategyId=Some(42) +INFO custom_fields: received ExecutionReport ClOrdID=demo-1 OrdStatus=Filled ClientStrategyId=Some(42) +INFO custom_fields: order filled, custom field round-tripped successfully +INFO custom_fields: shutting down +``` + +The example should then exit. From 1a7f8f68bf0bb59364ddc56ec0417e8c440eee2c Mon Sep 17 00:00:00 2001 From: David Steiner Date: Thu, 16 Apr 2026 22:28:32 +0200 Subject: [PATCH 10/11] Remove references to standard fix44 module --- examples/custom-fields/Cargo.toml | 6 +++--- examples/custom-fields/README.md | 18 +++++++++--------- examples/custom-fields/src/application.rs | 9 ++++----- examples/custom-fields/src/main.rs | 5 ++--- examples/custom-fields/src/messages.rs | 17 ++++++++--------- 5 files changed, 26 insertions(+), 29 deletions(-) diff --git a/examples/custom-fields/Cargo.toml b/examples/custom-fields/Cargo.toml index 51ee4d8..0592578 100644 --- a/examples/custom-fields/Cargo.toml +++ b/examples/custom-fields/Cargo.toml @@ -7,8 +7,8 @@ license.workspace = true publish = false [dependencies] -hotfix = { path = "../../crates/hotfix", features = ["fix44"] } -hotfix-message = { path = "../../crates/hotfix-message", features = ["fix44"] } +hotfix = { path = "../../crates/hotfix" } +hotfix-message = { path = "../../crates/hotfix-message" } anyhow.workspace = true async-trait.workspace = true @@ -19,4 +19,4 @@ tracing-subscriber = { workspace = true, features = ["env-filter"] } [build-dependencies] hotfix-codegen = { path = "../../crates/hotfix-codegen" } -hotfix-dictionary = { path = "../../crates/hotfix-dictionary", features = ["fix44"] } +hotfix-dictionary = { path = "../../crates/hotfix-dictionary" } diff --git a/examples/custom-fields/README.md b/examples/custom-fields/README.md index 3bdb02d..22b10e4 100644 --- a/examples/custom-fields/README.md +++ b/examples/custom-fields/README.md @@ -24,21 +24,21 @@ addition: a `` in the `` block, plus an optional reference to it on `NewOrderSingle` and `ExecutionReport`. -## Mixing `hotfix::fix44` and `custom_fix` +## Using the generated constants -The example uses stock field constants from `hotfix::fix44::*` (e.g. -`fix44::CL_ORD_ID`) and the new constant from `custom_fix::CLIENT_STRATEGY_ID`. -This is safe because the custom XML didn't change any FIX 4.4 tag — the -`custom_fix` constants for stock tags are bit-for-bit identical to the ones -in `hotfix::fix44`. If you'd prefer a single source of truth, switch your -imports to `custom_fix::*` everywhere. +All field constants and typed enums (`Side`, `OrdType`, `OrdStatus`, …) come +from the `custom_fix` module — including the ones for standard FIX 4.4 +tags. This keeps the example aligned with a single source of truth: the +custom XML drives both compile-time typing and runtime validation. The +`hotfix::fix44` re-exports are deliberately not used here, so the +`hotfix` dependency in `Cargo.toml` doesn't enable the `fix44` feature. ## Running the example -In one terminal, start the dummy executor: +In one terminal, build and start the dummy executor via the existing compose file: ```shell -cd dummy-executor && go run . +docker compose -f example.compose.yml up --build dummy-executor ``` In another, from the repo root, run the example: diff --git a/examples/custom-fields/src/application.rs b/examples/custom-fields/src/application.rs index caafcb6..96e619a 100644 --- a/examples/custom-fields/src/application.rs +++ b/examples/custom-fields/src/application.rs @@ -3,9 +3,8 @@ use std::sync::Arc; use hotfix::Application; use hotfix::Message; use hotfix::application::{InboundDecision, OutboundDecision}; -use hotfix::fix44; +use hotfix::message::Part; use hotfix::session::Status; -use hotfix_message::Part; use tokio::sync::{Notify, mpsc}; use tracing::{info, warn}; @@ -26,13 +25,13 @@ impl Application for TestApplication { } async fn on_inbound_message(&self, msg: &Message) -> InboundDecision { - let msg_type: Result<&str, _> = msg.header().get(hotfix_message::session_fields::MSG_TYPE); + let msg_type: Result<&str, _> = msg.header().get(custom_fix::MSG_TYPE); if !matches!(msg_type, Ok("8")) { return InboundDecision::Accept; } - let cl_ord_id: Result<&str, _> = msg.get(fix44::CL_ORD_ID); - let ord_status: Result = msg.get(fix44::ORD_STATUS); + let cl_ord_id: Result<&str, _> = msg.get(custom_fix::CL_ORD_ID); + let ord_status: Result = msg.get(custom_fix::ORD_STATUS); let client_strategy_id: Option = msg.get(custom_fix::CLIENT_STRATEGY_ID).ok(); match (cl_ord_id, ord_status) { diff --git a/examples/custom-fields/src/main.rs b/examples/custom-fields/src/main.rs index e0acaec..b791785 100644 --- a/examples/custom-fields/src/main.rs +++ b/examples/custom-fields/src/main.rs @@ -9,7 +9,6 @@ use anyhow::{Context, Result, anyhow}; use clap::Parser; use hotfix::config::Config; use hotfix::field_types::Timestamp; -use hotfix::fix44; use hotfix::initiator::Initiator; use hotfix::store::in_memory::InMemoryMessageStore; use tokio::sync::{Notify, mpsc}; @@ -71,7 +70,7 @@ async fn main() -> Result<()> { let order = NewOrderSingle { cl_ord_id: CL_ORD_ID.to_string(), symbol: "EUR/USD".to_string(), - side: fix44::Side::Buy, + side: custom_fix::Side::Buy, order_qty: 100, transact_time: Timestamp::utc_now(), client_strategy_id: STRATEGY_ID, @@ -128,7 +127,7 @@ async fn wait_for_fill(exec_rx: &mut mpsc::UnboundedReceiver) )); } - if matches!(summary.ord_status, fix44::OrdStatus::Filled) { + if matches!(summary.ord_status, custom_fix::OrdStatus::Filled) { info!("order filled, custom field round-tripped successfully"); return Ok(()); } diff --git a/examples/custom-fields/src/messages.rs b/examples/custom-fields/src/messages.rs index 1c5dd88..50a034f 100644 --- a/examples/custom-fields/src/messages.rs +++ b/examples/custom-fields/src/messages.rs @@ -1,6 +1,5 @@ use hotfix::Message as HotfixMessage; use hotfix::field_types::Timestamp; -use hotfix::fix44; use hotfix::message::{OutboundMessage, Part}; use crate::custom_fix; @@ -9,7 +8,7 @@ use crate::custom_fix; pub struct NewOrderSingle { pub cl_ord_id: String, pub symbol: String, - pub side: fix44::Side, + pub side: custom_fix::Side, pub order_qty: u32, pub transact_time: Timestamp, pub client_strategy_id: i32, @@ -23,7 +22,7 @@ pub enum OutboundMsg { #[derive(Debug, Clone)] pub struct ExecReportSummary { pub cl_ord_id: String, - pub ord_status: fix44::OrdStatus, + pub ord_status: custom_fix::OrdStatus, pub client_strategy_id: Option, } @@ -31,12 +30,12 @@ impl OutboundMessage for OutboundMsg { fn write(&self, msg: &mut HotfixMessage) { match self { OutboundMsg::NewOrderSingle(order) => { - msg.set(fix44::CL_ORD_ID, order.cl_ord_id.as_str()); - msg.set(fix44::SYMBOL, order.symbol.as_str()); - msg.set(fix44::SIDE, order.side); - msg.set(fix44::ORDER_QTY, order.order_qty); - msg.set(fix44::TRANSACT_TIME, order.transact_time.clone()); - msg.set(fix44::ORD_TYPE, fix44::OrdType::Market); + msg.set(custom_fix::CL_ORD_ID, order.cl_ord_id.as_str()); + msg.set(custom_fix::SYMBOL, order.symbol.as_str()); + msg.set(custom_fix::SIDE, order.side); + msg.set(custom_fix::ORDER_QTY, order.order_qty); + msg.set(custom_fix::TRANSACT_TIME, order.transact_time.clone()); + msg.set(custom_fix::ORD_TYPE, custom_fix::OrdType::Market); msg.set(custom_fix::CLIENT_STRATEGY_ID, order.client_strategy_id); } } From fbf97afccafe767b5bb280c401f91b9b36ae921f Mon Sep 17 00:00:00 2001 From: David Steiner Date: Thu, 16 Apr 2026 22:51:27 +0200 Subject: [PATCH 11/11] Drop config path as CLI arg --- Cargo.lock | 1 - examples/custom-fields/Cargo.toml | 1 - examples/custom-fields/README.md | 2 +- examples/custom-fields/src/main.rs | 51 +++++++++++++----------------- 4 files changed, 23 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c661514..27ea438 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -754,7 +754,6 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "clap", "hotfix", "hotfix-codegen", "hotfix-dictionary", diff --git a/examples/custom-fields/Cargo.toml b/examples/custom-fields/Cargo.toml index 0592578..e75492a 100644 --- a/examples/custom-fields/Cargo.toml +++ b/examples/custom-fields/Cargo.toml @@ -12,7 +12,6 @@ hotfix-message = { path = "../../crates/hotfix-message" } anyhow.workspace = true async-trait.workspace = true -clap = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ["full"] } tracing.workspace = true tracing-subscriber = { workspace = true, features = ["env-filter"] } diff --git a/examples/custom-fields/README.md b/examples/custom-fields/README.md index 22b10e4..31f562d 100644 --- a/examples/custom-fields/README.md +++ b/examples/custom-fields/README.md @@ -44,7 +44,7 @@ docker compose -f example.compose.yml up --build dummy-executor In another, from the repo root, run the example: ```shell -cargo run -p custom-fields -- --config examples/custom-fields/config/test-config.toml +cargo run -p custom-fields ``` Expected log output: diff --git a/examples/custom-fields/src/main.rs b/examples/custom-fields/src/main.rs index b791785..9474a68 100644 --- a/examples/custom-fields/src/main.rs +++ b/examples/custom-fields/src/main.rs @@ -6,7 +6,6 @@ use std::sync::Arc; use std::time::Duration; use anyhow::{Context, Result, anyhow}; -use clap::Parser; use hotfix::config::Config; use hotfix::field_types::Timestamp; use hotfix::initiator::Initiator; @@ -19,28 +18,21 @@ use tracing_subscriber::EnvFilter; use crate::application::TestApplication; use crate::messages::{ExecReportSummary, NewOrderSingle, OutboundMsg}; +const CONFIG_PATH: &str = "examples/custom-fields/config/test-config.toml"; const LOGON_TIMEOUT: Duration = Duration::from_secs(10); const FILL_TIMEOUT: Duration = Duration::from_secs(10); const STRATEGY_ID: i32 = 42; const CL_ORD_ID: &str = "demo-1"; -#[derive(Parser, Debug)] -#[command(author, version, about)] -struct Args { - #[arg(short, long)] - config: String, -} - #[tokio::main] async fn main() -> Result<()> { - let args = Args::parse(); - tracing_subscriber::fmt() - .with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"))) + .with_env_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), + ) .init(); - let mut config = Config::load_from_path(&args.config) - .context("failed to load config")?; + let mut config = Config::load_from_path(CONFIG_PATH).context("failed to load config")?; let session_config = config .sessions .pop() @@ -54,13 +46,10 @@ async fn main() -> Result<()> { exec_tx, }; - let initiator: Initiator = Initiator::start( - session_config, - app, - InMemoryMessageStore::default(), - ) - .await - .context("failed to start initiator")?; + let initiator: Initiator = + Initiator::start(session_config, app, InMemoryMessageStore::default()) + .await + .context("failed to start initiator")?; info!("waiting for logon (up to {:?})", LOGON_TIMEOUT); timeout(LOGON_TIMEOUT, logon_signal.notified()) @@ -75,9 +64,7 @@ async fn main() -> Result<()> { transact_time: Timestamp::utc_now(), client_strategy_id: STRATEGY_ID, }; - info!( - "sending NewOrderSingle ClOrdID={CL_ORD_ID} ClientStrategyId={STRATEGY_ID}" - ); + info!("sending NewOrderSingle ClOrdID={CL_ORD_ID} ClientStrategyId={STRATEGY_ID}"); initiator .send(OutboundMsg::NewOrderSingle(order)) .await @@ -99,13 +86,19 @@ async fn wait_for_fill(exec_rx: &mut mpsc::UnboundedReceiver) loop { let remaining = deadline.saturating_duration_since(tokio::time::Instant::now()); if remaining.is_zero() { - return Err(anyhow!("did not receive a Filled ExecutionReport within {FILL_TIMEOUT:?}")); + return Err(anyhow!( + "did not receive a Filled ExecutionReport within {FILL_TIMEOUT:?}" + )); } let summary = match timeout(remaining, exec_rx.recv()).await { Ok(Some(s)) => s, Ok(None) => return Err(anyhow!("execution-report channel closed unexpectedly")), - Err(_) => return Err(anyhow!("did not receive a Filled ExecutionReport within {FILL_TIMEOUT:?}")), + Err(_) => { + return Err(anyhow!( + "did not receive a Filled ExecutionReport within {FILL_TIMEOUT:?}" + )); + } }; info!( @@ -113,13 +106,13 @@ async fn wait_for_fill(exec_rx: &mut mpsc::UnboundedReceiver) summary.cl_ord_id, summary.ord_status, summary.client_strategy_id, ); - let echoed = summary - .client_strategy_id - .ok_or_else(|| anyhow!( + let echoed = summary.client_strategy_id.ok_or_else(|| { + anyhow!( "ExecutionReport for ClOrdID={} did not echo ClientStrategyId — \ the acceptor likely doesn't know about tag 6001", summary.cl_ord_id, - ))?; + ) + })?; if echoed != STRATEGY_ID { return Err(anyhow!(