use crate::domain::series::SeriesSnapshot;
use crate::session::{ExpiryRule, ExpiryRuleKind, SessionV2};
use chrono::{DateTime, SecondsFormat, Utc};
use optionstratlib::chains::OptionData;
use rust_decimal::Decimal;
use rust_decimal::prelude::ToPrimitive;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[must_use]
#[inline]
fn render_instant(instant: DateTime<Utc>) -> String {
instant.to_rfc3339_opts(SecondsFormat::Secs, true)
}
#[must_use]
#[inline]
fn decimal_to_f64(value: Option<Decimal>) -> Option<f64> {
value.and_then(|value| value.to_f64())
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
pub struct CursorResponse {
pub current_step: usize,
pub total_steps: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
pub struct ScheduleRuleResponse {
pub rule_id: String,
pub kind: String,
pub target_count: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub weekdays: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub weekday: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub month: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
pub struct SimulationParametersResponse {
pub symbol: String,
pub steps: usize,
pub seed: u64,
pub effective_start: String,
pub step_interval_seconds: u64,
pub time_frame: String,
pub timezone: String,
pub calendar: String,
pub tzdb_version: String,
pub expiration_time: String,
pub schedules: Vec<ScheduleRuleResponse>,
pub initial_price: f64,
pub volatility: f64,
pub risk_free_rate: f64,
pub dividend_yield: f64,
#[schema(value_type = Object)]
pub method: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub chain_size: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub strike_interval: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub skew_slope: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub smile_curve: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spread: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
pub struct SimulationResponse {
pub id: String,
pub state: String,
pub version: u64,
pub cursor: CursorResponse,
pub created_at: String,
pub updated_at: String,
pub parameters: SimulationParametersResponse,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
pub struct UnderlyingResponse {
pub symbol: String,
pub price: f64,
pub base_volatility: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema, Default)]
pub struct OptionQuoteResponse {
pub bid: Option<f64>,
pub ask: Option<f64>,
pub mid: Option<f64>,
pub delta: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
pub struct ContractResponse {
pub strike: f64,
pub implied_volatility: f64,
pub gamma: Option<f64>,
pub call: OptionQuoteResponse,
pub put: OptionQuoteResponse,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
pub struct ExpiryChainResponse {
pub expires_at: String,
pub days_to_expiration: f64,
pub labels: Vec<String>,
pub contracts: Vec<ContractResponse>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
pub struct SnapshotResponse {
pub id: String,
pub state: String,
pub version: u64,
pub cursor: CursorResponse,
pub simulated_at: String,
pub underlying: UnderlyingResponse,
pub chains: Vec<ExpiryChainResponse>,
}
impl From<&OptionData> for ContractResponse {
fn from(data: &OptionData) -> Self {
Self {
strike: data.strike_price.to_f64(),
implied_volatility: data.implied_volatility.to_f64(),
gamma: decimal_to_f64(data.gamma),
call: OptionQuoteResponse {
bid: data.call_bid.map(|value| value.to_f64()),
ask: data.call_ask.map(|value| value.to_f64()),
mid: data.call_middle.map(|value| value.to_f64()),
delta: decimal_to_f64(data.delta_call),
},
put: OptionQuoteResponse {
bid: data.put_bid.map(|value| value.to_f64()),
ask: data.put_ask.map(|value| value.to_f64()),
mid: data.put_middle.map(|value| value.to_f64()),
delta: decimal_to_f64(data.delta_put),
},
}
}
}
impl From<&ExpiryRule> for ScheduleRuleResponse {
fn from(rule: &ExpiryRule) -> Self {
let (kind, weekdays, weekday, month) = match rule.kind() {
ExpiryRuleKind::Daily => ("daily", None, None, None),
ExpiryRuleKind::Weekly { weekdays } => (
"weekly",
Some(weekdays.iter().map(ToString::to_string).collect()),
None,
None,
),
ExpiryRuleKind::Monthly { weekday } => {
("monthly", None, Some(weekday.to_string()), None)
}
ExpiryRuleKind::Yearly { weekday, month } => {
("yearly", None, Some(weekday.to_string()), Some(*month))
}
};
Self {
rule_id: rule.rule_id().to_string(),
kind: kind.to_string(),
target_count: rule.target_count().get(),
weekdays,
weekday,
month,
}
}
}
impl From<&SessionV2> for SimulationParametersResponse {
fn from(simulation: &SessionV2) -> Self {
let parameters = &simulation.parameters;
let schedule = ¶meters.schedule;
Self {
symbol: parameters.symbol.clone(),
steps: parameters.steps,
seed: parameters.seed,
effective_start: render_instant(parameters.effective_start),
step_interval_seconds: parameters.step_interval_seconds,
time_frame: parameters.time_frame.to_string(),
timezone: schedule.timezone().name().to_string(),
calendar: schedule.calendar().as_str().to_string(),
tzdb_version: parameters.tzdb_version.clone(),
expiration_time: schedule.expiration_time().format("%H:%M:%S").to_string(),
schedules: schedule.rules().iter().map(Into::into).collect(),
initial_price: parameters.initial_price.to_f64(),
volatility: parameters.volatility.to_f64(),
risk_free_rate: parameters.risk_free_rate.to_f64().unwrap_or_default(),
dividend_yield: parameters.dividend_yield.to_f64(),
method: serde_json::to_value(¶meters.method).unwrap_or(serde_json::Value::Null),
chain_size: parameters.chain_size,
strike_interval: parameters.strike_interval.map(|value| value.to_f64()),
skew_slope: parameters.skew_slope.and_then(|value| value.to_f64()),
smile_curve: parameters.smile_curve.and_then(|value| value.to_f64()),
spread: parameters.spread.map(|value| value.to_f64()),
}
}
}
impl From<&SessionV2> for SimulationResponse {
fn from(simulation: &SessionV2) -> Self {
Self {
id: simulation.id.to_string(),
state: render_state(simulation),
version: simulation.version,
cursor: CursorResponse {
current_step: simulation.current_step,
total_steps: simulation.total_steps,
},
created_at: render_system_time(simulation.created_at),
updated_at: render_system_time(simulation.updated_at),
parameters: simulation.into(),
}
}
}
#[must_use]
fn render_state(simulation: &SessionV2) -> String {
use crate::session::SessionState;
match simulation.state {
SessionState::Initialized => "initialized",
SessionState::InProgress => "in_progress",
SessionState::Completed => "completed",
SessionState::Error => "error",
SessionState::Modified => "modified",
SessionState::Reinitialized => "reinitialized",
}
.to_string()
}
#[must_use]
fn render_system_time(time: std::time::SystemTime) -> String {
render_instant(DateTime::<Utc>::from(time))
}
#[must_use]
pub(crate) fn snapshot_response(
simulation: &SessionV2,
snapshot: &SeriesSnapshot,
) -> SnapshotResponse {
SnapshotResponse {
id: simulation.id.to_string(),
state: render_state(simulation),
version: simulation.version,
cursor: CursorResponse {
current_step: simulation.current_step,
total_steps: simulation.total_steps,
},
simulated_at: render_instant(snapshot.simulated_at),
underlying: UnderlyingResponse {
symbol: simulation.parameters.symbol.clone(),
price: snapshot.spot.to_f64(),
base_volatility: snapshot.base_volatility.to_f64(),
},
chains: snapshot
.chains
.iter()
.map(|chain| ExpiryChainResponse {
expires_at: render_instant(chain.expires_at),
days_to_expiration: chain.days_to_expiration.to_f64(),
labels: chain.labels.clone(),
contracts: chain.chain.iter().map(Into::into).collect(),
})
.collect(),
}
}