use crate::common::*;
use crate::rest_regex::*;
use chrono::{NaiveDateTime, Utc};
use serde::de::DeserializeOwned;
use serde::ser::SerializeStruct;
use serde::{Deserialize, Serialize, Serializer};
use serde_json::Value;
use std::error::Error;
pub trait ValidateRequest: Serialize {
fn validate(&self) -> Result<(), Box<dyn Error>> {
Ok(())
}
}
pub trait ValidateResponse: DeserializeOwned {
fn deserialize<'de, T>(value: &'de Value) -> Result<T, Box<dyn Error>>
where
T: Deserialize<'de>,
{
let result = serde_path_to_error::deserialize(value);
match result {
Ok(value) => Ok(value),
Err(e) => Err(Box::new(ApiError {
message: format!("Failed to deserialize JSON serde_json::Value: {}", e),
})),
}
}
fn from_value(value: &Value) -> Result<Self, Box<dyn Error>>
where
Self: Sized,
{
let instance: Self = <Self as ValidateResponse>::deserialize(value)?;
match instance.validate() {
Ok(()) => Ok(instance),
Err(e) => Err(Box::new(ApiError {
message: format!("Validation failed: {}", e),
})),
}
}
fn validate(&self) -> Result<(), Box<dyn Error>> {
Ok(())
}
}
#[derive(Serialize)]
pub struct Empty {}
impl ValidateRequest for Empty {}
impl ValidateRequest for &Empty {}
#[derive(Debug, Default)]
pub struct SentimentQuery {
pub market_ids: Option<Vec<String>>,
}
impl Serialize for SentimentQuery {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut state = serializer.serialize_struct("SentimentQuery", 1)?;
match self.market_ids.as_ref() {
Some(ids) => {
state.serialize_field("marketIds", &ids.join(","))?;
}
None => {
state.serialize_field("marketIds", &None::<()>)?;
}
}
state.end()
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Sentiments {
pub client_sentiments: Vec<Sentiment>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Sentiment {
pub long_position_percentage: f64,
pub market_id: String,
pub short_position_percentage: f64,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Application {
pub allow_equities: bool,
pub allow_quote_orders: bool,
pub allowance_account_historical_data: f64,
pub allowance_account_overall: f64,
pub allowance_account_trading: f64,
pub allowance_application_overall: f64,
pub api_key: String,
pub concurrent_subscriptions_limit: f64,
pub created_date: String,
pub name: String,
pub status: ApplicationStatus,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ApplicationStatus {
Disabled,
Enabled,
Revoked,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateApplication {
pub allowance_account_overall: f64,
pub allowance_account_trading: f64,
pub api_key: String,
pub status: ApplicationStatus,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketSearch {
pub markets: Vec<MarketData>,
}
#[derive(Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PricesQuery {
pub resolution: Option<Resolution>,
pub from: Option<NaiveDateTime>,
pub to: Option<NaiveDateTime>,
pub max: Option<u32>,
pub page_size: Option<u32>,
pub page_number: Option<u32>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Resolution {
Day,
Hour,
Hour2,
Hour3,
Hour4,
Minute,
Minute10,
Minute15,
Minute2,
Minute3,
Minute30,
Minute5,
Month,
Second,
Week,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Prices {
pub instrument_type: InstrumentType,
pub metadata: PriceMetadata,
pub prices: Vec<Price>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PriceMetadata {
pub page_data: PricePaging,
pub size: f64,
pub allowance: PriceAllowance,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PricePaging {
pub page_number: u32,
pub page_size: u32,
pub total_pages: u32,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PriceAllowance {
pub allowance_expiry: u32,
pub remaining_allowance: u32,
pub total_allowance: u32,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Price {
pub close_price: AskBid,
pub high_price: AskBid,
pub last_traded_volume: f64,
pub low_price: AskBid,
pub open_price: AskBid,
pub snapshot_time: String,
#[serde(rename = "snapshotTimeUTC")]
pub snapshot_time_utc: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AskBid {
pub ask: f64,
pub bid: f64,
pub last_traded: Option<f64>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Watchlists {
pub watchlists: Vec<Watchlist>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Watchlist {
pub default_system_watchlist: bool,
pub deleteable: bool,
pub editable: bool,
pub id: String,
pub name: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateWatchlist {
pub epics: Vec<String>,
pub name: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateWatchlistResult {
pub status: CreateWatchlistStatus,
pub watchlist_id: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum CreateWatchlistStatus {
Success,
SuccesNotAllInstrumentsAdded,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AddToWatchlist {
pub epic: String,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Account {
pub account_alias: Option<String>,
pub account_id: String,
pub account_name: String,
pub account_type: AccountType,
pub balance: Balance,
pub can_transfer_from: bool,
pub can_transfer_to: bool,
pub currency: String,
pub preferred: bool,
pub status: AccountStatus,
}
#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum AccountStatus {
Disabled,
Enabled,
SuspendedFromDealing,
}
#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum AccountType {
Cfd,
Physical,
Spreadbet,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountsGetResponse {
pub accounts: Vec<Account>,
}
impl ValidateResponse for AccountsGetResponse {}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountsPreferencesPutRequest {
pub trailing_stops_enabled: bool,
}
impl ValidateRequest for AccountsPreferencesPutRequest {}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountsPreferencesGetResponse {
pub trailing_stops_enabled: bool,
}
impl ValidateResponse for AccountsPreferencesGetResponse {}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountsPreferencesStatusPutResponse {
pub status: AccountsPreferencesPutRequestStatus,
}
impl ValidateResponse for AccountsPreferencesStatusPutResponse {}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Balance {
pub available: f64,
pub balance: f64,
pub deposit: f64,
pub profit_loss: f64,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum AccountsPreferencesPutRequestStatus {
Success,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AffectedDeal {
pub deal_id: String,
pub status: AffectedDealStatus,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum AffectedDealStatus {
Amended,
Deleted,
FullyClosed,
Opened,
PartiallyClosed,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ConfirmsGetRequest {
pub deal_reference: String,
}
impl ValidateRequest for ConfirmsGetRequest {}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ConfirmsGetResponse {
pub affected_deals: Vec<AffectedDeal>,
pub date: String,
pub deal_id: String,
pub deal_reference: String,
pub deal_status: DealStatus,
pub direction: Direction,
pub epic: String,
pub expiry: Option<String>,
pub guaranteed_stop: bool,
pub level: Option<f64>,
pub limit_distance: Option<f64>,
pub limit_level: Option<f64>,
pub profit: Option<f64>,
pub profit_currency: Option<String>,
pub reason: DealReason,
pub size: Option<f64>,
pub status: Option<PositionStatus>,
pub stop_distance: Option<f64>,
pub stop_level: Option<f64>,
pub trailing_stop: bool,
}
impl ValidateResponse for ConfirmsGetResponse {}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum DealReason {
AccountNotEnabledToTrading,
AttachedOrderLevelError,
AttachedOrderTrailingStopError,
CannotChangeStopType,
CannotRemoveStop,
ClosingOnlyTradesAcceptedOnThisMarket,
ClosingsOnlyAccount,
ConflictingOrder,
ContactSupportInstrumentError,
CrSpacing,
DuplicateOrderError,
ExchangeManualOverride,
ExpiryLessThanSprintMarketMinExpiry,
FinanceRepeatDealing,
ForceOpenOnSameMarketDifferentCurrency,
GeneralError,
GoodTillDateInThePast,
InstrumentNotFound,
InstrumentNotTradeableInThisCurrency,
InsufficientFunds,
LevelToleranceError,
LimitOrderWrongSideOfMarket,
ManualOrderTimeout,
MarginError,
MarketClosed,
MarketClosedWithEdits,
MarketClosing,
MarketNotBorrowable,
MarketOffline,
MarketOrdersNotAllowedOnInstrument,
MarketPhoneOnly,
MarketRolled,
MarketUnavailableToClient,
MaxAutoSizeExceeded,
MinimumOrderSizeError,
MoveAwayOnlyLimit,
MoveAwayOnlyStop,
MoveAwayOnlyTriggerLevel,
NcrPositionsOnCrAccount,
OpposingDirectionOrdersNotAllowed,
OpposingPositionsNotAllowed,
OrderDeclined,
OrderLocked,
OrderNotFound,
OrderSizeCannotBeFilled,
OverNormalMarketSize,
PartialyClosedPositionNotDeleted,
PositionAlreadyExistsInOppositeDirection,
PositionNotAvailableToCancel,
PositionNotAvailableToClose,
PositionNotFound,
RejectCfdOrderOnSpreadbetAccount,
RejectSpreadbetOrderOnCfdAccount,
SizeIncrement,
SprintMarketExpiryAfterMarketClose,
StopOrLimitNotAllowed,
StopRequiredError,
StrikeLevelTolerance,
Success,
TrailingStopNotAllowed,
Unknown,
WrongSideOfMarket,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum DealStatus {
Accepted,
Rejected,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Activity {
pub channel: ActivityChannel,
pub date: String,
pub deal_id: String,
pub description: String,
pub details: Option<ActivityDetails>,
pub epic: String,
pub period: String,
pub status: ActivityStatus,
pub r#type: ActivityType,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ActivityAction {
pub action_type: ActivityActionType,
pub affected_deal_id: String,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ActivityActionType {
LimitOrderAmended,
LimitOrderDeleted,
LimitOrderFilled,
LimitOrderOpened,
LimitOrderRolled,
PositionClosed,
PositionDeleted,
PositionOpened,
PositionPartiallyClosed,
PositionRolled,
StopLimitAmended,
StopOrderAmended,
StopOrderDeleted,
StopOrderFilled,
StopOrderOpened,
StopOrderRolled,
Unknown,
WorkingOrderDeleted,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ActivityChannel {
Dealer,
Mobile,
PublicFixApi,
PublicWebApi,
System,
Web,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ActivityDetails {
pub actions: Vec<ActivityAction>,
pub currency: String,
pub deal_reference: String,
pub direction: Direction,
pub good_till_date: String,
pub guaranteed_stop: bool,
pub level: f64,
pub limit_distance: f64,
pub limit_level: f64,
pub market_name: String,
pub size: f64,
pub stop_distance: f64,
pub stop_level: f64,
pub trailing_step: f64,
pub trailing_stop_distance: f64,
}
#[derive(Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ActivityHistoryGetRequest {
pub from: NaiveDateTime,
pub to: Option<NaiveDateTime>,
pub detailed: Option<bool>,
pub deal_id: Option<String>,
pub filter: Option<String>,
pub page_size: Option<u32>,
}
impl ValidateRequest for ActivityHistoryGetRequest {
fn validate(&self) -> Result<(), Box<dyn Error>> {
if self.from > Utc::now().naive_utc() {
return Err(Box::new(ApiError {
message: "'From' date cannot be greater than today.".to_string(),
}));
}
if let Some(to) = self.to {
if self.from > to {
return Err(Box::new(ApiError {
message: "'From' date cannot be greater than 'to' date.".to_string(),
}));
}
}
Ok(())
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ActivityHistoryGetResponse {
pub activities: Vec<Activity>,
pub metadata: ActivityMetadata,
}
impl ValidateResponse for ActivityHistoryGetResponse {}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ActivityMetadata {
pub paging: ActivityPaging,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ActivityPaging {
pub next: Option<String>,
pub size: u32,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ActivityStatus {
Accepted,
Rejected,
Unknown,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ActivityType {
EditStopAndLimit,
Position,
System,
WorkingOrder,
}
#[derive(Debug, Default, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Direction {
#[default]
Buy,
Sell,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Transaction {
pub cash_transaction: bool,
pub close_level: String,
pub currency: String,
pub date: String,
pub date_utc: String,
pub instrument_name: String,
pub open_date_utc: String,
pub open_level: String,
pub period: String,
pub profit_and_loss: String,
pub reference: String,
pub size: String,
pub transaction_type: String,
}
#[derive(Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TransactionHistoryGetRequest {
pub r#type: Option<TransactionType>,
pub from: NaiveDateTime,
pub to: Option<NaiveDateTime>,
pub max_span_seconds: Option<u64>,
pub page_size: Option<u32>,
pub page_number: Option<u32>,
}
impl ValidateRequest for TransactionHistoryGetRequest {
fn validate(&self) -> Result<(), Box<dyn Error>> {
if self.from > Utc::now().naive_utc() {
return Err(Box::new(ApiError {
message: "'From' date cannot be greater than today.".to_string(),
}));
}
if let Some(to) = self.to {
if self.from > to {
return Err(Box::new(ApiError {
message: "'From' date cannot be greater than 'to' date.".to_string(),
}));
}
}
Ok(())
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TransactionHistoryGetResponse {
pub metadata: TransactionMetadata,
pub transactions: Vec<Transaction>,
}
impl ValidateResponse for TransactionHistoryGetResponse {}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TransactionMetadata {
pub page_data: TransactionPageData,
pub size: u32,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TransactionPageData {
pub page_number: u32,
pub page_size: u32,
pub total_pages: u32,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum TransactionType {
All,
AllDeal,
Deposit,
Withdrawal,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Currency {
pub base_exchange_rate: f64,
pub code: String,
pub exchange_rate: f64,
pub is_default: bool,
pub symbol: String,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DealingRule {
pub unit: RuleUnit,
pub value: f64,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DealingRules {
pub controlled_risk_spacing: DealingRule,
pub market_order_preference: MarketOrderPreference,
pub max_stop_or_limit_distance: DealingRule,
pub min_controlled_risk_stop_distance: DealingRule,
pub min_deal_size: DealingRule,
pub min_normal_stop_or_limit_distance: DealingRule,
pub min_step_distance: DealingRule,
pub trailing_stops_preference: TrailingStopsPreference,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DepositBand {
pub currency: String,
pub margin: f64,
pub max: Option<f64>,
pub min: f64,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Expiry {
pub last_dealing_date: String,
pub settlement_info: String,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InstrumentDetails {
pub chart_code: String,
pub contract_size: String,
pub controlled_risk_allowed: bool,
pub country: Option<String>,
pub currencies: Vec<Currency>,
pub epic: String,
pub expiry: String,
pub expiry_details: Option<Expiry>,
pub force_open_allowed: bool,
pub limited_risk_premium: DealingRule,
pub lot_size: f64,
pub margin_deposit_bands: Vec<DepositBand>,
pub margin_factor: f64,
pub margin_factor_unit: RuleUnit,
pub market_id: String,
pub name: String,
pub news_code: String,
pub one_pip_means: String,
pub opening_hours: Option<OpeningHours>,
pub rollover_details: Option<Rollover>,
pub slippage_factor: SlippageFactor,
pub special_info: Vec<String>,
pub sprint_markets_maximum_expiry_time: Option<f64>,
pub sprint_markets_minimum_expiry_time: Option<f64>,
pub stops_limits_allowed: bool,
pub streaming_prices_available: bool,
pub r#type: InstrumentType,
pub unit: InstrumentUnit,
pub value_of_one_pip: String,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum InstrumentUnit {
Amount,
Contracts,
Shares,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketDetails {
pub dealing_rules: DealingRules,
pub instrument: InstrumentDetails,
pub snapshot: MarketSnapshot,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum MarketDetailsFilterType {
All,
SnapshotOnly,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketNavigationGetRequest {
node_id: Option<String>,
}
impl ValidateRequest for MarketNavigationGetRequest {}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketNavigationGetResponse {
pub markets: Option<Vec<MarketData>>,
pub nodes: Option<Vec<MarketNode>>,
}
impl ValidateResponse for MarketNavigationGetResponse {}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketNode {
pub id: String,
pub name: String,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum MarketOrderPreference {
AvailableDefaultOff,
AvailableDefaultOn,
NotAvailable,
}
#[derive(Debug, Default)]
pub struct MarketsGetRequest {
pub epics: Vec<String>,
pub filter: Option<MarketDetailsFilterType>,
}
impl Serialize for MarketsGetRequest {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut state = serializer.serialize_struct("MarketsQuery", 2)?;
state.serialize_field("epics", &self.epics.join(","))?;
match self.filter.as_ref() {
Some(filter) => {
state.serialize_field("filter", filter)?;
}
None => {
state.serialize_field("marketIds", &None::<()>)?;
}
}
state.end()
}
}
impl ValidateRequest for MarketsGetRequest {
fn validate(&self) -> Result<(), Box<dyn Error>> {
if self.epics.is_empty() {
return Err(Box::new(ApiError {
message: "The 'epics' field cannot be empty.".to_string(),
}));
}
if self.epics.len() > 50 {
return Err(Box::new(ApiError {
message: "The 'epics' field cannot be greater than 50.".to_string(),
}));
}
let serialized_epics = self.epics.join(",");
if !EPICS_REGEX.is_match(&serialized_epics) {
return Err(Box::new(ApiError {
message: format!("Epics field is invalid. Fields: {}", serialized_epics),
}));
}
Ok(())
}
}
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketsGetResponse {
pub market_details: Vec<MarketDetails>,
}
impl ValidateResponse for MarketsGetResponse {}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum RuleUnit {
Percentage,
Points,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketSnapshot {
pub bid: f64,
pub binary_odds: Option<f64>,
pub controlled_risk_extra_spread: f64,
pub decimal_places_factor: f64,
pub delay_time: f64,
pub high: f64,
pub low: f64,
pub market_status: MarketStatus,
pub net_change: f64,
pub offer: f64,
pub percentage_change: f64,
pub scaling_factor: f64,
pub update_time: String,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketTime {
pub close_time: String,
pub open_time: String,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OpeningHours {
pub market_times: Vec<MarketTime>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Rollover {
pub last_rollover_time: String,
pub rollover_info: String,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SlippageFactor {
pub unit: String,
pub value: f64,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum TrailingStopsPreference {
Available,
NotAvailable,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketData {
pub bid: Option<f64>,
pub delay_time: f64,
pub epic: String,
pub expiry: String,
pub high: Option<f64>,
pub instrument_name: String,
pub instrument_type: InstrumentType,
pub lot_size: Option<f64>,
pub low: Option<f64>,
pub market_status: MarketStatus,
pub net_change: f64,
pub offer: Option<f64>,
pub percentage_change: f64,
pub scaling_factor: f64,
pub streaming_prices_available: bool,
pub update_time: String,
#[serde(rename = "updateTimeUTC")]
pub update_time_utc: String,
}
#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum MarketStatus {
Closed,
EditsOnly,
Offline,
OnAuction,
OnAuctionNoEdits,
Suspended,
Tradeable,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum InstrumentType {
Binary,
BungeeCapped,
BungeeCommodities,
BungeeCurrencies,
BungeeIndices,
Commodities,
Currencies,
Indices,
KnockoutsCommodities,
KnockoutsCurrencies,
KnockoutsIndices,
KnockoutsShares,
OptCommodities,
OptCurrencies,
OptIndices,
OptRates,
OptShares,
Rates,
Sectors,
Shares,
SprintMarket,
TestMarket,
Unknown,
}
#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum OrderType {
Limit,
#[default]
Market,
Quote,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PositionStatus {
Amended,
Closed,
Deleted,
Open,
PartiallyClosed,
}
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PositionDeleteRequest {
pub deal_id: Option<String>,
pub direction: Option<Direction>,
pub epic: Option<String>,
pub expiry: Option<String>,
pub level: Option<f64>,
pub order_type: Option<OrderType>,
pub quote_id: Option<String>,
pub size: f64,
pub time_in_force: Option<TimeInForce>,
}
impl ValidateRequest for PositionDeleteRequest {
fn validate(&self) -> Result<(), Box<dyn Error>> {
if let Some(deal_id) = &self.deal_id {
if !DEAL_ID_REGEX.is_match(deal_id) {
return Err(Box::new(ApiError {
message: "Deal ID field is invalid.".to_string(),
}));
}
}
if let Some(epic) = &self.epic {
if !EPIC_REGEX.is_match(epic) {
return Err(Box::new(ApiError {
message: "Epic field is invalid.".to_string(),
}));
}
}
if let Some(expiry) = &self.expiry {
if !EXPIRY_REGEX.is_match(expiry) {
return Err(Box::new(ApiError {
message: "Expiry field is invalid.".to_string(),
}));
}
}
let size_str = format!("{}", self.size);
let parts: Vec<&str> = size_str.split('.').collect();
if parts.len() == 2 && parts[1].len() > 12 {
return Err(Box::new(ApiError {
message: "Size field has more thatn 12 decimal places.".to_string(),
}));
}
if self.epic.is_some() && self.expiry.is_none() {
return Err(Box::new(ApiError {
message: "Expiry field is required when epic is defined.".to_string(),
}));
}
if self.order_type == Some(OrderType::Limit) && self.quote_id.is_some() {
return Err(Box::new(ApiError {
message: "Quote ID field cannot be set when order type is LIMIT.".to_string(),
}));
}
if self.order_type == Some(OrderType::Limit) && self.level.is_none() {
return Err(Box::new(ApiError {
message: "Level field is required when order type is LIMIT.".to_string(),
}));
}
if self.order_type == Some(OrderType::Market)
&& (self.level.is_some() || self.quote_id.is_some())
{
return Err(Box::new(ApiError {
message: "Level and quote ID fields cannot be set when order type is MARKET."
.to_string(),
}));
}
if self.order_type == Some(OrderType::Quote)
&& (self.level.is_none() || self.quote_id.is_none())
{
return Err(Box::new(ApiError {
message: "Level and quote ID fields are required when order type is QUOTE."
.to_string(),
}));
}
if self.deal_id.is_some() && self.epic.is_some() {
return Err(Box::new(ApiError {
message: "Set only one of {deal_id, epic}.".to_string(),
}));
}
Ok(())
}
}
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PositionDeleteResponse {
pub deal_reference: String,
}
impl ValidateResponse for PositionDeleteResponse {}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PositionGetRequest {
pub deal_id: String,
}
impl ValidateRequest for PositionGetRequest {
fn validate(&self) -> Result<(), Box<dyn Error>> {
if !DEAL_ID_REGEX.is_match(&self.deal_id) {
return Err(Box::new(ApiError {
message: "Deal ID field is invalid.".to_string(),
}));
}
Ok(())
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PositionsGetResponse {
pub positions: Vec<PositionGetResponse>,
}
impl ValidateResponse for PositionsGetResponse {}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PositionGetResponse {
pub market: MarketData,
pub position: PositionData,
}
impl ValidateResponse for PositionGetResponse {}
#[derive(Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PositionPostRequest {
pub currency_code: String,
pub deal_reference: Option<String>,
pub direction: Direction,
pub epic: String,
pub expiry: String,
pub force_open: bool,
pub guaranteed_stop: bool,
pub level: Option<f64>,
pub limit_distance: Option<f64>,
pub limit_level: Option<f64>,
pub order_type: OrderType,
pub quote_id: Option<String>,
pub size: f64,
pub stop_distance: Option<f64>,
pub stop_level: Option<f64>,
pub time_in_force: Option<TimeInForce>,
pub trailing_stop: Option<bool>,
pub trailing_stop_increment: Option<f64>,
}
impl ValidateRequest for PositionPostRequest {
fn validate(&self) -> Result<(), Box<dyn Error>> {
if self.limit_distance.is_some() && self.force_open != true {
return Err(Box::new(ApiError {
message: "force_open field must be true when limit_distance is set.".to_string(),
}));
}
if self.limit_level.is_some() && self.force_open != true {
return Err(Box::new(ApiError {
message: "force_open field must be true when limit_level is set.".to_string(),
}));
}
if self.stop_distance.is_some() && self.force_open != true {
return Err(Box::new(ApiError {
message: "force_open field must be true when stop_distance is set.".to_string(),
}));
}
if self.stop_level.is_some() && self.force_open != true {
return Err(Box::new(ApiError {
message: "force_open field must be true when stop_level is set.".to_string(),
}));
}
if self.guaranteed_stop == true && self.stop_level.is_some() && self.stop_distance.is_some()
{
return Err(Box::new(ApiError {
message: "Only one of stop_level or stop_distance can be set when guaranteed_stop is true.".to_string(),
}));
}
if self.order_type == OrderType::Limit && self.quote_id.is_some() {
return Err(Box::new(ApiError {
message: "quote_id cannot be set when order_type is LIMIT.".to_string(),
}));
}
if self.order_type == OrderType::Limit && self.level.is_none() {
return Err(Box::new(ApiError {
message: "level must be set when order_type is LIMIT.".to_string(),
}));
}
if self.order_type == OrderType::Market && (self.level.is_some() || self.quote_id.is_some())
{
return Err(Box::new(ApiError {
message: "Neither level nor quote_id can be set when order_type is MARKET."
.to_string(),
}));
}
if self.order_type == OrderType::Quote && (self.level.is_none() || self.quote_id.is_none())
{
return Err(Box::new(ApiError {
message: "Both level and quote_id must be set when order_type is QUOTE."
.to_string(),
}));
}
if self.trailing_stop == Some(false) && self.trailing_stop_increment.is_some() {
return Err(Box::new(ApiError {
message: "trailing_stop_increment cannot be set when trailing_stop is false."
.to_string(),
}));
}
if self.trailing_stop == Some(true) && self.stop_level.is_some() {
return Err(Box::new(ApiError {
message: "stop_level cannot be set when trailing_stop is true.".to_string(),
}));
}
if self.trailing_stop == Some(true) && self.guaranteed_stop != false {
return Err(Box::new(ApiError {
message: "guaranteed_stop must be false when trailing_stop is true.".to_string(),
}));
}
if self.trailing_stop == Some(true)
&& (self.stop_distance.is_none() || self.trailing_stop_increment.is_none())
{
return Err(Box::new(ApiError {
message: "Both stop_distance and trailing_stop_increment must be set when trailing_stop is true.".to_string(),
}));
}
if self.limit_level.is_some() && self.limit_distance.is_some() {
return Err(Box::new(ApiError {
message: "Only one of limit_level or limit_distance can be set.".to_string(),
}));
}
if self.stop_level.is_some() && self.stop_distance.is_some() {
return Err(Box::new(ApiError {
message: "Only one of stop_level or stop_distance can be set.".to_string(),
}));
}
if !CURRENCY_CODE_REGEX.is_match(&self.currency_code) {
return Err(Box::new(ApiError {
message: "Currency code field is invalid.".to_string(),
}));
}
if let Some(deal_reference) = &self.deal_reference {
if !DEAL_REFERENCE_REGEX.is_match(deal_reference) {
return Err(Box::new(ApiError {
message: "Deal reference field is invalid.".to_string(),
}));
}
}
if !EPIC_REGEX.is_match(&self.epic) {
return Err(Box::new(ApiError {
message: "Epic field is invalid.".to_string(),
}));
}
if !EXPIRY_REGEX.is_match(&self.expiry) {
return Err(Box::new(ApiError {
message: "Expiry field is invalid.".to_string(),
}));
}
let size_str = format!("{}", self.size);
let parts: Vec<&str> = size_str.split('.').collect();
if parts.len() == 2 && parts[1].len() > 12 {
return Err(Box::new(ApiError {
message: "Size field has more thatn 12 decimal places.".to_string(),
}));
}
Ok(())
}
}
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PositionPostResponse {
pub deal_reference: String,
}
impl ValidateResponse for PositionPostResponse {}
#[derive(Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PositionPutRequest {
pub guaranteed_stop: Option<bool>,
pub limit_level: Option<f64>,
pub stop_level: Option<f64>,
pub trailing_stop: Option<bool>,
pub trailing_stop_distance: Option<f64>,
pub trailing_stop_increment: Option<f64>,
}
impl ValidateRequest for PositionPutRequest {
fn validate(&self) -> Result<(), Box<dyn Error>> {
if self.guaranteed_stop == Some(true) && self.stop_level.is_none() {
return Err(Box::new(ApiError {
message: "stop_level must be set when guaranteed_stop is true.".to_string(),
}));
}
if self.guaranteed_stop == Some(true) && self.trailing_stop == Some(true) {
return Err(Box::new(ApiError {
message: "trailing_stop must be false when guaranteed_stop is true.".to_string(),
}));
}
if self.trailing_stop == Some(false)
&& (self.trailing_stop_distance.is_some() || self.trailing_stop_increment.is_some())
{
return Err(Box::new(ApiError {
message: "Neither trailing_stop_distance nor trailing_stop_increment can be set when trailing_stop is false.".to_string(),
}));
}
if self.trailing_stop == Some(true) && self.guaranteed_stop == Some(true) {
return Err(Box::new(ApiError {
message: "guaranteed_stop must be false when trailing_stop is true.".to_string(),
}));
}
if self.trailing_stop == Some(true)
&& (self.trailing_stop_distance.is_none()
|| self.trailing_stop_increment.is_none()
|| self.stop_level.is_none())
{
return Err(Box::new(ApiError {
message: "All of trailing_stop_distance, trailing_stop_increment, stop_level must be set when trailing_stop is true.".to_string(),
}));
}
Ok(())
}
}
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PositionPutResponse {
pub deal_reference: String,
}
impl ValidateResponse for PositionPutResponse {}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PositionData {
pub contract_size: f64,
pub controlled_risk: bool,
pub created_date: String,
#[serde(rename = "createdDateUTC")]
pub created_date_utc: String,
pub currency: String,
pub deal_id: String,
pub deal_reference: String,
pub direction: Direction,
pub level: f64,
pub limit_level: Option<f64>,
pub limited_risk_premium: Option<f64>,
pub size: f64,
pub stop_level: Option<f64>,
pub trailing_step: Option<f64>,
pub trailing_stop_distance: Option<f64>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum TimeInForce {
ExecuteAndEliminate,
FillOrKill,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum SprintMarketExpiryPeriod {
FiveMinutes,
OneMinute,
SixtyMinutes,
TwentyMinutes,
TwoMinutes,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SprintMarketPosition {
pub created_date: String,
pub currency: String,
pub deal_id: String,
pub description: String,
pub direction: Direction,
pub epic: String,
pub expiry_time: String,
pub instrument_name: String,
pub market_status: MarketStatus,
pub payout_amount: f64,
pub size: f64,
pub strike_level: f64,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SprintMarketPositionsGetResponse {
pub sprint_market_positions: Vec<SprintMarketPosition>,
}
impl ValidateResponse for SprintMarketPositionsGetResponse {
fn validate(&self) -> Result<(), Box<dyn Error>> {
for sprint_market_position in &self.sprint_market_positions {
if !CURRENCY_CODE_REGEX.is_match(&sprint_market_position.currency) {
return Err(Box::new(ApiError {
message: format!(
"Currency code '{}' field is invalid.",
sprint_market_position.currency
),
}));
}
}
Ok(())
}
}
#[derive(Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SprintMarketPositionsPostRequest {
pub deal_reference: Option<String>,
pub direction: Option<Direction>,
pub epic: String,
pub expiry_period: Option<SprintMarketExpiryPeriod>,
pub size: f64,
}
impl ValidateRequest for SprintMarketPositionsPostRequest {
fn validate(&self) -> Result<(), Box<dyn Error>> {
if let Some(deal_reference) = &self.deal_reference {
if !DEAL_REFERENCE_REGEX.is_match(deal_reference) {
return Err(Box::new(ApiError {
message: "Deal reference field is invalid.".to_string(),
}));
}
}
if !EPIC_REGEX.is_match(&self.epic) {
return Err(Box::new(ApiError {
message: "Epic field is invalid.".to_string(),
}));
}
let size_str = format!("{}", self.size);
let parts: Vec<&str> = size_str.split('.').collect();
if parts.len() == 2 && parts[1].len() > 12 {
return Err(Box::new(ApiError {
message: "Size field has more thatn 12 decimal places.".to_string(),
}));
}
Ok(())
}
}
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SprintMarketPositionsPostResponse {
pub deal_reference: String,
}
impl ValidateResponse for SprintMarketPositionsPostResponse {}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountSwitchPutRequest {
pub account_id: String,
pub default_account: Option<bool>,
}
impl ValidateRequest for AccountSwitchPutRequest {
fn validate(&self) -> Result<(), Box<dyn Error>> {
if !ACCOUNT_ID_REGEX.is_match(&self.account_id) {
return Err(Box::new(ApiError {
message: "Account ID field is invalid.".to_string(),
}));
}
Ok(())
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountSwitchPutResponse {
pub dealing_enabled: bool,
pub has_active_demo_accounts: bool,
pub has_active_live_accounts: bool,
pub trailing_stops_enabled: bool,
}
impl ValidateResponse for AccountSwitchPutResponse {}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthenticationPostRequest {
pub identifier: String,
pub password: String,
}
impl ValidateRequest for AuthenticationPostRequest {
fn validate(&self) -> Result<(), Box<dyn Error>> {
if !IDENTIFIER_REGEX.is_match(&self.identifier) {
return Err(Box::new(ApiError {
message: "Identifier field is invalid.".to_string(),
}));
}
if !PASSWORD_REGEX.is_match(&self.password) {
return Err(Box::new(ApiError {
message: "Password field is invalid.".to_string(),
}));
}
Ok(())
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthenticationPostResponseV3 {
pub account_id: String,
pub client_id: String,
pub lightstreamer_endpoint: String,
pub oauth_token: OauthToken,
pub timezone_offset: f64,
}
impl ValidateResponse for AuthenticationPostResponseV3 {}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionDetailsGetRequest {
pub fetch_session_tokens: bool,
}
impl ValidateRequest for SessionDetailsGetRequest {}
#[derive(Debug, Deserialize, Serialize)]
pub struct OauthToken {
pub access_token: String,
pub expires_in: String,
pub refresh_token: String,
pub scope: String,
pub token_type: String,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionDetailsGetResponse {
pub account_id: String,
pub client_id: String,
pub currency: String,
pub lightstreamer_endpoint: String,
pub locale: String,
pub timezone_offset: f64,
}
impl ValidateResponse for SessionDetailsGetResponse {}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionEncryptionKeyGetResponse {
pub encryption_key: String,
pub time_stamp: u64,
}
impl ValidateResponse for SessionEncryptionKeyGetResponse {}
#[derive(Debug, Serialize)]
pub struct SessionRefreshTokenPostRequest {
pub refresh_token: String,
}
impl ValidateRequest for SessionRefreshTokenPostRequest {
fn validate(&self) -> Result<(), Box<dyn Error>> {
if self.refresh_token.is_empty() {
return Err(Box::new(ApiError {
message: "Refresh token field is empty.".to_string(),
}));
}
Ok(())
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct SessionRefreshTokenPostResponse {
pub access_token: String,
pub expires_in: String,
pub refresh_token: String,
pub scope: String,
pub token_type: String,
}
impl ValidateResponse for SessionRefreshTokenPostResponse {}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkingOrder {
market_data: MarketData,
working_order_data: WorkingOrderData,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkingOrderData {
pub created_date: String,
#[serde(rename = "createdDateUTC")]
pub created_date_utc: String,
pub currency_code: String,
pub deal_id: String,
pub direction: Direction,
pub dma: Option<bool>,
pub epic: Option<String>,
pub good_till_date: Option<String>,
#[serde(rename = "goodTillDateISO")]
pub good_till_date_iso: Option<String>,
pub guaranteed_stop: bool,
pub limit_distance: Option<f64>,
pub limited_risk_premium: Option<f64>,
pub order_level: Option<f64>,
pub order_size: Option<f64>,
pub order_type: WorkingOrderType,
pub stop_distance: Option<f64>,
pub time_in_force: WorkingOrderTimeInForce,
}
#[derive(Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkingOrderDeleteRequest {
pub deal_id: String,
}
impl ValidateRequest for WorkingOrderDeleteRequest {
fn validate(&self) -> Result<(), Box<dyn Error>> {
if !DEAL_ID_REGEX.is_match(&self.deal_id) {
return Err(Box::new(ApiError {
message: "Deal ID field is invalid.".to_string(),
}));
}
Ok(())
}
}
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkingOrderDeleteResponse {
pub deal_reference: String,
}
impl ValidateResponse for WorkingOrderDeleteResponse {
fn validate(&self) -> Result<(), Box<dyn Error>> {
if !DEAL_REFERENCE_REGEX.is_match(&self.deal_reference) {
return Err(Box::new(ApiError {
message: "Deal reference field is invalid.".to_string(),
}));
}
Ok(())
}
}
#[derive(Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkingOrderPostRequest {
pub currency_code: String,
pub deal_reference: Option<String>,
pub direction: Direction,
pub epic: String,
pub expiry: String,
pub force_open: Option<bool>,
pub good_till_date: Option<String>,
pub guaranteed_stop: bool,
pub level: f64,
pub limit_distance: Option<f64>,
pub limit_level: Option<f64>,
pub size: f64,
pub stop_distance: Option<f64>,
pub stop_level: Option<f64>,
pub time_in_force: WorkingOrderTimeInForce,
pub r#type: WorkingOrderType,
}
impl ValidateRequest for WorkingOrderPostRequest {
fn validate(&self) -> Result<(), Box<dyn Error>> {
if !CURRENCY_CODE_REGEX.is_match(&self.currency_code) {
return Err(Box::new(ApiError {
message: "Currency code field is invalid.".to_string(),
}));
}
if let Some(deal_reference) = &self.deal_reference {
if !DEAL_REFERENCE_REGEX.is_match(deal_reference) {
return Err(Box::new(ApiError {
message: "Deal reference field is invalid.".to_string(),
}));
}
}
if !EPIC_REGEX.is_match(&self.epic) {
return Err(Box::new(ApiError {
message: "Epic field is invalid.".to_string(),
}));
}
if !EXPIRY_REGEX.is_match(&self.expiry) {
return Err(Box::new(ApiError {
message: "Expiry field is invalid.".to_string(),
}));
}
let size_str = format!("{}", self.size);
let parts: Vec<&str> = size_str.split('.').collect();
if parts.len() == 2 && parts[1].len() > 12 {
return Err(Box::new(ApiError {
message: "Size field has more thatn 12 decimal places.".to_string(),
}));
}
if self.guaranteed_stop == true && self.stop_distance.is_none() {
return Err(Box::new(ApiError {
message: "stop_distance field is required when guaranteed_stop is true."
.to_string(),
}));
}
match &self.time_in_force {
WorkingOrderTimeInForce::GoodTillDate => {
if self.good_till_date.is_none() {
return Err(Box::new(ApiError {
message:
"good_till_date field is required when time_in_force is GOOD_TILL_DATE."
.to_string(),
}));
}
}
_ => {}
}
if self.limit_level.is_some() && self.limit_distance.is_some() {
return Err(Box::new(ApiError {
message: "Set only one of {limit_level, limit_distance}.".to_string(),
}));
}
if self.stop_level.is_some() && self.stop_distance.is_some() {
return Err(Box::new(ApiError {
message: "Set only one of {stop_level, stop_distance}.".to_string(),
}));
}
Ok(())
}
}
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkingOrderPostResponse {
pub deal_reference: String,
}
impl ValidateResponse for WorkingOrderPostResponse {
fn validate(&self) -> Result<(), Box<dyn Error>> {
if !DEAL_REFERENCE_REGEX.is_match(&self.deal_reference) {
return Err(Box::new(ApiError {
message: "Deal reference field is invalid.".to_string(),
}));
}
Ok(())
}
}
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkingOrderPutRequest {
pub good_till_date: Option<String>,
pub guaranteed_stop: Option<bool>,
pub level: f64,
pub limit_distance: Option<f64>,
pub limit_level: Option<f64>,
pub stop_distance: Option<f64>,
pub stop_level: Option<f64>,
pub time_in_force: WorkingOrderTimeInForce,
pub r#type: WorkingOrderType,
}
impl ValidateRequest for WorkingOrderPutRequest {
fn validate(&self) -> Result<(), Box<dyn Error>> {
if self.guaranteed_stop == Some(true) && self.stop_level.is_none() {
return Err(Box::new(ApiError {
message: "stop_level must be set when guaranteed_stop is true.".to_string(),
}));
}
match &self.time_in_force {
WorkingOrderTimeInForce::GoodTillDate => {
if self.good_till_date.is_none() {
return Err(Box::new(ApiError {
message:
"good_till_date field is required when time_in_force is GOOD_TILL_DATE."
.to_string(),
}));
}
}
_ => {}
}
if self.limit_level.is_some() && self.limit_distance.is_some() {
return Err(Box::new(ApiError {
message: "Set only one of {limit_level, limit_distance}.".to_string(),
}));
}
if self.stop_level.is_some() && self.stop_distance.is_some() {
return Err(Box::new(ApiError {
message: "Set only one of {stop_level, stop_distance}.".to_string(),
}));
}
Ok(())
}
}
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkingOrderPutResponse {
pub deal_reference: String,
}
impl ValidateResponse for WorkingOrderPutResponse {
fn validate(&self) -> Result<(), Box<dyn Error>> {
if !DEAL_REFERENCE_REGEX.is_match(&self.deal_reference) {
return Err(Box::new(ApiError {
message: "Deal reference field is invalid.".to_string(),
}));
}
Ok(())
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkingOrdersGetResponse {
pub working_orders: Vec<WorkingOrder>,
}
impl ValidateResponse for WorkingOrdersGetResponse {}
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum WorkingOrderType {
Limit,
#[default]
Stop,
}
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum WorkingOrderTimeInForce {
#[default]
GoodTillCancelled,
GoodTillDate,
}
#[derive(Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PricesGetRequest {
pub resolution: Option<Resolution>,
pub from: Option<NaiveDateTime>,
pub to: Option<NaiveDateTime>,
pub max: Option<u32>,
pub page_size: Option<u32>,
pub page_number: Option<u32>,
}
impl ValidateRequest for PricesGetRequest {
fn validate(&self) -> Result<(), Box<dyn Error>> {
match (self.from, self.to) {
(Some(from), Some(to)) => {
if to < from {
return Err(Box::new(ApiError {
message: "End date cannot be before start date".to_string(),
}));
}
}
_ => {}
}
Ok(())
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PricesGetResponse {
pub metadata: PriceMetadata,
pub instrument_type: InstrumentType,
pub prices: Vec<Price>
}
impl ValidateResponse for PricesGetResponse {}