nautilus_kraken/execution/
mod.rs1mod futures;
35mod spot;
36
37pub use futures::KrakenFuturesExecutionClient;
38use nautilus_common::{cache::Cache, messages::execution::GenerateOrderStatusReport};
39use nautilus_core::UnixNanos;
40use nautilus_live::execution::failure::CommandFailure;
41use nautilus_model::{
42 identifiers::VenueOrderId,
43 orders::{Order, OrderAny},
44 reports::OrderStatusReport,
45};
46pub use spot::KrakenSpotExecutionClient;
47
48use crate::{
49 common::enums::KrakenApiResult,
50 http::{
51 coverage::ReportCoverage,
52 error::{
53 KrakenBatchOrderError, KrakenHttpError, KrakenModifyOrderError, KrakenSubmitOrderError,
54 },
55 futures::client::{FuturesBatchSubmitItem, is_futures_submit_rejection},
56 spot::models::SpotBatchOrderResponse,
57 },
58};
59
60fn cached_order_for_status_command(
63 cache: &Cache,
64 cmd: &GenerateOrderStatusReport,
65) -> Option<OrderAny> {
66 if let Some(client_order_id) = cmd.client_order_id {
67 return cache.order(&client_order_id).map(|o| o.clone());
68 }
69
70 let venue_order_id = cmd.venue_order_id?;
71 let client_order_id = *cache.client_order_id(&venue_order_id)?;
72 cache.order(&client_order_id).map(|o| o.clone())
73}
74
75fn absent_unless_unread(
79 coverage: &ReportCoverage,
80 venue_order_id: Option<VenueOrderId>,
81 placed_from: Option<UnixNanos>,
82 order: &str,
83) -> anyhow::Result<Option<OrderStatusReport>> {
84 if coverage.rules_out(venue_order_id, placed_from) {
85 return Ok(None);
86 }
87 anyhow::bail!(
88 "No report for order {order} in an order read that does not rule it out; deferring rather \
89 than reporting it missing"
90 )
91}
92
93fn status_command_label(
95 cmd: &GenerateOrderStatusReport,
96 venue_order_id: Option<VenueOrderId>,
97) -> String {
98 match (cmd.client_order_id, venue_order_id) {
99 (Some(client_order_id), _) => client_order_id.to_string(),
100 (None, Some(venue_order_id)) => venue_order_id.to_string(),
101 (None, None) => "with neither ID".to_string(),
102 }
103}
104
105fn earliest_venue_time(order: &OrderAny) -> UnixNanos {
108 let created = order.ts_init();
109 order
110 .ts_accepted()
111 .map_or(created, |accepted| accepted.min(created))
112}
113
114fn command_failure_from_submit_error(error: &anyhow::Error) -> CommandFailure {
115 for cause in error.chain() {
116 if let Some(error) = cause.downcast_ref::<KrakenSubmitOrderError>() {
117 return match error {
118 KrakenSubmitOrderError::Rejected { reason } => {
119 CommandFailure::venue_rejected(reason)
120 }
121 KrakenSubmitOrderError::MissingStatus
122 | KrakenSubmitOrderError::UnknownStatus { .. }
123 | KrakenSubmitOrderError::MissingOrderId { .. }
124 | KrakenSubmitOrderError::PostSubmitLookup { .. } => {
125 CommandFailure::ambiguous(error.to_string())
126 }
127 };
128 }
129 }
130
131 command_failure_from_order_error(error, true)
132}
133
134fn command_failure_from_modify_error(error: &anyhow::Error) -> CommandFailure {
135 for cause in error.chain() {
136 if let Some(error) = cause.downcast_ref::<KrakenModifyOrderError>() {
137 return match error {
138 KrakenModifyOrderError::Rejected { reason } => {
139 CommandFailure::venue_rejected(reason)
140 }
141 KrakenModifyOrderError::UnknownStatus { .. }
142 | KrakenModifyOrderError::MissingOrderId => {
143 CommandFailure::ambiguous(error.to_string())
144 }
145 };
146 }
147 }
148
149 command_failure_from_order_error(error, true)
150}
151
152fn command_failure_from_spot_batch_error(error: &anyhow::Error) -> CommandFailure {
153 command_failure_from_batch_error(error, true)
154}
155
156fn command_failure_from_futures_batch_error(error: &anyhow::Error) -> CommandFailure {
157 command_failure_from_batch_error(error, false)
158}
159
160fn command_failure_from_batch_error(
161 error: &anyhow::Error,
162 whole_order_rejection: bool,
163) -> CommandFailure {
164 for cause in error.chain() {
165 if let Some(error) = cause.downcast_ref::<KrakenBatchOrderError>() {
166 return match error {
167 KrakenBatchOrderError::Validation { .. } | KrakenBatchOrderError::NotAttempted => {
168 CommandFailure::not_sent(error.to_string())
169 }
170 KrakenBatchOrderError::ResponseCount { .. }
171 | KrakenBatchOrderError::MissingResponse { .. }
172 | KrakenBatchOrderError::DuplicateResponse { .. } => {
173 CommandFailure::ambiguous(error.to_string())
174 }
175 };
176 }
177 }
178
179 command_failure_from_order_error(error, whole_order_rejection)
180}
181
182fn command_failure_from_order_error(
183 error: &anyhow::Error,
184 order_api_rejection: bool,
185) -> CommandFailure {
186 let reason = error.to_string();
187
188 for cause in error.chain() {
189 let Some(error) = cause.downcast_ref::<KrakenHttpError>() else {
190 continue;
191 };
192
193 return match error {
194 KrakenHttpError::RequestNotStarted(_) | KrakenHttpError::MissingCredentials => {
195 CommandFailure::not_sent(reason)
196 }
197 KrakenHttpError::ApiError(errors)
198 if order_api_rejection && contains_spot_order_rejection(errors) =>
199 {
200 CommandFailure::venue_rejected(format_api_errors(errors))
201 }
202 KrakenHttpError::NetworkError(_)
203 | KrakenHttpError::ApiError(_)
204 | KrakenHttpError::ParseError(_)
205 | KrakenHttpError::AuthenticationError(_) => CommandFailure::ambiguous(reason),
206 };
207 }
208
209 CommandFailure::not_sent(reason)
210}
211
212fn command_failure_from_spot_batch_item(
213 item: SpotBatchOrderResponse,
214) -> Result<(), CommandFailure> {
215 match (item.txid, item.error) {
216 (Some(_), None) => Ok(()),
217 (None, Some(reason)) => Err(CommandFailure::venue_rejected(reason)),
218 (Some(_), Some(_)) => Err(CommandFailure::ambiguous(
219 "Batch item response contained both a transaction ID and an error",
220 )),
221 (None, None) => Err(CommandFailure::ambiguous(
222 "Batch item response had no transaction ID or error",
223 )),
224 }
225}
226
227fn command_failure_from_futures_batch_item(
228 item: FuturesBatchSubmitItem,
229) -> Result<(), CommandFailure> {
230 let status = item.status.status;
231
232 if item.result != KrakenApiResult::Success {
233 return if is_futures_submit_rejection(&status) {
234 Err(CommandFailure::venue_rejected(status))
235 } else {
236 Err(CommandFailure::ambiguous(format!(
237 "Batch response reported an error with item status: {status}"
238 )))
239 };
240 }
241
242 match status.as_str() {
243 "placed" | "filled" => Ok(()),
244 reason if is_futures_submit_rejection(reason) => {
245 Err(CommandFailure::venue_rejected(reason))
246 }
247 "" => Err(CommandFailure::ambiguous("Empty batch item status")),
248 reason => Err(CommandFailure::ambiguous(format!(
249 "Unknown batch item status: {reason}"
250 ))),
251 }
252}
253
254fn command_failure_from_cancel_error(error: KrakenHttpError) -> CommandFailure {
255 match error {
256 KrakenHttpError::RequestNotStarted(message) => CommandFailure::not_sent(message),
257 KrakenHttpError::AuthenticationError(message) => CommandFailure::not_sent(message),
258 KrakenHttpError::MissingCredentials => CommandFailure::not_sent("Missing credentials"),
259 KrakenHttpError::NetworkError(message) | KrakenHttpError::ParseError(message) => {
260 CommandFailure::ambiguous(message)
261 }
262 KrakenHttpError::ApiError(message) => {
263 CommandFailure::ambiguous(format_cancel_api_errors(&message))
264 }
265 }
266}
267
268fn command_failure_from_spot_cancel_error(error: KrakenHttpError) -> CommandFailure {
269 match error {
270 KrakenHttpError::ApiError(message) if contains_spot_cancel_rejection(&message) => {
271 CommandFailure::venue_rejected(format_cancel_api_errors(&message))
272 }
273 KrakenHttpError::ApiError(message) => {
274 CommandFailure::ambiguous(format_cancel_api_errors(&message))
275 }
276 other => command_failure_from_cancel_error(other),
277 }
278}
279
280fn contains_spot_order_rejection(errors: &[String]) -> bool {
281 errors.iter().any(|e| e.trim_start().starts_with("EOrder:"))
282}
283
284fn contains_spot_cancel_rejection(errors: &[String]) -> bool {
285 contains_spot_order_rejection(errors)
286}
287
288fn format_api_errors(errors: &[String]) -> String {
289 if errors.is_empty() {
290 "unknown error (empty error list)".to_string()
291 } else {
292 errors.join(", ")
293 }
294}
295
296fn format_cancel_api_errors(errors: &[String]) -> String {
297 format_api_errors(errors)
298}
299
300#[cfg(test)]
301mod tests {
302 use rstest::rstest;
303
304 use super::*;
305
306 #[rstest]
307 fn test_post_submit_phase_overrides_nested_not_sent_error() {
308 let error = anyhow::Error::new(KrakenSubmitOrderError::PostSubmitLookup {
309 source: KrakenHttpError::RequestNotStarted("lookup was not sent".to_string()).into(),
310 });
311
312 assert_eq!(
313 command_failure_from_submit_error(&error),
314 CommandFailure::Ambiguous(
315 "Order lookup failed after submission: Request not started: lookup was not sent"
316 .to_string()
317 )
318 );
319 }
320
321 #[rstest]
322 fn test_request_not_started_is_not_sent() {
323 let error = anyhow::Error::new(KrakenHttpError::RequestNotStarted(
324 "request encoding failed".to_string(),
325 ));
326
327 assert_eq!(
328 command_failure_from_submit_error(&error),
329 CommandFailure::NotSent("Request not started: request encoding failed".to_string())
330 );
331 }
332}