1use std::collections::{BTreeMap, HashMap};
7
8use serde::{Deserialize, Serialize, de::DeserializeOwned};
9
10fn default_true() -> bool {
11 true
12}
13
14fn default_target_source_info() -> String {
15 "FromSignal".into()
16}
17
18pub const RESULT_FORMAT_VERSION: u32 = 1;
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct PingResponse {
26 pub status: String,
27 pub uptime_secs: u64,
28 pub data_dir: String,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ProfileInfo {
36 pub name: String,
37 pub use_targets: Vec<usize>,
38 pub close_ratios: Vec<f64>,
39 #[serde(default = "default_target_source_info")]
40 pub target_source: String,
41 pub stoploss_mode: String,
42 pub rules_count: usize,
43 pub let_remainder_run: bool,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct ListProfilesResponse {
49 pub profiles: Vec<ProfileInfo>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct ListSymbolsRequest {
57 pub exchange: Option<String>,
58 pub data_type: Option<String>,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct SymbolAvailability {
64 pub exchange: String,
65 pub symbol: String,
66 pub data_type: String,
67 pub timeframe: Option<String>,
68 pub row_count: u64,
69 pub earliest: String,
70 pub latest: String,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct ListSymbolsResponse {
76 pub symbols: Vec<SymbolAvailability>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct BacktestConfigMsg {
84 pub initial_balance: Option<f64>,
86 pub close_on_finish: Option<bool>,
88 pub fill_model: Option<String>,
90 #[serde(default)]
92 pub sizing: Option<SizingPolicyMsg>,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97#[serde(tag = "type", deny_unknown_fields)]
98pub enum SizingPolicyMsg {
99 FixedLot { lots: f64 },
101 FixedRiskAmount { amount: f64 },
103 BalanceRiskPercent { percent: f64 },
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct SubmitBacktestResponse {
112 pub success: bool,
113 pub job_id: Option<String>,
114 pub error: Option<String>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct GetBacktestStatusRequest {
120 pub job_id: String,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct WatchBacktestRequest {
126 pub job_id: String,
127}
128
129#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
131#[serde(default)]
132pub struct BacktestProgress {
133 pub stage: String,
134 pub processed_events: u64,
135 pub total_events: u64,
136 pub processed_signals: u64,
137 pub total_signals: u64,
138 pub processed_symbols: u64,
139 pub total_symbols: u64,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144pub struct BacktestStatusResponse {
145 pub success: bool,
146 pub job_id: String,
147 pub status: String,
148 pub error: Option<String>,
149 pub elapsed_ms: Option<u64>,
150 #[serde(default)]
152 pub progress: BacktestProgress,
153}
154
155impl BacktestStatusResponse {
156 pub fn is_terminal(&self) -> bool {
158 matches!(self.status.as_str(), "Completed" | "Failed" | "Cancelled")
159 }
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164#[serde(tag = "type", rename_all = "snake_case")]
165pub enum BacktestEvent {
166 Snapshot { status: BacktestStatusResponse },
167 Heartbeat { job_id: String, elapsed_ms: u64 },
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct GetBacktestResultRequest {
173 pub job_id: String,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct GetBacktestResultResponse {
179 pub success: bool,
180 pub job_id: String,
181 pub result: Option<BacktestResultMsg>,
182 pub error: Option<String>,
183 #[serde(default)]
184 pub artifact: Option<ResultArtifactRefMsg>,
185 #[serde(default = "default_true")]
186 pub inline_complete: bool,
187 #[serde(default)]
189 pub artifact_consumed: bool,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct CancelBacktestRequest {
195 pub job_id: String,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct CancelBacktestResponse {
201 pub success: bool,
202 pub job_id: String,
203 pub error: Option<String>,
204}
205
206#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
210#[serde(rename_all = "snake_case")]
211pub enum ResultDeliveryMsg {
212 #[default]
213 Auto,
214 Inline,
215 Artifact,
216}
217
218#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
220pub struct ResultArtifactRefMsg {
221 pub format_version: u32,
222 pub artifact_id: String,
223 pub byte_len: u64,
224 pub sha256: String,
225 pub chunk_size: u64,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
230#[serde(deny_unknown_fields)]
231pub struct GetResultArtifactChunkRequest {
232 pub artifact_id: String,
233 pub offset: u64,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct GetResultArtifactChunkResponse {
239 pub success: bool,
240 pub artifact_id: String,
241 pub offset: u64,
242 pub data_base64: String,
243 pub eof: bool,
244 pub error: Option<String>,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
249#[serde(deny_unknown_fields)]
250pub struct DeleteResultArtifactRequest {
251 pub artifact_id: String,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct DeleteResultArtifactResponse {
257 pub success: bool,
258 pub artifact_id: String,
259 pub error: Option<String>,
260}
261
262#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
264#[serde(rename_all = "snake_case", deny_unknown_fields)]
265pub enum MtmOutputPolicyMsg {
266 None,
267 Bounded { max_points: usize },
268 Full,
269}
270
271impl Default for MtmOutputPolicyMsg {
272 fn default() -> Self {
273 Self::Bounded { max_points: 4_096 }
274 }
275}
276
277#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
279#[serde(rename_all = "snake_case", deny_unknown_fields)]
280pub enum MarketEntrySizingBasisMsg {
281 #[default]
282 FillPrice,
283 SignalEntryPrice,
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize)]
288#[serde(default, deny_unknown_fields)]
289pub struct FutureQuoteConfigMsg {
290 pub signal_latency_ms: i64,
291 pub slippage_pips: f64,
292 pub stale_quote_after_ms: Option<i64>,
293 pub pnl_epsilon: f64,
294 pub account_currency: String,
295 pub conversion_stale_after_ms: i64,
296 pub mtm_output: MtmOutputPolicyMsg,
297 pub market_entry_sizing_basis: MarketEntrySizingBasisMsg,
298}
299
300impl Default for FutureQuoteConfigMsg {
301 fn default() -> Self {
302 Self {
303 signal_latency_ms: 0,
304 slippage_pips: 0.0,
305 stale_quote_after_ms: None,
306 pnl_epsilon: 1.0e-9,
307 account_currency: String::new(),
308 conversion_stale_after_ms: 300_000,
309 mtm_output: MtmOutputPolicyMsg::default(),
310 market_entry_sizing_basis: MarketEntrySizingBasisMsg::default(),
311 }
312 }
313}
314
315#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
317#[serde(default, deny_unknown_fields)]
318pub struct EvaluationContextMsg {
319 pub provider_id: Option<String>,
320 pub source_id: Option<String>,
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
325#[serde(rename_all = "snake_case")]
326pub enum EvaluationSectionMsg {
327 Coverage,
328 PositionPerformance,
329 RMetrics,
330 Excursions,
331 Execution,
332 Robustness,
333 Breakdowns,
334}
335
336impl EvaluationSectionMsg {
337 pub const ALL: [Self; 7] = [
338 Self::Coverage,
339 Self::PositionPerformance,
340 Self::RMetrics,
341 Self::Excursions,
342 Self::Execution,
343 Self::Robustness,
344 Self::Breakdowns,
345 ];
346}
347
348#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
349#[serde(rename_all = "snake_case")]
350pub enum EvaluationPositionSideMsg {
351 Long,
352 Short,
353}
354
355#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
356#[serde(rename_all = "snake_case")]
357pub enum EvaluationGroupFilterMsg {
358 Named(String),
359 Ungrouped,
360}
361
362#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
365#[serde(default, deny_unknown_fields)]
366pub struct PositionFilterMsg {
367 pub symbols: Vec<String>,
368 pub sides: Vec<EvaluationPositionSideMsg>,
369 pub groups: Vec<EvaluationGroupFilterMsg>,
370 pub close_reasons: Vec<String>,
371 pub tags: BTreeMap<String, Vec<String>>,
372}
373
374#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
376#[serde(rename_all = "snake_case")]
377pub enum BreakdownDimensionMsg {
378 Symbol,
379 Side,
380 Group,
381 CloseReason,
382 Tag(String),
383}
384
385#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
386#[serde(default, deny_unknown_fields)]
387pub struct BootstrapConfigMsg {
388 pub samples: usize,
389 pub confidence_level: f64,
390 pub seed: u64,
391 pub minimum_sample_size: usize,
392}
393
394impl Default for BootstrapConfigMsg {
395 fn default() -> Self {
396 Self {
397 samples: 2_000,
398 confidence_level: 0.95,
399 seed: 0xA076_1D64_78BD_642F,
400 minimum_sample_size: 5,
401 }
402 }
403}
404
405#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
407#[serde(default, deny_unknown_fields)]
408pub struct SourceCoverageCountsMsg {
409 pub raw_messages: u64,
410 pub parsed_messages: u64,
411 pub skipped_messages: u64,
412 pub failed_messages: u64,
413 pub emitted_signals: u64,
414 pub emitted_entry_signals: u64,
415}
416
417#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
419#[serde(default, deny_unknown_fields)]
420pub struct ProviderEvaluationOptionsMsg {
421 pub context: EvaluationContextMsg,
422 pub source_coverage: Option<SourceCoverageCountsMsg>,
423 pub sections: Vec<EvaluationSectionMsg>,
425 pub filter: PositionFilterMsg,
426 pub breakdowns: Vec<BreakdownDimensionMsg>,
427 pub bootstrap: BootstrapConfigMsg,
428 pub rolling_window: usize,
429 pub minimum_breakdown_bucket_count: usize,
430 pub maximum_breakdown_rows: Option<usize>,
431 pub include_positions: bool,
432 pub maximum_position_rows: Option<usize>,
433}
434
435impl Default for ProviderEvaluationOptionsMsg {
436 fn default() -> Self {
437 Self {
438 context: EvaluationContextMsg::default(),
439 source_coverage: None,
440 sections: EvaluationSectionMsg::ALL.to_vec(),
441 filter: PositionFilterMsg::default(),
442 breakdowns: Vec::new(),
443 bootstrap: BootstrapConfigMsg::default(),
444 rolling_window: 20,
445 minimum_breakdown_bucket_count: 1,
446 maximum_breakdown_rows: None,
447 include_positions: false,
448 maximum_position_rows: None,
449 }
450 }
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize)]
457pub struct BacktestRunSpec {
458 pub symbol: String,
459 #[serde(default)]
460 pub symbols: Vec<String>,
461 #[serde(default)]
462 pub all_symbols: bool,
463 pub exchange: String,
464 pub data_type: String,
465 pub timeframe: Option<String>,
466 pub from: Option<String>,
467 pub to: Option<String>,
468 #[serde(default)]
470 pub raw_signals: Vec<RawSignalMsg>,
471 pub profile: Option<String>,
472 #[serde(default)]
473 pub profile_def: Option<ManagementProfileMsg>,
474 #[serde(default, skip_serializing_if = "Vec::is_empty")]
475 pub entry_profile_routes: Vec<EntryProfileRouteMsg>,
476 pub config: BacktestConfigMsg,
477}
478
479#[derive(Debug, Clone, Serialize, Deserialize)]
481pub struct RunBacktestResponse {
482 pub success: bool,
483 pub error: Option<String>,
484 pub result: Option<BacktestResultMsg>,
486 pub elapsed_ms: u64,
487 #[serde(default)]
488 pub artifact: Option<ResultArtifactRefMsg>,
489 #[serde(default = "default_true")]
490 pub inline_complete: bool,
491}
492
493#[derive(Debug, Clone, Serialize)]
495pub struct RunBacktestRequest {
496 pub request: BacktestRunSpec,
497 #[serde(default)]
498 pub future: FutureQuoteConfigMsg,
499 #[serde(default)]
500 pub evaluation: ProviderEvaluationOptionsMsg,
501 #[serde(default)]
502 pub result_delivery: ResultDeliveryMsg,
503}
504
505#[derive(Debug, Clone, Serialize)]
507pub struct SubmitBacktestRequest {
508 pub request: RunBacktestRequest,
509}
510
511#[derive(Debug, Clone, Serialize, Deserialize)]
515pub struct BacktestMultiRunSpec {
516 pub symbol: String,
517 #[serde(default)]
518 pub symbols: Vec<String>,
519 #[serde(default)]
520 pub all_symbols: bool,
521 pub exchange: String,
522 pub data_type: String,
523 pub timeframe: Option<String>,
524 pub from: Option<String>,
525 pub to: Option<String>,
526 #[serde(default)]
528 pub raw_signals: Vec<RawSignalMsg>,
529 pub profiles: Vec<ProfileRef>,
530 #[serde(default, skip_serializing_if = "Vec::is_empty")]
531 pub entry_profile_routes: Vec<EntryProfileRouteMsg>,
532 pub config: BacktestConfigMsg,
533}
534
535#[derive(Debug, Clone, Serialize, Deserialize)]
537pub struct ProfileResult {
538 pub profile: String,
539 pub success: bool,
540 pub error: Option<String>,
541 pub result: Option<BacktestResultMsg>,
542}
543
544#[derive(Debug, Clone, Serialize, Deserialize)]
546pub struct RunBacktestMultiResponse {
547 #[serde(default = "default_true")]
549 pub success: bool,
550 #[serde(default)]
551 pub error: Option<String>,
552 pub results: Vec<ProfileResult>,
554 pub elapsed_ms: u64,
555 #[serde(default)]
556 pub artifact: Option<ResultArtifactRefMsg>,
557 #[serde(default = "default_true")]
558 pub inline_complete: bool,
559}
560
561#[derive(Debug, Clone, Serialize)]
562pub struct RunBacktestMultiRequest {
563 pub request: BacktestMultiRunSpec,
564 #[serde(default)]
565 pub future: FutureQuoteConfigMsg,
566 #[serde(default)]
567 pub evaluation: ProviderEvaluationOptionsMsg,
568 #[serde(default)]
569 pub result_delivery: ResultDeliveryMsg,
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize)]
576pub struct BacktestResultMsg {
577 pub initial_balance: f64,
578 pub final_balance: f64,
579 pub total_pnl: f64,
580 pub total_trades: usize,
581 pub winning_trades: usize,
582 pub losing_trades: usize,
583 pub win_rate: f64,
584 pub profit_factor: f64,
585 pub max_drawdown: f64,
586 pub max_drawdown_pct: f64,
587
588 pub summary: SubsetStatsMsg,
590 pub per_symbol: HashMap<String, SubsetStatsMsg>,
591 pub per_group: HashMap<String, SubsetStatsMsg>,
592 pub long_stats: SubsetStatsMsg,
593 pub short_stats: SubsetStatsMsg,
594 pub per_close_reason: Vec<CloseReasonStatsMsg>,
595 pub streaks: StreakStatsMsg,
596 pub risk_metrics: RiskMetricsMsg,
597 pub duration_stats: Option<DurationStatsMsg>,
598 pub monthly_returns: Vec<MonthlyReturnMsg>,
599
600 pub equity_curve: Vec<EquityPoint>,
601 pub trade_log: Vec<TradeResultMsg>,
602
603 pub positions: Vec<PositionSummaryMsg>,
605 pub total_positions: usize,
606 pub winning_positions: usize,
607 pub losing_positions: usize,
608 pub position_win_rate: f64,
609
610 #[serde(default)]
612 pub future: Option<FutureBacktestResultMsg>,
613}
614
615#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
617#[serde(default)]
618pub struct MtmOutputSummaryMsg {
619 pub policy: MtmOutputPolicyMsg,
620 pub observed_points: u64,
621 pub retained_points: u64,
622 pub omitted_points: u64,
623}
624
625impl Default for MtmOutputSummaryMsg {
626 fn default() -> Self {
627 Self {
628 policy: MtmOutputPolicyMsg::Full,
629 observed_points: 0,
630 retained_points: 0,
631 omitted_points: 0,
632 }
633 }
634}
635
636#[derive(Debug, Clone, Serialize, Deserialize)]
640pub struct FutureBacktestResultMsg {
641 pub format_version: u32,
642 pub execution_metadata: serde_json::Value,
643 pub recorded_fills: serde_json::Value,
644 pub action_dispositions: serde_json::Value,
645 pub close_events: serde_json::Value,
646 pub completed_positions: serde_json::Value,
647 pub open_positions: serde_json::Value,
648 pub pending_orders: serde_json::Value,
649 #[serde(default)]
651 pub pending_order_lifecycle: Vec<PendingOrderLifecycleEventMsg>,
652 pub mtm_equity_curve: serde_json::Value,
653 #[serde(default)]
654 pub mtm_output_summary: MtmOutputSummaryMsg,
655 pub mtm_max_drawdown: Option<f64>,
656 pub mtm_max_drawdown_pct: Option<f64>,
657 pub provider_evaluation: serde_json::Value,
658}
659
660#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
662#[serde(rename_all = "snake_case")]
663pub enum PendingOrderLifecycleStateMsg {
664 #[default]
665 Placed,
666 Filled,
667 Cancelled,
668 UnfilledAtEnd,
669}
670
671#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
673#[serde(default)]
674pub struct PendingOrderLifecycleEventMsg {
675 pub id: String,
676 pub sequence: u64,
677 pub position_id: String,
678 pub placement_action_id: Option<String>,
679 pub terminal_action_id: Option<String>,
680 pub state: PendingOrderLifecycleStateMsg,
681 pub symbol: String,
682 pub side: String,
683 pub order_type: String,
684 pub requested_size: f64,
685 pub filled_size: Option<f64>,
686 pub requested_price: Option<f64>,
687 pub fill_price: Option<f64>,
688 pub signal_ts: Option<String>,
689 pub placed_ts: Option<String>,
690 pub effective_ts: Option<String>,
691 pub terminal_ts: Option<String>,
692 pub wait_latency_ms: Option<i64>,
693 pub fill_ratio: Option<f64>,
694}
695
696#[derive(Debug, Clone, Serialize, Deserialize)]
700pub struct SubsetStatsMsg {
701 pub total_trades: usize,
702 pub winning_trades: usize,
703 pub losing_trades: usize,
704 pub breakeven_trades: usize,
705 pub total_pnl: f64,
706 pub gross_profit: f64,
707 pub gross_loss: f64,
708 pub win_rate: f64,
709 pub profit_factor: f64,
710 pub avg_win: f64,
711 pub avg_loss: f64,
712 pub win_loss_ratio: f64,
713 pub expectancy: f64,
714 pub largest_win: f64,
715 pub largest_loss: f64,
716}
717
718#[derive(Debug, Clone, Serialize, Deserialize)]
720pub struct StreakStatsMsg {
721 pub max_consecutive_wins: u32,
722 pub max_consecutive_losses: u32,
723 pub current_streak: i32,
724}
725
726#[derive(Debug, Clone, Serialize, Deserialize)]
728pub struct RiskMetricsMsg {
729 pub sharpe_ratio: Option<f64>,
730 pub sortino_ratio: Option<f64>,
731 pub calmar_ratio: Option<f64>,
732 pub return_on_max_drawdown: Option<f64>,
733 pub max_drawdown: f64,
734 pub max_drawdown_pct: f64,
735 pub max_drawdown_duration_secs: Option<i64>,
736}
737
738#[derive(Debug, Clone, Serialize, Deserialize)]
740pub struct DurationStatsMsg {
741 pub avg_duration_secs: i64,
742 pub min_duration_secs: i64,
743 pub max_duration_secs: i64,
744 pub avg_winner_duration_secs: i64,
745 pub avg_loser_duration_secs: i64,
746}
747
748#[derive(Debug, Clone, Serialize, Deserialize)]
750pub struct MonthlyReturnMsg {
751 pub year: i32,
752 pub month: u32,
753 pub pnl: f64,
754 pub trade_count: usize,
755 pub ending_balance: f64,
756}
757
758#[derive(Debug, Clone, Serialize, Deserialize)]
760pub struct CloseReasonStatsMsg {
761 pub reason: String,
762 pub count: usize,
763 pub total_pnl: f64,
764 pub avg_pnl: f64,
765 pub percentage: f64,
766}
767
768#[derive(Debug, Clone, Serialize, Deserialize)]
770pub struct PositionSummaryMsg {
771 pub position_id: String,
772 pub symbol: String,
773 pub side: String,
774 pub group: Option<String>,
775 pub entry_price: f64,
776 pub avg_exit_price: f64,
777 pub original_size: f64,
778 pub close_count: usize,
779 pub net_pnl: f64,
780 pub close_reasons: Vec<String>,
781 pub open_ts: String,
782 pub final_close_ts: Option<String>,
783 pub duration_seconds: i64,
784}
785
786#[derive(Debug, Clone, Serialize, Deserialize)]
788pub struct EquityPoint {
789 pub ts: String,
790 pub balance: f64,
791}
792
793#[derive(Debug, Clone, Serialize, Deserialize)]
795pub struct TradeResultMsg {
796 pub position_id: String,
797 pub symbol: String,
798 pub side: String,
799 pub entry_price: f64,
800 pub exit_price: f64,
801 pub size: f64,
802 pub pnl: f64,
803 pub open_ts: String,
804 pub close_ts: String,
805 pub close_reason: String,
806 pub group: Option<String>,
807}
808
809#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
813pub enum TargetSelectionMsg {
814 All,
816 None,
818 Selected(Vec<usize>),
820}
821
822#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
824#[serde(tag = "type", deny_unknown_fields)]
825pub enum TargetSourceMsg {
826 #[default]
827 FromSignal,
828 StopDistanceMultiples {
829 multiples: Vec<f64>,
830 },
831}
832
833#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
835#[serde(rename_all = "lowercase")]
836pub enum EntryGeometryPolicyMsg {
837 #[default]
840 Strict,
841 Permissive,
844}
845
846#[derive(Debug, Clone, Serialize, Deserialize)]
848pub struct ManagementProfileMsg {
849 pub name: String,
850 #[serde(default, skip_serializing_if = "Option::is_none")]
853 pub target_selection: Option<TargetSelectionMsg>,
854 pub use_targets: Vec<usize>,
856 pub close_ratios: Vec<f64>,
857 #[serde(default, skip_serializing_if = "Option::is_none")]
858 pub target_source: Option<TargetSourceMsg>,
859 #[serde(default)]
860 pub stoploss_mode: Option<StoplossModeMsg>,
861 #[serde(default)]
862 pub rules: Vec<RuleConfigDefMsg>,
863 #[serde(default)]
864 pub group_override: Option<String>,
865 #[serde(default)]
866 pub let_remainder_run: bool,
867 #[serde(default, skip_serializing_if = "Option::is_none")]
870 pub entry_geometry: Option<EntryGeometryPolicyMsg>,
871}
872
873#[derive(Debug, Clone, Serialize, Deserialize)]
875#[serde(tag = "type")]
876pub enum StoplossModeMsg {
877 FromSignal,
878 None,
879 FixedDistance { distance: f64 },
880 FixedPrice { price: f64 },
881 FromSignalDistance { multiplier: f64 },
882}
883
884#[derive(Debug, Clone, Serialize, Deserialize)]
886#[serde(tag = "type")]
887pub enum RuleConfigDefMsg {
888 FixedStoploss { price: f64 },
889 TrailingStop { distance: f64 },
890 TakeProfit { price: f64, close_ratio: f64 },
891 BreakevenWhen { trigger_price: f64 },
892 BreakevenWhenOffset { trigger_price_offset: f64 },
893 BreakevenAfterTargets { after_n: u32 },
894 TimeExit { max_seconds: u64 },
895}
896
897#[derive(Debug, Clone, Serialize, Deserialize)]
899#[serde(untagged)]
900pub enum ProfileRef {
901 Named(String),
902 Inline(ManagementProfileMsg),
903}
904
905#[derive(Debug, Clone, Serialize, Deserialize)]
907#[serde(deny_unknown_fields)]
908pub struct EntryProfileRouteMsg {
909 pub entry_class: String,
910 pub profile: ProfileRef,
911}
912
913#[derive(Debug, Clone, Serialize, Deserialize)]
920#[serde(tag = "action", deny_unknown_fields)]
921pub enum RawSignalMsg {
922 Entry {
924 ts: String,
925 symbol: String,
926 side: String,
927 order_type: String,
928 price: Option<f64>,
929 risk: f64,
930 stoploss: Option<f64>,
931 #[serde(default)]
932 targets: Vec<f64>,
933 #[serde(default)]
934 group: Option<String>,
935 #[serde(default)]
937 trade_id: Option<String>,
938 #[serde(default, skip_serializing_if = "Option::is_none")]
940 entry_class: Option<String>,
941 },
942 Close {
944 ts: String,
945 position: PositionRefMsg,
946 },
947 ClosePartial {
949 ts: String,
950 position: PositionRefMsg,
951 ratio: f64,
952 },
953 ModifyStoploss {
955 ts: String,
956 position: PositionRefMsg,
957 price: f64,
958 },
959 MoveStoplossToEntry {
961 ts: String,
962 position: PositionRefMsg,
963 },
964 AddTarget {
966 ts: String,
967 position: PositionRefMsg,
968 price: f64,
969 close_ratio: f64,
970 },
971 RemoveTarget {
973 ts: String,
974 position: PositionRefMsg,
975 price: f64,
976 },
977 ModifyTarget {
979 ts: String,
980 position: PositionRefMsg,
981 old_price: f64,
982 new_price: f64,
983 },
984 AddRule {
986 ts: String,
987 position: PositionRefMsg,
988 rule: RuleConfigDefMsg,
989 },
990 RemoveRule {
992 ts: String,
993 position: PositionRefMsg,
994 rule_name: String,
995 },
996 ScaleIn {
998 ts: String,
999 position: PositionRefMsg,
1000 price: Option<f64>,
1001 size: f64,
1002 },
1003 CancelPending {
1005 ts: String,
1006 position: PositionRefMsg,
1007 },
1008 CloseAllOf { ts: String, symbol: String },
1010 CloseAll { ts: String },
1012 CancelAllPending { ts: String },
1014 ModifyAllStoploss {
1016 ts: String,
1017 symbol: String,
1018 price: f64,
1019 },
1020 CloseAllInGroup { ts: String, group_id: String },
1022 ModifyAllStoplossInGroup {
1024 ts: String,
1025 group_id: String,
1026 price: f64,
1027 },
1028}
1029
1030impl RawSignalMsg {
1031 pub fn ts(&self) -> &str {
1033 match self {
1034 RawSignalMsg::Entry { ts, .. }
1035 | RawSignalMsg::Close { ts, .. }
1036 | RawSignalMsg::ClosePartial { ts, .. }
1037 | RawSignalMsg::ModifyStoploss { ts, .. }
1038 | RawSignalMsg::MoveStoplossToEntry { ts, .. }
1039 | RawSignalMsg::AddTarget { ts, .. }
1040 | RawSignalMsg::RemoveTarget { ts, .. }
1041 | RawSignalMsg::ModifyTarget { ts, .. }
1042 | RawSignalMsg::AddRule { ts, .. }
1043 | RawSignalMsg::RemoveRule { ts, .. }
1044 | RawSignalMsg::ScaleIn { ts, .. }
1045 | RawSignalMsg::CancelPending { ts, .. }
1046 | RawSignalMsg::CloseAllOf { ts, .. }
1047 | RawSignalMsg::CloseAll { ts }
1048 | RawSignalMsg::CancelAllPending { ts }
1049 | RawSignalMsg::ModifyAllStoploss { ts, .. }
1050 | RawSignalMsg::CloseAllInGroup { ts, .. }
1051 | RawSignalMsg::ModifyAllStoplossInGroup { ts, .. } => ts,
1052 }
1053 }
1054}
1055
1056#[derive(Debug, Clone, Serialize, Deserialize)]
1058#[serde(tag = "type")]
1059pub enum PositionRefMsg {
1060 ByTradeId { trade_id: String },
1062 AllOnSymbol { symbol: String },
1064 AllInGroup { group_id: String },
1066}
1067
1068#[derive(Serialize, Deserialize)]
1071#[serde(tag = "type", deny_unknown_fields)]
1072enum StrictSizingPolicyMsg {
1073 FixedLot { lots: f64 },
1074 FixedRiskAmount { amount: f64 },
1075 BalanceRiskPercent { percent: f64 },
1076}
1077
1078#[derive(Serialize, Deserialize)]
1079#[serde(deny_unknown_fields)]
1080struct StrictBacktestConfigMsg {
1081 initial_balance: Option<f64>,
1082 close_on_finish: Option<bool>,
1083 fill_model: Option<String>,
1084 #[serde(default)]
1085 sizing: Option<StrictSizingPolicyMsg>,
1086}
1087
1088#[derive(Serialize, Deserialize)]
1089#[serde(tag = "type", deny_unknown_fields)]
1090enum StrictStoplossModeMsg {
1091 FromSignal,
1092 None,
1093 FixedDistance { distance: f64 },
1094 FixedPrice { price: f64 },
1095 FromSignalDistance { multiplier: f64 },
1096}
1097
1098#[derive(Serialize, Deserialize)]
1099#[serde(tag = "type", deny_unknown_fields)]
1100enum StrictTargetSourceMsg {
1101 FromSignal,
1102 StopDistanceMultiples { multiples: Vec<f64> },
1103}
1104
1105#[derive(Serialize, Deserialize)]
1106#[serde(tag = "type", deny_unknown_fields)]
1107enum StrictRuleConfigDefMsg {
1108 FixedStoploss { price: f64 },
1109 TrailingStop { distance: f64 },
1110 TakeProfit { price: f64, close_ratio: f64 },
1111 BreakevenWhen { trigger_price: f64 },
1112 BreakevenWhenOffset { trigger_price_offset: f64 },
1113 BreakevenAfterTargets { after_n: u32 },
1114 TimeExit { max_seconds: u64 },
1115}
1116
1117#[derive(Serialize, Deserialize)]
1118#[serde(deny_unknown_fields)]
1119struct StrictManagementProfileMsg {
1120 name: String,
1121 #[serde(default)]
1122 target_selection: Option<TargetSelectionMsg>,
1123 use_targets: Vec<usize>,
1124 close_ratios: Vec<f64>,
1125 #[serde(default)]
1126 target_source: Option<StrictTargetSourceMsg>,
1127 #[serde(default)]
1128 stoploss_mode: Option<StrictStoplossModeMsg>,
1129 #[serde(default)]
1130 rules: Vec<StrictRuleConfigDefMsg>,
1131 #[serde(default)]
1132 group_override: Option<String>,
1133 #[serde(default)]
1134 let_remainder_run: bool,
1135 #[serde(default)]
1136 entry_geometry: Option<EntryGeometryPolicyMsg>,
1137}
1138
1139#[derive(Serialize, Deserialize)]
1140#[serde(tag = "type", deny_unknown_fields)]
1141enum StrictPositionRefMsg {
1142 ByTradeId { trade_id: String },
1143 AllOnSymbol { symbol: String },
1144 AllInGroup { group_id: String },
1145}
1146
1147#[derive(Serialize, Deserialize)]
1148#[serde(tag = "action", deny_unknown_fields)]
1149enum StrictRawSignalMsg {
1150 Entry {
1151 ts: String,
1152 symbol: String,
1153 side: String,
1154 order_type: String,
1155 price: Option<f64>,
1156 risk: f64,
1157 stoploss: Option<f64>,
1158 #[serde(default)]
1159 targets: Vec<f64>,
1160 #[serde(default)]
1161 group: Option<String>,
1162 #[serde(default)]
1163 trade_id: Option<String>,
1164 #[serde(default)]
1165 entry_class: Option<String>,
1166 },
1167 Close {
1168 ts: String,
1169 position: StrictPositionRefMsg,
1170 },
1171 ClosePartial {
1172 ts: String,
1173 position: StrictPositionRefMsg,
1174 ratio: f64,
1175 },
1176 ModifyStoploss {
1177 ts: String,
1178 position: StrictPositionRefMsg,
1179 price: f64,
1180 },
1181 MoveStoplossToEntry {
1182 ts: String,
1183 position: StrictPositionRefMsg,
1184 },
1185 AddTarget {
1186 ts: String,
1187 position: StrictPositionRefMsg,
1188 price: f64,
1189 close_ratio: f64,
1190 },
1191 RemoveTarget {
1192 ts: String,
1193 position: StrictPositionRefMsg,
1194 price: f64,
1195 },
1196 ModifyTarget {
1197 ts: String,
1198 position: StrictPositionRefMsg,
1199 old_price: f64,
1200 new_price: f64,
1201 },
1202 AddRule {
1203 ts: String,
1204 position: StrictPositionRefMsg,
1205 rule: StrictRuleConfigDefMsg,
1206 },
1207 RemoveRule {
1208 ts: String,
1209 position: StrictPositionRefMsg,
1210 rule_name: String,
1211 },
1212 ScaleIn {
1213 ts: String,
1214 position: StrictPositionRefMsg,
1215 price: Option<f64>,
1216 size: f64,
1217 },
1218 CancelPending {
1219 ts: String,
1220 position: StrictPositionRefMsg,
1221 },
1222 CloseAllOf {
1223 ts: String,
1224 symbol: String,
1225 },
1226 CloseAll {
1227 ts: String,
1228 },
1229 CancelAllPending {
1230 ts: String,
1231 },
1232 ModifyAllStoploss {
1233 ts: String,
1234 symbol: String,
1235 price: f64,
1236 },
1237 CloseAllInGroup {
1238 ts: String,
1239 group_id: String,
1240 },
1241 ModifyAllStoplossInGroup {
1242 ts: String,
1243 group_id: String,
1244 price: f64,
1245 },
1246}
1247
1248#[derive(Serialize, Deserialize)]
1249#[serde(deny_unknown_fields)]
1250struct StrictBacktestRunSpec {
1251 symbol: String,
1252 #[serde(default)]
1253 symbols: Vec<String>,
1254 #[serde(default)]
1255 all_symbols: bool,
1256 exchange: String,
1257 data_type: String,
1258 timeframe: Option<String>,
1259 from: Option<String>,
1260 to: Option<String>,
1261 #[serde(default)]
1262 raw_signals: Vec<StrictRawSignalMsg>,
1263 profile: Option<String>,
1264 #[serde(default)]
1265 profile_def: Option<StrictManagementProfileMsg>,
1266 #[serde(default)]
1267 entry_profile_routes: Vec<StrictEntryProfileRouteMsg>,
1268 config: StrictBacktestConfigMsg,
1269}
1270
1271#[derive(Serialize, Deserialize)]
1272#[serde(untagged)]
1273enum StrictProfileRef {
1274 Named(String),
1275 Inline(StrictManagementProfileMsg),
1276}
1277
1278#[derive(Serialize, Deserialize)]
1279#[serde(deny_unknown_fields)]
1280struct StrictEntryProfileRouteMsg {
1281 entry_class: String,
1282 profile: StrictProfileRef,
1283}
1284
1285#[derive(Serialize, Deserialize)]
1286#[serde(deny_unknown_fields)]
1287struct StrictBacktestMultiRunSpec {
1288 symbol: String,
1289 #[serde(default)]
1290 symbols: Vec<String>,
1291 #[serde(default)]
1292 all_symbols: bool,
1293 exchange: String,
1294 data_type: String,
1295 timeframe: Option<String>,
1296 from: Option<String>,
1297 to: Option<String>,
1298 #[serde(default)]
1299 raw_signals: Vec<StrictRawSignalMsg>,
1300 profiles: Vec<StrictProfileRef>,
1301 #[serde(default)]
1302 entry_profile_routes: Vec<StrictEntryProfileRouteMsg>,
1303 config: StrictBacktestConfigMsg,
1304}
1305
1306#[derive(Deserialize)]
1307#[serde(deny_unknown_fields)]
1308struct StrictRunBacktestRequest {
1309 request: StrictBacktestRunSpec,
1310 #[serde(default)]
1311 future: FutureQuoteConfigMsg,
1312 #[serde(default)]
1313 evaluation: ProviderEvaluationOptionsMsg,
1314 #[serde(default)]
1315 result_delivery: ResultDeliveryMsg,
1316}
1317
1318#[derive(Deserialize)]
1319#[serde(deny_unknown_fields)]
1320struct StrictSubmitBacktestRequest {
1321 request: RunBacktestRequest,
1322}
1323
1324#[derive(Deserialize)]
1325#[serde(deny_unknown_fields)]
1326struct StrictRunBacktestMultiRequest {
1327 request: StrictBacktestMultiRunSpec,
1328 #[serde(default)]
1329 future: FutureQuoteConfigMsg,
1330 #[serde(default)]
1331 evaluation: ProviderEvaluationOptionsMsg,
1332 #[serde(default)]
1333 result_delivery: ResultDeliveryMsg,
1334}
1335
1336fn strict_into_wire<T>(value: impl Serialize) -> Result<T, serde_json::Error>
1337where
1338 T: DeserializeOwned,
1339{
1340 serde_json::from_value(serde_json::to_value(value)?)
1341}
1342
1343impl<'de> Deserialize<'de> for RunBacktestRequest {
1344 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1345 where
1346 D: serde::Deserializer<'de>,
1347 {
1348 let strict = StrictRunBacktestRequest::deserialize(deserializer)?;
1349 Ok(Self {
1350 request: strict_into_wire(strict.request).map_err(serde::de::Error::custom)?,
1351 future: strict.future,
1352 evaluation: strict.evaluation,
1353 result_delivery: strict.result_delivery,
1354 })
1355 }
1356}
1357
1358impl<'de> Deserialize<'de> for SubmitBacktestRequest {
1359 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1360 where
1361 D: serde::Deserializer<'de>,
1362 {
1363 let strict = StrictSubmitBacktestRequest::deserialize(deserializer)?;
1364 Ok(Self {
1365 request: strict.request,
1366 })
1367 }
1368}
1369
1370impl<'de> Deserialize<'de> for RunBacktestMultiRequest {
1371 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1372 where
1373 D: serde::Deserializer<'de>,
1374 {
1375 let strict = StrictRunBacktestMultiRequest::deserialize(deserializer)?;
1376 Ok(Self {
1377 request: strict_into_wire(strict.request).map_err(serde::de::Error::custom)?,
1378 future: strict.future,
1379 evaluation: strict.evaluation,
1380 result_delivery: strict.result_delivery,
1381 })
1382 }
1383}
1384
1385#[derive(Debug, Clone, Serialize, Deserialize)]
1389pub struct AddProfileRequest {
1390 pub profile: ManagementProfileMsg,
1391 #[serde(default)]
1392 pub overwrite: bool,
1393}
1394
1395#[derive(Debug, Clone, Serialize, Deserialize)]
1397pub struct AddProfileResponse {
1398 pub success: bool,
1399 pub error: Option<String>,
1400 pub profile_count: usize,
1401}
1402
1403#[derive(Debug, Clone, Serialize, Deserialize)]
1405pub struct RemoveProfileRequest {
1406 pub name: String,
1407}
1408
1409#[derive(Debug, Clone, Serialize, Deserialize)]
1411pub struct RemoveProfileResponse {
1412 pub success: bool,
1413 pub error: Option<String>,
1414 pub profile_count: usize,
1415}
1416
1417#[derive(Debug, Clone, Serialize, Deserialize)]
1419pub struct ReloadProfilesResponse {
1420 pub success: bool,
1421 pub error: Option<String>,
1422 pub profile_count: usize,
1423 pub loaded_from: String,
1424}
1425
1426#[cfg(test)]
1427mod tests {
1428 use super::*;
1429 use serde_json::json;
1430
1431 fn entry_fields() -> serde_json::Value {
1432 json!({
1433 "action": "Entry",
1434 "ts": "2026-01-02T10:00:00",
1435 "symbol": "eurusd",
1436 "side": "Buy",
1437 "order_type": "Market",
1438 "price": null,
1439 "risk": 1.5,
1440 "stoploss": 1.08
1441 })
1442 }
1443
1444 #[test]
1445 fn entry_requires_risk_and_rejects_size() {
1446 let entry: RawSignalMsg = serde_json::from_value(entry_fields()).unwrap();
1447 assert!(matches!(entry, RawSignalMsg::Entry { risk, .. } if risk == 1.5));
1448
1449 let mut with_size = entry_fields();
1450 with_size.as_object_mut().unwrap().remove("risk");
1451 with_size["size"] = json!(0.1);
1452 let error = serde_json::from_value::<RawSignalMsg>(with_size).unwrap_err();
1453 assert!(error.to_string().contains("unknown field `size`"));
1454
1455 let mut missing_risk = entry_fields();
1456 missing_risk.as_object_mut().unwrap().remove("risk");
1457 let error = serde_json::from_value::<RawSignalMsg>(missing_risk).unwrap_err();
1458 assert!(error.to_string().contains("missing field `risk`"));
1459 }
1460
1461 #[test]
1462 fn scale_in_keeps_size_field() {
1463 let signal: RawSignalMsg = serde_json::from_value(json!({
1464 "action": "ScaleIn",
1465 "ts": "2026-01-02T10:01:00",
1466 "position": { "type": "ByTradeId", "trade_id": "trade-1" },
1467 "price": null,
1468 "size": 0.25
1469 }))
1470 .unwrap();
1471 assert!(matches!(signal, RawSignalMsg::ScaleIn { size, .. } if size == 0.25));
1472 }
1473
1474 #[test]
1475 fn current_sizing_and_future_fields_are_strict() {
1476 let sizing: SizingPolicyMsg = serde_json::from_value(json!({
1477 "type": "FixedRiskAmount",
1478 "amount": 100.0
1479 }))
1480 .unwrap();
1481 assert!(matches!(
1482 sizing,
1483 SizingPolicyMsg::FixedRiskAmount { amount } if amount == 100.0
1484 ));
1485 assert!(
1486 serde_json::from_value::<SizingPolicyMsg>(json!({
1487 "type": "FixedLot",
1488 "qty": "all=0.01"
1489 }))
1490 .is_err()
1491 );
1492
1493 let future: FutureQuoteConfigMsg = serde_json::from_value(json!({
1494 "account_currency": "USD",
1495 "conversion_stale_after_ms": 15_000
1496 }))
1497 .unwrap();
1498 assert_eq!(future.account_currency, "USD");
1499 assert_eq!(future.conversion_stale_after_ms, 15_000);
1500 assert_eq!(
1501 future.mtm_output,
1502 MtmOutputPolicyMsg::Bounded { max_points: 4_096 }
1503 );
1504 assert_eq!(
1505 future.market_entry_sizing_basis,
1506 MarketEntrySizingBasisMsg::FillPrice
1507 );
1508 assert!(
1509 serde_json::from_value::<FutureQuoteConfigMsg>(json!({
1510 "account_currency": "USD",
1511 "conversion_rates": {}
1512 }))
1513 .is_err()
1514 );
1515 }
1516
1517 #[test]
1518 fn market_entry_sizing_basis_uses_strict_snake_case_wire_values() {
1519 for (value, expected) in [
1520 (json!("fill_price"), MarketEntrySizingBasisMsg::FillPrice),
1521 (
1522 json!("signal_entry_price"),
1523 MarketEntrySizingBasisMsg::SignalEntryPrice,
1524 ),
1525 ] {
1526 let basis: MarketEntrySizingBasisMsg = serde_json::from_value(value.clone()).unwrap();
1527 assert_eq!(basis, expected);
1528 assert_eq!(serde_json::to_value(basis).unwrap(), value);
1529 }
1530
1531 assert!(serde_json::from_value::<MarketEntrySizingBasisMsg>(json!("mark_price")).is_err());
1532 assert!(
1533 serde_json::from_value::<FutureQuoteConfigMsg>(json!({
1534 "market_entry_sizing_basis": "mark_price"
1535 }))
1536 .is_err()
1537 );
1538 }
1539
1540 #[test]
1541 fn mtm_output_policy_uses_strict_snake_case_wire_values() {
1542 for (value, expected) in [
1543 (json!("none"), MtmOutputPolicyMsg::None),
1544 (
1545 json!({ "bounded": { "max_points": 512 } }),
1546 MtmOutputPolicyMsg::Bounded { max_points: 512 },
1547 ),
1548 (json!("full"), MtmOutputPolicyMsg::Full),
1549 ] {
1550 let policy: MtmOutputPolicyMsg = serde_json::from_value(value.clone()).unwrap();
1551 assert_eq!(policy, expected);
1552 assert_eq!(serde_json::to_value(policy).unwrap(), value);
1553 }
1554
1555 assert!(
1556 serde_json::from_value::<MtmOutputPolicyMsg>(json!({
1557 "bounded": { "max_points": 512, "unexpected": true }
1558 }))
1559 .is_err()
1560 );
1561 }
1562
1563 #[test]
1564 fn future_result_defaults_missing_mtm_output_summary() {
1565 let result: FutureBacktestResultMsg = serde_json::from_value(json!({
1566 "format_version": 1,
1567 "execution_metadata": null,
1568 "recorded_fills": null,
1569 "action_dispositions": null,
1570 "close_events": null,
1571 "completed_positions": null,
1572 "open_positions": null,
1573 "pending_orders": null,
1574 "pending_order_lifecycle": [],
1575 "mtm_equity_curve": null,
1576 "mtm_max_drawdown": null,
1577 "mtm_max_drawdown_pct": null,
1578 "provider_evaluation": null
1579 }))
1580 .unwrap();
1581
1582 assert_eq!(result.mtm_output_summary, MtmOutputSummaryMsg::default());
1583 assert_eq!(result.mtm_output_summary.policy, MtmOutputPolicyMsg::Full);
1584
1585 let summary: MtmOutputSummaryMsg = serde_json::from_value(json!({
1586 "policy": "full",
1587 "observed_points": 4,
1588 "retained_points": 4,
1589 "omitted_points": 0,
1590 "future_addition": true
1591 }))
1592 .unwrap();
1593 assert_eq!(summary.policy, MtmOutputPolicyMsg::Full);
1594 }
1595
1596 fn minimal_request_json() -> serde_json::Value {
1597 json!({
1598 "request": {
1599 "symbol": "EURUSD",
1600 "exchange": "fixture",
1601 "data_type": "tick",
1602 "timeframe": null,
1603 "from": null,
1604 "to": null,
1605 "raw_signals": [],
1606 "profile": null,
1607 "config": {
1608 "initial_balance": null,
1609 "close_on_finish": null,
1610 "fill_model": null
1611 }
1612 },
1613 "future": {
1614 "account_currency": "USD"
1615 }
1616 })
1617 }
1618
1619 #[test]
1620 fn omitted_delivery_and_mtm_fields_use_current_defaults() {
1621 let request: RunBacktestRequest = serde_json::from_value(minimal_request_json()).unwrap();
1622 assert_eq!(request.result_delivery, ResultDeliveryMsg::Auto);
1623 assert_eq!(
1624 request.future.mtm_output,
1625 MtmOutputPolicyMsg::Bounded { max_points: 4_096 }
1626 );
1627 assert_eq!(
1628 request.future.market_entry_sizing_basis,
1629 MarketEntrySizingBasisMsg::FillPrice
1630 );
1631
1632 let submitted: SubmitBacktestRequest = serde_json::from_value(json!({
1633 "request": minimal_request_json()
1634 }))
1635 .unwrap();
1636 assert_eq!(submitted.request.result_delivery, ResultDeliveryMsg::Auto);
1637 assert_eq!(
1638 submitted.request.future.mtm_output,
1639 MtmOutputPolicyMsg::Bounded { max_points: 4_096 }
1640 );
1641
1642 let mut explicit = minimal_request_json();
1643 explicit["result_delivery"] = json!("auto");
1644 explicit["future"]["mtm_output"] = json!({
1645 "bounded": { "max_points": 4_096 }
1646 });
1647 let explicit: RunBacktestRequest = serde_json::from_value(explicit).unwrap();
1648 assert_eq!(explicit.result_delivery, ResultDeliveryMsg::Auto);
1649 assert_eq!(
1650 explicit.future.mtm_output,
1651 MtmOutputPolicyMsg::Bounded { max_points: 4_096 }
1652 );
1653
1654 let mut multi_request = minimal_request_json()["request"].clone();
1655 let fields = multi_request.as_object_mut().unwrap();
1656 fields.remove("profile");
1657 fields.insert("profiles".into(), json!([]));
1658 let multi: RunBacktestMultiRequest = serde_json::from_value(json!({
1659 "request": multi_request,
1660 "future": { "account_currency": "USD" }
1661 }))
1662 .unwrap();
1663 assert_eq!(multi.result_delivery, ResultDeliveryMsg::Auto);
1664 assert_eq!(
1665 multi.future.mtm_output,
1666 MtmOutputPolicyMsg::Bounded { max_points: 4_096 }
1667 );
1668 }
1669
1670 #[test]
1671 fn older_async_result_responses_default_artifact_consumption_state() {
1672 let response: GetBacktestResultResponse = serde_json::from_value(json!({
1673 "success": true,
1674 "job_id": "job-old",
1675 "result": null,
1676 "error": null,
1677 "inline_complete": true
1678 }))
1679 .unwrap();
1680 assert!(!response.artifact_consumed);
1681 }
1682}