Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions dummy-executor/cmd/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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)
Expand Down
21 changes: 21 additions & 0 deletions examples/custom-fields/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "custom-fields"
version = "0.1.0"
authors.workspace = true
edition.workspace = true
license.workspace = true
publish = false

[dependencies]
hotfix = { path = "../../crates/hotfix" }
hotfix-message = { path = "../../crates/hotfix-message" }

anyhow.workspace = true
async-trait.workspace = true
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" }
62 changes: 62 additions & 0 deletions examples/custom-fields/README.md
Original file line number Diff line number Diff line change
@@ -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 `<field number="6001" name="ClientStrategyId" type="INT"/>`
in the `<fields>` block, plus an optional reference to it on
`NewOrderSingle` and `ExecutionReport`.

## Using the generated constants

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, build and start the dummy executor via the existing compose file:

```shell
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
```

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.
29 changes: 29 additions & 0 deletions examples/custom-fields/build.rs
Original file line number Diff line number Diff line change
@@ -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 `<crate>::dict::FieldLocation`, `<crate>::FieldType`,
// and `<crate>::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(())
}
12 changes: 12 additions & 0 deletions examples/custom-fields/config/test-config.toml
Original file line number Diff line number Diff line change
@@ -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"
Loading
Loading