Skip to main content

nautilus_kraken/execution/
mod.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Live execution client implementations for the Kraken adapter.
17//!
18//! This module provides separate execution clients for Kraken Spot and Futures markets:
19//!
20//! - [`KrakenSpotExecutionClient`] - For Spot markets using WebSocket v2
21//! - [`KrakenFuturesExecutionClient`] - For Futures markets
22//!
23//! # Supported Operations
24//!
25//! ## Common
26//! - Order submission (market, limit, stop)
27//! - Order modification
28//! - Order cancellation (single, batch, cancel-all)
29//! - Account state and balance queries
30//!
31//! ## Futures Only
32//! - Position management
33
34mod 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}