Skip to main content

netget/client/fix/
actions.rs

1use crate::llm::actions::{
2    client_trait::{Client, ClientActionResult},
3    protocol_trait::Protocol,
4    ActionDefinition, ParameterDefinition,
5};
6use crate::protocol::{ConnectContext, EventType};
7use crate::server::fix::actions::{action, check_answer, logout, parameter, send};
8use anyhow::{bail, Result};
9use serde_json::{json, Value};
10use std::sync::LazyLock;
11
12#[derive(Default)]
13pub struct FixClientProtocol;
14impl FixClientProtocol {
15    pub fn new() -> Self {
16        Self
17    }
18}
19
20fn disconnect() -> ActionDefinition {
21    action(
22        "disconnect",
23        "Log out and close the connection without waiting for the counterparty's Logout",
24        vec![],
25        json!({"type": "disconnect"}),
26    )
27}
28fn actions() -> Vec<ActionDefinition> {
29    vec![send(), logout(), disconnect()]
30}
31
32pub static LOGGED_ON_EVENT: LazyLock<EventType> = LazyLock::new(|| {
33    EventType::new(
34        "fix_logged_on",
35        "The acceptor answered the Logon; the session is up",
36        send().example.clone(),
37    )
38    .with_parameters(vec![
39        parameter(
40            "sender_comp_id",
41            "string",
42            "This initiator's SenderCompID",
43            true,
44        ),
45        parameter("target_comp_id", "string", "The acceptor's CompID", true),
46        parameter("heartbeat_secs", "number", "The agreed HeartBtInt", true),
47    ])
48    .with_actions(actions())
49});
50pub static MESSAGE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
51    EventType::new(
52        "fix_message",
53        "An in-sequence application message from the acceptor",
54        json!({"type": "fix_logout"}),
55    )
56    .with_parameters(vec![
57        parameter("msg_type", "string", "MsgType code, e.g. 8", true),
58        parameter(
59            "msg_type_name",
60            "string",
61            "MsgType name, e.g. ExecutionReport",
62            true,
63        ),
64        parameter(
65            "seq",
66            "number",
67            "The message's MsgSeqNum (34) in this session, which a BusinessMessageReject refers to",
68            true,
69        ),
70        parameter(
71            "fields",
72            "array",
73            "Body fields in wire order: [{tag, name, value}]",
74            true,
75        ),
76    ])
77    .with_actions(actions())
78});
79pub static LOGGED_OUT_EVENT: LazyLock<EventType> = LazyLock::new(|| {
80    EventType::new(
81        "fix_logged_out",
82        "The session ended",
83        json!({"type": "disconnect"}),
84    )
85    .with_parameters(vec![parameter(
86        "reason",
87        "string",
88        "Why: the counterparty's Logout text, a sequence problem or a heartbeat timeout",
89        true,
90    )])
91    .with_actions(vec![disconnect()])
92});
93
94impl Protocol for FixClientProtocol {
95    fn protocol_name(&self) -> &'static str {
96        "FIX"
97    }
98    fn stack_name(&self) -> &'static str {
99        "ETH>IP>TCP>FIX"
100    }
101    fn keywords(&self) -> Vec<&'static str> {
102        vec![
103            "fix",
104            "fix protocol",
105            "initiator",
106            "order entry",
107            "new order single",
108        ]
109    }
110    fn description(&self) -> &'static str {
111        "FIX 4.x initiator: logs on, keeps the session (sequence numbers, resend and gap fill, heartbeats) and sends application messages"
112    }
113    fn get_async_actions(&self, _: &crate::state::app_state::AppState) -> Vec<ActionDefinition> {
114        actions()
115    }
116    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
117        vec![]
118    }
119    fn get_event_types(&self) -> Vec<EventType> {
120        vec![
121            LOGGED_ON_EVENT.clone(),
122            MESSAGE_EVENT.clone(),
123            LOGGED_OUT_EVENT.clone(),
124        ]
125    }
126    fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
127        let p =
128            |name: &str, kind: &str, description: &str, example: Value, default: Option<Value>| {
129                ParameterDefinition {
130                    name: name.into(),
131                    type_hint: kind.into(),
132                    description: description.into(),
133                    required: false,
134                    example,
135                    default,
136                }
137            };
138        vec![
139            p(
140                "sender_comp_id",
141                "string",
142                "This initiator's SenderCompID",
143                json!("BUYSIDE"),
144                Some(json!(crate::server::fix::DEFAULT_COMP_ID)),
145            ),
146            p(
147                "target_comp_id",
148                "string",
149                "The acceptor's CompID",
150                json!("EXCHANGE"),
151                Some(json!(super::DEFAULT_TARGET)),
152            ),
153            p(
154                "begin_string",
155                "string",
156                "FIX.4.0 to FIX.4.4",
157                json!("FIX.4.2"),
158                Some(json!(crate::server::fix::DEFAULT_BEGIN_STRING)),
159            ),
160            p(
161                "heartbeat_secs",
162                "integer",
163                "HeartBtInt to propose (0 to 3600)",
164                json!(10),
165                Some(json!(super::DEFAULT_HEARTBEAT.as_secs())),
166            ),
167            p(
168                "reset_seq_num",
169                "boolean",
170                "Ask the acceptor to reset sequence numbers (141=Y)",
171                json!(true),
172                Some(json!(true)),
173            ),
174            p(
175                "username",
176                "string",
177                "Username (553) for the Logon",
178                json!("trader"),
179                None,
180            ),
181            p(
182                "password",
183                "string",
184                "Password (554) for the Logon",
185                json!("secret"),
186                None,
187            ),
188        ]
189    }
190    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
191        use crate::protocol::metadata::*;
192        ProtocolMetadataV2::builder()
193            .state(DevelopmentState::Experimental)
194            .privilege_requirement(PrivilegeRequirement::None)
195            .implementation("The FIX codec and session layer the acceptor uses, as initiator")
196            .llm_control("Which application messages to send and how to react to the acceptor's")
197            .e2e_testing("tests/client/fix: QuickFIX/Go 0.9.12 (independent, validating against FIX44.xml) as acceptor answers a NewOrderSingle with an ExecutionReport and an OrderCancelRequest with a BusinessMessageReject; logout")
198            .notes("One session per client, in memory. No FIXT.1.1/FIX 5.0 and no reconnect.")
199            .max_inbound_bytes(crate::server::fix::codec::MAX_BODY)
200            .build()
201    }
202    fn example_prompt(&self) -> &'static str {
203        "Log on to the FIX acceptor at 127.0.0.1:9876 as BUYSIDE and buy 100 AAPL at 150"
204    }
205    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
206        let llm = json!({"type":"open_client","protocol":"fix","remote_addr":"127.0.0.1:9876","instruction":"Buy 100 AAPL at 150","startup_params":{"sender_comp_id":"BUYSIDE","target_comp_id":"EXCHANGE"}});
207        let mut static_example = llm.clone();
208        static_example["event_handlers"] = json!([
209            {"event_pattern":"fix_logged_on","handler":{"type":"static","actions":[{"type":"fix_send","msg_type":"NewOrderSingle","fields":[{"name":"ClOrdID","value":"1"},{"name":"HandlInst","value":"1"},{"name":"Symbol","value":"AAPL"},{"name":"Side","value":"1"},{"name":"TransactTime","value":"20261001-12:00:00.000"},{"name":"OrderQty","value":"100"},{"name":"OrdType","value":"2"},{"name":"Price","value":"150"}]}]}},
210            {"event_pattern":"fix_message","handler":{"type":"static","actions":[]}}
211        ]);
212        let mut scripted = static_example.clone();
213        scripted["event_handlers"][1]["handler"] = json!({"type":"script","language":"python","code":"import json,sys\ne=json.load(sys.stdin)['event']\nprint(json.dumps({'actions':[{'type':'fix_logout','text':'done'}] if e['msg_type']=='8' else []}))"});
214        crate::llm::actions::StartupExamples::new(llm, scripted, static_example)
215    }
216    fn group_name(&self) -> &'static str {
217        "Application"
218    }
219}
220
221impl Client for FixClientProtocol {
222    fn connect(
223        &self,
224        ctx: ConnectContext,
225    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
226    {
227        Box::pin(super::connect(ctx))
228    }
229    fn execute_action(&self, v: Value) -> Result<ClientActionResult> {
230        match v["type"].as_str() {
231            Some("disconnect") => return Ok(ClientActionResult::Disconnect),
232            Some("fix_send" | "fix_logout") => check_answer(&v)?,
233            _ => bail!("Unknown FIX client action"),
234        }
235        Ok(ClientActionResult::Custom {
236            name: v["type"].as_str().unwrap_or_default().into(),
237            data: v,
238        })
239    }
240}