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 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
95 pub costs: BTreeMap<String, InstrumentCostsMsg>,
96}
97
98#[derive(Debug, Clone, Default, Serialize, Deserialize)]
100#[serde(deny_unknown_fields)]
101pub struct InstrumentCostsMsg {
102 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub commission: Option<CommissionModelMsg>,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub swap: Option<SwapScheduleMsg>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
112#[serde(tag = "type", deny_unknown_fields)]
113pub enum CommissionModelMsg {
114 #[serde(alias = "per_lot_per_side")]
116 PerLotPerSide { amount: f64, currency: String },
117 #[serde(alias = "notional_rate_per_side")]
119 NotionalRatePerSide { buy_rate: f64, sell_rate: f64 },
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
124#[serde(tag = "unit", deny_unknown_fields)]
125pub enum SwapAmountMsg {
126 #[serde(alias = "points")]
128 Points { long: f64, short: f64 },
129 #[serde(alias = "currency")]
131 Currency {
132 long: f64,
133 short: f64,
134 currency: String,
135 },
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
140#[serde(deny_unknown_fields)]
141pub struct SwapScheduleMsg {
142 pub amount: SwapAmountMsg,
143 pub rollover: String,
145 pub triple_weekday: String,
147 #[serde(default, skip_serializing_if = "Vec::is_empty")]
149 pub skipped_weekdays: Vec<String>,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154#[serde(tag = "type", deny_unknown_fields)]
155pub enum SizingPolicyMsg {
156 FixedLot { lots: f64 },
158 FixedRiskAmount { amount: f64 },
160 BalanceRiskPercent { percent: f64 },
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct SubmitBacktestResponse {
169 pub success: bool,
170 pub job_id: Option<String>,
171 pub error: Option<String>,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct GetBacktestStatusRequest {
177 pub job_id: String,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct WatchBacktestRequest {
183 pub job_id: String,
184}
185
186#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
188#[serde(default)]
189pub struct BacktestProgress {
190 pub stage: String,
191 pub processed_events: u64,
192 pub total_events: u64,
193 pub processed_signals: u64,
194 pub total_signals: u64,
195 pub processed_symbols: u64,
196 pub total_symbols: u64,
197}
198
199#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
201pub struct BacktestStatusResponse {
202 pub success: bool,
203 pub job_id: String,
204 pub status: String,
205 pub error: Option<String>,
206 pub elapsed_ms: Option<u64>,
207 #[serde(default)]
209 pub progress: BacktestProgress,
210}
211
212impl BacktestStatusResponse {
213 pub fn is_terminal(&self) -> bool {
215 matches!(self.status.as_str(), "Completed" | "Failed" | "Cancelled")
216 }
217}
218
219#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
221#[serde(tag = "type", rename_all = "snake_case")]
222pub enum BacktestEvent {
223 Snapshot { status: BacktestStatusResponse },
224 Heartbeat { job_id: String, elapsed_ms: u64 },
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct GetBacktestResultRequest {
230 pub job_id: String,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct GetBacktestResultResponse {
236 pub success: bool,
237 pub job_id: String,
238 pub result: Option<BacktestResultMsg>,
239 pub error: Option<String>,
240 #[serde(default)]
241 pub artifact: Option<ResultArtifactRefMsg>,
242 #[serde(default = "default_true")]
243 pub inline_complete: bool,
244 #[serde(default)]
246 pub artifact_consumed: bool,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct CancelBacktestRequest {
252 pub job_id: String,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct CancelBacktestResponse {
258 pub success: bool,
259 pub job_id: String,
260 pub error: Option<String>,
261}
262
263#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
267#[serde(rename_all = "snake_case")]
268pub enum ResultDeliveryMsg {
269 #[default]
270 Auto,
271 Inline,
272 Artifact,
273}
274
275#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
277pub struct ResultArtifactRefMsg {
278 pub format_version: u32,
279 pub artifact_id: String,
280 pub byte_len: u64,
281 pub sha256: String,
282 pub chunk_size: u64,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
287#[serde(deny_unknown_fields)]
288pub struct GetResultArtifactChunkRequest {
289 pub artifact_id: String,
290 pub offset: u64,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct GetResultArtifactChunkResponse {
296 pub success: bool,
297 pub artifact_id: String,
298 pub offset: u64,
299 pub data_base64: String,
300 pub eof: bool,
301 pub error: Option<String>,
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
306#[serde(deny_unknown_fields)]
307pub struct DeleteResultArtifactRequest {
308 pub artifact_id: String,
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct DeleteResultArtifactResponse {
314 pub success: bool,
315 pub artifact_id: String,
316 pub error: Option<String>,
317}
318
319#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
321#[serde(rename_all = "snake_case", deny_unknown_fields)]
322pub enum MtmOutputPolicyMsg {
323 None,
324 Bounded { max_points: usize },
325 Full,
326}
327
328impl Default for MtmOutputPolicyMsg {
329 fn default() -> Self {
330 Self::Bounded { max_points: 4_096 }
331 }
332}
333
334#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
336#[serde(rename_all = "snake_case", deny_unknown_fields)]
337pub enum MarketEntrySizingBasisMsg {
338 #[default]
339 FillPrice,
340 SignalEntryPrice,
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize)]
345#[serde(default, deny_unknown_fields)]
346pub struct FutureQuoteConfigMsg {
347 pub signal_latency_ms: i64,
348 pub slippage_pips: f64,
349 pub stale_quote_after_ms: Option<i64>,
350 pub pnl_epsilon: f64,
351 pub account_currency: String,
352 pub conversion_stale_after_ms: i64,
353 pub mtm_output: MtmOutputPolicyMsg,
354 pub market_entry_sizing_basis: MarketEntrySizingBasisMsg,
355}
356
357impl Default for FutureQuoteConfigMsg {
358 fn default() -> Self {
359 Self {
360 signal_latency_ms: 0,
361 slippage_pips: 0.0,
362 stale_quote_after_ms: None,
363 pnl_epsilon: 1.0e-9,
364 account_currency: String::new(),
365 conversion_stale_after_ms: 300_000,
366 mtm_output: MtmOutputPolicyMsg::default(),
367 market_entry_sizing_basis: MarketEntrySizingBasisMsg::default(),
368 }
369 }
370}
371
372#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
374#[serde(default, deny_unknown_fields)]
375pub struct EvaluationContextMsg {
376 pub provider_id: Option<String>,
377 pub source_id: Option<String>,
378}
379
380#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
382#[serde(rename_all = "snake_case")]
383pub enum EvaluationSectionMsg {
384 Coverage,
385 PositionPerformance,
386 RMetrics,
387 Excursions,
388 Execution,
389 Costs,
390 Robustness,
391 Breakdowns,
392}
393
394impl EvaluationSectionMsg {
395 pub const ALL: [Self; 8] = [
396 Self::Coverage,
397 Self::PositionPerformance,
398 Self::RMetrics,
399 Self::Excursions,
400 Self::Execution,
401 Self::Costs,
402 Self::Robustness,
403 Self::Breakdowns,
404 ];
405}
406
407#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
408#[serde(rename_all = "snake_case")]
409pub enum EvaluationPositionSideMsg {
410 Long,
411 Short,
412}
413
414#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
415#[serde(rename_all = "snake_case")]
416pub enum EvaluationGroupFilterMsg {
417 Named(String),
418 Ungrouped,
419}
420
421#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
424#[serde(default, deny_unknown_fields)]
425pub struct PositionFilterMsg {
426 pub symbols: Vec<String>,
427 pub sides: Vec<EvaluationPositionSideMsg>,
428 pub groups: Vec<EvaluationGroupFilterMsg>,
429 pub close_reasons: Vec<String>,
430 pub tags: BTreeMap<String, Vec<String>>,
431}
432
433#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
435#[serde(rename_all = "snake_case")]
436pub enum BreakdownDimensionMsg {
437 Symbol,
438 Side,
439 Group,
440 CloseReason,
441 Tag(String),
442}
443
444#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
445#[serde(default, deny_unknown_fields)]
446pub struct BootstrapConfigMsg {
447 pub samples: usize,
448 pub confidence_level: f64,
449 pub seed: u64,
450 pub minimum_sample_size: usize,
451}
452
453impl Default for BootstrapConfigMsg {
454 fn default() -> Self {
455 Self {
456 samples: 2_000,
457 confidence_level: 0.95,
458 seed: 0xA076_1D64_78BD_642F,
459 minimum_sample_size: 5,
460 }
461 }
462}
463
464#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
466#[serde(default, deny_unknown_fields)]
467pub struct SourceCoverageCountsMsg {
468 pub raw_messages: u64,
469 pub parsed_messages: u64,
470 pub skipped_messages: u64,
471 pub failed_messages: u64,
472 pub emitted_signals: u64,
473 pub emitted_entry_signals: u64,
474}
475
476#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
478#[serde(default, deny_unknown_fields)]
479pub struct ProviderEvaluationOptionsMsg {
480 pub context: EvaluationContextMsg,
481 pub source_coverage: Option<SourceCoverageCountsMsg>,
482 pub sections: Vec<EvaluationSectionMsg>,
484 pub filter: PositionFilterMsg,
485 pub breakdowns: Vec<BreakdownDimensionMsg>,
486 pub bootstrap: BootstrapConfigMsg,
487 pub rolling_window: usize,
488 pub minimum_breakdown_bucket_count: usize,
489 pub maximum_breakdown_rows: Option<usize>,
490 pub include_positions: bool,
491 pub maximum_position_rows: Option<usize>,
492}
493
494impl Default for ProviderEvaluationOptionsMsg {
495 fn default() -> Self {
496 Self {
497 context: EvaluationContextMsg::default(),
498 source_coverage: None,
499 sections: EvaluationSectionMsg::ALL.to_vec(),
500 filter: PositionFilterMsg::default(),
501 breakdowns: Vec::new(),
502 bootstrap: BootstrapConfigMsg::default(),
503 rolling_window: 20,
504 minimum_breakdown_bucket_count: 1,
505 maximum_breakdown_rows: None,
506 include_positions: false,
507 maximum_position_rows: None,
508 }
509 }
510}
511
512#[derive(Debug, Clone, Serialize, Deserialize)]
516pub struct BacktestRunSpec {
517 pub symbol: String,
518 #[serde(default)]
519 pub symbols: Vec<String>,
520 #[serde(default)]
521 pub all_symbols: bool,
522 pub exchange: String,
523 pub data_type: String,
524 pub timeframe: Option<String>,
525 pub from: Option<String>,
526 pub to: Option<String>,
527 #[serde(default)]
529 pub raw_signals: Vec<RawSignalMsg>,
530 pub profile: Option<String>,
531 #[serde(default)]
532 pub profile_def: Option<ManagementProfileMsg>,
533 #[serde(default, skip_serializing_if = "Vec::is_empty")]
534 pub entry_profile_routes: Vec<EntryProfileRouteMsg>,
535 pub config: BacktestConfigMsg,
536}
537
538#[derive(Debug, Clone, Serialize, Deserialize)]
540pub struct RunBacktestResponse {
541 pub success: bool,
542 pub error: Option<String>,
543 pub result: Option<BacktestResultMsg>,
545 pub elapsed_ms: u64,
546 #[serde(default)]
547 pub artifact: Option<ResultArtifactRefMsg>,
548 #[serde(default = "default_true")]
549 pub inline_complete: bool,
550}
551
552#[derive(Debug, Clone, Serialize)]
554pub struct RunBacktestRequest {
555 pub request: BacktestRunSpec,
556 #[serde(default)]
557 pub future: FutureQuoteConfigMsg,
558 #[serde(default)]
559 pub evaluation: ProviderEvaluationOptionsMsg,
560 #[serde(default)]
561 pub result_delivery: ResultDeliveryMsg,
562}
563
564#[derive(Debug, Clone, Serialize)]
566pub struct SubmitBacktestRequest {
567 pub request: RunBacktestRequest,
568}
569
570#[derive(Debug, Clone, Serialize, Deserialize)]
574pub struct BacktestMultiRunSpec {
575 pub symbol: String,
576 #[serde(default)]
577 pub symbols: Vec<String>,
578 #[serde(default)]
579 pub all_symbols: bool,
580 pub exchange: String,
581 pub data_type: String,
582 pub timeframe: Option<String>,
583 pub from: Option<String>,
584 pub to: Option<String>,
585 #[serde(default)]
587 pub raw_signals: Vec<RawSignalMsg>,
588 pub profiles: Vec<ProfileRef>,
589 #[serde(default, skip_serializing_if = "Vec::is_empty")]
590 pub entry_profile_routes: Vec<EntryProfileRouteMsg>,
591 pub config: BacktestConfigMsg,
592}
593
594#[derive(Debug, Clone, Serialize, Deserialize)]
596pub struct ProfileResult {
597 pub profile: String,
598 pub success: bool,
599 pub error: Option<String>,
600 pub result: Option<BacktestResultMsg>,
601}
602
603#[derive(Debug, Clone, Serialize, Deserialize)]
605pub struct RunBacktestMultiResponse {
606 #[serde(default = "default_true")]
608 pub success: bool,
609 #[serde(default)]
610 pub error: Option<String>,
611 pub results: Vec<ProfileResult>,
613 pub elapsed_ms: u64,
614 #[serde(default)]
615 pub artifact: Option<ResultArtifactRefMsg>,
616 #[serde(default = "default_true")]
617 pub inline_complete: bool,
618}
619
620#[derive(Debug, Clone, Serialize)]
621pub struct RunBacktestMultiRequest {
622 pub request: BacktestMultiRunSpec,
623 #[serde(default)]
624 pub future: FutureQuoteConfigMsg,
625 #[serde(default)]
626 pub evaluation: ProviderEvaluationOptionsMsg,
627 #[serde(default)]
628 pub result_delivery: ResultDeliveryMsg,
629}
630
631#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
635#[serde(rename_all = "snake_case")]
636pub enum PriceBasisMsg {
637 Bid,
638 Ask,
639 Mid,
640}
641
642#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
644#[serde(deny_unknown_fields)]
645pub struct SourceBindingMsg {
646 pub source: String,
647 pub timeframe_seconds: u32,
648 pub price_basis: PriceBasisMsg,
649 #[serde(default)]
650 pub alignment_offset_seconds: i32,
651}
652
653#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
654#[serde(deny_unknown_fields)]
655pub struct HistoricalInputsMsg {
656 pub calendars: BTreeMap<String, TradingCalendarMsg>,
657 pub inputs: Vec<CalendarInputMsg>,
658 #[serde(default)]
659 pub limits: Option<CalendarLimitsMsg>,
660}
661
662#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
663#[serde(deny_unknown_fields)]
664pub struct TradingCalendarMsg {
665 pub id: String,
666 pub timezone: String,
667 #[serde(default = "default_day_boundary")]
668 pub day_boundary: String,
669 #[serde(default)]
670 pub sessions: SessionScheduleMsg,
671 #[serde(default)]
672 pub market: MarketScheduleMsg,
673}
674
675fn default_day_boundary() -> String {
676 "00:00:00".into()
677}
678
679#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
680#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
681pub enum SessionScheduleMsg {
682 #[default]
683 FullDay,
684 Custom {
685 items: Vec<NamedSessionMsg>,
686 },
687}
688
689#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
690#[serde(deny_unknown_fields)]
691pub struct NamedSessionMsg {
692 pub id: String,
693 #[serde(default, skip_serializing_if = "Option::is_none")]
694 pub timezone: Option<String>,
695 pub span: SessionSpanMsg,
696 #[serde(default)]
697 pub weekdays: Vec<u8>,
698}
699
700#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
701#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
702pub enum SessionSpanMsg {
703 FullDay,
704 Timed {
705 start: String,
706 end: String,
707 end_day_offset: u8,
708 },
709}
710
711#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
712#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
713pub enum MarketScheduleMsg {
714 #[default]
715 Unspecified,
716 Continuous,
717 Weekly {
718 intervals: Vec<WeeklyMarketIntervalMsg>,
719 #[serde(default)]
720 exceptions: BTreeMap<String, Vec<LocalMarketIntervalMsg>>,
721 },
722}
723
724#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
725#[serde(deny_unknown_fields)]
726pub struct WeeklyMarketIntervalMsg {
727 pub weekday: u8,
728 pub start: String,
729 pub end: String,
730 pub end_day_offset: u8,
731}
732
733#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
734#[serde(deny_unknown_fields)]
735pub struct LocalMarketIntervalMsg {
736 pub start: String,
737 pub end: String,
738 pub end_day_offset: u8,
739}
740
741#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
742#[serde(deny_unknown_fields)]
743pub struct CalendarInputMsg {
744 pub name: String,
745 pub source: String,
746 pub calendar: String,
747 pub feature: CalendarFeatureMsg,
748 #[serde(default, skip_serializing_if = "Option::is_none")]
749 pub session: Option<String>,
750 #[serde(default)]
751 pub time_basis: CalendarTimeBasisMsg,
752 #[serde(default = "default_opening_range_minutes")]
753 pub opening_range_minutes: u32,
754 pub child_seconds: u64,
755 #[serde(default)]
756 pub alignment_offset_seconds: i64,
757 pub maximum_history: usize,
758}
759
760fn default_opening_range_minutes() -> u32 {
761 5
762}
763
764#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
765#[serde(rename_all = "snake_case")]
766pub enum CalendarFeatureMsg {
767 LocalSecondOfDay,
768 SessionElapsedSeconds,
769 SessionMembership,
770 PreviousSessionHigh,
771 PreviousSessionLow,
772 PreviousDayHigh,
773 PreviousDayLow,
774 PreviousWeekHigh,
775 PreviousWeekLow,
776 OpeningRangeHighSoFar,
777 OpeningRangeLowSoFar,
778 OpeningRangeHighFinal,
779 OpeningRangeLowFinal,
780 PastSameSlotRangeRatio,
781 PastSameSlotCount,
782}
783
784#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
785#[serde(rename_all = "snake_case")]
786pub enum CalendarTimeBasisMsg {
787 #[default]
788 SourceOpen,
789 DecisionTime,
790}
791
792#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
793#[serde(deny_unknown_fields)]
794pub struct CalendarLimitsMsg {
795 pub max_sessions: usize,
796 pub max_market_intervals: usize,
797 pub max_exceptions: usize,
798 pub max_history_occurrences: usize,
799 pub max_resolved_children: usize,
800 pub max_owned_bytes: usize,
801}
802
803#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
805#[serde(deny_unknown_fields)]
806pub struct ConfiguredStrategyRunMsg {
807 pub document: serde_json::Value,
809 pub sources: Vec<SourceBindingMsg>,
810 #[serde(default, skip_serializing_if = "Option::is_none")]
812 pub instance_id: Option<String>,
813 #[serde(default)]
815 pub decision_latency_ms: u64,
816 #[serde(default, skip_serializing_if = "Option::is_none")]
817 pub historical_inputs: Option<HistoricalInputsMsg>,
818}
819
820#[derive(Debug, Clone, Serialize, Deserialize)]
822#[serde(deny_unknown_fields)]
823pub struct ConfiguredStrategyRunSpec {
824 pub symbol: String,
825 pub exchange: String,
826 pub data_type: String,
828 #[serde(default)]
829 pub timeframe: Option<String>,
830 #[serde(default)]
831 pub from: Option<String>,
832 #[serde(default)]
833 pub to: Option<String>,
834 pub strategy: ConfiguredStrategyRunMsg,
835 #[serde(default)]
836 pub profile: Option<String>,
837 #[serde(default)]
838 pub profile_def: Option<ManagementProfileMsg>,
839 #[serde(default, skip_serializing_if = "Vec::is_empty")]
840 pub entry_profile_routes: Vec<EntryProfileRouteMsg>,
841 pub config: BacktestConfigMsg,
842}
843
844#[derive(Debug, Clone, Serialize)]
846pub struct RunConfiguredStrategyRequest {
847 pub request: ConfiguredStrategyRunSpec,
848 #[serde(default)]
849 pub future: FutureQuoteConfigMsg,
850 #[serde(default)]
851 pub evaluation: ProviderEvaluationOptionsMsg,
852 #[serde(default)]
853 pub result_delivery: ResultDeliveryMsg,
854}
855
856#[derive(Debug, Clone, Serialize)]
858pub struct SubmitConfiguredStrategyRequest {
859 pub request: RunConfiguredStrategyRequest,
860}
861
862#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
864pub struct ConfiguredStrategyOutputMsg {
865 pub document: serde_json::Value,
867 pub sources: Vec<SourceBindingMsg>,
868 pub requirements: serde_json::Value,
870 pub data_mode: String,
872 pub decisions: serde_json::Value,
874 pub research: serde_json::Value,
876 #[serde(default, skip_serializing_if = "Option::is_none")]
877 pub historical_inputs: Option<serde_json::Value>,
878}
879
880#[derive(Debug, Clone, Serialize, Deserialize)]
884#[serde(deny_unknown_fields)]
885pub struct PortfolioInstanceMsg {
886 pub symbol: String,
887 pub strategy: ConfiguredStrategyRunMsg,
889 #[serde(default)]
890 pub profile: Option<String>,
891 #[serde(default)]
892 pub profile_def: Option<ManagementProfileMsg>,
893 #[serde(default, skip_serializing_if = "Vec::is_empty")]
894 pub entry_profile_routes: Vec<EntryProfileRouteMsg>,
895}
896
897#[derive(Debug, Clone, Serialize, Deserialize)]
899#[serde(deny_unknown_fields)]
900pub struct PortfolioRunSpec {
901 pub instances: Vec<PortfolioInstanceMsg>,
902 pub exchange: String,
903 pub data_type: String,
905 #[serde(default)]
906 pub timeframe: Option<String>,
907 #[serde(default)]
908 pub from: Option<String>,
909 #[serde(default)]
910 pub to: Option<String>,
911 pub config: BacktestConfigMsg,
912 #[serde(default, skip_serializing_if = "Option::is_none")]
914 pub policies: Option<serde_json::Value>,
915 #[serde(default, skip_serializing_if = "Option::is_none")]
917 pub groups: Option<serde_json::Value>,
918}
919
920#[derive(Debug, Clone, Serialize)]
922pub struct RunPortfolioRequest {
923 pub request: PortfolioRunSpec,
924 #[serde(default)]
925 pub future: FutureQuoteConfigMsg,
926 #[serde(default)]
927 pub evaluation: ProviderEvaluationOptionsMsg,
928 #[serde(default)]
929 pub result_delivery: ResultDeliveryMsg,
930}
931
932#[derive(Debug, Clone, Serialize)]
934pub struct SubmitPortfolioRequest {
935 pub request: RunPortfolioRequest,
936}
937
938#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
940pub struct PortfolioInstanceOutputMsg {
941 pub instance_id: String,
942 pub symbol: String,
943 pub strategy: ConfiguredStrategyOutputMsg,
944}
945
946#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
948pub struct PortfolioOutputMsg {
949 pub instances: Vec<PortfolioInstanceOutputMsg>,
950 #[serde(default, skip_serializing_if = "Option::is_none")]
952 pub policies: Option<serde_json::Value>,
953 #[serde(default, skip_serializing_if = "Option::is_none")]
955 pub supervisor: Option<serde_json::Value>,
956}
957
958#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
962#[serde(deny_unknown_fields)]
963pub struct SearchWindowMsg {
964 pub label: String,
965 pub from: String,
966 pub to: String,
967}
968
969#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
971#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
972pub enum SearchWindowsMsg {
973 Fixed {
974 in_sample: SearchWindowMsg,
975 out_of_sample: SearchWindowMsg,
976 },
977 RollingWalkForward {
978 start: String,
979 end: String,
980 train_seconds: i64,
981 test_seconds: i64,
982 step_seconds: i64,
983 },
984}
985
986#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
987#[serde(rename_all = "snake_case")]
988pub enum SearchEvaluationRoleMsg {
989 Search,
990 Validation,
991 Final,
992}
993
994#[derive(Debug, Clone, Serialize, Deserialize)]
995#[serde(deny_unknown_fields)]
996pub struct SearchSelectedRerunMsg {
997 pub experiment_recipe: serde_json::Value,
998 pub candidate_recipe: serde_json::Value,
999 pub run_recipe: serde_json::Value,
1000 pub role: SearchEvaluationRoleMsg,
1001 pub caller_revision: String,
1002 #[serde(default)]
1003 pub release_final: bool,
1004 #[serde(default)]
1005 pub future_horizon_millis: Option<u64>,
1006 #[serde(default)]
1007 pub embargo_millis: Option<u64>,
1008}
1009
1010#[derive(Debug, Clone, Serialize, Deserialize)]
1011#[serde(deny_unknown_fields)]
1012pub struct SearchDirectPortfolioInstanceMsg {
1013 pub instance_id: String,
1014 pub symbol: String,
1015 pub factory: SearchDirectFactoryMsg,
1016 #[serde(default)]
1017 pub profile: Option<String>,
1018}
1019
1020#[derive(Debug, Clone, Serialize, Deserialize)]
1021#[serde(deny_unknown_fields)]
1022pub struct SearchPortfolioCandidateMsg {
1023 pub id: String,
1024 #[serde(default)]
1025 pub instances: Vec<PortfolioInstanceMsg>,
1026 #[serde(default)]
1027 pub direct_instances: Vec<SearchDirectPortfolioInstanceMsg>,
1028 #[serde(default, skip_serializing_if = "Option::is_none")]
1029 pub policies: Option<serde_json::Value>,
1030 #[serde(default, skip_serializing_if = "Option::is_none")]
1031 pub groups: Option<serde_json::Value>,
1032}
1033
1034#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1035#[serde(tag = "type", content = "value", rename_all = "snake_case")]
1036pub enum SearchFactoryParameterMsg {
1037 Integer(i64),
1038 Number(f64),
1039 Choice(String),
1040}
1041
1042#[derive(Debug, Clone, Serialize, Deserialize)]
1043#[serde(deny_unknown_fields)]
1044pub struct SearchDirectFactoryPointMsg {
1045 pub parameters: BTreeMap<String, SearchFactoryParameterMsg>,
1046}
1047
1048#[derive(Debug, Clone, Serialize, Deserialize)]
1049#[serde(deny_unknown_fields)]
1050pub struct SearchDirectFactoryMsg {
1051 pub name: String,
1052 pub revision: String,
1053 pub points: Vec<SearchDirectFactoryPointMsg>,
1054}
1055
1056#[derive(Debug, Clone, Serialize, Deserialize)]
1057#[serde(deny_unknown_fields)]
1058pub struct SearchExecutionVariantMsg {
1059 pub id: String,
1060 pub config: BacktestConfigMsg,
1061 #[serde(default)]
1062 pub future: FutureQuoteConfigMsg,
1063 #[serde(default)]
1064 pub profile: Option<String>,
1065}
1066
1067#[derive(Debug, Clone, Serialize, Deserialize)]
1069#[serde(deny_unknown_fields)]
1070pub struct SearchRunSpec {
1071 pub template: serde_json::Value,
1073 pub space: serde_json::Value,
1075 pub symbols: Vec<String>,
1076 pub exchange: String,
1077 pub data_type: String,
1079 #[serde(default)]
1080 pub timeframe: Option<String>,
1081 pub windows: SearchWindowsMsg,
1082 pub config: BacktestConfigMsg,
1083 #[serde(default)]
1085 pub profile: Option<String>,
1086 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1088 pub entry_profile_routes: Vec<EntryProfileRouteMsg>,
1089 #[serde(default)]
1091 pub workers: Option<usize>,
1092 #[serde(default)]
1093 pub decision_latency_ms: u64,
1094 #[serde(default)]
1095 pub structural: Option<serde_json::Value>,
1096 #[serde(default)]
1097 pub resource_limits: Option<serde_json::Value>,
1098 #[serde(default)]
1099 pub variants: Vec<SearchExecutionVariantMsg>,
1100 #[serde(default)]
1102 pub direct_factory: Option<SearchDirectFactoryMsg>,
1103 #[serde(default)]
1105 pub resume_checkpoint: Option<serde_json::Value>,
1106 #[serde(default)]
1108 pub selected_rerun: Option<SearchSelectedRerunMsg>,
1109 #[serde(default)]
1111 pub portfolio_candidates: Vec<SearchPortfolioCandidateMsg>,
1112 #[serde(default)]
1114 pub series_descriptors: Vec<serde_json::Value>,
1115}
1116
1117#[derive(Debug, Clone, Serialize)]
1119pub struct SubmitSearchRequest {
1120 pub request: SearchRunSpec,
1121 #[serde(default)]
1122 pub future: FutureQuoteConfigMsg,
1123 #[serde(default)]
1124 pub evaluation: ProviderEvaluationOptionsMsg,
1125}
1126
1127#[derive(Debug, Clone, Serialize, Deserialize)]
1129#[serde(deny_unknown_fields)]
1130pub struct GetSearchResultRequest {
1131 pub job_id: String,
1132}
1133
1134#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1136pub struct SearchSummaryMsg {
1137 pub points_total: usize,
1138 pub rows: usize,
1139 pub completed_rows: usize,
1140 pub failed_rows: usize,
1141 pub data_mode: String,
1143}
1144
1145#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1147pub struct SearchResultMsg {
1148 pub summary: SearchSummaryMsg,
1149 pub table_csv: String,
1151 pub evaluation: serde_json::Value,
1153 pub bound_documents: Vec<serde_json::Value>,
1155 #[serde(default)]
1156 pub experiment_recipe: Option<serde_json::Value>,
1157 #[serde(default)]
1158 pub candidate_recipes: Vec<serde_json::Value>,
1159 #[serde(default)]
1160 pub run_recipes: Vec<serde_json::Value>,
1161 #[serde(default)]
1162 pub generation_dispositions: Option<serde_json::Value>,
1163 #[serde(default)]
1164 pub checkpoint: Option<serde_json::Value>,
1165 #[serde(default)]
1166 pub selected_evidence: Option<serde_json::Value>,
1167 #[serde(default)]
1168 pub trace: Option<serde_json::Value>,
1169}
1170
1171#[derive(Debug, Clone, Serialize, Deserialize)]
1173pub struct GetSearchResultResponse {
1174 pub success: bool,
1175 pub job_id: String,
1176 #[serde(default)]
1177 pub error: Option<String>,
1178 #[serde(default)]
1179 pub summary: Option<SearchSummaryMsg>,
1180 #[serde(default)]
1181 pub artifact: Option<ResultArtifactRefMsg>,
1182 #[serde(default)]
1184 pub checkpoint_artifact: Option<ResultArtifactRefMsg>,
1185}
1186
1187#[derive(Debug, Clone, Serialize, Deserialize)]
1191pub struct BacktestResultMsg {
1192 #[serde(default, skip_serializing_if = "Option::is_none")]
1194 pub strategy: Option<ConfiguredStrategyOutputMsg>,
1195 #[serde(default, skip_serializing_if = "Option::is_none")]
1197 pub portfolio: Option<PortfolioOutputMsg>,
1198 pub initial_balance: f64,
1199 pub final_balance: f64,
1200 pub total_pnl: f64,
1201 pub total_trades: usize,
1202 pub winning_trades: usize,
1203 pub losing_trades: usize,
1204 pub win_rate: f64,
1205 pub profit_factor: f64,
1206 pub max_drawdown: f64,
1207 pub max_drawdown_pct: f64,
1208
1209 pub summary: SubsetStatsMsg,
1211 pub per_symbol: HashMap<String, SubsetStatsMsg>,
1212 pub per_group: HashMap<String, SubsetStatsMsg>,
1213 pub long_stats: SubsetStatsMsg,
1214 pub short_stats: SubsetStatsMsg,
1215 pub per_close_reason: Vec<CloseReasonStatsMsg>,
1216 pub streaks: StreakStatsMsg,
1217 pub risk_metrics: RiskMetricsMsg,
1218 pub duration_stats: Option<DurationStatsMsg>,
1219 pub monthly_returns: Vec<MonthlyReturnMsg>,
1220
1221 pub equity_curve: Vec<EquityPoint>,
1222 pub trade_log: Vec<TradeResultMsg>,
1223
1224 pub positions: Vec<PositionSummaryMsg>,
1226 pub total_positions: usize,
1227 pub winning_positions: usize,
1228 pub losing_positions: usize,
1229 pub position_win_rate: f64,
1230
1231 #[serde(default)]
1233 pub future: Option<FutureBacktestResultMsg>,
1234
1235 #[serde(default)]
1237 pub total_commission: f64,
1238 #[serde(default)]
1240 pub total_swap: f64,
1241 #[serde(default)]
1243 pub gross_pnl: Option<f64>,
1244 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1246 pub cost_events: Vec<CostEventMsg>,
1247}
1248
1249#[derive(Debug, Clone, Serialize, Deserialize)]
1251pub struct CostEventMsg {
1252 pub id: String,
1253 pub position_id: String,
1254 pub symbol: String,
1255 pub side: String,
1256 pub ts: String,
1257 pub kind: String,
1259 pub amount: f64,
1261 #[serde(default)]
1262 pub native_amount: Option<f64>,
1263 #[serde(default)]
1264 pub native_currency: Option<String>,
1265 pub size: f64,
1266 #[serde(default)]
1267 pub nights: Option<u32>,
1268}
1269
1270#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1272#[serde(default)]
1273pub struct MtmOutputSummaryMsg {
1274 pub policy: MtmOutputPolicyMsg,
1275 pub observed_points: u64,
1276 pub retained_points: u64,
1277 pub omitted_points: u64,
1278}
1279
1280impl Default for MtmOutputSummaryMsg {
1281 fn default() -> Self {
1282 Self {
1283 policy: MtmOutputPolicyMsg::Full,
1284 observed_points: 0,
1285 retained_points: 0,
1286 omitted_points: 0,
1287 }
1288 }
1289}
1290
1291#[derive(Debug, Clone, Serialize, Deserialize)]
1295pub struct FutureBacktestResultMsg {
1296 pub format_version: u32,
1297 pub execution_metadata: serde_json::Value,
1298 pub recorded_fills: serde_json::Value,
1299 pub action_dispositions: serde_json::Value,
1300 pub close_events: serde_json::Value,
1301 pub completed_positions: serde_json::Value,
1302 pub open_positions: serde_json::Value,
1303 pub pending_orders: serde_json::Value,
1304 #[serde(default)]
1306 pub pending_order_lifecycle: Vec<PendingOrderLifecycleEventMsg>,
1307 pub mtm_equity_curve: serde_json::Value,
1308 #[serde(default)]
1309 pub mtm_output_summary: MtmOutputSummaryMsg,
1310 pub mtm_max_drawdown: Option<f64>,
1311 pub mtm_max_drawdown_pct: Option<f64>,
1312 pub provider_evaluation: serde_json::Value,
1313}
1314
1315#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1317#[serde(rename_all = "snake_case")]
1318pub enum PendingOrderLifecycleStateMsg {
1319 #[default]
1320 Placed,
1321 Filled,
1322 Cancelled,
1323 UnfilledAtEnd,
1324}
1325
1326#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1328#[serde(default)]
1329pub struct PendingOrderLifecycleEventMsg {
1330 pub id: String,
1331 pub sequence: u64,
1332 pub position_id: String,
1333 pub placement_action_id: Option<String>,
1334 pub terminal_action_id: Option<String>,
1335 pub state: PendingOrderLifecycleStateMsg,
1336 pub symbol: String,
1337 pub side: String,
1338 pub order_type: String,
1339 pub requested_size: f64,
1340 pub filled_size: Option<f64>,
1341 pub requested_price: Option<f64>,
1342 pub fill_price: Option<f64>,
1343 pub signal_ts: Option<String>,
1344 pub placed_ts: Option<String>,
1345 pub effective_ts: Option<String>,
1346 pub terminal_ts: Option<String>,
1347 pub wait_latency_ms: Option<i64>,
1348 pub fill_ratio: Option<f64>,
1349}
1350
1351#[derive(Debug, Clone, Serialize, Deserialize)]
1355pub struct SubsetStatsMsg {
1356 pub total_trades: usize,
1357 pub winning_trades: usize,
1358 pub losing_trades: usize,
1359 pub breakeven_trades: usize,
1360 pub total_pnl: f64,
1361 pub gross_profit: f64,
1362 pub gross_loss: f64,
1363 pub win_rate: f64,
1364 pub profit_factor: f64,
1365 pub avg_win: f64,
1366 pub avg_loss: f64,
1367 pub win_loss_ratio: f64,
1368 pub expectancy: f64,
1369 pub largest_win: f64,
1370 pub largest_loss: f64,
1371 #[serde(default)]
1373 pub commission: f64,
1374 #[serde(default)]
1376 pub swap: f64,
1377 #[serde(default)]
1379 pub gross_pnl: Option<f64>,
1380}
1381
1382#[derive(Debug, Clone, Serialize, Deserialize)]
1384pub struct StreakStatsMsg {
1385 pub max_consecutive_wins: u32,
1386 pub max_consecutive_losses: u32,
1387 pub current_streak: i32,
1388}
1389
1390#[derive(Debug, Clone, Serialize, Deserialize)]
1392pub struct RiskMetricsMsg {
1393 pub sharpe_ratio: Option<f64>,
1394 pub sortino_ratio: Option<f64>,
1395 pub calmar_ratio: Option<f64>,
1396 pub return_on_max_drawdown: Option<f64>,
1397 pub max_drawdown: f64,
1398 pub max_drawdown_pct: f64,
1399 pub max_drawdown_duration_secs: Option<i64>,
1400}
1401
1402#[derive(Debug, Clone, Serialize, Deserialize)]
1404pub struct DurationStatsMsg {
1405 pub avg_duration_secs: i64,
1406 pub min_duration_secs: i64,
1407 pub max_duration_secs: i64,
1408 pub avg_winner_duration_secs: i64,
1409 pub avg_loser_duration_secs: i64,
1410}
1411
1412#[derive(Debug, Clone, Serialize, Deserialize)]
1414pub struct MonthlyReturnMsg {
1415 pub year: i32,
1416 pub month: u32,
1417 pub pnl: f64,
1418 pub trade_count: usize,
1419 pub ending_balance: f64,
1420}
1421
1422#[derive(Debug, Clone, Serialize, Deserialize)]
1424pub struct CloseReasonStatsMsg {
1425 pub reason: String,
1426 pub count: usize,
1427 pub total_pnl: f64,
1428 pub avg_pnl: f64,
1429 pub percentage: f64,
1430}
1431
1432#[derive(Debug, Clone, Serialize, Deserialize)]
1434pub struct PositionSummaryMsg {
1435 pub position_id: String,
1436 pub symbol: String,
1437 pub side: String,
1438 pub group: Option<String>,
1439 pub entry_price: f64,
1440 pub avg_exit_price: f64,
1441 pub original_size: f64,
1442 pub close_count: usize,
1443 pub net_pnl: f64,
1444 pub close_reasons: Vec<String>,
1445 pub open_ts: String,
1446 pub final_close_ts: Option<String>,
1447 pub duration_seconds: i64,
1448}
1449
1450#[derive(Debug, Clone, Serialize, Deserialize)]
1452pub struct EquityPoint {
1453 pub ts: String,
1454 pub balance: f64,
1455}
1456
1457#[derive(Debug, Clone, Serialize, Deserialize)]
1459pub struct TradeResultMsg {
1460 pub position_id: String,
1461 pub symbol: String,
1462 pub side: String,
1463 pub entry_price: f64,
1464 pub exit_price: f64,
1465 pub size: f64,
1466 pub pnl: f64,
1468 #[serde(default)]
1472 pub commission: f64,
1473 #[serde(default)]
1475 pub swap: f64,
1476 #[serde(default)]
1478 pub gross_pnl: Option<f64>,
1479 pub open_ts: String,
1480 pub close_ts: String,
1481 pub close_reason: String,
1482 pub group: Option<String>,
1483}
1484
1485#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1489pub enum TargetSelectionMsg {
1490 All,
1492 None,
1494 Selected(Vec<usize>),
1496}
1497
1498#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1500#[serde(tag = "type", deny_unknown_fields)]
1501pub enum TargetSourceMsg {
1502 #[default]
1503 FromSignal,
1504 StopDistanceMultiples {
1505 multiples: Vec<f64>,
1506 },
1507}
1508
1509#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1511#[serde(rename_all = "lowercase")]
1512pub enum EntryGeometryPolicyMsg {
1513 #[default]
1516 Strict,
1517 Permissive,
1520}
1521
1522#[derive(Debug, Clone, Serialize, Deserialize)]
1524pub struct ManagementProfileMsg {
1525 pub name: String,
1526 #[serde(default, skip_serializing_if = "Option::is_none")]
1529 pub target_selection: Option<TargetSelectionMsg>,
1530 pub use_targets: Vec<usize>,
1532 pub close_ratios: Vec<f64>,
1533 #[serde(default, skip_serializing_if = "Option::is_none")]
1534 pub target_source: Option<TargetSourceMsg>,
1535 #[serde(default)]
1536 pub stoploss_mode: Option<StoplossModeMsg>,
1537 #[serde(default)]
1538 pub rules: Vec<RuleConfigDefMsg>,
1539 #[serde(default)]
1540 pub group_override: Option<String>,
1541 #[serde(default)]
1542 pub let_remainder_run: bool,
1543 #[serde(default, skip_serializing_if = "Option::is_none")]
1546 pub entry_geometry: Option<EntryGeometryPolicyMsg>,
1547}
1548
1549#[derive(Debug, Clone, Serialize, Deserialize)]
1551#[serde(tag = "type")]
1552pub enum StoplossModeMsg {
1553 FromSignal,
1554 None,
1555 FixedDistance { distance: f64 },
1556 FixedPrice { price: f64 },
1557 FromSignalDistance { multiplier: f64 },
1558}
1559
1560#[derive(Debug, Clone, Serialize, Deserialize)]
1562#[serde(tag = "type")]
1563pub enum RuleConfigDefMsg {
1564 FixedStoploss { price: f64 },
1565 TrailingStop { distance: f64 },
1566 TakeProfit { price: f64, close_ratio: f64 },
1567 BreakevenWhen { trigger_price: f64 },
1568 BreakevenWhenOffset { trigger_price_offset: f64 },
1569 BreakevenAfterTargets { after_n: u32 },
1570 TimeExit { max_seconds: u64 },
1571}
1572
1573#[derive(Debug, Clone, Serialize, Deserialize)]
1575#[serde(untagged)]
1576pub enum ProfileRef {
1577 Named(String),
1578 Inline(ManagementProfileMsg),
1579}
1580
1581#[derive(Debug, Clone, Serialize, Deserialize)]
1583#[serde(deny_unknown_fields)]
1584pub struct EntryProfileRouteMsg {
1585 pub entry_class: String,
1586 pub profile: ProfileRef,
1587}
1588
1589#[derive(Debug, Clone, Serialize, Deserialize)]
1596#[serde(tag = "action", deny_unknown_fields)]
1597pub enum RawSignalMsg {
1598 Entry {
1600 ts: String,
1601 symbol: String,
1602 side: String,
1603 order_type: String,
1604 price: Option<f64>,
1605 risk: f64,
1606 stoploss: Option<f64>,
1607 #[serde(default)]
1608 targets: Vec<f64>,
1609 #[serde(default)]
1610 group: Option<String>,
1611 #[serde(default)]
1613 trade_id: Option<String>,
1614 #[serde(default, skip_serializing_if = "Option::is_none")]
1616 entry_class: Option<String>,
1617 },
1618 Close {
1620 ts: String,
1621 position: PositionRefMsg,
1622 },
1623 ClosePartial {
1625 ts: String,
1626 position: PositionRefMsg,
1627 ratio: f64,
1628 },
1629 ModifyStoploss {
1631 ts: String,
1632 position: PositionRefMsg,
1633 price: f64,
1634 },
1635 MoveStoplossToEntry {
1637 ts: String,
1638 position: PositionRefMsg,
1639 },
1640 AddTarget {
1642 ts: String,
1643 position: PositionRefMsg,
1644 price: f64,
1645 close_ratio: f64,
1646 },
1647 RemoveTarget {
1649 ts: String,
1650 position: PositionRefMsg,
1651 price: f64,
1652 },
1653 ModifyTarget {
1655 ts: String,
1656 position: PositionRefMsg,
1657 old_price: f64,
1658 new_price: f64,
1659 },
1660 AddRule {
1662 ts: String,
1663 position: PositionRefMsg,
1664 rule: RuleConfigDefMsg,
1665 },
1666 RemoveRule {
1668 ts: String,
1669 position: PositionRefMsg,
1670 rule_name: String,
1671 },
1672 ScaleIn {
1674 ts: String,
1675 position: PositionRefMsg,
1676 price: Option<f64>,
1677 size: f64,
1678 },
1679 CancelPending {
1681 ts: String,
1682 position: PositionRefMsg,
1683 },
1684 CloseAllOf { ts: String, symbol: String },
1686 CloseAll { ts: String },
1688 CancelAllPending { ts: String },
1690 ModifyAllStoploss {
1692 ts: String,
1693 symbol: String,
1694 price: f64,
1695 },
1696 CloseAllInGroup { ts: String, group_id: String },
1698 ModifyAllStoplossInGroup {
1700 ts: String,
1701 group_id: String,
1702 price: f64,
1703 },
1704}
1705
1706impl RawSignalMsg {
1707 pub fn ts(&self) -> &str {
1709 match self {
1710 RawSignalMsg::Entry { ts, .. }
1711 | RawSignalMsg::Close { ts, .. }
1712 | RawSignalMsg::ClosePartial { ts, .. }
1713 | RawSignalMsg::ModifyStoploss { ts, .. }
1714 | RawSignalMsg::MoveStoplossToEntry { ts, .. }
1715 | RawSignalMsg::AddTarget { ts, .. }
1716 | RawSignalMsg::RemoveTarget { ts, .. }
1717 | RawSignalMsg::ModifyTarget { ts, .. }
1718 | RawSignalMsg::AddRule { ts, .. }
1719 | RawSignalMsg::RemoveRule { ts, .. }
1720 | RawSignalMsg::ScaleIn { ts, .. }
1721 | RawSignalMsg::CancelPending { ts, .. }
1722 | RawSignalMsg::CloseAllOf { ts, .. }
1723 | RawSignalMsg::CloseAll { ts }
1724 | RawSignalMsg::CancelAllPending { ts }
1725 | RawSignalMsg::ModifyAllStoploss { ts, .. }
1726 | RawSignalMsg::CloseAllInGroup { ts, .. }
1727 | RawSignalMsg::ModifyAllStoplossInGroup { ts, .. } => ts,
1728 }
1729 }
1730}
1731
1732#[derive(Debug, Clone, Serialize, Deserialize)]
1734#[serde(tag = "type")]
1735pub enum PositionRefMsg {
1736 ByTradeId { trade_id: String },
1738 AllOnSymbol { symbol: String },
1740 AllInGroup { group_id: String },
1742}
1743
1744#[derive(Serialize, Deserialize)]
1747#[serde(tag = "type", deny_unknown_fields)]
1748enum StrictSizingPolicyMsg {
1749 FixedLot { lots: f64 },
1750 FixedRiskAmount { amount: f64 },
1751 BalanceRiskPercent { percent: f64 },
1752}
1753
1754#[derive(Serialize, Deserialize)]
1755#[serde(tag = "type", deny_unknown_fields)]
1756enum StrictCommissionModelMsg {
1757 #[serde(alias = "per_lot_per_side")]
1758 PerLotPerSide { amount: f64, currency: String },
1759 #[serde(alias = "notional_rate_per_side")]
1760 NotionalRatePerSide { buy_rate: f64, sell_rate: f64 },
1761}
1762
1763#[derive(Serialize, Deserialize)]
1764#[serde(tag = "unit", deny_unknown_fields)]
1765enum StrictSwapAmountMsg {
1766 #[serde(alias = "points")]
1767 Points { long: f64, short: f64 },
1768 #[serde(alias = "currency")]
1769 Currency {
1770 long: f64,
1771 short: f64,
1772 currency: String,
1773 },
1774}
1775
1776#[derive(Serialize, Deserialize)]
1777#[serde(deny_unknown_fields)]
1778struct StrictSwapScheduleMsg {
1779 amount: StrictSwapAmountMsg,
1780 rollover: String,
1781 triple_weekday: String,
1782 #[serde(default)]
1783 skipped_weekdays: Vec<String>,
1784}
1785
1786#[derive(Serialize, Deserialize)]
1787#[serde(deny_unknown_fields)]
1788struct StrictInstrumentCostsMsg {
1789 #[serde(default)]
1790 commission: Option<StrictCommissionModelMsg>,
1791 #[serde(default)]
1792 swap: Option<StrictSwapScheduleMsg>,
1793}
1794
1795#[derive(Serialize, Deserialize)]
1796#[serde(deny_unknown_fields)]
1797struct StrictBacktestConfigMsg {
1798 initial_balance: Option<f64>,
1799 close_on_finish: Option<bool>,
1800 fill_model: Option<String>,
1801 #[serde(default)]
1802 sizing: Option<StrictSizingPolicyMsg>,
1803 #[serde(default)]
1804 costs: BTreeMap<String, StrictInstrumentCostsMsg>,
1805}
1806
1807#[derive(Serialize, Deserialize)]
1808#[serde(tag = "type", deny_unknown_fields)]
1809enum StrictStoplossModeMsg {
1810 FromSignal,
1811 None,
1812 FixedDistance { distance: f64 },
1813 FixedPrice { price: f64 },
1814 FromSignalDistance { multiplier: f64 },
1815}
1816
1817#[derive(Serialize, Deserialize)]
1818#[serde(tag = "type", deny_unknown_fields)]
1819enum StrictTargetSourceMsg {
1820 FromSignal,
1821 StopDistanceMultiples { multiples: Vec<f64> },
1822}
1823
1824#[derive(Serialize, Deserialize)]
1825#[serde(tag = "type", deny_unknown_fields)]
1826enum StrictRuleConfigDefMsg {
1827 FixedStoploss { price: f64 },
1828 TrailingStop { distance: f64 },
1829 TakeProfit { price: f64, close_ratio: f64 },
1830 BreakevenWhen { trigger_price: f64 },
1831 BreakevenWhenOffset { trigger_price_offset: f64 },
1832 BreakevenAfterTargets { after_n: u32 },
1833 TimeExit { max_seconds: u64 },
1834}
1835
1836#[derive(Serialize, Deserialize)]
1837#[serde(deny_unknown_fields)]
1838struct StrictManagementProfileMsg {
1839 name: String,
1840 #[serde(default)]
1841 target_selection: Option<TargetSelectionMsg>,
1842 use_targets: Vec<usize>,
1843 close_ratios: Vec<f64>,
1844 #[serde(default)]
1845 target_source: Option<StrictTargetSourceMsg>,
1846 #[serde(default)]
1847 stoploss_mode: Option<StrictStoplossModeMsg>,
1848 #[serde(default)]
1849 rules: Vec<StrictRuleConfigDefMsg>,
1850 #[serde(default)]
1851 group_override: Option<String>,
1852 #[serde(default)]
1853 let_remainder_run: bool,
1854 #[serde(default)]
1855 entry_geometry: Option<EntryGeometryPolicyMsg>,
1856}
1857
1858#[derive(Serialize, Deserialize)]
1859#[serde(tag = "type", deny_unknown_fields)]
1860enum StrictPositionRefMsg {
1861 ByTradeId { trade_id: String },
1862 AllOnSymbol { symbol: String },
1863 AllInGroup { group_id: String },
1864}
1865
1866#[derive(Serialize, Deserialize)]
1867#[serde(tag = "action", deny_unknown_fields)]
1868enum StrictRawSignalMsg {
1869 Entry {
1870 ts: String,
1871 symbol: String,
1872 side: String,
1873 order_type: String,
1874 price: Option<f64>,
1875 risk: f64,
1876 stoploss: Option<f64>,
1877 #[serde(default)]
1878 targets: Vec<f64>,
1879 #[serde(default)]
1880 group: Option<String>,
1881 #[serde(default)]
1882 trade_id: Option<String>,
1883 #[serde(default)]
1884 entry_class: Option<String>,
1885 },
1886 Close {
1887 ts: String,
1888 position: StrictPositionRefMsg,
1889 },
1890 ClosePartial {
1891 ts: String,
1892 position: StrictPositionRefMsg,
1893 ratio: f64,
1894 },
1895 ModifyStoploss {
1896 ts: String,
1897 position: StrictPositionRefMsg,
1898 price: f64,
1899 },
1900 MoveStoplossToEntry {
1901 ts: String,
1902 position: StrictPositionRefMsg,
1903 },
1904 AddTarget {
1905 ts: String,
1906 position: StrictPositionRefMsg,
1907 price: f64,
1908 close_ratio: f64,
1909 },
1910 RemoveTarget {
1911 ts: String,
1912 position: StrictPositionRefMsg,
1913 price: f64,
1914 },
1915 ModifyTarget {
1916 ts: String,
1917 position: StrictPositionRefMsg,
1918 old_price: f64,
1919 new_price: f64,
1920 },
1921 AddRule {
1922 ts: String,
1923 position: StrictPositionRefMsg,
1924 rule: StrictRuleConfigDefMsg,
1925 },
1926 RemoveRule {
1927 ts: String,
1928 position: StrictPositionRefMsg,
1929 rule_name: String,
1930 },
1931 ScaleIn {
1932 ts: String,
1933 position: StrictPositionRefMsg,
1934 price: Option<f64>,
1935 size: f64,
1936 },
1937 CancelPending {
1938 ts: String,
1939 position: StrictPositionRefMsg,
1940 },
1941 CloseAllOf {
1942 ts: String,
1943 symbol: String,
1944 },
1945 CloseAll {
1946 ts: String,
1947 },
1948 CancelAllPending {
1949 ts: String,
1950 },
1951 ModifyAllStoploss {
1952 ts: String,
1953 symbol: String,
1954 price: f64,
1955 },
1956 CloseAllInGroup {
1957 ts: String,
1958 group_id: String,
1959 },
1960 ModifyAllStoplossInGroup {
1961 ts: String,
1962 group_id: String,
1963 price: f64,
1964 },
1965}
1966
1967#[derive(Serialize, Deserialize)]
1968#[serde(deny_unknown_fields)]
1969struct StrictBacktestRunSpec {
1970 symbol: String,
1971 #[serde(default)]
1972 symbols: Vec<String>,
1973 #[serde(default)]
1974 all_symbols: bool,
1975 exchange: String,
1976 data_type: String,
1977 timeframe: Option<String>,
1978 from: Option<String>,
1979 to: Option<String>,
1980 #[serde(default)]
1981 raw_signals: Vec<StrictRawSignalMsg>,
1982 profile: Option<String>,
1983 #[serde(default)]
1984 profile_def: Option<StrictManagementProfileMsg>,
1985 #[serde(default)]
1986 entry_profile_routes: Vec<StrictEntryProfileRouteMsg>,
1987 config: StrictBacktestConfigMsg,
1988}
1989
1990#[derive(Serialize, Deserialize)]
1991#[serde(untagged)]
1992enum StrictProfileRef {
1993 Named(String),
1994 Inline(StrictManagementProfileMsg),
1995}
1996
1997#[derive(Serialize, Deserialize)]
1998#[serde(deny_unknown_fields)]
1999struct StrictEntryProfileRouteMsg {
2000 entry_class: String,
2001 profile: StrictProfileRef,
2002}
2003
2004#[derive(Serialize, Deserialize)]
2005#[serde(deny_unknown_fields)]
2006struct StrictBacktestMultiRunSpec {
2007 symbol: String,
2008 #[serde(default)]
2009 symbols: Vec<String>,
2010 #[serde(default)]
2011 all_symbols: bool,
2012 exchange: String,
2013 data_type: String,
2014 timeframe: Option<String>,
2015 from: Option<String>,
2016 to: Option<String>,
2017 #[serde(default)]
2018 raw_signals: Vec<StrictRawSignalMsg>,
2019 profiles: Vec<StrictProfileRef>,
2020 #[serde(default)]
2021 entry_profile_routes: Vec<StrictEntryProfileRouteMsg>,
2022 config: StrictBacktestConfigMsg,
2023}
2024
2025#[derive(Deserialize)]
2026#[serde(deny_unknown_fields)]
2027struct StrictRunBacktestRequest {
2028 request: StrictBacktestRunSpec,
2029 #[serde(default)]
2030 future: FutureQuoteConfigMsg,
2031 #[serde(default)]
2032 evaluation: ProviderEvaluationOptionsMsg,
2033 #[serde(default)]
2034 result_delivery: ResultDeliveryMsg,
2035}
2036
2037#[derive(Deserialize)]
2038#[serde(deny_unknown_fields)]
2039struct StrictSubmitBacktestRequest {
2040 request: RunBacktestRequest,
2041}
2042
2043#[derive(Deserialize)]
2044#[serde(deny_unknown_fields)]
2045struct StrictRunBacktestMultiRequest {
2046 request: StrictBacktestMultiRunSpec,
2047 #[serde(default)]
2048 future: FutureQuoteConfigMsg,
2049 #[serde(default)]
2050 evaluation: ProviderEvaluationOptionsMsg,
2051 #[serde(default)]
2052 result_delivery: ResultDeliveryMsg,
2053}
2054
2055fn strict_into_wire<T>(value: impl Serialize) -> Result<T, serde_json::Error>
2056where
2057 T: DeserializeOwned,
2058{
2059 serde_json::from_value(serde_json::to_value(value)?)
2060}
2061
2062impl<'de> Deserialize<'de> for RunBacktestRequest {
2063 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2064 where
2065 D: serde::Deserializer<'de>,
2066 {
2067 let strict = StrictRunBacktestRequest::deserialize(deserializer)?;
2068 Ok(Self {
2069 request: strict_into_wire(strict.request).map_err(serde::de::Error::custom)?,
2070 future: strict.future,
2071 evaluation: strict.evaluation,
2072 result_delivery: strict.result_delivery,
2073 })
2074 }
2075}
2076
2077impl<'de> Deserialize<'de> for SubmitBacktestRequest {
2078 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2079 where
2080 D: serde::Deserializer<'de>,
2081 {
2082 let strict = StrictSubmitBacktestRequest::deserialize(deserializer)?;
2083 Ok(Self {
2084 request: strict.request,
2085 })
2086 }
2087}
2088
2089impl<'de> Deserialize<'de> for RunBacktestMultiRequest {
2090 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2091 where
2092 D: serde::Deserializer<'de>,
2093 {
2094 let strict = StrictRunBacktestMultiRequest::deserialize(deserializer)?;
2095 Ok(Self {
2096 request: strict_into_wire(strict.request).map_err(serde::de::Error::custom)?,
2097 future: strict.future,
2098 evaluation: strict.evaluation,
2099 result_delivery: strict.result_delivery,
2100 })
2101 }
2102}
2103
2104#[derive(Serialize, Deserialize)]
2105#[serde(deny_unknown_fields)]
2106struct StrictConfiguredStrategyRunSpec {
2107 symbol: String,
2108 exchange: String,
2109 data_type: String,
2110 #[serde(default)]
2111 timeframe: Option<String>,
2112 #[serde(default)]
2113 from: Option<String>,
2114 #[serde(default)]
2115 to: Option<String>,
2116 strategy: ConfiguredStrategyRunMsg,
2117 #[serde(default)]
2118 profile: Option<String>,
2119 #[serde(default)]
2120 profile_def: Option<StrictManagementProfileMsg>,
2121 #[serde(default)]
2122 entry_profile_routes: Vec<StrictEntryProfileRouteMsg>,
2123 config: StrictBacktestConfigMsg,
2124}
2125
2126#[derive(Deserialize)]
2127#[serde(deny_unknown_fields)]
2128struct StrictRunConfiguredStrategyRequest {
2129 request: StrictConfiguredStrategyRunSpec,
2130 #[serde(default)]
2131 future: FutureQuoteConfigMsg,
2132 #[serde(default)]
2133 evaluation: ProviderEvaluationOptionsMsg,
2134 #[serde(default)]
2135 result_delivery: ResultDeliveryMsg,
2136}
2137
2138#[derive(Deserialize)]
2139#[serde(deny_unknown_fields)]
2140struct StrictSubmitConfiguredStrategyRequest {
2141 request: RunConfiguredStrategyRequest,
2142}
2143
2144#[derive(Serialize, Deserialize)]
2145#[serde(deny_unknown_fields)]
2146struct StrictSearchExecutionVariantMsg {
2147 id: String,
2148 config: StrictBacktestConfigMsg,
2149 #[serde(default)]
2150 future: FutureQuoteConfigMsg,
2151 #[serde(default)]
2152 profile: Option<String>,
2153}
2154
2155#[derive(Serialize, Deserialize)]
2156#[serde(deny_unknown_fields)]
2157struct StrictSearchPortfolioCandidateMsg {
2158 id: String,
2159 #[serde(default)]
2160 instances: Vec<StrictPortfolioInstanceMsg>,
2161 #[serde(default)]
2162 direct_instances: Vec<SearchDirectPortfolioInstanceMsg>,
2163 #[serde(default)]
2164 policies: Option<serde_json::Value>,
2165 #[serde(default)]
2166 groups: Option<serde_json::Value>,
2167}
2168
2169#[derive(Serialize, Deserialize)]
2170#[serde(deny_unknown_fields)]
2171struct StrictSearchRunSpec {
2172 template: serde_json::Value,
2173 space: serde_json::Value,
2174 symbols: Vec<String>,
2175 exchange: String,
2176 data_type: String,
2177 #[serde(default)]
2178 timeframe: Option<String>,
2179 windows: SearchWindowsMsg,
2180 config: StrictBacktestConfigMsg,
2181 #[serde(default)]
2182 profile: Option<String>,
2183 #[serde(default)]
2184 entry_profile_routes: Vec<StrictEntryProfileRouteMsg>,
2185 #[serde(default)]
2186 workers: Option<usize>,
2187 #[serde(default)]
2188 decision_latency_ms: u64,
2189 #[serde(default)]
2190 structural: Option<serde_json::Value>,
2191 #[serde(default)]
2192 resource_limits: Option<serde_json::Value>,
2193 #[serde(default)]
2194 variants: Vec<StrictSearchExecutionVariantMsg>,
2195 #[serde(default)]
2196 direct_factory: Option<SearchDirectFactoryMsg>,
2197 #[serde(default)]
2198 resume_checkpoint: Option<serde_json::Value>,
2199 #[serde(default)]
2200 selected_rerun: Option<SearchSelectedRerunMsg>,
2201 #[serde(default)]
2202 portfolio_candidates: Vec<StrictSearchPortfolioCandidateMsg>,
2203 #[serde(default)]
2204 series_descriptors: Vec<serde_json::Value>,
2205}
2206
2207#[derive(Deserialize)]
2208#[serde(deny_unknown_fields)]
2209struct StrictSubmitSearchRequest {
2210 request: StrictSearchRunSpec,
2211 #[serde(default)]
2212 future: FutureQuoteConfigMsg,
2213 #[serde(default)]
2214 evaluation: ProviderEvaluationOptionsMsg,
2215}
2216
2217#[derive(Serialize, Deserialize)]
2218#[serde(deny_unknown_fields)]
2219struct StrictPortfolioInstanceMsg {
2220 symbol: String,
2221 strategy: ConfiguredStrategyRunMsg,
2222 #[serde(default)]
2223 profile: Option<String>,
2224 #[serde(default)]
2225 profile_def: Option<StrictManagementProfileMsg>,
2226 #[serde(default)]
2227 entry_profile_routes: Vec<StrictEntryProfileRouteMsg>,
2228}
2229
2230#[derive(Serialize, Deserialize)]
2231#[serde(deny_unknown_fields)]
2232struct StrictPortfolioRunSpec {
2233 instances: Vec<StrictPortfolioInstanceMsg>,
2234 exchange: String,
2235 data_type: String,
2236 #[serde(default)]
2237 timeframe: Option<String>,
2238 #[serde(default)]
2239 from: Option<String>,
2240 #[serde(default)]
2241 to: Option<String>,
2242 config: StrictBacktestConfigMsg,
2243 #[serde(default)]
2244 policies: Option<serde_json::Value>,
2245 #[serde(default)]
2246 groups: Option<serde_json::Value>,
2247}
2248
2249#[derive(Deserialize)]
2250#[serde(deny_unknown_fields)]
2251struct StrictRunPortfolioRequest {
2252 request: StrictPortfolioRunSpec,
2253 #[serde(default)]
2254 future: FutureQuoteConfigMsg,
2255 #[serde(default)]
2256 evaluation: ProviderEvaluationOptionsMsg,
2257 #[serde(default)]
2258 result_delivery: ResultDeliveryMsg,
2259}
2260
2261#[derive(Deserialize)]
2262#[serde(deny_unknown_fields)]
2263struct StrictSubmitPortfolioRequest {
2264 request: RunPortfolioRequest,
2265}
2266
2267impl<'de> Deserialize<'de> for RunPortfolioRequest {
2268 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2269 where
2270 D: serde::Deserializer<'de>,
2271 {
2272 let strict = StrictRunPortfolioRequest::deserialize(deserializer)?;
2273 Ok(Self {
2274 request: strict_into_wire(strict.request).map_err(serde::de::Error::custom)?,
2275 future: strict.future,
2276 evaluation: strict.evaluation,
2277 result_delivery: strict.result_delivery,
2278 })
2279 }
2280}
2281
2282impl<'de> Deserialize<'de> for SubmitPortfolioRequest {
2283 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2284 where
2285 D: serde::Deserializer<'de>,
2286 {
2287 let strict = StrictSubmitPortfolioRequest::deserialize(deserializer)?;
2288 Ok(Self {
2289 request: strict.request,
2290 })
2291 }
2292}
2293
2294impl<'de> Deserialize<'de> for RunConfiguredStrategyRequest {
2295 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2296 where
2297 D: serde::Deserializer<'de>,
2298 {
2299 let strict = StrictRunConfiguredStrategyRequest::deserialize(deserializer)?;
2300 Ok(Self {
2301 request: strict_into_wire(strict.request).map_err(serde::de::Error::custom)?,
2302 future: strict.future,
2303 evaluation: strict.evaluation,
2304 result_delivery: strict.result_delivery,
2305 })
2306 }
2307}
2308
2309impl<'de> Deserialize<'de> for SubmitConfiguredStrategyRequest {
2310 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2311 where
2312 D: serde::Deserializer<'de>,
2313 {
2314 let strict = StrictSubmitConfiguredStrategyRequest::deserialize(deserializer)?;
2315 Ok(Self {
2316 request: strict.request,
2317 })
2318 }
2319}
2320
2321impl<'de> Deserialize<'de> for SubmitSearchRequest {
2322 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2323 where
2324 D: serde::Deserializer<'de>,
2325 {
2326 let strict = StrictSubmitSearchRequest::deserialize(deserializer)?;
2327 Ok(Self {
2328 request: strict_into_wire(strict.request).map_err(serde::de::Error::custom)?,
2329 future: strict.future,
2330 evaluation: strict.evaluation,
2331 })
2332 }
2333}
2334
2335#[derive(Debug, Clone, Serialize, Deserialize)]
2339pub struct AddProfileRequest {
2340 pub profile: ManagementProfileMsg,
2341 #[serde(default)]
2342 pub overwrite: bool,
2343}
2344
2345#[derive(Debug, Clone, Serialize, Deserialize)]
2347pub struct AddProfileResponse {
2348 pub success: bool,
2349 pub error: Option<String>,
2350 pub profile_count: usize,
2351}
2352
2353#[derive(Debug, Clone, Serialize, Deserialize)]
2355pub struct RemoveProfileRequest {
2356 pub name: String,
2357}
2358
2359#[derive(Debug, Clone, Serialize, Deserialize)]
2361pub struct RemoveProfileResponse {
2362 pub success: bool,
2363 pub error: Option<String>,
2364 pub profile_count: usize,
2365}
2366
2367#[derive(Debug, Clone, Serialize, Deserialize)]
2369pub struct ReloadProfilesResponse {
2370 pub success: bool,
2371 pub error: Option<String>,
2372 pub profile_count: usize,
2373 pub loaded_from: String,
2374}
2375
2376#[cfg(test)]
2377mod tests {
2378 use super::*;
2379 use serde_json::json;
2380
2381 fn entry_fields() -> serde_json::Value {
2382 json!({
2383 "action": "Entry",
2384 "ts": "2026-01-02T10:00:00",
2385 "symbol": "eurusd",
2386 "side": "Buy",
2387 "order_type": "Market",
2388 "price": null,
2389 "risk": 1.5,
2390 "stoploss": 1.08
2391 })
2392 }
2393
2394 #[test]
2395 fn entry_requires_risk_and_rejects_size() {
2396 let entry: RawSignalMsg = serde_json::from_value(entry_fields()).unwrap();
2397 assert!(matches!(entry, RawSignalMsg::Entry { risk, .. } if risk == 1.5));
2398
2399 let mut with_size = entry_fields();
2400 with_size.as_object_mut().unwrap().remove("risk");
2401 with_size["size"] = json!(0.1);
2402 let error = serde_json::from_value::<RawSignalMsg>(with_size).unwrap_err();
2403 assert!(error.to_string().contains("unknown field `size`"));
2404
2405 let mut missing_risk = entry_fields();
2406 missing_risk.as_object_mut().unwrap().remove("risk");
2407 let error = serde_json::from_value::<RawSignalMsg>(missing_risk).unwrap_err();
2408 assert!(error.to_string().contains("missing field `risk`"));
2409 }
2410
2411 #[test]
2412 fn scale_in_keeps_size_field() {
2413 let signal: RawSignalMsg = serde_json::from_value(json!({
2414 "action": "ScaleIn",
2415 "ts": "2026-01-02T10:01:00",
2416 "position": { "type": "ByTradeId", "trade_id": "trade-1" },
2417 "price": null,
2418 "size": 0.25
2419 }))
2420 .unwrap();
2421 assert!(matches!(signal, RawSignalMsg::ScaleIn { size, .. } if size == 0.25));
2422 }
2423
2424 #[test]
2425 fn current_sizing_and_future_fields_are_strict() {
2426 let sizing: SizingPolicyMsg = serde_json::from_value(json!({
2427 "type": "FixedRiskAmount",
2428 "amount": 100.0
2429 }))
2430 .unwrap();
2431 assert!(matches!(
2432 sizing,
2433 SizingPolicyMsg::FixedRiskAmount { amount } if amount == 100.0
2434 ));
2435 assert!(
2436 serde_json::from_value::<SizingPolicyMsg>(json!({
2437 "type": "FixedLot",
2438 "qty": "all=0.01"
2439 }))
2440 .is_err()
2441 );
2442
2443 let future: FutureQuoteConfigMsg = serde_json::from_value(json!({
2444 "account_currency": "USD",
2445 "conversion_stale_after_ms": 15_000
2446 }))
2447 .unwrap();
2448 assert_eq!(future.account_currency, "USD");
2449 assert_eq!(future.conversion_stale_after_ms, 15_000);
2450 assert_eq!(
2451 future.mtm_output,
2452 MtmOutputPolicyMsg::Bounded { max_points: 4_096 }
2453 );
2454 assert_eq!(
2455 future.market_entry_sizing_basis,
2456 MarketEntrySizingBasisMsg::FillPrice
2457 );
2458 assert!(
2459 serde_json::from_value::<FutureQuoteConfigMsg>(json!({
2460 "account_currency": "USD",
2461 "conversion_rates": {}
2462 }))
2463 .is_err()
2464 );
2465 }
2466
2467 #[test]
2468 fn market_entry_sizing_basis_uses_strict_snake_case_wire_values() {
2469 for (value, expected) in [
2470 (json!("fill_price"), MarketEntrySizingBasisMsg::FillPrice),
2471 (
2472 json!("signal_entry_price"),
2473 MarketEntrySizingBasisMsg::SignalEntryPrice,
2474 ),
2475 ] {
2476 let basis: MarketEntrySizingBasisMsg = serde_json::from_value(value.clone()).unwrap();
2477 assert_eq!(basis, expected);
2478 assert_eq!(serde_json::to_value(basis).unwrap(), value);
2479 }
2480
2481 assert!(serde_json::from_value::<MarketEntrySizingBasisMsg>(json!("mark_price")).is_err());
2482 assert!(
2483 serde_json::from_value::<FutureQuoteConfigMsg>(json!({
2484 "market_entry_sizing_basis": "mark_price"
2485 }))
2486 .is_err()
2487 );
2488 }
2489
2490 #[test]
2491 fn mtm_output_policy_uses_strict_snake_case_wire_values() {
2492 for (value, expected) in [
2493 (json!("none"), MtmOutputPolicyMsg::None),
2494 (
2495 json!({ "bounded": { "max_points": 512 } }),
2496 MtmOutputPolicyMsg::Bounded { max_points: 512 },
2497 ),
2498 (json!("full"), MtmOutputPolicyMsg::Full),
2499 ] {
2500 let policy: MtmOutputPolicyMsg = serde_json::from_value(value.clone()).unwrap();
2501 assert_eq!(policy, expected);
2502 assert_eq!(serde_json::to_value(policy).unwrap(), value);
2503 }
2504
2505 assert!(
2506 serde_json::from_value::<MtmOutputPolicyMsg>(json!({
2507 "bounded": { "max_points": 512, "unexpected": true }
2508 }))
2509 .is_err()
2510 );
2511 }
2512
2513 #[test]
2514 fn future_result_defaults_missing_mtm_output_summary() {
2515 let result: FutureBacktestResultMsg = serde_json::from_value(json!({
2516 "format_version": 1,
2517 "execution_metadata": null,
2518 "recorded_fills": null,
2519 "action_dispositions": null,
2520 "close_events": null,
2521 "completed_positions": null,
2522 "open_positions": null,
2523 "pending_orders": null,
2524 "pending_order_lifecycle": [],
2525 "mtm_equity_curve": null,
2526 "mtm_max_drawdown": null,
2527 "mtm_max_drawdown_pct": null,
2528 "provider_evaluation": null
2529 }))
2530 .unwrap();
2531
2532 assert_eq!(result.mtm_output_summary, MtmOutputSummaryMsg::default());
2533 assert_eq!(result.mtm_output_summary.policy, MtmOutputPolicyMsg::Full);
2534
2535 let summary: MtmOutputSummaryMsg = serde_json::from_value(json!({
2536 "policy": "full",
2537 "observed_points": 4,
2538 "retained_points": 4,
2539 "omitted_points": 0,
2540 "future_addition": true
2541 }))
2542 .unwrap();
2543 assert_eq!(summary.policy, MtmOutputPolicyMsg::Full);
2544 }
2545
2546 fn minimal_request_json() -> serde_json::Value {
2547 json!({
2548 "request": {
2549 "symbol": "EURUSD",
2550 "exchange": "fixture",
2551 "data_type": "tick",
2552 "timeframe": null,
2553 "from": null,
2554 "to": null,
2555 "raw_signals": [],
2556 "profile": null,
2557 "config": {
2558 "initial_balance": null,
2559 "close_on_finish": null,
2560 "fill_model": null
2561 }
2562 },
2563 "future": {
2564 "account_currency": "USD"
2565 }
2566 })
2567 }
2568
2569 #[test]
2570 fn omitted_delivery_and_mtm_fields_use_current_defaults() {
2571 let request: RunBacktestRequest = serde_json::from_value(minimal_request_json()).unwrap();
2572 assert_eq!(request.result_delivery, ResultDeliveryMsg::Auto);
2573 assert_eq!(
2574 request.future.mtm_output,
2575 MtmOutputPolicyMsg::Bounded { max_points: 4_096 }
2576 );
2577 assert_eq!(
2578 request.future.market_entry_sizing_basis,
2579 MarketEntrySizingBasisMsg::FillPrice
2580 );
2581
2582 let submitted: SubmitBacktestRequest = serde_json::from_value(json!({
2583 "request": minimal_request_json()
2584 }))
2585 .unwrap();
2586 assert_eq!(submitted.request.result_delivery, ResultDeliveryMsg::Auto);
2587 assert_eq!(
2588 submitted.request.future.mtm_output,
2589 MtmOutputPolicyMsg::Bounded { max_points: 4_096 }
2590 );
2591
2592 let mut explicit = minimal_request_json();
2593 explicit["result_delivery"] = json!("auto");
2594 explicit["future"]["mtm_output"] = json!({
2595 "bounded": { "max_points": 4_096 }
2596 });
2597 let explicit: RunBacktestRequest = serde_json::from_value(explicit).unwrap();
2598 assert_eq!(explicit.result_delivery, ResultDeliveryMsg::Auto);
2599 assert_eq!(
2600 explicit.future.mtm_output,
2601 MtmOutputPolicyMsg::Bounded { max_points: 4_096 }
2602 );
2603
2604 let mut multi_request = minimal_request_json()["request"].clone();
2605 let fields = multi_request.as_object_mut().unwrap();
2606 fields.remove("profile");
2607 fields.insert("profiles".into(), json!([]));
2608 let multi: RunBacktestMultiRequest = serde_json::from_value(json!({
2609 "request": multi_request,
2610 "future": { "account_currency": "USD" }
2611 }))
2612 .unwrap();
2613 assert_eq!(multi.result_delivery, ResultDeliveryMsg::Auto);
2614 assert_eq!(
2615 multi.future.mtm_output,
2616 MtmOutputPolicyMsg::Bounded { max_points: 4_096 }
2617 );
2618 }
2619
2620 #[test]
2621 fn older_async_result_responses_default_artifact_consumption_state() {
2622 let response: GetBacktestResultResponse = serde_json::from_value(json!({
2623 "success": true,
2624 "job_id": "job-old",
2625 "result": null,
2626 "error": null,
2627 "inline_complete": true
2628 }))
2629 .unwrap();
2630 assert!(!response.artifact_consumed);
2631 }
2632}