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