nautilus_kraken/execution/
mod.rs1mod futures;
35mod spot;
36
37pub use futures::KrakenFuturesExecutionClient;
38use nautilus_live::execution::failure::CommandFailure;
39pub use spot::KrakenSpotExecutionClient;
40
41use crate::{
42 common::enums::KrakenApiResult,
43 http::{
44 error::{
45 KrakenBatchOrderError, KrakenHttpError, KrakenModifyOrderError, KrakenSubmitOrderError,
46 },
47 futures::client::{FuturesBatchSubmitItem, is_futures_submit_rejection},
48 spot::models::SpotBatchOrderResponse,
49 },
50};
51
52fn command_failure_from_submit_error(error: &anyhow::Error) -> CommandFailure {
53 for cause in error.chain() {
54 if let Some(error) = cause.downcast_ref::<KrakenSubmitOrderError>() {
55 return match error {
56 KrakenSubmitOrderError::Rejected { reason } => {
57 CommandFailure::venue_rejected(reason)
58 }
59 KrakenSubmitOrderError::MissingStatus
60 | KrakenSubmitOrderError::UnknownStatus { .. }
61 | KrakenSubmitOrderError::MissingOrderId { .. }
62 | KrakenSubmitOrderError::PostSubmitLookup { .. } => {
63 CommandFailure::ambiguous(error.to_string())
64 }
65 };
66 }
67 }
68
69 command_failure_from_order_error(error, true)
70}
71
72fn command_failure_from_modify_error(error: &anyhow::Error) -> CommandFailure {
73 for cause in error.chain() {
74 if let Some(error) = cause.downcast_ref::<KrakenModifyOrderError>() {
75 return match error {
76 KrakenModifyOrderError::Rejected { reason } => {
77 CommandFailure::venue_rejected(reason)
78 }
79 KrakenModifyOrderError::UnknownStatus { .. }
80 | KrakenModifyOrderError::MissingOrderId => {
81 CommandFailure::ambiguous(error.to_string())
82 }
83 };
84 }
85 }
86
87 command_failure_from_order_error(error, true)
88}
89
90fn command_failure_from_spot_batch_error(error: &anyhow::Error) -> CommandFailure {
91 command_failure_from_batch_error(error, true)
92}
93
94fn command_failure_from_futures_batch_error(error: &anyhow::Error) -> CommandFailure {
95 command_failure_from_batch_error(error, false)
96}
97
98fn command_failure_from_batch_error(
99 error: &anyhow::Error,
100 whole_order_rejection: bool,
101) -> CommandFailure {
102 for cause in error.chain() {
103 if let Some(error) = cause.downcast_ref::<KrakenBatchOrderError>() {
104 return match error {
105 KrakenBatchOrderError::Validation { .. } | KrakenBatchOrderError::NotAttempted => {
106 CommandFailure::not_sent(error.to_string())
107 }
108 KrakenBatchOrderError::ResponseCount { .. }
109 | KrakenBatchOrderError::MissingResponse { .. }
110 | KrakenBatchOrderError::DuplicateResponse { .. } => {
111 CommandFailure::ambiguous(error.to_string())
112 }
113 };
114 }
115 }
116
117 command_failure_from_order_error(error, whole_order_rejection)
118}
119
120fn command_failure_from_order_error(
121 error: &anyhow::Error,
122 order_api_rejection: bool,
123) -> CommandFailure {
124 let reason = error.to_string();
125
126 for cause in error.chain() {
127 let Some(error) = cause.downcast_ref::<KrakenHttpError>() else {
128 continue;
129 };
130
131 return match error {
132 KrakenHttpError::RequestNotStarted(_) | KrakenHttpError::MissingCredentials => {
133 CommandFailure::not_sent(reason)
134 }
135 KrakenHttpError::ApiError(errors)
136 if order_api_rejection && contains_spot_order_rejection(errors) =>
137 {
138 CommandFailure::venue_rejected(format_api_errors(errors))
139 }
140 KrakenHttpError::NetworkError(_)
141 | KrakenHttpError::ApiError(_)
142 | KrakenHttpError::ParseError(_)
143 | KrakenHttpError::AuthenticationError(_) => CommandFailure::ambiguous(reason),
144 };
145 }
146
147 CommandFailure::not_sent(reason)
148}
149
150fn command_failure_from_spot_batch_item(
151 item: SpotBatchOrderResponse,
152) -> Result<(), CommandFailure> {
153 match (item.txid, item.error) {
154 (Some(_), None) => Ok(()),
155 (None, Some(reason)) => Err(CommandFailure::venue_rejected(reason)),
156 (Some(_), Some(_)) => Err(CommandFailure::ambiguous(
157 "Batch item response contained both a transaction ID and an error",
158 )),
159 (None, None) => Err(CommandFailure::ambiguous(
160 "Batch item response had no transaction ID or error",
161 )),
162 }
163}
164
165fn command_failure_from_futures_batch_item(
166 item: FuturesBatchSubmitItem,
167) -> Result<(), CommandFailure> {
168 let status = item.status.status;
169
170 if item.result != KrakenApiResult::Success {
171 return if is_futures_submit_rejection(&status) {
172 Err(CommandFailure::venue_rejected(status))
173 } else {
174 Err(CommandFailure::ambiguous(format!(
175 "Batch response reported an error with item status: {status}"
176 )))
177 };
178 }
179
180 match status.as_str() {
181 "placed" | "filled" => Ok(()),
182 reason if is_futures_submit_rejection(reason) => {
183 Err(CommandFailure::venue_rejected(reason))
184 }
185 "" => Err(CommandFailure::ambiguous("Empty batch item status")),
186 reason => Err(CommandFailure::ambiguous(format!(
187 "Unknown batch item status: {reason}"
188 ))),
189 }
190}
191
192fn command_failure_from_cancel_error(error: KrakenHttpError) -> CommandFailure {
193 match error {
194 KrakenHttpError::RequestNotStarted(message) => CommandFailure::not_sent(message),
195 KrakenHttpError::AuthenticationError(message) => CommandFailure::not_sent(message),
196 KrakenHttpError::MissingCredentials => CommandFailure::not_sent("Missing credentials"),
197 KrakenHttpError::NetworkError(message) | KrakenHttpError::ParseError(message) => {
198 CommandFailure::ambiguous(message)
199 }
200 KrakenHttpError::ApiError(message) => {
201 CommandFailure::ambiguous(format_cancel_api_errors(&message))
202 }
203 }
204}
205
206fn command_failure_from_spot_cancel_error(error: KrakenHttpError) -> CommandFailure {
207 match error {
208 KrakenHttpError::ApiError(message) if contains_spot_cancel_rejection(&message) => {
209 CommandFailure::venue_rejected(format_cancel_api_errors(&message))
210 }
211 KrakenHttpError::ApiError(message) => {
212 CommandFailure::ambiguous(format_cancel_api_errors(&message))
213 }
214 other => command_failure_from_cancel_error(other),
215 }
216}
217
218fn contains_spot_order_rejection(errors: &[String]) -> bool {
219 errors.iter().any(|e| e.trim_start().starts_with("EOrder:"))
220}
221
222fn contains_spot_cancel_rejection(errors: &[String]) -> bool {
223 contains_spot_order_rejection(errors)
224}
225
226fn format_api_errors(errors: &[String]) -> String {
227 if errors.is_empty() {
228 "unknown error (empty error list)".to_string()
229 } else {
230 errors.join(", ")
231 }
232}
233
234fn format_cancel_api_errors(errors: &[String]) -> String {
235 format_api_errors(errors)
236}
237
238#[cfg(test)]
239mod tests {
240 use rstest::rstest;
241
242 use super::*;
243
244 #[rstest]
245 fn test_post_submit_phase_overrides_nested_not_sent_error() {
246 let error = anyhow::Error::new(KrakenSubmitOrderError::PostSubmitLookup {
247 source: KrakenHttpError::RequestNotStarted("lookup was not sent".to_string()).into(),
248 });
249
250 assert_eq!(
251 command_failure_from_submit_error(&error),
252 CommandFailure::Ambiguous(
253 "Order lookup failed after submission: Request not started: lookup was not sent"
254 .to_string()
255 )
256 );
257 }
258
259 #[rstest]
260 fn test_request_not_started_is_not_sent() {
261 let error = anyhow::Error::new(KrakenHttpError::RequestNotStarted(
262 "request encoding failed".to_string(),
263 ));
264
265 assert_eq!(
266 command_failure_from_submit_error(&error),
267 CommandFailure::NotSent("Request not started: request encoding failed".to_string())
268 );
269 }
270}