use crate::presentation::instrument::InstrumentType;
use crate::presentation::serialization::{string_as_bool_opt, string_as_float_opt};
use lightstreamer_rs::subscription::ItemUpdate;
use pretty_simple_display::{DebugPretty, DisplaySimple};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq)]
pub struct Instrument {
pub epic: String,
pub name: String,
pub expiry: String,
#[serde(rename = "contractSize")]
pub contract_size: String,
#[serde(rename = "lotSize")]
pub lot_size: Option<f64>,
#[serde(rename = "highLimitPrice")]
pub high_limit_price: Option<f64>,
#[serde(rename = "lowLimitPrice")]
pub low_limit_price: Option<f64>,
#[serde(rename = "marginFactor")]
pub margin_factor: Option<f64>,
#[serde(rename = "marginFactorUnit")]
pub margin_factor_unit: Option<String>,
pub currencies: Option<Vec<Currency>>,
#[serde(rename = "valueOfOnePip")]
pub value_of_one_pip: String,
#[serde(rename = "instrumentType")]
pub instrument_type: Option<InstrumentType>,
#[serde(rename = "expiryDetails")]
pub expiry_details: Option<ExpiryDetails>,
#[serde(rename = "slippageFactor")]
pub slippage_factor: Option<StepDistance>,
#[serde(rename = "limitedRiskPremium")]
pub limited_risk_premium: Option<StepDistance>,
#[serde(rename = "newsCode")]
pub news_code: Option<String>,
#[serde(rename = "chartCode")]
pub chart_code: Option<String>,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq)]
pub struct Currency {
pub code: String,
pub symbol: Option<String>,
#[serde(rename = "baseExchangeRate")]
pub base_exchange_rate: Option<f64>,
#[serde(rename = "exchangeRate")]
pub exchange_rate: Option<f64>,
#[serde(rename = "isDefault")]
pub is_default: Option<bool>,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct MarketDetails {
pub instrument: Instrument,
pub snapshot: MarketSnapshot,
#[serde(rename = "dealingRules")]
pub dealing_rules: DealingRules,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct DealingRules {
#[serde(rename = "minStepDistance")]
pub min_step_distance: Option<StepDistance>,
#[serde(rename = "minDealSize")]
pub min_deal_size: Option<StepDistance>,
#[serde(rename = "minControlledRiskStopDistance")]
pub min_controlled_risk_stop_distance: Option<StepDistance>,
#[serde(rename = "minNormalStopOrLimitDistance")]
pub min_normal_stop_or_limit_distance: Option<StepDistance>,
#[serde(rename = "maxStopOrLimitDistance")]
pub max_stop_or_limit_distance: Option<StepDistance>,
#[serde(rename = "controlledRiskSpacing")]
pub controlled_risk_spacing: Option<StepDistance>,
#[serde(rename = "marketOrderPreference")]
pub market_order_preference: String,
#[serde(rename = "trailingStopsPreference")]
pub trailing_stops_preference: String,
#[serde(rename = "maxDealSize")]
pub max_deal_size: Option<f64>,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct MarketSnapshot {
#[serde(rename = "marketStatus")]
pub market_status: String,
#[serde(rename = "netChange")]
pub net_change: Option<f64>,
#[serde(rename = "percentageChange")]
pub percentage_change: Option<f64>,
#[serde(rename = "updateTime")]
pub update_time: Option<String>,
#[serde(rename = "delayTime")]
pub delay_time: Option<i64>,
pub bid: Option<f64>,
pub offer: Option<f64>,
pub high: Option<f64>,
pub low: Option<f64>,
#[serde(rename = "binaryOdds")]
pub binary_odds: Option<f64>,
#[serde(rename = "decimalPlacesFactor")]
pub decimal_places_factor: Option<i64>,
#[serde(rename = "scalingFactor")]
pub scaling_factor: Option<i64>,
#[serde(rename = "controlledRiskExtraSpread")]
pub controlled_risk_extra_spread: Option<f64>,
}
#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
pub struct MarketData {
pub epic: String,
#[serde(rename = "instrumentName")]
pub instrument_name: String,
#[serde(rename = "instrumentType")]
pub instrument_type: InstrumentType,
pub expiry: String,
#[serde(rename = "highLimitPrice")]
pub high_limit_price: Option<f64>,
#[serde(rename = "lowLimitPrice")]
pub low_limit_price: Option<f64>,
#[serde(rename = "marketStatus")]
pub market_status: String,
#[serde(rename = "netChange")]
pub net_change: Option<f64>,
#[serde(rename = "percentageChange")]
pub percentage_change: Option<f64>,
#[serde(rename = "updateTime")]
pub update_time: Option<String>,
#[serde(rename = "updateTimeUTC")]
pub update_time_utc: Option<String>,
pub bid: Option<f64>,
pub offer: Option<f64>,
}
impl MarketData {
pub fn is_call(&self) -> bool {
self.instrument_name.contains("CALL")
}
pub fn is_put(&self) -> bool {
self.instrument_name.contains("PUT")
}
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct HistoricalPrice {
#[serde(rename = "snapshotTime")]
pub snapshot_time: String,
#[serde(rename = "openPrice")]
pub open_price: PricePoint,
#[serde(rename = "highPrice")]
pub high_price: PricePoint,
#[serde(rename = "lowPrice")]
pub low_price: PricePoint,
#[serde(rename = "closePrice")]
pub close_price: PricePoint,
#[serde(rename = "lastTradedVolume")]
pub last_traded_volume: Option<i64>,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct PricePoint {
pub bid: Option<f64>,
pub ask: Option<f64>,
#[serde(rename = "lastTraded")]
pub last_traded: Option<f64>,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct PriceAllowance {
#[serde(rename = "remainingAllowance")]
pub remaining_allowance: i64,
#[serde(rename = "totalAllowance")]
pub total_allowance: i64,
#[serde(rename = "allowanceExpiry")]
pub allowance_expiry: i64,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq)]
pub struct ExpiryDetails {
#[serde(rename = "lastDealingDate")]
pub last_dealing_date: String,
#[serde(rename = "settlementInfo")]
pub settlement_info: Option<String>,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum StepUnit {
#[serde(rename = "POINTS")]
Points,
#[serde(rename = "PERCENTAGE")]
Percentage,
#[serde(rename = "pct")]
Pct,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq)]
pub struct StepDistance {
pub unit: Option<StepUnit>,
pub value: Option<f64>,
}
#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
pub struct MarketNavigationNode {
pub id: String,
pub name: String,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct MarketNode {
pub id: String,
pub name: String,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub children: Vec<MarketNode>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub markets: Vec<MarketData>,
}
#[repr(u8)]
#[derive(
DebugPretty, DisplaySimple, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash, Default,
)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum MarketState {
Closed,
#[default]
Offline,
Tradeable,
Edit,
EditsOnly,
Auction,
AuctionNoEdit,
Suspended,
OnAuction,
OnAuctionNoEdits,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
pub struct PresentationMarketData {
pub item_name: String,
pub item_pos: i32,
pub fields: MarketFields,
pub changed_fields: MarketFields,
pub is_snapshot: bool,
}
impl PresentationMarketData {
pub fn from_item_update(item_update: &ItemUpdate) -> Result<Self, String> {
let item_name = item_update.item_name.clone().unwrap_or_default();
let item_pos = item_update.item_pos as i32;
let is_snapshot = item_update.is_snapshot;
let fields = Self::create_market_fields(&item_update.fields)?;
let mut changed_fields_map: HashMap<String, Option<String>> = HashMap::new();
for (key, value) in &item_update.changed_fields {
changed_fields_map.insert(key.clone(), Some(value.clone()));
}
let changed_fields = Self::create_market_fields(&changed_fields_map)?;
Ok(PresentationMarketData {
item_name,
item_pos,
fields,
changed_fields,
is_snapshot,
})
}
fn create_market_fields(
fields_map: &HashMap<String, Option<String>>,
) -> Result<MarketFields, String> {
let get_field = |key: &str| -> Option<String> { fields_map.get(key).cloned().flatten() };
let market_state = match get_field("MARKET_STATE").as_deref() {
Some("closed") => Some(MarketState::Closed),
Some("offline") => Some(MarketState::Offline),
Some("tradeable") => Some(MarketState::Tradeable),
Some("edit") => Some(MarketState::Edit),
Some("auction") => Some(MarketState::Auction),
Some("auction_no_edit") => Some(MarketState::AuctionNoEdit),
Some("suspended") => Some(MarketState::Suspended),
Some("on_auction") => Some(MarketState::OnAuction),
Some("on_auction_no_edit") => Some(MarketState::OnAuctionNoEdits),
Some(unknown) => return Err(format!("Unknown market state: {unknown}")),
None => None,
};
let market_delay = match get_field("MARKET_DELAY").as_deref() {
Some("0") => Some(false),
Some("1") => Some(true),
Some(val) => return Err(format!("Invalid MARKET_DELAY value: {val}")),
None => None,
};
let parse_float = |key: &str| -> Result<Option<f64>, String> {
match get_field(key) {
Some(val) if !val.is_empty() => val
.parse::<f64>()
.map(Some)
.map_err(|_| format!("Failed to parse {key} as float: {val}")),
_ => Ok(None),
}
};
Ok(MarketFields {
mid_open: parse_float("MID_OPEN")?,
high: parse_float("HIGH")?,
offer: parse_float("OFFER")?,
change: parse_float("CHANGE")?,
market_delay,
low: parse_float("LOW")?,
bid: parse_float("BID")?,
change_pct: parse_float("CHANGE_PCT")?,
market_state,
update_time: get_field("UPDATE_TIME"),
})
}
}
impl From<&ItemUpdate> for PresentationMarketData {
fn from(item_update: &ItemUpdate) -> Self {
Self::from_item_update(item_update).unwrap_or_else(|_| PresentationMarketData {
item_name: String::new(),
item_pos: 0,
fields: MarketFields::default(),
changed_fields: MarketFields::default(),
is_snapshot: false,
})
}
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct Category {
pub code: String,
#[serde(rename = "nonTradeable")]
pub non_tradeable: bool,
}
#[repr(u8)]
#[derive(
DebugPretty, DisplaySimple, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq, Hash,
)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum CategoryMarketStatus {
#[default]
Offline,
Closed,
Suspended,
OnAuction,
OnAuctionNoEdits,
EditsOnly,
ClosingsOnly,
DealNoEdit,
Tradeable,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct CategoryInstrument {
pub epic: String,
#[serde(rename = "instrumentName")]
pub instrument_name: String,
pub expiry: String,
#[serde(rename = "instrumentType")]
pub instrument_type: InstrumentType,
#[serde(rename = "lotSize")]
pub lot_size: Option<f64>,
#[serde(rename = "otcTradeable")]
pub otc_tradeable: bool,
#[serde(rename = "marketStatus")]
pub market_status: CategoryMarketStatus,
#[serde(rename = "delayTime")]
pub delay_time: Option<i64>,
pub bid: Option<f64>,
pub offer: Option<f64>,
pub high: Option<f64>,
pub low: Option<f64>,
#[serde(rename = "netChange")]
pub net_change: Option<f64>,
#[serde(rename = "percentageChange")]
pub percentage_change: Option<f64>,
#[serde(rename = "updateTime")]
pub update_time: Option<String>,
#[serde(rename = "scalingFactor")]
pub scaling_factor: Option<i64>,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct CategoryInstrumentsMetadata {
#[serde(rename = "pageNumber")]
pub page_number: i32,
#[serde(rename = "pageSize")]
pub page_size: i32,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct MarketFields {
#[serde(rename = "MID_OPEN")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub mid_open: Option<f64>,
#[serde(rename = "HIGH")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub high: Option<f64>,
#[serde(rename = "OFFER")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub offer: Option<f64>,
#[serde(rename = "CHANGE")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub change: Option<f64>,
#[serde(rename = "MARKET_DELAY")]
#[serde(with = "string_as_bool_opt")]
#[serde(default)]
pub market_delay: Option<bool>,
#[serde(rename = "LOW")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub low: Option<f64>,
#[serde(rename = "BID")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub bid: Option<f64>,
#[serde(rename = "CHANGE_PCT")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub change_pct: Option<f64>,
#[serde(rename = "MARKET_STATE")]
#[serde(default)]
pub market_state: Option<MarketState>,
#[serde(rename = "UPDATE_TIME")]
#[serde(default)]
pub update_time: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_market_data_is_call_returns_true_for_call_option() {
let market = MarketData {
epic: "test".to_string(),
instrument_name: "DAX CALL 18000".to_string(),
instrument_type: InstrumentType::default(),
expiry: "-".to_string(),
high_limit_price: None,
low_limit_price: None,
market_status: "TRADEABLE".to_string(),
net_change: None,
percentage_change: None,
update_time: None,
update_time_utc: None,
bid: Some(100.0),
offer: Some(101.0),
};
assert!(market.is_call());
assert!(!market.is_put());
}
#[test]
fn test_market_data_is_put_returns_true_for_put_option() {
let market = MarketData {
epic: "test".to_string(),
instrument_name: "DAX PUT 17000".to_string(),
instrument_type: InstrumentType::default(),
expiry: "-".to_string(),
high_limit_price: None,
low_limit_price: None,
market_status: "TRADEABLE".to_string(),
net_change: None,
percentage_change: None,
update_time: None,
update_time_utc: None,
bid: Some(50.0),
offer: Some(51.0),
};
assert!(market.is_put());
assert!(!market.is_call());
}
#[test]
fn test_market_data_neither_call_nor_put() {
let market = MarketData {
epic: "IX.D.DAX.DAILY.IP".to_string(),
instrument_name: "Germany 40".to_string(),
instrument_type: InstrumentType::default(),
expiry: "-".to_string(),
high_limit_price: None,
low_limit_price: None,
market_status: "TRADEABLE".to_string(),
net_change: None,
percentage_change: None,
update_time: None,
update_time_utc: None,
bid: Some(18000.0),
offer: Some(18001.0),
};
assert!(!market.is_call());
assert!(!market.is_put());
}
#[test]
fn test_market_state_default() {
let state = MarketState::default();
assert_eq!(state, MarketState::Offline);
}
#[test]
fn test_category_market_status_default() {
let status = CategoryMarketStatus::default();
assert_eq!(status, CategoryMarketStatus::Offline);
}
#[test]
fn test_step_unit_serialization() {
let points = StepUnit::Points;
let json = serde_json::to_string(&points).expect("serialize failed");
assert_eq!(json, "\"POINTS\"");
let pct = StepUnit::Percentage;
let json = serde_json::to_string(&pct).expect("serialize failed");
assert_eq!(json, "\"PERCENTAGE\"");
}
#[test]
fn test_step_distance_creation() {
let distance = StepDistance {
unit: Some(StepUnit::Points),
value: Some(1.5),
};
assert_eq!(distance.unit, Some(StepUnit::Points));
assert_eq!(distance.value, Some(1.5));
}
#[test]
fn test_market_fields_default() {
let fields = MarketFields::default();
assert!(fields.mid_open.is_none());
assert!(fields.high.is_none());
assert!(fields.offer.is_none());
assert!(fields.change.is_none());
assert!(fields.market_delay.is_none());
assert!(fields.low.is_none());
assert!(fields.bid.is_none());
assert!(fields.change_pct.is_none());
assert!(fields.market_state.is_none());
assert!(fields.update_time.is_none());
}
#[test]
fn test_presentation_market_data_default() {
let data = PresentationMarketData::default();
assert!(data.item_name.is_empty());
assert_eq!(data.item_pos, 0);
assert!(!data.is_snapshot);
}
#[test]
fn test_category_default() {
let cat = Category::default();
assert!(cat.code.is_empty());
assert!(!cat.non_tradeable);
}
#[test]
fn test_category_instrument_default() {
let inst = CategoryInstrument::default();
assert!(inst.epic.is_empty());
assert!(inst.instrument_name.is_empty());
assert_eq!(inst.market_status, CategoryMarketStatus::Offline);
}
#[test]
fn test_market_state_serialization() {
let tradeable = MarketState::Tradeable;
let json = serde_json::to_string(&tradeable).expect("serialize failed");
assert_eq!(json, "\"TRADEABLE\"");
let closed = MarketState::Closed;
let json = serde_json::to_string(&closed).expect("serialize failed");
assert_eq!(json, "\"CLOSED\"");
}
#[test]
fn test_price_point_creation() {
let point = PricePoint {
bid: Some(100.5),
ask: Some(101.0),
last_traded: Some(100.75),
};
assert_eq!(point.bid, Some(100.5));
assert_eq!(point.ask, Some(101.0));
assert_eq!(point.last_traded, Some(100.75));
}
#[test]
fn test_price_allowance_creation() {
let allowance = PriceAllowance {
remaining_allowance: 100,
total_allowance: 1000,
allowance_expiry: 3600,
};
assert_eq!(allowance.remaining_allowance, 100);
assert_eq!(allowance.total_allowance, 1000);
assert_eq!(allowance.allowance_expiry, 3600);
}
#[test]
fn test_expiry_details_creation() {
let expiry = ExpiryDetails {
last_dealing_date: "2024-12-31".to_string(),
settlement_info: Some("Cash settlement".to_string()),
};
assert_eq!(expiry.last_dealing_date, "2024-12-31");
assert_eq!(expiry.settlement_info, Some("Cash settlement".to_string()));
}
#[test]
fn test_market_navigation_node_creation() {
let node = MarketNavigationNode {
id: "12345".to_string(),
name: "Indices".to_string(),
};
assert_eq!(node.id, "12345");
assert_eq!(node.name, "Indices");
}
#[test]
fn test_market_node_creation() {
let node = MarketNode {
id: "node1".to_string(),
name: "Test Node".to_string(),
children: Vec::new(),
markets: Vec::new(),
};
assert_eq!(node.id, "node1");
assert_eq!(node.name, "Test Node");
assert!(node.children.is_empty());
assert!(node.markets.is_empty());
}
#[test]
fn test_currency_creation() {
let currency = Currency {
code: "USD".to_string(),
symbol: Some("$".to_string()),
base_exchange_rate: Some(1.0),
exchange_rate: Some(1.0),
is_default: Some(true),
};
assert_eq!(currency.code, "USD");
assert_eq!(currency.symbol, Some("$".to_string()));
assert_eq!(currency.is_default, Some(true));
}
#[test]
fn test_create_market_fields_with_valid_data() {
let mut fields_map: HashMap<String, Option<String>> = HashMap::new();
fields_map.insert("BID".to_string(), Some("100.5".to_string()));
fields_map.insert("OFFER".to_string(), Some("101.0".to_string()));
fields_map.insert("HIGH".to_string(), Some("102.0".to_string()));
fields_map.insert("LOW".to_string(), Some("99.0".to_string()));
fields_map.insert("CHANGE".to_string(), Some("1.5".to_string()));
fields_map.insert("CHANGE_PCT".to_string(), Some("1.5".to_string()));
fields_map.insert("MARKET_STATE".to_string(), Some("tradeable".to_string()));
fields_map.insert("MARKET_DELAY".to_string(), Some("0".to_string()));
fields_map.insert("UPDATE_TIME".to_string(), Some("12:30:00".to_string()));
let result = PresentationMarketData::create_market_fields(&fields_map);
assert!(result.is_ok());
let fields = result.expect("should parse");
assert_eq!(fields.bid, Some(100.5));
assert_eq!(fields.offer, Some(101.0));
assert_eq!(fields.high, Some(102.0));
assert_eq!(fields.low, Some(99.0));
assert_eq!(fields.change, Some(1.5));
assert_eq!(fields.market_state, Some(MarketState::Tradeable));
assert_eq!(fields.market_delay, Some(false));
assert_eq!(fields.update_time, Some("12:30:00".to_string()));
}
#[test]
fn test_create_market_fields_with_empty_map() {
let fields_map: HashMap<String, Option<String>> = HashMap::new();
let result = PresentationMarketData::create_market_fields(&fields_map);
assert!(result.is_ok());
let fields = result.expect("should parse");
assert!(fields.bid.is_none());
assert!(fields.offer.is_none());
}
#[test]
fn test_create_market_fields_invalid_market_state() {
let mut fields_map: HashMap<String, Option<String>> = HashMap::new();
fields_map.insert(
"MARKET_STATE".to_string(),
Some("invalid_state".to_string()),
);
let result = PresentationMarketData::create_market_fields(&fields_map);
assert!(result.is_err());
}
#[test]
fn test_create_market_fields_invalid_market_delay() {
let mut fields_map: HashMap<String, Option<String>> = HashMap::new();
fields_map.insert("MARKET_DELAY".to_string(), Some("invalid".to_string()));
let result = PresentationMarketData::create_market_fields(&fields_map);
assert!(result.is_err());
}
#[test]
fn test_create_market_fields_all_market_states() {
let states = vec![
("closed", MarketState::Closed),
("offline", MarketState::Offline),
("tradeable", MarketState::Tradeable),
("edit", MarketState::Edit),
("auction", MarketState::Auction),
("auction_no_edit", MarketState::AuctionNoEdit),
("suspended", MarketState::Suspended),
("on_auction", MarketState::OnAuction),
("on_auction_no_edit", MarketState::OnAuctionNoEdits),
];
for (state_str, expected_state) in states {
let mut fields_map: HashMap<String, Option<String>> = HashMap::new();
fields_map.insert("MARKET_STATE".to_string(), Some(state_str.to_string()));
let result = PresentationMarketData::create_market_fields(&fields_map);
assert!(result.is_ok(), "Failed for state: {}", state_str);
let fields = result.expect("should parse");
assert_eq!(fields.market_state, Some(expected_state));
}
}
#[test]
fn test_market_delay_values() {
let mut fields_map: HashMap<String, Option<String>> = HashMap::new();
fields_map.insert("MARKET_DELAY".to_string(), Some("1".to_string()));
let result = PresentationMarketData::create_market_fields(&fields_map);
assert!(result.is_ok());
let fields = result.expect("should parse");
assert_eq!(fields.market_delay, Some(true));
}
}