FIXEdge Java Trade Capture solution
Trade Capture solution in FIXEdge Java is available starting from version 1.13.0.
ICE Trade Capture setup (root rule template)
This document provides a copy/paste root rule template to run the ICE integration using:
container/src/main/dist/conf/ICECore_extension.groovy(session orchestration + subscription sequencing)container/src/main/dist/conf/ICEPersist_extension.groovy(optional JDBC persistence)
Prerequisites
FIX session named
ICEis configured and can connect. A sample initiator is shipped ascontainer/src/main/dist/conf/session/s_fix_ICE.properties(adjust host/credentials for your environment);schedules.xmlmust include<schedule id="ICE">so FEJ starts it on load.If you want persistence, the datasource name you reference (example:
ice_datasource) must be configured inhistory.properties.
Root rule template
Use a Groovy package that matches your deployment (example: configuration.icetcinfej.businessrules). sessionIds must list every FIX session id (comma-separated) that runs ICE trade capture; it drives ICECore_extension, ICEPersist_extension (async consumer JDBC resolution), and the connect event below.
package configuration.icetcinfej.businessrules
import com.epam.fej.routing.RoutingContext
import static dsl.CommonRulesDsl.rulesDSL
rulesDSL(routingContext as RoutingContext) {
// --- Persistence wiring (optional, but recommended) ---
// Declares a JDBC repository name used by ICEPersist_extension.groovy.
histories {
db("ice_datasource") {}
}
config {
// FIX session ids (comma-separated). Same list drives ICECore + ICEPersist (and should match s_fix_<id>.properties + schedules).
set "sessionIds" value "ICE"
// Example second session: set "sessionIds" value "ICE,ICE1"
// --- Security Definition Request (35=c, 321=3): "<SecurityID>:<CFICode>" (48 + 461) ---
set "ICE.secDefSettings" value (["5:FXXXXX", "0:OXXXXX", "1:OXXXXX"])
// Per-session copy when using multiple ICE sessions (ICECore reads "<sessionId>.secDefSettings"):
// set "ICE1.secDefSettings" value (["5:FXXXXX", "0:OXXXXX"])
// --- UDS (35=c, 321=101): "<SecurityID>:<anything>" (only the part before ':' is sent as 48) ---
set "ICE.udsSettings" value (["5:FXXXXX", "0:OXXXXX", "1:OXXXXX"])
// set "ICE1.udsSettings" value ([])
// JDBC: per-session first, then shared fallback (ICEPersist_extension.groovy).
set "ICE.datasource" value "ice_datasource"
// set "ICE1.datasource" value "ice_datasource"
// Persist path for ICECore (UDS/SecDef daily flags, AE dedup, last report time).
set "persistPath" value "./logs/persist"
// Optional: async JDBC via JMS — set per FIX session id (not one flag for all sessions).
set "ICE.persist.async" value true
set "ICE1.persist.async" value true
}
extension {
set "**/ICECore_extension.groovy;**/ICEPersist_extension.groovy"
}
eventRules {
// session(...) uses full-regex match on session id; align with sessionIds (single: "ICE", two: "(ICE|ICE1)").
FixSessionStateEventRule("On ICE session(s) connected") {
condition {
session "ICE"
sessionState connected
}
action {
log "On ICE session connected"
}
}
}
messageRules {
messageRule("ICE observe: Security Definition response (35=d)") {
source { id "ICE" }
condition { msgType "d" }
action { log "On d message received" }
}
messageRule("ICE observe: UDS (msgType UDS)") {
source { id "ICE" }
condition { msgType "UDS" }
action { log "On UDS message received" }
}
messageRule("ICE observe: Trade Capture Report (35=AE) + dedup check") {
source { id "ICE" }
condition { msgType "AE" }
action {
log "On AE message received"
custom { ctx ->
def duplicated = RoutingContext.isTrdCapRepDuplicated(ctx.sourceParams.id, ctx.message, routingContext)
if (duplicated) {
log "AE is duplicated (skipping custom processing)"
}
}
}
}
}
}Multi-session observe / events: when sessionIds is ICE,ICE1, set the same alternation everywhere you filter by source, e.g. session "(ICE|ICE1)" and source { id "(ICE|ICE1)" } (regex is full match on the session id).
Local ActiveMQ (optional, for JMS / async persist)
Download and run ActiveMQ from https://activemq.apache.org/components/classic/download/ (e.g., bin\activemq.bat start).
Start a local ActiveMQ instance.
ActiveMQ exposes OpenWire on localhost:61616 and a web console on port 8161.
Async JDBC persist (JMS queue)
Async offload is per FIX session: set <sessionId>.persist.async to true for each session that should enqueue (examples: ICE.persist.async, ICE1.persist.async). There is no single global flag that applies to every id in sessionIds.
How to set up async mode (checklist)
Do these in order before relying on async persist in production.
JDBC persistence still required
Async only changes where saves run (off the FIX thread). You still need:histories { db("ice_datasource") {} }(or your repository name) in the root rule.ICE.datasourceand/or<sessionId>.datasourcepointing at that repository name (see Notes below).The JDBC URL/user/password for that repository in
history.properties(or your deployment’s history config), same as synchronous ICE persist.
JMS broker running
Start ActiveMQ (see Local ActiveMQ above) or any broker compatible withjms-adaptor.propertiesConnection1. By defaultProviderURIistcp://localhost:61616. Adjustjms.adaptor.Connection.Connection1.*if your broker URL or credentials differ.Register ICE persist JMS clients
Incontainer/src/main/dist/conf/jms-adaptor.properties:Set
jms.adaptor.ClientNamesto include both endpoints, for example:jms.adaptor.ClientNames=ICE_PERSIST_IN, ICE_PERSIST_OUT
Remove or avoid leaving
ClientNamesempty while async is enabled in rules - otherwise enqueue toICE_PERSIST_OUTwill not work.Update DeliveryMode and TimeToLive parameters to handle many messages without loss:
jms.adaptor.Client.ICE_PERSIST_OUT.DeliveryMode = Persist jms.adaptor.Client.ICE_PERSIST_OUT.TimeToLive = 43200000 jms.adaptor.Client.ICE_PERSIST_IN.DeliveryMode = Persist jms.adaptor.Client.ICE_PERSIST_IN.TimeToLive = 43200000
The file already defines ICE_PERSIST_OUT (producer) and ICE_PERSIST_IN (consumer) on queue ICE.PERSIST.JDBC with MessageType = Bytes (raw FIX bytes). See the block starting at “ICE async persist queue” in the same file.
Start JMS schedules with FEJ
Incontainer/src/main/dist/conf/schedules.xml, uncomment theICE_PERSIST_INandICE_PERSIST_OUTschedules (the block commented with “ICE async JDBC persist”). Both must startonLoad="true"so the producer and consumer connect when FEJ loads.Turn on async per FIX session in root rules
In your root ruleconfig { ... }(same package as Root rule template above), for each FIX session id that should offload JDBC:set "ICE.persist.async" value true // set "ICE1.persist.async" value trueMatch
sessionIds: every session listed there can have its own<sessionId>.persist.async. Sessions you omit or set tofalsekeep synchronous JDBC on the FIX thread.Reload / restart
Restart FEJ (or reload configuration per your ops process) sojms-adaptor.properties,schedules.xml, and business rules are picked up together.
Flow: for a session with ICE.persist.async=true (or ICE1.persist.async=true, etc.), inbound FIX on that session → ICEPersist_extension.groovy enqueues raw bytes to ICE_PERSIST_OUT → queue ICE.PERSIST.JDBC → ICE_PERSIST_IN → same entity mapping and JdbcRepository.save() as in synchronous mode. The JMS consumer session id is internal (ICE_PERSIST_IN); it is not the FIX session id.
Datasource resolution for the async consumer: for messages that originated on a FIX session, JDBC uses that session’s <sessionId>.datasource if set; otherwise ICE.datasource. For work pulled from the queue without a prior FIX context, ICEPersist_extension.groovy resolves JDBC by scanning sessionIds in order, then ICE.datasource (see class comment on IcePersistAsyncConfig in ICEPersist_extension.groovy).
Default: each <sessionId>.persist.async is unset or false → that session persists on the FIX thread (unless the message was already produced by the async path).
Quick disable: set each <sessionId>.persist.async to false or remove those lines; optionally clear ICE_PERSIST_OUT / ICE_PERSIST_IN from jms.adaptor.ClientNames and comment the ICE_PERSIST_* schedules again so FEJ does not hold JMS connections.
Notes
Package and imports in the template match a typical business-rules module; adjust
packageto your project layout.RoutingContextis required forisTrdCapRepDuplicatedin the samplemessageRule.Per-session async JDBC:
ICEPersist_extension.groovyreadsICE.persist.asynconly for FIX sessionICE,ICE1.persist.asyncforICE1, and so on. Mixed setups (one session async, another sync) are supported.ICE.secDefSettingsmust include CFICode (tag 461) because ICE requires it for outgoing Security Definition Request (35=cwith321=3).Persistence is controlled by per-session
<sessionId>.datasource, with optional shared fallbackICE.datasource(checked after the per-session property):if it’s set,
ICEPersist_extension.groovysaves mapped entities to the JDBC repository.if it’s not set, persistence rules will skip saving.
Turning off async JDBC offload: see Quick disable under Async JDBC persist (JMS queue) above (same steps as disabling
ICE_PERSIST_*clients and schedules).How to turn off JDBC saving:
Option 1 (recommended): remove (or comment out)
ICE.datasourceand any<sessionId>.datasourcefrom your root rule config.ICEPersist_extension.groovywill keep running but will not write anything.Option 2: remove
ICEPersist_extension.groovyfrom theextension { ... }list if you don’t want persistence rules loaded at all.
The ICE core extension stores lightweight state in
persistPathto avoid repeating UDS/SecDef requests every day and to support AE dedup.
Where isTrdCapRepDuplicated cache is stored (and how to clean it)
RoutingContext.isTrdCapRepDuplicated(...) is provided by ICECore_extension.groovy and checks whether the current AE report was already seen. Internally it stores a dedup key in the routing context persistence map.
Storage location:
persistPath/<sessionId>_<weekBucket>(example:./logs/persist/ICE_2871)The
<weekBucket>is computed byweeksSinceEpochLegacy()(roughly “current epoch week”).
On-disk format: FEJ persistence maps are backed by an embedded file DB. Expect files like:
<alias>.data,<alias>.script,<alias>.properties,<alias>.logThey are created under the alias path derived from
persistPath.
Why it can grow: every unique
AEadds a new dedup key entry; long-running sessions with high AE volume will increase disk usage underpersistPath.
Cleanup recommendation (disk space)
Rotate by age: keep only the last N week buckets per session (example: last 2–4 weeks) under
persistPath, delete olderICE_<weekBucket>*folders/files.Planned maintenance: perform cleanup during a maintenance window (best-effort: stop FEJ or disconnect the session) to avoid deleting files that are currently open.
If you need to force refresh / clear cache: delete the current week bucket for the session (example:
./logs/persist/ICE_2871*). This will reset:AE dedup cache
last trade capture timestamp
“UDS/SecDef already refreshed today” flags