Skip to content

Commit 5491727

Browse files
authored
chore: add dummy executor to run examples against (#311)
* Add initial readme for dummy executor * Add .gitignore * First cut of dummy executor code * Create Docker setup for dummy executor * Add compose file to run dummy executor * Add example initiator to compose * Add web interface for sending orders in sample app * Rename simple-new-order to order-entry * Clean up Docker file for order-entry example * Update README to reflect changes made to example app for order entry * Fix load-testing app after changes made to executor
1 parent 393165d commit 5491727

28 files changed

Lines changed: 951 additions & 270 deletions

Cargo.lock

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

README.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,22 @@ various QuickFIX implementations, with long-term plans to optimise further.
4040
| FIX 4.4 | Fully supported |
4141
| FIX 5.0 | Planned |
4242

43-
Check out the [examples](https://github.com/Validus-Risk-Management/hotfix/tree/main/examples)
44-
to get started.
43+
### Getting started
44+
45+
The quickest way to see HotFIX in action is with Docker Compose. The
46+
repository includes an [order-entry](https://github.com/Validus-Risk-Management/hotfix/tree/main/examples/order-entry) example
47+
initiator and a dummy acceptor that you can start together:
48+
49+
```shell
50+
docker compose -f example.compose.yml up --build
51+
```
52+
53+
Once both services are running, open
54+
[http://localhost:9881/order](http://localhost:9881/order) to send FIX
55+
orders through the web UI and watch the messages flow in real time.
56+
57+
See the [examples](https://github.com/Validus-Risk-Management/hotfix/tree/main/examples) directory for more details and
58+
additional ways to run.
4559

4660
### Prior Art
4761

dummy-executor/.gitignore

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# IDEs
2+
.idea/
3+
.vscode/
4+
5+
# Go
6+
go.work
7+
go.work.sum
8+
*.test
9+
10+
# Test artifacts
11+
*.out
12+
coverage.*
13+
*.coverprofile
14+
profile.cov
15+
16+
# Binaries for programs and plugins
17+
*.exe
18+
*.exe~
19+
*.dll
20+
*.so
21+
*.dylib

dummy-executor/Dockerfile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
FROM golang:1.25-alpine AS build
2+
WORKDIR /src
3+
COPY go.mod go.sum ./
4+
RUN go mod download
5+
COPY . .
6+
RUN CGO_ENABLED=0 go build -o /executor .
7+
8+
FROM alpine:3.21
9+
COPY --from=build /executor /executor
10+
COPY config/executor.cfg /config/executor.cfg
11+
EXPOSE 9880
12+
ENTRYPOINT ["/executor"]

dummy-executor/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
This is a dummy acceptor executor using QuickFIX/Go.
2+
As HotFIX currently doesn't support sell-side use cases,
3+
there is no way to set up end-to-end examples where HotFIX
4+
acts as both sides of the connection.
5+
6+
The acceptor implementation here provides a "ping-pong"
7+
server, which responds to new orders with an ack and
8+
a fill.
9+
10+
## Acknowledgement
11+
12+
This product includes software developed by
13+
quickfixengine.org (http://www.quickfixengine.org/).

dummy-executor/cmd/executor.go

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"os"
7+
"os/signal"
8+
"strconv"
9+
"syscall"
10+
11+
"github.com/quickfixgo/enum"
12+
"github.com/quickfixgo/field"
13+
fix44er "github.com/quickfixgo/fix44/executionreport"
14+
"github.com/quickfixgo/fix44/newordersingle"
15+
"github.com/quickfixgo/quickfix"
16+
"github.com/quickfixgo/quickfix/log/screen"
17+
"github.com/shopspring/decimal"
18+
)
19+
20+
// Realistic FX mid-market rates for common currency pairs.
21+
var fxRates = map[string]decimal.Decimal{
22+
"EUR/USD": decimal.NewFromFloat(1.0850),
23+
"GBP/USD": decimal.NewFromFloat(1.2650),
24+
"USD/JPY": decimal.NewFromFloat(149.50),
25+
"USD/CHF": decimal.NewFromFloat(0.8820),
26+
"AUD/USD": decimal.NewFromFloat(0.6540),
27+
"USD/CAD": decimal.NewFromFloat(1.3580),
28+
"NZD/USD": decimal.NewFromFloat(0.6120),
29+
"EUR/GBP": decimal.NewFromFloat(0.8580),
30+
"EUR/JPY": decimal.NewFromFloat(162.20),
31+
"GBP/JPY": decimal.NewFromFloat(189.10),
32+
}
33+
34+
// Executor implements quickfix.Application and handles incoming FIX messages.
35+
type Executor struct {
36+
orderID int
37+
execID int
38+
*quickfix.MessageRouter
39+
}
40+
41+
func newExecutor() *Executor {
42+
e := &Executor{MessageRouter: quickfix.NewMessageRouter()}
43+
e.AddRoute(newordersingle.Route(e.onNewOrderSingle))
44+
return e
45+
}
46+
47+
func (e *Executor) genOrderID() string {
48+
e.orderID++
49+
return strconv.Itoa(e.orderID)
50+
}
51+
52+
func (e *Executor) genExecID() string {
53+
e.execID++
54+
return strconv.Itoa(e.execID)
55+
}
56+
57+
// OnCreate is called when a FIX session is created.
58+
func (e *Executor) OnCreate(sessionID quickfix.SessionID) {
59+
log.Printf("Session created: %s", sessionID)
60+
}
61+
62+
// OnLogon is called when a FIX session logs on.
63+
func (e *Executor) OnLogon(sessionID quickfix.SessionID) {
64+
log.Printf("Session logon: %s", sessionID)
65+
}
66+
67+
// OnLogout is called when a FIX session logs out.
68+
func (e *Executor) OnLogout(sessionID quickfix.SessionID) {
69+
log.Printf("Session logout: %s", sessionID)
70+
}
71+
72+
// ToAdmin is called for outgoing admin messages.
73+
func (e *Executor) ToAdmin(msg *quickfix.Message, sessionID quickfix.SessionID) {}
74+
75+
// ToApp is called for outgoing application messages.
76+
func (e *Executor) ToApp(msg *quickfix.Message, sessionID quickfix.SessionID) error { return nil }
77+
78+
// FromAdmin is called for incoming admin messages.
79+
func (e *Executor) FromAdmin(msg *quickfix.Message, sessionID quickfix.SessionID) quickfix.MessageRejectError {
80+
return nil
81+
}
82+
83+
// FromApp is called for incoming application messages and routes them.
84+
func (e *Executor) FromApp(msg *quickfix.Message, sessionID quickfix.SessionID) quickfix.MessageRejectError {
85+
return e.Route(msg, sessionID)
86+
}
87+
88+
func (e *Executor) onNewOrderSingle(msg newordersingle.NewOrderSingle, sessionID quickfix.SessionID) quickfix.MessageRejectError {
89+
clOrdID, err := msg.GetClOrdID()
90+
if err != nil {
91+
return err
92+
}
93+
94+
symbol, err := msg.GetSymbol()
95+
if err != nil {
96+
return err
97+
}
98+
99+
side, err := msg.GetSide()
100+
if err != nil {
101+
return err
102+
}
103+
104+
orderQty, err := msg.GetOrderQty()
105+
if err != nil {
106+
return err
107+
}
108+
109+
log.Printf("Received NewOrderSingle: ClOrdID=%s Symbol=%s Side=%s Qty=%s",
110+
clOrdID, symbol, string(side), orderQty.String())
111+
112+
// Look up FX rate; default to 1.0000 for unknown pairs.
113+
price, ok := fxRates[symbol]
114+
if !ok {
115+
price = decimal.NewFromFloat(1.0000)
116+
log.Printf("Unknown symbol %s, using default rate 1.0000", symbol)
117+
}
118+
119+
orderID := e.genOrderID()
120+
zero := decimal.NewFromInt(0)
121+
122+
// --- ACK (New) ---
123+
ack := fix44er.New(
124+
field.NewOrderID(orderID),
125+
field.NewExecID(e.genExecID()),
126+
field.NewExecType(enum.ExecType_NEW),
127+
field.NewOrdStatus(enum.OrdStatus_NEW),
128+
field.NewSide(side),
129+
field.NewLeavesQty(orderQty, 2),
130+
field.NewCumQty(zero, 2),
131+
field.NewAvgPx(zero, 2),
132+
)
133+
ack.Set(field.NewClOrdID(clOrdID))
134+
ack.Set(field.NewSymbol(symbol))
135+
ack.Set(field.NewOrderQty(orderQty, 2))
136+
137+
if sendErr := quickfix.SendToTarget(ack.ToMessage(), sessionID); sendErr != nil {
138+
log.Printf("Error sending ACK: %v", sendErr)
139+
} else {
140+
log.Printf("Sent ACK for OrderID=%s", orderID)
141+
}
142+
143+
// --- FILL ---
144+
fill := fix44er.New(
145+
field.NewOrderID(orderID),
146+
field.NewExecID(e.genExecID()),
147+
field.NewExecType(enum.ExecType_TRADE),
148+
field.NewOrdStatus(enum.OrdStatus_FILLED),
149+
field.NewSide(side),
150+
field.NewLeavesQty(zero, 2),
151+
field.NewCumQty(orderQty, 2),
152+
field.NewAvgPx(price, 4),
153+
)
154+
fill.Set(field.NewClOrdID(clOrdID))
155+
fill.Set(field.NewSymbol(symbol))
156+
fill.Set(field.NewOrderQty(orderQty, 2))
157+
fill.Set(field.NewLastQty(orderQty, 2))
158+
fill.Set(field.NewLastPx(price, 4))
159+
160+
if sendErr := quickfix.SendToTarget(fill.ToMessage(), sessionID); sendErr != nil {
161+
log.Printf("Error sending FILL: %v", sendErr)
162+
} else {
163+
log.Printf("Sent FILL for OrderID=%s at %s", orderID, price.String())
164+
}
165+
166+
return nil
167+
}
168+
169+
// Run starts the FIX acceptor with the given config file path and blocks until interrupted.
170+
func Run(cfgFileName string) error {
171+
cfg, err := os.Open(cfgFileName)
172+
if err != nil {
173+
return fmt.Errorf("open config: %w", err)
174+
}
175+
defer func(cfg *os.File) {
176+
err := cfg.Close()
177+
if err != nil {
178+
log.Printf("Error closing config file: %v", err)
179+
}
180+
}(cfg)
181+
182+
appSettings, err := quickfix.ParseSettings(cfg)
183+
if err != nil {
184+
return fmt.Errorf("parse settings: %w", err)
185+
}
186+
187+
app := newExecutor()
188+
storeFactory := quickfix.NewMemoryStoreFactory()
189+
logFactory := screen.NewLogFactory()
190+
191+
acceptor, err := quickfix.NewAcceptor(app, storeFactory, appSettings, logFactory)
192+
if err != nil {
193+
return fmt.Errorf("create acceptor: %w", err)
194+
}
195+
196+
if err := acceptor.Start(); err != nil {
197+
return fmt.Errorf("start acceptor: %w", err)
198+
}
199+
log.Printf("FIX acceptor started on port 9880, waiting for connections...")
200+
201+
sig := make(chan os.Signal, 1)
202+
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
203+
<-sig
204+
205+
log.Println("Shutting down...")
206+
acceptor.Stop()
207+
return nil
208+
}

dummy-executor/config/executor.cfg

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[DEFAULT]
2+
SocketAcceptPort=9880
3+
SenderCompID=dummy-acceptor
4+
TargetCompID=dummy-initiator
5+
ResetOnLogon=Y
6+
ResetOnLogout=Y
7+
ResetOnDisconnect=Y
8+
9+
[SESSION]
10+
BeginString=FIX.4.4

dummy-executor/go.mod

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
module dummy-executor
2+
3+
go 1.25.6
4+
5+
require (
6+
github.com/quickfixgo/enum v0.1.0
7+
github.com/quickfixgo/field v0.1.0
8+
github.com/quickfixgo/fix44 v0.1.0
9+
github.com/quickfixgo/quickfix v0.9.7
10+
github.com/shopspring/decimal v1.4.0
11+
)
12+
13+
require (
14+
github.com/pires/go-proxyproto v0.7.0 // indirect
15+
github.com/pkg/errors v0.9.1 // indirect
16+
github.com/quagmt/udecimal v1.8.0 // indirect
17+
github.com/quickfixgo/tag v0.1.0 // indirect
18+
golang.org/x/net v0.24.0 // indirect
19+
)

dummy-executor/go.sum

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
4+
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
5+
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
6+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
7+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
8+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
9+
github.com/quagmt/udecimal v1.8.0 h1:d4MJNGb/dg8r03AprkeSiDlVKtkZnL10L3de/YGOiiI=
10+
github.com/quagmt/udecimal v1.8.0/go.mod h1:ScmJ/xTGZcEoYiyMMzgDLn79PEJHcMBiJ4NNRT3FirA=
11+
github.com/quickfixgo/enum v0.1.0 h1:TnCPOqxAWA5/IWp7lsvj97x7oyuHYgj3STBJlBzZGjM=
12+
github.com/quickfixgo/enum v0.1.0/go.mod h1:65gdG2/8vr6uOYcjZBObVHMuTEYc5rr/+aKVWTrFIrQ=
13+
github.com/quickfixgo/field v0.1.0 h1:JVO6fVD6Nkyy8e/ROYQtV/nQhMX/BStD5Lq7XIgYz2g=
14+
github.com/quickfixgo/field v0.1.0/go.mod h1:Zu0qYmpj+gljlB2HgpUt9EcTIThs2lIQb8C57qbJr8o=
15+
github.com/quickfixgo/fix44 v0.1.0 h1:g/rTl6mXDlG7iIMbY7zaPbHcj9N/B+tteOZ01yGzeSQ=
16+
github.com/quickfixgo/fix44 v0.1.0/go.mod h1:d6Ia02Eq/JYgKCn/2V9FHxguAl1Alp/yu/xVpry82dA=
17+
github.com/quickfixgo/quickfix v0.9.7 h1:vvx/cydUG6cnGDyYeUxKA5MTzbYFQulthdTHzmmsvmc=
18+
github.com/quickfixgo/quickfix v0.9.7/go.mod h1:LpvubslWDsNapeQDvhYS2Qty9gJtm2vr/gSdUcpdEwU=
19+
github.com/quickfixgo/tag v0.1.0 h1:R2A1Zf7CBE903+mOQlmTlfTmNZQz/yh7HunMbgcsqsA=
20+
github.com/quickfixgo/tag v0.1.0/go.mod h1:l/drB1eO3PwN9JQTDC9Vt2EqOcaXk3kGJ+eeCQljvAI=
21+
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
22+
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
23+
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
24+
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
25+
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
26+
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
27+
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
28+
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
29+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
30+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)