use crate::model::order::{OrderSide, OrderType, TimeInForce};
use base64::prelude::*;
use chrono::{DateTime, Utc};
use rand::{RngExt, rng};
use std::time::{SystemTime, UNIX_EPOCH};
#[allow(dead_code)]
pub fn generate_nonce(length: usize) -> String {
let mut rng = rng();
let bytes: Vec<u8> = (0..length).map(|_| rng.random()).collect();
base64::prelude::BASE64_STANDARD.encode(bytes)
}
#[allow(dead_code)]
pub fn generate_timestamp() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64
}
#[allow(dead_code)]
pub fn format_fix_time(time: DateTime<Utc>) -> String {
time.format("%Y%m%d-%H:%M:%S%.3f").to_string()
}
#[allow(dead_code)]
pub fn parse_fix_time(time_str: &str) -> Result<DateTime<Utc>, chrono::ParseError> {
DateTime::parse_from_str(&format!("{time_str}+00:00"), "%Y%m%d-%H:%M:%S%.3f%z")
.map(|dt| dt.with_timezone(&Utc))
}
#[allow(dead_code)]
pub fn calculate_checksum(message: &str) -> u8 {
let sum: u32 = message.bytes().map(|b| b as u32).sum();
(sum % 256) as u8
}
#[allow(dead_code)]
pub fn validate_checksum(message: &str) -> bool {
if let Some(checksum_pos) = message.rfind("10=") {
let message_without_checksum = &message[..checksum_pos];
let expected_checksum = calculate_checksum(message_without_checksum);
if let Some(checksum_str) = message[checksum_pos + 3..].split('\x01').next()
&& let Ok(actual_checksum) = checksum_str.parse::<u8>()
{
return expected_checksum == actual_checksum;
}
}
false
}
#[allow(dead_code)]
pub fn generate_client_order_id(prefix: &str) -> String {
format!("{}_{}", prefix, generate_timestamp())
}
#[allow(dead_code)]
pub fn format_price(price: f64, precision: usize) -> String {
format!("{price:.precision$}")
}
#[allow(dead_code)]
pub fn format_quantity(quantity: f64, precision: usize) -> String {
format!("{quantity:.precision$}")
}
#[allow(dead_code)]
pub fn parse_decimal(decimal_str: &str) -> Result<f64, std::num::ParseFloatError> {
decimal_str.parse::<f64>()
}
#[allow(dead_code)]
pub fn escape_fix_value(value: &str) -> String {
value.replace('\x01', "\\001") }
#[allow(dead_code)]
pub fn unescape_fix_value(value: &str) -> String {
value.replace("\\001", "\x01") }
#[allow(dead_code)]
pub fn generate_request_id(prefix: &str) -> String {
let mut rng = rng();
let random_part: u32 = rng.random();
format!("{prefix}_{random_part}")
}
#[allow(dead_code)]
pub fn side_to_fix(side: OrderSide) -> &'static str {
match side {
OrderSide::Buy => "1",
OrderSide::Sell => "2",
}
}
#[allow(dead_code)]
pub fn order_type_to_fix(order_type: OrderType) -> &'static str {
match order_type {
OrderType::Market => "1",
OrderType::Limit => "2",
OrderType::StopMarket => "3",
OrderType::StopLimit => "4",
OrderType::TrailingStop => "5",
OrderType::TakeLimit => "6",
OrderType::MarketLimit => "7",
OrderType::TakeMarket => "8",
}
}
#[allow(dead_code)]
pub fn time_in_force_to_fix(tif: TimeInForce) -> &'static str {
match tif {
TimeInForce::GoodTilDay => "0",
TimeInForce::GoodTilCancelled => "1",
TimeInForce::ImmediateOrCancel => "3",
TimeInForce::FillOrKill => "4",
}
}
#[allow(dead_code)]
pub fn validate_instrument_name(instrument: &str) -> bool {
if instrument.is_empty() {
return false;
}
if !instrument.contains('-') {
return false;
}
let valid_currencies = ["BTC", "ETH", "USD", "USDC"];
valid_currencies
.iter()
.any(|¤cy| instrument.starts_with(currency))
}
#[allow(dead_code)]
pub fn extract_currency_from_instrument(instrument: &str) -> Option<&str> {
if let Some(dash_pos) = instrument.find('-') {
Some(&instrument[..dash_pos])
} else {
None
}
}
#[allow(dead_code)]
pub fn format_deribit_instrument(
currency: &str,
expiry: Option<&str>,
strike: Option<f64>,
option_type: Option<&str>,
) -> String {
let mut instrument = currency.to_string();
if let Some(exp) = expiry {
instrument.push('-');
instrument.push_str(exp);
if let Some(strike_price) = strike {
instrument.push('-');
instrument.push_str(&strike_price.to_string());
if let Some(opt_type) = option_type {
instrument.push('-');
instrument.push_str(opt_type);
}
}
} else {
instrument.push_str("-PERPETUAL");
}
instrument
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{Datelike, Timelike};
#[test]
fn test_generate_nonce() {
let nonce1 = generate_nonce(32);
let nonce2 = generate_nonce(32);
assert_ne!(nonce1, nonce2);
assert!(!nonce1.is_empty());
assert!(!nonce2.is_empty());
}
#[test]
fn test_checksum_calculation() {
let message = "8=FIX.4.4\x019=61\x0135=A\x0149=CLIENT\x0156=DERIBITSERVER\x0134=1\x01";
let checksum = calculate_checksum(message);
assert_eq!(checksum, 169);
}
#[test]
fn test_instrument_validation() {
assert!(validate_instrument_name("BTC-PERPETUAL"));
assert!(validate_instrument_name("ETH-25DEC20-600-C"));
assert!(validate_instrument_name("BTC-25DEC20"));
assert!(!validate_instrument_name("INVALID"));
assert!(!validate_instrument_name(""));
}
#[test]
fn test_currency_extraction() {
assert_eq!(
extract_currency_from_instrument("BTC-PERPETUAL"),
Some("BTC")
);
assert_eq!(
extract_currency_from_instrument("ETH-25DEC20-600-C"),
Some("ETH")
);
assert_eq!(extract_currency_from_instrument("INVALID"), None);
}
#[test]
fn test_instrument_formatting() {
assert_eq!(
format_deribit_instrument("BTC", None, None, None),
"BTC-PERPETUAL"
);
assert_eq!(
format_deribit_instrument("ETH", Some("25DEC20"), Some(600.0), Some("C")),
"ETH-25DEC20-600-C"
);
assert_eq!(
format_deribit_instrument("BTC", Some("25DEC20"), None, None),
"BTC-25DEC20"
);
}
#[test]
fn test_generate_timestamp() {
let timestamp1 = generate_timestamp();
std::thread::sleep(std::time::Duration::from_millis(1));
let timestamp2 = generate_timestamp();
assert!(timestamp2 > timestamp1);
assert!(timestamp1 > 0);
}
#[test]
fn test_format_fix_time() {
use chrono::{TimeZone, Utc};
let dt = Utc
.with_ymd_and_hms(2023, 12, 25, 14, 30, 45)
.unwrap()
.with_nanosecond(123_000_000)
.unwrap();
let formatted = format_fix_time(dt);
assert_eq!(formatted, "20231225-14:30:45.123");
}
#[test]
fn test_parse_fix_time() {
let time_str = "20231225-14:30:45.123";
let parsed = parse_fix_time(time_str).unwrap();
assert_eq!(parsed.year(), 2023);
assert_eq!(parsed.month(), 12);
assert_eq!(parsed.day(), 25);
assert_eq!(parsed.hour(), 14);
assert_eq!(parsed.minute(), 30);
assert_eq!(parsed.second(), 45);
}
#[test]
fn test_validate_checksum() {
let message =
"8=FIX.4.4\x019=61\x0135=A\x0149=CLIENT\x0156=DERIBITSERVER\x0134=1\x0110=169\x01";
assert!(validate_checksum(message));
let invalid_message =
"8=FIX.4.4\x019=61\x0135=A\x0149=CLIENT\x0156=DERIBITSERVER\x0134=1\x0110=170\x01";
assert!(!validate_checksum(invalid_message));
}
#[test]
fn test_generate_client_order_id() {
let order_id1 = generate_client_order_id("TEST");
std::thread::sleep(std::time::Duration::from_millis(1));
let order_id2 = generate_client_order_id("TEST");
assert!(order_id1.starts_with("TEST_"));
assert!(order_id2.starts_with("TEST_"));
assert_ne!(order_id1, order_id2);
}
#[test]
fn test_format_price() {
assert_eq!(format_price(123.456789, 2), "123.46");
assert_eq!(format_price(123.456789, 4), "123.4568");
assert_eq!(format_price(123.0, 2), "123.00");
}
#[test]
fn test_format_quantity() {
assert_eq!(format_quantity(0.12345, 3), "0.123");
assert_eq!(format_quantity(1.0, 1), "1.0");
assert_eq!(format_quantity(10.5678, 2), "10.57");
}
#[test]
fn test_parse_decimal() {
assert_eq!(parse_decimal("123.456").unwrap(), 123.456);
assert_eq!(parse_decimal("0.001").unwrap(), 0.001);
assert!(parse_decimal("invalid").is_err());
}
#[test]
fn test_escape_fix_value() {
assert_eq!(escape_fix_value("test\x01value"), "test\\001value");
assert_eq!(escape_fix_value("normal_text"), "normal_text");
}
#[test]
fn test_unescape_fix_value() {
assert_eq!(unescape_fix_value("test\\001value"), "test\x01value");
assert_eq!(unescape_fix_value("normal_text"), "normal_text");
}
#[test]
fn test_generate_request_id() {
let req_id1 = generate_request_id("REQ");
let req_id2 = generate_request_id("REQ");
assert!(req_id1.starts_with("REQ_"));
assert!(req_id2.starts_with("REQ_"));
assert_ne!(req_id1, req_id2);
}
#[test]
fn test_side_to_fix() {
assert_eq!(side_to_fix(OrderSide::Buy), "1");
assert_eq!(side_to_fix(OrderSide::Sell), "2");
}
}