Skip to content

Commit a722447

Browse files
authored
feat: add example for using custom XMLs (#337)
* Scaffold new crate for custom fields example * Add custom XML spec for custom fields example * Add build.rs to generate code from custom XML * Add outbound message type * Add inbound message type and application * Add test config * Add end-to-end flow for submitting an order in custom-fields example * Update dummy executor to make it work with new custom tag * Add README for custom fields example * Remove references to standard fix44 module * Drop config path as CLI arg
1 parent 2939930 commit a722447

11 files changed

Lines changed: 7000 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dummy-executor/cmd/executor.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,14 @@ func (e *Executor) onNewOrderSingle(msg newordersingle.NewOrderSingle, sessionID
109109
log.Printf("Received NewOrderSingle: ClOrdID=%s Symbol=%s Side=%s Qty=%s",
110110
clOrdID, symbol, string(side), orderQty.String())
111111

112+
// Read the optional custom tag (6001 = ClientStrategyId).
113+
var clientStrategyID quickfix.FIXInt
114+
hasClientStrategyID := false
115+
if err := msg.Body.GetField(quickfix.Tag(6001), &clientStrategyID); err == nil {
116+
hasClientStrategyID = true
117+
log.Printf(" ClientStrategyId=%d", int(clientStrategyID))
118+
}
119+
112120
// Look up FX rate; default to 1.0000 for unknown pairs.
113121
price, ok := fxRates[symbol]
114122
if !ok {
@@ -133,6 +141,9 @@ func (e *Executor) onNewOrderSingle(msg newordersingle.NewOrderSingle, sessionID
133141
ack.Set(field.NewClOrdID(clOrdID))
134142
ack.Set(field.NewSymbol(symbol))
135143
ack.Set(field.NewOrderQty(orderQty, 2))
144+
if hasClientStrategyID {
145+
ack.Body.SetField(quickfix.Tag(6001), clientStrategyID)
146+
}
136147

137148
if sendErr := quickfix.SendToTarget(ack.ToMessage(), sessionID); sendErr != nil {
138149
log.Printf("Error sending ACK: %v", sendErr)
@@ -156,6 +167,9 @@ func (e *Executor) onNewOrderSingle(msg newordersingle.NewOrderSingle, sessionID
156167
fill.Set(field.NewOrderQty(orderQty, 2))
157168
fill.Set(field.NewLastQty(orderQty, 2))
158169
fill.Set(field.NewLastPx(price, 4))
170+
if hasClientStrategyID {
171+
fill.Body.SetField(quickfix.Tag(6001), clientStrategyID)
172+
}
159173

160174
if sendErr := quickfix.SendToTarget(fill.ToMessage(), sessionID); sendErr != nil {
161175
log.Printf("Error sending FILL: %v", sendErr)

examples/custom-fields/Cargo.toml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "custom-fields"
3+
version = "0.1.0"
4+
authors.workspace = true
5+
edition.workspace = true
6+
license.workspace = true
7+
publish = false
8+
9+
[dependencies]
10+
hotfix = { path = "../../crates/hotfix" }
11+
hotfix-message = { path = "../../crates/hotfix-message" }
12+
13+
anyhow.workspace = true
14+
async-trait.workspace = true
15+
tokio = { workspace = true, features = ["full"] }
16+
tracing.workspace = true
17+
tracing-subscriber = { workspace = true, features = ["env-filter"] }
18+
19+
[build-dependencies]
20+
hotfix-codegen = { path = "../../crates/hotfix-codegen" }
21+
hotfix-dictionary = { path = "../../crates/hotfix-dictionary" }

examples/custom-fields/README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Custom Fields — using a custom XML dictionary
2+
3+
This example demonstrates how to use a QuickFIX-style XML dictionary that
4+
extends the bundled FIX 4.4 spec with your own custom fields, exercising both
5+
sides of HotFIX's custom-XML support:
6+
7+
- **Build-time codegen**`build.rs` runs `hotfix-codegen` against
8+
`spec/FIX44-custom.xml` to produce typed field constants under a
9+
`custom_fix` module (e.g. `custom_fix::CLIENT_STRATEGY_ID`).
10+
- **Runtime dictionary validation** — the session loads the same XML at
11+
startup via `data_dictionary_path` and uses it to validate inbound and
12+
outbound messages.
13+
14+
The example sends a `NewOrderSingle (D)` carrying `ClientStrategyId=42`
15+
and expects the dummy executor to echo the field on the resulting
16+
`ExecutionReport`s. If the field doesn't round-trip, the example exits
17+
non-zero with a descriptive error.
18+
19+
## The custom XML
20+
21+
`spec/FIX44-custom.xml` is a verbatim copy of the bundled
22+
`crates/hotfix-dictionary/src/resources/quickfix/FIX-4.4.xml` with one
23+
addition: a `<field number="6001" name="ClientStrategyId" type="INT"/>`
24+
in the `<fields>` block, plus an optional reference to it on
25+
`NewOrderSingle` and `ExecutionReport`.
26+
27+
## Using the generated constants
28+
29+
All field constants and typed enums (`Side`, `OrdType`, `OrdStatus`, …) come
30+
from the `custom_fix` module — including the ones for standard FIX 4.4
31+
tags. This keeps the example aligned with a single source of truth: the
32+
custom XML drives both compile-time typing and runtime validation. The
33+
`hotfix::fix44` re-exports are deliberately not used here, so the
34+
`hotfix` dependency in `Cargo.toml` doesn't enable the `fix44` feature.
35+
36+
## Running the example
37+
38+
In one terminal, build and start the dummy executor via the existing compose file:
39+
40+
```shell
41+
docker compose -f example.compose.yml up --build dummy-executor
42+
```
43+
44+
In another, from the repo root, run the example:
45+
46+
```shell
47+
cargo run -p custom-fields
48+
```
49+
50+
Expected log output:
51+
52+
```
53+
INFO custom_fields: waiting for logon (up to 10s)
54+
INFO custom_fields::application: logged on
55+
INFO custom_fields: sending NewOrderSingle ClOrdID=demo-1 ClientStrategyId=42
56+
INFO custom_fields: received ExecutionReport ClOrdID=demo-1 OrdStatus=New ClientStrategyId=Some(42)
57+
INFO custom_fields: received ExecutionReport ClOrdID=demo-1 OrdStatus=Filled ClientStrategyId=Some(42)
58+
INFO custom_fields: order filled, custom field round-tripped successfully
59+
INFO custom_fields: shutting down
60+
```
61+
62+
The example should then exit.

examples/custom-fields/build.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use hotfix_codegen as codegen;
2+
use hotfix_dictionary::Dictionary;
3+
use std::env::var;
4+
use std::fs::File;
5+
use std::io::Write;
6+
use std::path::PathBuf;
7+
8+
fn main() -> std::io::Result<()> {
9+
let spec_path = "spec/FIX44-custom.xml";
10+
println!("cargo:rerun-if-changed={spec_path}");
11+
12+
let dict =
13+
Dictionary::load_from_file(spec_path).expect("failed to load custom FIX 4.4 dictionary");
14+
15+
let mut settings = codegen::Settings::default();
16+
// The generated code uses `<crate>::dict::FieldLocation`, `<crate>::FieldType`,
17+
// and `<crate>::HardCodedFixFieldDefinition` — re-exported by `hotfix-message`
18+
// but not by `hotfix`, so we point codegen at `hotfix_message`.
19+
settings.hotfix_crate_name = "hotfix_message".to_string();
20+
21+
let code = codegen::gen_definitions(&dict, &settings);
22+
23+
let out_dir = PathBuf::from(var("OUT_DIR").expect("OUT_DIR not set by cargo"));
24+
let out_path = out_dir.join("custom_fix.rs");
25+
let mut file = File::create(&out_path)?;
26+
file.write_all(code.as_bytes())?;
27+
28+
Ok(())
29+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[[sessions]]
2+
begin_string = "FIX.4.4"
3+
sender_comp_id = "dummy-initiator"
4+
target_comp_id = "dummy-acceptor"
5+
6+
connection_port = 9880
7+
connection_host = "127.0.0.1"
8+
9+
heartbeat_interval = 30
10+
reset_on_logon = true
11+
12+
data_dictionary_path = "examples/custom-fields/spec/FIX44-custom.xml"

0 commit comments

Comments
 (0)