use std::collections::{BTreeMap, HashMap};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
fn default_true() -> bool {
true
}
fn default_target_source_info() -> String {
"FromSignal".into()
}
pub const RESULT_FORMAT_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PingResponse {
pub status: String,
pub uptime_secs: u64,
pub data_dir: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProfileInfo {
pub name: String,
pub use_targets: Vec<usize>,
pub close_ratios: Vec<f64>,
#[serde(default = "default_target_source_info")]
pub target_source: String,
pub stoploss_mode: String,
pub rules_count: usize,
pub let_remainder_run: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListProfilesResponse {
pub profiles: Vec<ProfileInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListSymbolsRequest {
pub exchange: Option<String>,
pub data_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SymbolAvailability {
pub exchange: String,
pub symbol: String,
pub data_type: String,
pub timeframe: Option<String>,
pub row_count: u64,
pub earliest: String,
pub latest: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListSymbolsResponse {
pub symbols: Vec<SymbolAvailability>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BacktestConfigMsg {
pub initial_balance: Option<f64>,
pub close_on_finish: Option<bool>,
pub fill_model: Option<String>,
#[serde(default)]
pub sizing: Option<SizingPolicyMsg>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub costs: BTreeMap<String, InstrumentCostsMsg>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct InstrumentCostsMsg {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub commission: Option<CommissionModelMsg>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub swap: Option<SwapScheduleMsg>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", deny_unknown_fields)]
pub enum CommissionModelMsg {
#[serde(alias = "per_lot_per_side")]
PerLotPerSide { amount: f64, currency: String },
#[serde(alias = "notional_rate_per_side")]
NotionalRatePerSide { buy_rate: f64, sell_rate: f64 },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "unit", deny_unknown_fields)]
pub enum SwapAmountMsg {
#[serde(alias = "points")]
Points { long: f64, short: f64 },
#[serde(alias = "currency")]
Currency {
long: f64,
short: f64,
currency: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SwapScheduleMsg {
pub amount: SwapAmountMsg,
pub rollover: String,
pub triple_weekday: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub skipped_weekdays: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", deny_unknown_fields)]
pub enum SizingPolicyMsg {
FixedLot { lots: f64 },
FixedRiskAmount { amount: f64 },
BalanceRiskPercent { percent: f64 },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubmitBacktestResponse {
pub success: bool,
pub job_id: Option<String>,
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetBacktestStatusRequest {
pub job_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatchBacktestRequest {
pub job_id: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct BacktestProgress {
pub stage: String,
pub processed_events: u64,
pub total_events: u64,
pub processed_signals: u64,
pub total_signals: u64,
pub processed_symbols: u64,
pub total_symbols: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BacktestStatusResponse {
pub success: bool,
pub job_id: String,
pub status: String,
pub error: Option<String>,
pub elapsed_ms: Option<u64>,
#[serde(default)]
pub progress: BacktestProgress,
}
impl BacktestStatusResponse {
pub fn is_terminal(&self) -> bool {
matches!(self.status.as_str(), "Completed" | "Failed" | "Cancelled")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum BacktestEvent {
Snapshot { status: BacktestStatusResponse },
Heartbeat { job_id: String, elapsed_ms: u64 },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetBacktestResultRequest {
pub job_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetBacktestResultResponse {
pub success: bool,
pub job_id: String,
pub result: Option<BacktestResultMsg>,
pub error: Option<String>,
#[serde(default)]
pub artifact: Option<ResultArtifactRefMsg>,
#[serde(default = "default_true")]
pub inline_complete: bool,
#[serde(default)]
pub artifact_consumed: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CancelBacktestRequest {
pub job_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CancelBacktestResponse {
pub success: bool,
pub job_id: String,
pub error: Option<String>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResultDeliveryMsg {
#[default]
Auto,
Inline,
Artifact,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResultArtifactRefMsg {
pub format_version: u32,
pub artifact_id: String,
pub byte_len: u64,
pub sha256: String,
pub chunk_size: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GetResultArtifactChunkRequest {
pub artifact_id: String,
pub offset: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetResultArtifactChunkResponse {
pub success: bool,
pub artifact_id: String,
pub offset: u64,
pub data_base64: String,
pub eof: bool,
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DeleteResultArtifactRequest {
pub artifact_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeleteResultArtifactResponse {
pub success: bool,
pub artifact_id: String,
pub error: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum MtmOutputPolicyMsg {
None,
Bounded { max_points: usize },
Full,
}
impl Default for MtmOutputPolicyMsg {
fn default() -> Self {
Self::Bounded { max_points: 4_096 }
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum MarketEntrySizingBasisMsg {
#[default]
FillPrice,
SignalEntryPrice,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct FutureQuoteConfigMsg {
pub signal_latency_ms: i64,
pub slippage_pips: f64,
pub stale_quote_after_ms: Option<i64>,
pub pnl_epsilon: f64,
pub account_currency: String,
pub conversion_stale_after_ms: i64,
pub mtm_output: MtmOutputPolicyMsg,
pub market_entry_sizing_basis: MarketEntrySizingBasisMsg,
}
impl Default for FutureQuoteConfigMsg {
fn default() -> Self {
Self {
signal_latency_ms: 0,
slippage_pips: 0.0,
stale_quote_after_ms: None,
pnl_epsilon: 1.0e-9,
account_currency: String::new(),
conversion_stale_after_ms: 300_000,
mtm_output: MtmOutputPolicyMsg::default(),
market_entry_sizing_basis: MarketEntrySizingBasisMsg::default(),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct EvaluationContextMsg {
pub provider_id: Option<String>,
pub source_id: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EvaluationSectionMsg {
Coverage,
PositionPerformance,
RMetrics,
Excursions,
Execution,
Costs,
Robustness,
Breakdowns,
}
impl EvaluationSectionMsg {
pub const ALL: [Self; 8] = [
Self::Coverage,
Self::PositionPerformance,
Self::RMetrics,
Self::Excursions,
Self::Execution,
Self::Costs,
Self::Robustness,
Self::Breakdowns,
];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EvaluationPositionSideMsg {
Long,
Short,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EvaluationGroupFilterMsg {
Named(String),
Ungrouped,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct PositionFilterMsg {
pub symbols: Vec<String>,
pub sides: Vec<EvaluationPositionSideMsg>,
pub groups: Vec<EvaluationGroupFilterMsg>,
pub close_reasons: Vec<String>,
pub tags: BTreeMap<String, Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BreakdownDimensionMsg {
Symbol,
Side,
Group,
CloseReason,
Tag(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct BootstrapConfigMsg {
pub samples: usize,
pub confidence_level: f64,
pub seed: u64,
pub minimum_sample_size: usize,
}
impl Default for BootstrapConfigMsg {
fn default() -> Self {
Self {
samples: 2_000,
confidence_level: 0.95,
seed: 0xA076_1D64_78BD_642F,
minimum_sample_size: 5,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct SourceCoverageCountsMsg {
pub raw_messages: u64,
pub parsed_messages: u64,
pub skipped_messages: u64,
pub failed_messages: u64,
pub emitted_signals: u64,
pub emitted_entry_signals: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ProviderEvaluationOptionsMsg {
pub context: EvaluationContextMsg,
pub source_coverage: Option<SourceCoverageCountsMsg>,
pub sections: Vec<EvaluationSectionMsg>,
pub filter: PositionFilterMsg,
pub breakdowns: Vec<BreakdownDimensionMsg>,
pub bootstrap: BootstrapConfigMsg,
pub rolling_window: usize,
pub minimum_breakdown_bucket_count: usize,
pub maximum_breakdown_rows: Option<usize>,
pub include_positions: bool,
pub maximum_position_rows: Option<usize>,
}
impl Default for ProviderEvaluationOptionsMsg {
fn default() -> Self {
Self {
context: EvaluationContextMsg::default(),
source_coverage: None,
sections: EvaluationSectionMsg::ALL.to_vec(),
filter: PositionFilterMsg::default(),
breakdowns: Vec::new(),
bootstrap: BootstrapConfigMsg::default(),
rolling_window: 20,
minimum_breakdown_bucket_count: 1,
maximum_breakdown_rows: None,
include_positions: false,
maximum_position_rows: None,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum UnavailableInstrumentPolicyMsg {
#[default]
Skip,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum InstrumentExclusionReasonMsg {
NotSelected,
UnknownInstrument,
UnsupportedEconomics,
NoMarketData,
NoConversionData,
AmbiguousMapping,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExcludedSignalRefMsg {
pub input_index: usize,
pub ts: String,
pub action: String,
pub trade_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExcludedInstrumentMsg {
pub symbol: String,
pub reason: InstrumentExclusionReasonMsg,
pub details: String,
pub skipped_entries: usize,
pub skipped_management: usize,
pub signal_references: Vec<ExcludedSignalRefMsg>,
pub omitted_signal_references: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReplayAdmissionReportMsg {
#[serde(default)]
pub omitted_instruments: usize,
#[serde(default)]
pub omitted_entries: usize,
#[serde(default)]
pub omitted_management: usize,
pub policy: UnavailableInstrumentPolicyMsg,
pub input_signals: usize,
pub retained_signals: usize,
pub excluded_instruments: Vec<ExcludedInstrumentMsg>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BacktestRunSpec {
#[serde(default)]
pub on_unavailable: UnavailableInstrumentPolicyMsg,
pub symbol: String,
#[serde(default)]
pub symbols: Vec<String>,
#[serde(default)]
pub all_symbols: bool,
pub exchange: String,
pub data_type: String,
pub timeframe: Option<String>,
pub from: Option<String>,
pub to: Option<String>,
#[serde(default)]
pub raw_signals: Vec<RawSignalMsg>,
pub profile: Option<String>,
#[serde(default)]
pub profile_def: Option<ManagementProfileMsg>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub entry_profile_routes: Vec<EntryProfileRouteMsg>,
pub config: BacktestConfigMsg,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunBacktestResponse {
pub success: bool,
pub error: Option<String>,
pub result: Option<BacktestResultMsg>,
pub elapsed_ms: u64,
#[serde(default)]
pub artifact: Option<ResultArtifactRefMsg>,
#[serde(default = "default_true")]
pub inline_complete: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct RunBacktestRequest {
pub request: BacktestRunSpec,
#[serde(default)]
pub future: FutureQuoteConfigMsg,
#[serde(default)]
pub evaluation: ProviderEvaluationOptionsMsg,
#[serde(default)]
pub result_delivery: ResultDeliveryMsg,
}
#[derive(Debug, Clone, Serialize)]
pub struct SubmitBacktestRequest {
pub request: RunBacktestRequest,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BacktestMultiRunSpec {
#[serde(default)]
pub on_unavailable: UnavailableInstrumentPolicyMsg,
pub symbol: String,
#[serde(default)]
pub symbols: Vec<String>,
#[serde(default)]
pub all_symbols: bool,
pub exchange: String,
pub data_type: String,
pub timeframe: Option<String>,
pub from: Option<String>,
pub to: Option<String>,
#[serde(default)]
pub raw_signals: Vec<RawSignalMsg>,
pub profiles: Vec<ProfileRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub entry_profile_routes: Vec<EntryProfileRouteMsg>,
pub config: BacktestConfigMsg,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProfileResult {
pub profile: String,
pub success: bool,
pub error: Option<String>,
pub result: Option<BacktestResultMsg>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunBacktestMultiResponse {
#[serde(default = "default_true")]
pub success: bool,
#[serde(default)]
pub error: Option<String>,
pub results: Vec<ProfileResult>,
pub elapsed_ms: u64,
#[serde(default)]
pub artifact: Option<ResultArtifactRefMsg>,
#[serde(default = "default_true")]
pub inline_complete: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct RunBacktestMultiRequest {
pub request: BacktestMultiRunSpec,
#[serde(default)]
pub future: FutureQuoteConfigMsg,
#[serde(default)]
pub evaluation: ProviderEvaluationOptionsMsg,
#[serde(default)]
pub result_delivery: ResultDeliveryMsg,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PriceBasisMsg {
Bid,
Ask,
Mid,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SourceBindingMsg {
pub source: String,
pub timeframe_seconds: u32,
pub price_basis: PriceBasisMsg,
#[serde(default)]
pub alignment_offset_seconds: i32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HistoricalInputsMsg {
pub calendars: BTreeMap<String, TradingCalendarMsg>,
pub inputs: Vec<CalendarInputMsg>,
#[serde(default)]
pub limits: Option<CalendarLimitsMsg>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TradingCalendarMsg {
pub id: String,
pub timezone: String,
#[serde(default = "default_day_boundary")]
pub day_boundary: String,
#[serde(default)]
pub sessions: SessionScheduleMsg,
#[serde(default)]
pub market: MarketScheduleMsg,
}
fn default_day_boundary() -> String {
"00:00:00".into()
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
pub enum SessionScheduleMsg {
#[default]
FullDay,
Custom {
items: Vec<NamedSessionMsg>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NamedSessionMsg {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timezone: Option<String>,
pub span: SessionSpanMsg,
#[serde(default)]
pub weekdays: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum SessionSpanMsg {
FullDay,
Timed {
start: String,
end: String,
end_day_offset: u8,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
pub enum MarketScheduleMsg {
#[default]
Unspecified,
Continuous,
Weekly {
intervals: Vec<WeeklyMarketIntervalMsg>,
#[serde(default)]
exceptions: BTreeMap<String, Vec<LocalMarketIntervalMsg>>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WeeklyMarketIntervalMsg {
pub weekday: u8,
pub start: String,
pub end: String,
pub end_day_offset: u8,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LocalMarketIntervalMsg {
pub start: String,
pub end: String,
pub end_day_offset: u8,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CalendarInputMsg {
pub name: String,
pub source: String,
pub calendar: String,
pub feature: CalendarFeatureMsg,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session: Option<String>,
#[serde(default)]
pub time_basis: CalendarTimeBasisMsg,
#[serde(default = "default_opening_range_minutes")]
pub opening_range_minutes: u32,
pub child_seconds: u64,
#[serde(default)]
pub alignment_offset_seconds: i64,
pub maximum_history: usize,
}
fn default_opening_range_minutes() -> u32 {
5
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CalendarFeatureMsg {
LocalSecondOfDay,
SessionElapsedSeconds,
SessionMembership,
PreviousSessionHigh,
PreviousSessionLow,
PreviousDayHigh,
PreviousDayLow,
PreviousWeekHigh,
PreviousWeekLow,
OpeningRangeHighSoFar,
OpeningRangeLowSoFar,
OpeningRangeHighFinal,
OpeningRangeLowFinal,
PastSameSlotRangeRatio,
PastSameSlotCount,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum CalendarTimeBasisMsg {
#[default]
SourceOpen,
DecisionTime,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CalendarLimitsMsg {
pub max_sessions: usize,
pub max_market_intervals: usize,
pub max_exceptions: usize,
pub max_history_occurrences: usize,
pub max_resolved_children: usize,
pub max_owned_bytes: usize,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ConfiguredStrategyRunMsg {
pub document: serde_json::Value,
pub sources: Vec<SourceBindingMsg>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub instance_id: Option<String>,
#[serde(default)]
pub decision_latency_ms: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub historical_inputs: Option<HistoricalInputsMsg>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ConfiguredStrategyRunSpec {
pub symbol: String,
pub exchange: String,
pub data_type: String,
#[serde(default)]
pub timeframe: Option<String>,
#[serde(default)]
pub from: Option<String>,
#[serde(default)]
pub to: Option<String>,
pub strategy: ConfiguredStrategyRunMsg,
#[serde(default)]
pub profile: Option<String>,
#[serde(default)]
pub profile_def: Option<ManagementProfileMsg>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub entry_profile_routes: Vec<EntryProfileRouteMsg>,
pub config: BacktestConfigMsg,
}
#[derive(Debug, Clone, Serialize)]
pub struct RunConfiguredStrategyRequest {
pub request: ConfiguredStrategyRunSpec,
#[serde(default)]
pub future: FutureQuoteConfigMsg,
#[serde(default)]
pub evaluation: ProviderEvaluationOptionsMsg,
#[serde(default)]
pub result_delivery: ResultDeliveryMsg,
}
#[derive(Debug, Clone, Serialize)]
pub struct SubmitConfiguredStrategyRequest {
pub request: RunConfiguredStrategyRequest,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ConfiguredStrategyOutputMsg {
pub document: serde_json::Value,
pub sources: Vec<SourceBindingMsg>,
pub requirements: serde_json::Value,
pub data_mode: String,
pub decisions: serde_json::Value,
pub research: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub historical_inputs: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PortfolioInstanceMsg {
pub symbol: String,
pub strategy: ConfiguredStrategyRunMsg,
#[serde(default)]
pub profile: Option<String>,
#[serde(default)]
pub profile_def: Option<ManagementProfileMsg>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub entry_profile_routes: Vec<EntryProfileRouteMsg>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PortfolioRunSpec {
pub instances: Vec<PortfolioInstanceMsg>,
pub exchange: String,
pub data_type: String,
#[serde(default)]
pub timeframe: Option<String>,
#[serde(default)]
pub from: Option<String>,
#[serde(default)]
pub to: Option<String>,
pub config: BacktestConfigMsg,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub policies: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub groups: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize)]
pub struct RunPortfolioRequest {
pub request: PortfolioRunSpec,
#[serde(default)]
pub future: FutureQuoteConfigMsg,
#[serde(default)]
pub evaluation: ProviderEvaluationOptionsMsg,
#[serde(default)]
pub result_delivery: ResultDeliveryMsg,
}
#[derive(Debug, Clone, Serialize)]
pub struct SubmitPortfolioRequest {
pub request: RunPortfolioRequest,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PortfolioInstanceOutputMsg {
pub instance_id: String,
pub symbol: String,
pub strategy: ConfiguredStrategyOutputMsg,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PortfolioOutputMsg {
pub instances: Vec<PortfolioInstanceOutputMsg>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub policies: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub supervisor: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SearchWindowMsg {
pub label: String,
pub from: String,
pub to: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
pub enum SearchWindowsMsg {
Fixed {
in_sample: SearchWindowMsg,
out_of_sample: SearchWindowMsg,
},
RollingWalkForward {
start: String,
end: String,
train_seconds: i64,
test_seconds: i64,
step_seconds: i64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SearchEvaluationRoleMsg {
Search,
Validation,
Final,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SearchSelectedRerunMsg {
pub experiment_recipe: serde_json::Value,
pub candidate_recipe: serde_json::Value,
pub run_recipe: serde_json::Value,
pub role: SearchEvaluationRoleMsg,
pub caller_revision: String,
#[serde(default)]
pub release_final: bool,
#[serde(default)]
pub future_horizon_millis: Option<u64>,
#[serde(default)]
pub embargo_millis: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SearchDirectPortfolioInstanceMsg {
pub instance_id: String,
pub symbol: String,
pub factory: SearchDirectFactoryMsg,
#[serde(default)]
pub profile: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SearchPortfolioCandidateMsg {
pub id: String,
#[serde(default)]
pub instances: Vec<PortfolioInstanceMsg>,
#[serde(default)]
pub direct_instances: Vec<SearchDirectPortfolioInstanceMsg>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub policies: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub groups: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", content = "value", rename_all = "snake_case")]
pub enum SearchFactoryParameterMsg {
Integer(i64),
Number(f64),
Choice(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SearchDirectFactoryPointMsg {
pub parameters: BTreeMap<String, SearchFactoryParameterMsg>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SearchDirectFactoryMsg {
pub name: String,
pub revision: String,
pub points: Vec<SearchDirectFactoryPointMsg>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SearchExecutionVariantMsg {
pub id: String,
pub config: BacktestConfigMsg,
#[serde(default)]
pub future: FutureQuoteConfigMsg,
#[serde(default)]
pub profile: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SearchRunSpec {
pub template: serde_json::Value,
pub space: serde_json::Value,
pub symbols: Vec<String>,
pub exchange: String,
pub data_type: String,
#[serde(default)]
pub timeframe: Option<String>,
pub windows: SearchWindowsMsg,
pub config: BacktestConfigMsg,
#[serde(default)]
pub profile: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub entry_profile_routes: Vec<EntryProfileRouteMsg>,
#[serde(default)]
pub workers: Option<usize>,
#[serde(default)]
pub decision_latency_ms: u64,
#[serde(default)]
pub structural: Option<serde_json::Value>,
#[serde(default)]
pub resource_limits: Option<serde_json::Value>,
#[serde(default)]
pub variants: Vec<SearchExecutionVariantMsg>,
#[serde(default)]
pub direct_factory: Option<SearchDirectFactoryMsg>,
#[serde(default)]
pub resume_checkpoint: Option<serde_json::Value>,
#[serde(default)]
pub selected_rerun: Option<SearchSelectedRerunMsg>,
#[serde(default)]
pub portfolio_candidates: Vec<SearchPortfolioCandidateMsg>,
#[serde(default)]
pub series_descriptors: Vec<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize)]
pub struct SubmitSearchRequest {
pub request: SearchRunSpec,
#[serde(default)]
pub future: FutureQuoteConfigMsg,
#[serde(default)]
pub evaluation: ProviderEvaluationOptionsMsg,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GetSearchResultRequest {
pub job_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SearchSummaryMsg {
pub points_total: usize,
pub rows: usize,
pub completed_rows: usize,
pub failed_rows: usize,
pub data_mode: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SearchResultMsg {
pub summary: SearchSummaryMsg,
pub table_csv: String,
pub evaluation: serde_json::Value,
pub bound_documents: Vec<serde_json::Value>,
#[serde(default)]
pub experiment_recipe: Option<serde_json::Value>,
#[serde(default)]
pub candidate_recipes: Vec<serde_json::Value>,
#[serde(default)]
pub run_recipes: Vec<serde_json::Value>,
#[serde(default)]
pub generation_dispositions: Option<serde_json::Value>,
#[serde(default)]
pub checkpoint: Option<serde_json::Value>,
#[serde(default)]
pub selected_evidence: Option<serde_json::Value>,
#[serde(default)]
pub trace: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetSearchResultResponse {
pub success: bool,
pub job_id: String,
#[serde(default)]
pub error: Option<String>,
#[serde(default)]
pub summary: Option<SearchSummaryMsg>,
#[serde(default)]
pub artifact: Option<ResultArtifactRefMsg>,
#[serde(default)]
pub checkpoint_artifact: Option<ResultArtifactRefMsg>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BacktestResultMsg {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub admission_report: Option<ReplayAdmissionReportMsg>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub strategy: Option<ConfiguredStrategyOutputMsg>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub portfolio: Option<PortfolioOutputMsg>,
pub initial_balance: f64,
pub final_balance: f64,
pub total_pnl: f64,
pub total_trades: usize,
pub winning_trades: usize,
pub losing_trades: usize,
pub win_rate: f64,
pub profit_factor: f64,
pub max_drawdown: f64,
pub max_drawdown_pct: f64,
pub summary: SubsetStatsMsg,
pub per_symbol: HashMap<String, SubsetStatsMsg>,
pub per_group: HashMap<String, SubsetStatsMsg>,
pub long_stats: SubsetStatsMsg,
pub short_stats: SubsetStatsMsg,
pub per_close_reason: Vec<CloseReasonStatsMsg>,
pub streaks: StreakStatsMsg,
pub risk_metrics: RiskMetricsMsg,
pub duration_stats: Option<DurationStatsMsg>,
pub monthly_returns: Vec<MonthlyReturnMsg>,
pub equity_curve: Vec<EquityPoint>,
pub trade_log: Vec<TradeResultMsg>,
pub positions: Vec<PositionSummaryMsg>,
pub total_positions: usize,
pub winning_positions: usize,
pub losing_positions: usize,
pub position_win_rate: f64,
#[serde(default)]
pub future: Option<FutureBacktestResultMsg>,
#[serde(default)]
pub total_commission: f64,
#[serde(default)]
pub total_swap: f64,
#[serde(default)]
pub gross_pnl: Option<f64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub cost_events: Vec<CostEventMsg>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CostEventMsg {
pub id: String,
pub position_id: String,
pub symbol: String,
pub side: String,
pub ts: String,
pub kind: String,
pub amount: f64,
#[serde(default)]
pub native_amount: Option<f64>,
#[serde(default)]
pub native_currency: Option<String>,
pub size: f64,
#[serde(default)]
pub nights: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct MtmOutputSummaryMsg {
pub policy: MtmOutputPolicyMsg,
pub observed_points: u64,
pub retained_points: u64,
pub omitted_points: u64,
}
impl Default for MtmOutputSummaryMsg {
fn default() -> Self {
Self {
policy: MtmOutputPolicyMsg::Full,
observed_points: 0,
retained_points: 0,
omitted_points: 0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FutureBacktestResultMsg {
pub format_version: u32,
pub execution_metadata: serde_json::Value,
pub recorded_fills: serde_json::Value,
pub action_dispositions: serde_json::Value,
pub close_events: serde_json::Value,
pub completed_positions: serde_json::Value,
pub open_positions: serde_json::Value,
pub pending_orders: serde_json::Value,
#[serde(default)]
pub pending_order_lifecycle: Vec<PendingOrderLifecycleEventMsg>,
pub mtm_equity_curve: serde_json::Value,
#[serde(default)]
pub mtm_output_summary: MtmOutputSummaryMsg,
pub mtm_max_drawdown: Option<f64>,
pub mtm_max_drawdown_pct: Option<f64>,
pub provider_evaluation: serde_json::Value,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum PendingOrderLifecycleStateMsg {
#[default]
Placed,
Filled,
Cancelled,
UnfilledAtEnd,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct PendingOrderLifecycleEventMsg {
pub id: String,
pub sequence: u64,
pub position_id: String,
pub placement_action_id: Option<String>,
pub terminal_action_id: Option<String>,
pub state: PendingOrderLifecycleStateMsg,
pub symbol: String,
pub side: String,
pub order_type: String,
pub requested_size: f64,
pub filled_size: Option<f64>,
pub requested_price: Option<f64>,
pub fill_price: Option<f64>,
pub signal_ts: Option<String>,
pub placed_ts: Option<String>,
pub effective_ts: Option<String>,
pub terminal_ts: Option<String>,
pub wait_latency_ms: Option<i64>,
pub fill_ratio: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubsetStatsMsg {
pub total_trades: usize,
pub winning_trades: usize,
pub losing_trades: usize,
pub breakeven_trades: usize,
pub total_pnl: f64,
pub gross_profit: f64,
pub gross_loss: f64,
pub win_rate: f64,
pub profit_factor: f64,
pub avg_win: f64,
pub avg_loss: f64,
pub win_loss_ratio: f64,
pub expectancy: f64,
pub largest_win: f64,
pub largest_loss: f64,
#[serde(default)]
pub commission: f64,
#[serde(default)]
pub swap: f64,
#[serde(default)]
pub gross_pnl: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreakStatsMsg {
pub max_consecutive_wins: u32,
pub max_consecutive_losses: u32,
pub current_streak: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RiskMetricsMsg {
pub sharpe_ratio: Option<f64>,
pub sortino_ratio: Option<f64>,
pub calmar_ratio: Option<f64>,
pub return_on_max_drawdown: Option<f64>,
pub max_drawdown: f64,
pub max_drawdown_pct: f64,
pub max_drawdown_duration_secs: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DurationStatsMsg {
pub avg_duration_secs: i64,
pub min_duration_secs: i64,
pub max_duration_secs: i64,
pub avg_winner_duration_secs: i64,
pub avg_loser_duration_secs: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MonthlyReturnMsg {
pub year: i32,
pub month: u32,
pub pnl: f64,
pub trade_count: usize,
pub ending_balance: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CloseReasonStatsMsg {
pub reason: String,
pub count: usize,
pub total_pnl: f64,
pub avg_pnl: f64,
pub percentage: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PositionSummaryMsg {
pub position_id: String,
pub symbol: String,
pub side: String,
pub group: Option<String>,
pub entry_price: f64,
pub avg_exit_price: f64,
pub original_size: f64,
pub close_count: usize,
pub net_pnl: f64,
pub close_reasons: Vec<String>,
pub open_ts: String,
pub final_close_ts: Option<String>,
pub duration_seconds: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EquityPoint {
pub ts: String,
pub balance: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TradeResultMsg {
pub position_id: String,
pub symbol: String,
pub side: String,
pub entry_price: f64,
pub exit_price: f64,
pub size: f64,
pub pnl: f64,
#[serde(default)]
pub commission: f64,
#[serde(default)]
pub swap: f64,
#[serde(default)]
pub gross_pnl: Option<f64>,
pub open_ts: String,
pub close_ts: String,
pub close_reason: String,
pub group: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TargetSelectionMsg {
All,
None,
Selected(Vec<usize>),
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", deny_unknown_fields)]
pub enum TargetSourceMsg {
#[default]
FromSignal,
StopDistanceMultiples {
multiples: Vec<f64>,
},
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum EntryGeometryPolicyMsg {
#[default]
Strict,
Permissive,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManagementProfileMsg {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_selection: Option<TargetSelectionMsg>,
pub use_targets: Vec<usize>,
pub close_ratios: Vec<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_source: Option<TargetSourceMsg>,
#[serde(default)]
pub stoploss_mode: Option<StoplossModeMsg>,
#[serde(default)]
pub rules: Vec<RuleConfigDefMsg>,
#[serde(default)]
pub group_override: Option<String>,
#[serde(default)]
pub let_remainder_run: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub entry_geometry: Option<EntryGeometryPolicyMsg>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum StoplossModeMsg {
FromSignal,
None,
FixedDistance { distance: f64 },
FixedPrice { price: f64 },
FromSignalDistance { multiplier: f64 },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum RuleConfigDefMsg {
FixedStoploss { price: f64 },
TrailingStop { distance: f64 },
TakeProfit { price: f64, close_ratio: f64 },
BreakevenWhen { trigger_price: f64 },
BreakevenWhenOffset { trigger_price_offset: f64 },
BreakevenAfterTargets { after_n: u32 },
TimeExit { max_seconds: u64 },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ProfileRef {
Named(String),
Inline(ManagementProfileMsg),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EntryProfileRouteMsg {
pub entry_class: String,
pub profile: ProfileRef,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "action", deny_unknown_fields)]
pub enum RawSignalMsg {
Entry {
ts: String,
symbol: String,
side: String,
order_type: String,
price: Option<f64>,
risk: f64,
stoploss: Option<f64>,
#[serde(default)]
targets: Vec<f64>,
#[serde(default)]
group: Option<String>,
#[serde(default)]
trade_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
entry_class: Option<String>,
},
Close {
ts: String,
position: PositionRefMsg,
},
ClosePartial {
ts: String,
position: PositionRefMsg,
ratio: f64,
},
ModifyStoploss {
ts: String,
position: PositionRefMsg,
price: f64,
},
MoveStoplossToEntry {
ts: String,
position: PositionRefMsg,
},
AddTarget {
ts: String,
position: PositionRefMsg,
price: f64,
close_ratio: f64,
},
RemoveTarget {
ts: String,
position: PositionRefMsg,
price: f64,
},
ModifyTarget {
ts: String,
position: PositionRefMsg,
old_price: f64,
new_price: f64,
},
AddRule {
ts: String,
position: PositionRefMsg,
rule: RuleConfigDefMsg,
},
RemoveRule {
ts: String,
position: PositionRefMsg,
rule_name: String,
},
ScaleIn {
ts: String,
position: PositionRefMsg,
price: Option<f64>,
size: f64,
},
CancelPending {
ts: String,
position: PositionRefMsg,
},
CloseAllOf { ts: String, symbol: String },
CloseAll { ts: String },
CancelAllPending { ts: String },
ModifyAllStoploss {
ts: String,
symbol: String,
price: f64,
},
CloseAllInGroup { ts: String, group_id: String },
ModifyAllStoplossInGroup {
ts: String,
group_id: String,
price: f64,
},
}
impl RawSignalMsg {
pub fn ts(&self) -> &str {
match self {
RawSignalMsg::Entry { ts, .. }
| RawSignalMsg::Close { ts, .. }
| RawSignalMsg::ClosePartial { ts, .. }
| RawSignalMsg::ModifyStoploss { ts, .. }
| RawSignalMsg::MoveStoplossToEntry { ts, .. }
| RawSignalMsg::AddTarget { ts, .. }
| RawSignalMsg::RemoveTarget { ts, .. }
| RawSignalMsg::ModifyTarget { ts, .. }
| RawSignalMsg::AddRule { ts, .. }
| RawSignalMsg::RemoveRule { ts, .. }
| RawSignalMsg::ScaleIn { ts, .. }
| RawSignalMsg::CancelPending { ts, .. }
| RawSignalMsg::CloseAllOf { ts, .. }
| RawSignalMsg::CloseAll { ts }
| RawSignalMsg::CancelAllPending { ts }
| RawSignalMsg::ModifyAllStoploss { ts, .. }
| RawSignalMsg::CloseAllInGroup { ts, .. }
| RawSignalMsg::ModifyAllStoplossInGroup { ts, .. } => ts,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum PositionRefMsg {
ByTradeId { trade_id: String },
AllOnSymbol { symbol: String },
AllInGroup { group_id: String },
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", deny_unknown_fields)]
enum StrictSizingPolicyMsg {
FixedLot { lots: f64 },
FixedRiskAmount { amount: f64 },
BalanceRiskPercent { percent: f64 },
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", deny_unknown_fields)]
enum StrictCommissionModelMsg {
#[serde(alias = "per_lot_per_side")]
PerLotPerSide { amount: f64, currency: String },
#[serde(alias = "notional_rate_per_side")]
NotionalRatePerSide { buy_rate: f64, sell_rate: f64 },
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "unit", deny_unknown_fields)]
enum StrictSwapAmountMsg {
#[serde(alias = "points")]
Points { long: f64, short: f64 },
#[serde(alias = "currency")]
Currency {
long: f64,
short: f64,
currency: String,
},
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictSwapScheduleMsg {
amount: StrictSwapAmountMsg,
rollover: String,
triple_weekday: String,
#[serde(default)]
skipped_weekdays: Vec<String>,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictInstrumentCostsMsg {
#[serde(default)]
commission: Option<StrictCommissionModelMsg>,
#[serde(default)]
swap: Option<StrictSwapScheduleMsg>,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictBacktestConfigMsg {
initial_balance: Option<f64>,
close_on_finish: Option<bool>,
fill_model: Option<String>,
#[serde(default)]
sizing: Option<StrictSizingPolicyMsg>,
#[serde(default)]
costs: BTreeMap<String, StrictInstrumentCostsMsg>,
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", deny_unknown_fields)]
enum StrictStoplossModeMsg {
FromSignal,
None,
FixedDistance { distance: f64 },
FixedPrice { price: f64 },
FromSignalDistance { multiplier: f64 },
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", deny_unknown_fields)]
enum StrictTargetSourceMsg {
FromSignal,
StopDistanceMultiples { multiples: Vec<f64> },
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", deny_unknown_fields)]
enum StrictRuleConfigDefMsg {
FixedStoploss { price: f64 },
TrailingStop { distance: f64 },
TakeProfit { price: f64, close_ratio: f64 },
BreakevenWhen { trigger_price: f64 },
BreakevenWhenOffset { trigger_price_offset: f64 },
BreakevenAfterTargets { after_n: u32 },
TimeExit { max_seconds: u64 },
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictManagementProfileMsg {
name: String,
#[serde(default)]
target_selection: Option<TargetSelectionMsg>,
use_targets: Vec<usize>,
close_ratios: Vec<f64>,
#[serde(default)]
target_source: Option<StrictTargetSourceMsg>,
#[serde(default)]
stoploss_mode: Option<StrictStoplossModeMsg>,
#[serde(default)]
rules: Vec<StrictRuleConfigDefMsg>,
#[serde(default)]
group_override: Option<String>,
#[serde(default)]
let_remainder_run: bool,
#[serde(default)]
entry_geometry: Option<EntryGeometryPolicyMsg>,
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", deny_unknown_fields)]
enum StrictPositionRefMsg {
ByTradeId { trade_id: String },
AllOnSymbol { symbol: String },
AllInGroup { group_id: String },
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "action", deny_unknown_fields)]
enum StrictRawSignalMsg {
Entry {
ts: String,
symbol: String,
side: String,
order_type: String,
price: Option<f64>,
risk: f64,
stoploss: Option<f64>,
#[serde(default)]
targets: Vec<f64>,
#[serde(default)]
group: Option<String>,
#[serde(default)]
trade_id: Option<String>,
#[serde(default)]
entry_class: Option<String>,
},
Close {
ts: String,
position: StrictPositionRefMsg,
},
ClosePartial {
ts: String,
position: StrictPositionRefMsg,
ratio: f64,
},
ModifyStoploss {
ts: String,
position: StrictPositionRefMsg,
price: f64,
},
MoveStoplossToEntry {
ts: String,
position: StrictPositionRefMsg,
},
AddTarget {
ts: String,
position: StrictPositionRefMsg,
price: f64,
close_ratio: f64,
},
RemoveTarget {
ts: String,
position: StrictPositionRefMsg,
price: f64,
},
ModifyTarget {
ts: String,
position: StrictPositionRefMsg,
old_price: f64,
new_price: f64,
},
AddRule {
ts: String,
position: StrictPositionRefMsg,
rule: StrictRuleConfigDefMsg,
},
RemoveRule {
ts: String,
position: StrictPositionRefMsg,
rule_name: String,
},
ScaleIn {
ts: String,
position: StrictPositionRefMsg,
price: Option<f64>,
size: f64,
},
CancelPending {
ts: String,
position: StrictPositionRefMsg,
},
CloseAllOf {
ts: String,
symbol: String,
},
CloseAll {
ts: String,
},
CancelAllPending {
ts: String,
},
ModifyAllStoploss {
ts: String,
symbol: String,
price: f64,
},
CloseAllInGroup {
ts: String,
group_id: String,
},
ModifyAllStoplossInGroup {
ts: String,
group_id: String,
price: f64,
},
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictBacktestRunSpec {
#[serde(default)]
on_unavailable: UnavailableInstrumentPolicyMsg,
symbol: String,
#[serde(default)]
symbols: Vec<String>,
#[serde(default)]
all_symbols: bool,
exchange: String,
data_type: String,
timeframe: Option<String>,
from: Option<String>,
to: Option<String>,
#[serde(default)]
raw_signals: Vec<StrictRawSignalMsg>,
profile: Option<String>,
#[serde(default)]
profile_def: Option<StrictManagementProfileMsg>,
#[serde(default)]
entry_profile_routes: Vec<StrictEntryProfileRouteMsg>,
config: StrictBacktestConfigMsg,
}
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum StrictProfileRef {
Named(String),
Inline(StrictManagementProfileMsg),
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictEntryProfileRouteMsg {
entry_class: String,
profile: StrictProfileRef,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictBacktestMultiRunSpec {
#[serde(default)]
on_unavailable: UnavailableInstrumentPolicyMsg,
symbol: String,
#[serde(default)]
symbols: Vec<String>,
#[serde(default)]
all_symbols: bool,
exchange: String,
data_type: String,
timeframe: Option<String>,
from: Option<String>,
to: Option<String>,
#[serde(default)]
raw_signals: Vec<StrictRawSignalMsg>,
profiles: Vec<StrictProfileRef>,
#[serde(default)]
entry_profile_routes: Vec<StrictEntryProfileRouteMsg>,
config: StrictBacktestConfigMsg,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictRunBacktestRequest {
request: StrictBacktestRunSpec,
#[serde(default)]
future: FutureQuoteConfigMsg,
#[serde(default)]
evaluation: ProviderEvaluationOptionsMsg,
#[serde(default)]
result_delivery: ResultDeliveryMsg,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictSubmitBacktestRequest {
request: RunBacktestRequest,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictRunBacktestMultiRequest {
request: StrictBacktestMultiRunSpec,
#[serde(default)]
future: FutureQuoteConfigMsg,
#[serde(default)]
evaluation: ProviderEvaluationOptionsMsg,
#[serde(default)]
result_delivery: ResultDeliveryMsg,
}
fn strict_into_wire<T>(value: impl Serialize) -> Result<T, serde_json::Error>
where
T: DeserializeOwned,
{
serde_json::from_value(serde_json::to_value(value)?)
}
impl<'de> Deserialize<'de> for RunBacktestRequest {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let strict = StrictRunBacktestRequest::deserialize(deserializer)?;
Ok(Self {
request: strict_into_wire(strict.request).map_err(serde::de::Error::custom)?,
future: strict.future,
evaluation: strict.evaluation,
result_delivery: strict.result_delivery,
})
}
}
impl<'de> Deserialize<'de> for SubmitBacktestRequest {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let strict = StrictSubmitBacktestRequest::deserialize(deserializer)?;
Ok(Self {
request: strict.request,
})
}
}
impl<'de> Deserialize<'de> for RunBacktestMultiRequest {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let strict = StrictRunBacktestMultiRequest::deserialize(deserializer)?;
Ok(Self {
request: strict_into_wire(strict.request).map_err(serde::de::Error::custom)?,
future: strict.future,
evaluation: strict.evaluation,
result_delivery: strict.result_delivery,
})
}
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictConfiguredStrategyRunSpec {
symbol: String,
exchange: String,
data_type: String,
#[serde(default)]
timeframe: Option<String>,
#[serde(default)]
from: Option<String>,
#[serde(default)]
to: Option<String>,
strategy: ConfiguredStrategyRunMsg,
#[serde(default)]
profile: Option<String>,
#[serde(default)]
profile_def: Option<StrictManagementProfileMsg>,
#[serde(default)]
entry_profile_routes: Vec<StrictEntryProfileRouteMsg>,
config: StrictBacktestConfigMsg,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictRunConfiguredStrategyRequest {
request: StrictConfiguredStrategyRunSpec,
#[serde(default)]
future: FutureQuoteConfigMsg,
#[serde(default)]
evaluation: ProviderEvaluationOptionsMsg,
#[serde(default)]
result_delivery: ResultDeliveryMsg,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictSubmitConfiguredStrategyRequest {
request: RunConfiguredStrategyRequest,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictSearchExecutionVariantMsg {
id: String,
config: StrictBacktestConfigMsg,
#[serde(default)]
future: FutureQuoteConfigMsg,
#[serde(default)]
profile: Option<String>,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictSearchPortfolioCandidateMsg {
id: String,
#[serde(default)]
instances: Vec<StrictPortfolioInstanceMsg>,
#[serde(default)]
direct_instances: Vec<SearchDirectPortfolioInstanceMsg>,
#[serde(default)]
policies: Option<serde_json::Value>,
#[serde(default)]
groups: Option<serde_json::Value>,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictSearchRunSpec {
template: serde_json::Value,
space: serde_json::Value,
symbols: Vec<String>,
exchange: String,
data_type: String,
#[serde(default)]
timeframe: Option<String>,
windows: SearchWindowsMsg,
config: StrictBacktestConfigMsg,
#[serde(default)]
profile: Option<String>,
#[serde(default)]
entry_profile_routes: Vec<StrictEntryProfileRouteMsg>,
#[serde(default)]
workers: Option<usize>,
#[serde(default)]
decision_latency_ms: u64,
#[serde(default)]
structural: Option<serde_json::Value>,
#[serde(default)]
resource_limits: Option<serde_json::Value>,
#[serde(default)]
variants: Vec<StrictSearchExecutionVariantMsg>,
#[serde(default)]
direct_factory: Option<SearchDirectFactoryMsg>,
#[serde(default)]
resume_checkpoint: Option<serde_json::Value>,
#[serde(default)]
selected_rerun: Option<SearchSelectedRerunMsg>,
#[serde(default)]
portfolio_candidates: Vec<StrictSearchPortfolioCandidateMsg>,
#[serde(default)]
series_descriptors: Vec<serde_json::Value>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictSubmitSearchRequest {
request: StrictSearchRunSpec,
#[serde(default)]
future: FutureQuoteConfigMsg,
#[serde(default)]
evaluation: ProviderEvaluationOptionsMsg,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictPortfolioInstanceMsg {
symbol: String,
strategy: ConfiguredStrategyRunMsg,
#[serde(default)]
profile: Option<String>,
#[serde(default)]
profile_def: Option<StrictManagementProfileMsg>,
#[serde(default)]
entry_profile_routes: Vec<StrictEntryProfileRouteMsg>,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictPortfolioRunSpec {
instances: Vec<StrictPortfolioInstanceMsg>,
exchange: String,
data_type: String,
#[serde(default)]
timeframe: Option<String>,
#[serde(default)]
from: Option<String>,
#[serde(default)]
to: Option<String>,
config: StrictBacktestConfigMsg,
#[serde(default)]
policies: Option<serde_json::Value>,
#[serde(default)]
groups: Option<serde_json::Value>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictRunPortfolioRequest {
request: StrictPortfolioRunSpec,
#[serde(default)]
future: FutureQuoteConfigMsg,
#[serde(default)]
evaluation: ProviderEvaluationOptionsMsg,
#[serde(default)]
result_delivery: ResultDeliveryMsg,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictSubmitPortfolioRequest {
request: RunPortfolioRequest,
}
impl<'de> Deserialize<'de> for RunPortfolioRequest {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let strict = StrictRunPortfolioRequest::deserialize(deserializer)?;
Ok(Self {
request: strict_into_wire(strict.request).map_err(serde::de::Error::custom)?,
future: strict.future,
evaluation: strict.evaluation,
result_delivery: strict.result_delivery,
})
}
}
impl<'de> Deserialize<'de> for SubmitPortfolioRequest {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let strict = StrictSubmitPortfolioRequest::deserialize(deserializer)?;
Ok(Self {
request: strict.request,
})
}
}
impl<'de> Deserialize<'de> for RunConfiguredStrategyRequest {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let strict = StrictRunConfiguredStrategyRequest::deserialize(deserializer)?;
Ok(Self {
request: strict_into_wire(strict.request).map_err(serde::de::Error::custom)?,
future: strict.future,
evaluation: strict.evaluation,
result_delivery: strict.result_delivery,
})
}
}
impl<'de> Deserialize<'de> for SubmitConfiguredStrategyRequest {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let strict = StrictSubmitConfiguredStrategyRequest::deserialize(deserializer)?;
Ok(Self {
request: strict.request,
})
}
}
impl<'de> Deserialize<'de> for SubmitSearchRequest {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let strict = StrictSubmitSearchRequest::deserialize(deserializer)?;
Ok(Self {
request: strict_into_wire(strict.request).map_err(serde::de::Error::custom)?,
future: strict.future,
evaluation: strict.evaluation,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AddProfileRequest {
pub profile: ManagementProfileMsg,
#[serde(default)]
pub overwrite: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AddProfileResponse {
pub success: bool,
pub error: Option<String>,
pub profile_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RemoveProfileRequest {
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RemoveProfileResponse {
pub success: bool,
pub error: Option<String>,
pub profile_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReloadProfilesResponse {
pub success: bool,
pub error: Option<String>,
pub profile_count: usize,
pub loaded_from: String,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn entry_fields() -> serde_json::Value {
json!({
"action": "Entry",
"ts": "2026-01-02T10:00:00",
"symbol": "eurusd",
"side": "Buy",
"order_type": "Market",
"price": null,
"risk": 1.5,
"stoploss": 1.08
})
}
#[test]
fn entry_requires_risk_and_rejects_size() {
let entry: RawSignalMsg = serde_json::from_value(entry_fields()).unwrap();
assert!(matches!(entry, RawSignalMsg::Entry { risk, .. } if risk == 1.5));
let mut with_size = entry_fields();
with_size.as_object_mut().unwrap().remove("risk");
with_size["size"] = json!(0.1);
let error = serde_json::from_value::<RawSignalMsg>(with_size).unwrap_err();
assert!(error.to_string().contains("unknown field `size`"));
let mut missing_risk = entry_fields();
missing_risk.as_object_mut().unwrap().remove("risk");
let error = serde_json::from_value::<RawSignalMsg>(missing_risk).unwrap_err();
assert!(error.to_string().contains("missing field `risk`"));
}
#[test]
fn scale_in_keeps_size_field() {
let signal: RawSignalMsg = serde_json::from_value(json!({
"action": "ScaleIn",
"ts": "2026-01-02T10:01:00",
"position": { "type": "ByTradeId", "trade_id": "trade-1" },
"price": null,
"size": 0.25
}))
.unwrap();
assert!(matches!(signal, RawSignalMsg::ScaleIn { size, .. } if size == 0.25));
}
#[test]
fn current_sizing_and_future_fields_are_strict() {
let sizing: SizingPolicyMsg = serde_json::from_value(json!({
"type": "FixedRiskAmount",
"amount": 100.0
}))
.unwrap();
assert!(matches!(
sizing,
SizingPolicyMsg::FixedRiskAmount { amount } if amount == 100.0
));
assert!(
serde_json::from_value::<SizingPolicyMsg>(json!({
"type": "FixedLot",
"qty": "all=0.01"
}))
.is_err()
);
let future: FutureQuoteConfigMsg = serde_json::from_value(json!({
"account_currency": "USD",
"conversion_stale_after_ms": 15_000
}))
.unwrap();
assert_eq!(future.account_currency, "USD");
assert_eq!(future.conversion_stale_after_ms, 15_000);
assert_eq!(
future.mtm_output,
MtmOutputPolicyMsg::Bounded { max_points: 4_096 }
);
assert_eq!(
future.market_entry_sizing_basis,
MarketEntrySizingBasisMsg::FillPrice
);
assert!(
serde_json::from_value::<FutureQuoteConfigMsg>(json!({
"account_currency": "USD",
"conversion_rates": {}
}))
.is_err()
);
}
#[test]
fn market_entry_sizing_basis_uses_strict_snake_case_wire_values() {
for (value, expected) in [
(json!("fill_price"), MarketEntrySizingBasisMsg::FillPrice),
(
json!("signal_entry_price"),
MarketEntrySizingBasisMsg::SignalEntryPrice,
),
] {
let basis: MarketEntrySizingBasisMsg = serde_json::from_value(value.clone()).unwrap();
assert_eq!(basis, expected);
assert_eq!(serde_json::to_value(basis).unwrap(), value);
}
assert!(serde_json::from_value::<MarketEntrySizingBasisMsg>(json!("mark_price")).is_err());
assert!(
serde_json::from_value::<FutureQuoteConfigMsg>(json!({
"market_entry_sizing_basis": "mark_price"
}))
.is_err()
);
}
#[test]
fn mtm_output_policy_uses_strict_snake_case_wire_values() {
for (value, expected) in [
(json!("none"), MtmOutputPolicyMsg::None),
(
json!({ "bounded": { "max_points": 512 } }),
MtmOutputPolicyMsg::Bounded { max_points: 512 },
),
(json!("full"), MtmOutputPolicyMsg::Full),
] {
let policy: MtmOutputPolicyMsg = serde_json::from_value(value.clone()).unwrap();
assert_eq!(policy, expected);
assert_eq!(serde_json::to_value(policy).unwrap(), value);
}
assert!(
serde_json::from_value::<MtmOutputPolicyMsg>(json!({
"bounded": { "max_points": 512, "unexpected": true }
}))
.is_err()
);
}
#[test]
fn future_result_defaults_missing_mtm_output_summary() {
let result: FutureBacktestResultMsg = serde_json::from_value(json!({
"format_version": 1,
"execution_metadata": null,
"recorded_fills": null,
"action_dispositions": null,
"close_events": null,
"completed_positions": null,
"open_positions": null,
"pending_orders": null,
"pending_order_lifecycle": [],
"mtm_equity_curve": null,
"mtm_max_drawdown": null,
"mtm_max_drawdown_pct": null,
"provider_evaluation": null
}))
.unwrap();
assert_eq!(result.mtm_output_summary, MtmOutputSummaryMsg::default());
assert_eq!(result.mtm_output_summary.policy, MtmOutputPolicyMsg::Full);
let summary: MtmOutputSummaryMsg = serde_json::from_value(json!({
"policy": "full",
"observed_points": 4,
"retained_points": 4,
"omitted_points": 0,
"future_addition": true
}))
.unwrap();
assert_eq!(summary.policy, MtmOutputPolicyMsg::Full);
}
fn minimal_request_json() -> serde_json::Value {
json!({
"request": {
"symbol": "EURUSD",
"exchange": "fixture",
"data_type": "tick",
"timeframe": null,
"from": null,
"to": null,
"raw_signals": [],
"profile": null,
"config": {
"initial_balance": null,
"close_on_finish": null,
"fill_model": null
}
},
"future": {
"account_currency": "USD"
}
})
}
#[test]
fn omitted_delivery_and_mtm_fields_use_current_defaults() {
let request: RunBacktestRequest = serde_json::from_value(minimal_request_json()).unwrap();
assert_eq!(request.result_delivery, ResultDeliveryMsg::Auto);
assert_eq!(
request.future.mtm_output,
MtmOutputPolicyMsg::Bounded { max_points: 4_096 }
);
assert_eq!(
request.future.market_entry_sizing_basis,
MarketEntrySizingBasisMsg::FillPrice
);
let submitted: SubmitBacktestRequest = serde_json::from_value(json!({
"request": minimal_request_json()
}))
.unwrap();
assert_eq!(submitted.request.result_delivery, ResultDeliveryMsg::Auto);
assert_eq!(
submitted.request.future.mtm_output,
MtmOutputPolicyMsg::Bounded { max_points: 4_096 }
);
let mut explicit = minimal_request_json();
explicit["result_delivery"] = json!("auto");
explicit["future"]["mtm_output"] = json!({
"bounded": { "max_points": 4_096 }
});
let explicit: RunBacktestRequest = serde_json::from_value(explicit).unwrap();
assert_eq!(explicit.result_delivery, ResultDeliveryMsg::Auto);
assert_eq!(
explicit.future.mtm_output,
MtmOutputPolicyMsg::Bounded { max_points: 4_096 }
);
let mut multi_request = minimal_request_json()["request"].clone();
let fields = multi_request.as_object_mut().unwrap();
fields.remove("profile");
fields.insert("profiles".into(), json!([]));
let multi: RunBacktestMultiRequest = serde_json::from_value(json!({
"request": multi_request,
"future": { "account_currency": "USD" }
}))
.unwrap();
assert_eq!(multi.result_delivery, ResultDeliveryMsg::Auto);
assert_eq!(
multi.future.mtm_output,
MtmOutputPolicyMsg::Bounded { max_points: 4_096 }
);
}
#[test]
fn older_async_result_responses_default_artifact_consumption_state() {
let response: GetBacktestResultResponse = serde_json::from_value(json!({
"success": true,
"job_id": "job-old",
"result": null,
"error": null,
"inline_complete": true
}))
.unwrap();
assert!(!response.artifact_consumed);
}
}