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, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
516#[serde(rename_all = "snake_case")]
517pub enum UnavailableInstrumentPolicyMsg {
518 #[default]
519 Skip,
520 Error,
521}
522
523#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
525#[serde(rename_all = "snake_case")]
526pub enum InstrumentExclusionReasonMsg {
527 NotSelected,
528 UnknownInstrument,
529 UnsupportedEconomics,
530 NoMarketData,
531 NoConversionData,
532 AmbiguousMapping,
533}
534
535#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
537#[serde(deny_unknown_fields)]
538pub struct ExcludedSignalRefMsg {
539 pub input_index: usize,
540 pub ts: String,
541 pub action: String,
542 pub trade_id: Option<String>,
543}
544
545#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
547#[serde(deny_unknown_fields)]
548pub struct ExcludedInstrumentMsg {
549 pub symbol: String,
550 pub reason: InstrumentExclusionReasonMsg,
551 pub details: String,
552 pub skipped_entries: usize,
553 pub skipped_management: usize,
554 pub signal_references: Vec<ExcludedSignalRefMsg>,
555 pub omitted_signal_references: usize,
556}
557
558#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
560#[serde(deny_unknown_fields)]
561pub struct ReplayAdmissionReportMsg {
562 #[serde(default)]
563 pub omitted_instruments: usize,
564 #[serde(default)]
565 pub omitted_entries: usize,
566 #[serde(default)]
567 pub omitted_management: usize,
568 pub policy: UnavailableInstrumentPolicyMsg,
569 pub input_signals: usize,
570 pub retained_signals: usize,
571 pub excluded_instruments: Vec<ExcludedInstrumentMsg>,
572}
573
574#[derive(Debug, Clone, Serialize, Deserialize)]
576pub struct BacktestRunSpec {
577 #[serde(default)]
578 pub on_unavailable: UnavailableInstrumentPolicyMsg,
579 pub symbol: String,
580 #[serde(default)]
581 pub symbols: Vec<String>,
582 #[serde(default)]
583 pub all_symbols: bool,
584 pub exchange: String,
585 pub data_type: String,
586 pub timeframe: Option<String>,
587 pub from: Option<String>,
588 pub to: Option<String>,
589 #[serde(default)]
591 pub raw_signals: Vec<RawSignalMsg>,
592 pub profile: Option<String>,
593 #[serde(default)]
594 pub profile_def: Option<ManagementProfileMsg>,
595 #[serde(default, skip_serializing_if = "Vec::is_empty")]
596 pub entry_profile_routes: Vec<EntryProfileRouteMsg>,
597 pub config: BacktestConfigMsg,
598}
599
600#[derive(Debug, Clone, Serialize, Deserialize)]
602pub struct RunBacktestResponse {
603 pub success: bool,
604 pub error: Option<String>,
605 pub result: Option<BacktestResultMsg>,
607 pub elapsed_ms: u64,
608 #[serde(default)]
609 pub artifact: Option<ResultArtifactRefMsg>,
610 #[serde(default = "default_true")]
611 pub inline_complete: bool,
612}
613
614#[derive(Debug, Clone, Serialize)]
616pub struct RunBacktestRequest {
617 pub request: BacktestRunSpec,
618 #[serde(default)]
619 pub future: FutureQuoteConfigMsg,
620 #[serde(default)]
621 pub evaluation: ProviderEvaluationOptionsMsg,
622 #[serde(default)]
623 pub result_delivery: ResultDeliveryMsg,
624}
625
626#[derive(Debug, Clone, Serialize)]
628pub struct SubmitBacktestRequest {
629 pub request: RunBacktestRequest,
630}
631
632#[derive(Debug, Clone, Serialize, Deserialize)]
636pub struct BacktestMultiRunSpec {
637 #[serde(default)]
638 pub on_unavailable: UnavailableInstrumentPolicyMsg,
639 pub symbol: String,
640 #[serde(default)]
641 pub symbols: Vec<String>,
642 #[serde(default)]
643 pub all_symbols: bool,
644 pub exchange: String,
645 pub data_type: String,
646 pub timeframe: Option<String>,
647 pub from: Option<String>,
648 pub to: Option<String>,
649 #[serde(default)]
651 pub raw_signals: Vec<RawSignalMsg>,
652 pub profiles: Vec<ProfileRef>,
653 #[serde(default, skip_serializing_if = "Vec::is_empty")]
654 pub entry_profile_routes: Vec<EntryProfileRouteMsg>,
655 pub config: BacktestConfigMsg,
656}
657
658#[derive(Debug, Clone, Serialize, Deserialize)]
660pub struct ProfileResult {
661 pub profile: String,
662 pub success: bool,
663 pub error: Option<String>,
664 pub result: Option<BacktestResultMsg>,
665}
666
667#[derive(Debug, Clone, Serialize, Deserialize)]
669pub struct RunBacktestMultiResponse {
670 #[serde(default = "default_true")]
672 pub success: bool,
673 #[serde(default)]
674 pub error: Option<String>,
675 pub results: Vec<ProfileResult>,
677 pub elapsed_ms: u64,
678 #[serde(default)]
679 pub artifact: Option<ResultArtifactRefMsg>,
680 #[serde(default = "default_true")]
681 pub inline_complete: bool,
682}
683
684#[derive(Debug, Clone, Serialize)]
685pub struct RunBacktestMultiRequest {
686 pub request: BacktestMultiRunSpec,
687 #[serde(default)]
688 pub future: FutureQuoteConfigMsg,
689 #[serde(default)]
690 pub evaluation: ProviderEvaluationOptionsMsg,
691 #[serde(default)]
692 pub result_delivery: ResultDeliveryMsg,
693}
694
695#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
699#[serde(rename_all = "snake_case")]
700pub enum PriceBasisMsg {
701 Bid,
702 Ask,
703 Mid,
704}
705
706#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
708#[serde(deny_unknown_fields)]
709pub struct SourceBindingMsg {
710 pub source: String,
711 pub timeframe_seconds: u32,
712 pub price_basis: PriceBasisMsg,
713 #[serde(default)]
714 pub alignment_offset_seconds: i32,
715}
716
717#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
718#[serde(deny_unknown_fields)]
719pub struct HistoricalInputsMsg {
720 pub calendars: BTreeMap<String, TradingCalendarMsg>,
721 pub inputs: Vec<CalendarInputMsg>,
722 #[serde(default)]
723 pub limits: Option<CalendarLimitsMsg>,
724}
725
726#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
727#[serde(deny_unknown_fields)]
728pub struct TradingCalendarMsg {
729 pub id: String,
730 pub timezone: String,
731 #[serde(default = "default_day_boundary")]
732 pub day_boundary: String,
733 #[serde(default)]
734 pub sessions: SessionScheduleMsg,
735 #[serde(default)]
736 pub market: MarketScheduleMsg,
737}
738
739fn default_day_boundary() -> String {
740 "00:00:00".into()
741}
742
743#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
744#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
745pub enum SessionScheduleMsg {
746 #[default]
747 FullDay,
748 Custom {
749 items: Vec<NamedSessionMsg>,
750 },
751}
752
753#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
754#[serde(deny_unknown_fields)]
755pub struct NamedSessionMsg {
756 pub id: String,
757 #[serde(default, skip_serializing_if = "Option::is_none")]
758 pub timezone: Option<String>,
759 pub span: SessionSpanMsg,
760 #[serde(default)]
761 pub weekdays: Vec<u8>,
762}
763
764#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
765#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
766pub enum SessionSpanMsg {
767 FullDay,
768 Timed {
769 start: String,
770 end: String,
771 end_day_offset: u8,
772 },
773}
774
775#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
776#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
777pub enum MarketScheduleMsg {
778 #[default]
779 Unspecified,
780 Continuous,
781 Weekly {
782 intervals: Vec<WeeklyMarketIntervalMsg>,
783 #[serde(default)]
784 exceptions: BTreeMap<String, Vec<LocalMarketIntervalMsg>>,
785 },
786}
787
788#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
789#[serde(deny_unknown_fields)]
790pub struct WeeklyMarketIntervalMsg {
791 pub weekday: u8,
792 pub start: String,
793 pub end: String,
794 pub end_day_offset: u8,
795}
796
797#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
798#[serde(deny_unknown_fields)]
799pub struct LocalMarketIntervalMsg {
800 pub start: String,
801 pub end: String,
802 pub end_day_offset: u8,
803}
804
805#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
806#[serde(deny_unknown_fields)]
807pub struct CalendarInputMsg {
808 pub name: String,
809 pub source: String,
810 pub calendar: String,
811 pub feature: CalendarFeatureMsg,
812 #[serde(default, skip_serializing_if = "Option::is_none")]
813 pub session: Option<String>,
814 #[serde(default)]
815 pub time_basis: CalendarTimeBasisMsg,
816 #[serde(default = "default_opening_range_minutes")]
817 pub opening_range_minutes: u32,
818 pub child_seconds: u64,
819 #[serde(default)]
820 pub alignment_offset_seconds: i64,
821 pub maximum_history: usize,
822}
823
824fn default_opening_range_minutes() -> u32 {
825 5
826}
827
828#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
829#[serde(rename_all = "snake_case")]
830pub enum CalendarFeatureMsg {
831 LocalSecondOfDay,
832 SessionElapsedSeconds,
833 SessionMembership,
834 PreviousSessionHigh,
835 PreviousSessionLow,
836 PreviousDayHigh,
837 PreviousDayLow,
838 PreviousWeekHigh,
839 PreviousWeekLow,
840 OpeningRangeHighSoFar,
841 OpeningRangeLowSoFar,
842 OpeningRangeHighFinal,
843 OpeningRangeLowFinal,
844 PastSameSlotRangeRatio,
845 PastSameSlotCount,
846}
847
848#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
849#[serde(rename_all = "snake_case")]
850pub enum CalendarTimeBasisMsg {
851 #[default]
852 SourceOpen,
853 DecisionTime,
854}
855
856#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
857#[serde(deny_unknown_fields)]
858pub struct CalendarLimitsMsg {
859 pub max_sessions: usize,
860 pub max_market_intervals: usize,
861 pub max_exceptions: usize,
862 pub max_history_occurrences: usize,
863 pub max_resolved_children: usize,
864 pub max_owned_bytes: usize,
865}
866
867#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
869#[serde(deny_unknown_fields)]
870pub struct ConfiguredStrategyRunMsg {
871 pub document: serde_json::Value,
873 pub sources: Vec<SourceBindingMsg>,
874 #[serde(default, skip_serializing_if = "Option::is_none")]
876 pub instance_id: Option<String>,
877 #[serde(default)]
879 pub decision_latency_ms: u64,
880 #[serde(default, skip_serializing_if = "Option::is_none")]
881 pub historical_inputs: Option<HistoricalInputsMsg>,
882}
883
884#[derive(Debug, Clone, Serialize, Deserialize)]
886#[serde(deny_unknown_fields)]
887pub struct ConfiguredStrategyRunSpec {
888 pub symbol: String,
889 pub exchange: String,
890 pub data_type: String,
892 #[serde(default)]
893 pub timeframe: Option<String>,
894 #[serde(default)]
895 pub from: Option<String>,
896 #[serde(default)]
897 pub to: Option<String>,
898 pub strategy: ConfiguredStrategyRunMsg,
899 #[serde(default)]
900 pub profile: Option<String>,
901 #[serde(default)]
902 pub profile_def: Option<ManagementProfileMsg>,
903 #[serde(default, skip_serializing_if = "Vec::is_empty")]
904 pub entry_profile_routes: Vec<EntryProfileRouteMsg>,
905 pub config: BacktestConfigMsg,
906}
907
908#[derive(Debug, Clone, Serialize)]
910pub struct RunConfiguredStrategyRequest {
911 pub request: ConfiguredStrategyRunSpec,
912 #[serde(default)]
913 pub future: FutureQuoteConfigMsg,
914 #[serde(default)]
915 pub evaluation: ProviderEvaluationOptionsMsg,
916 #[serde(default)]
917 pub result_delivery: ResultDeliveryMsg,
918}
919
920#[derive(Debug, Clone, Serialize)]
922pub struct SubmitConfiguredStrategyRequest {
923 pub request: RunConfiguredStrategyRequest,
924}
925
926#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
928pub struct ConfiguredStrategyOutputMsg {
929 pub document: serde_json::Value,
931 pub sources: Vec<SourceBindingMsg>,
932 pub requirements: serde_json::Value,
934 pub data_mode: String,
936 pub decisions: serde_json::Value,
938 pub research: serde_json::Value,
940 #[serde(default, skip_serializing_if = "Option::is_none")]
941 pub historical_inputs: Option<serde_json::Value>,
942}
943
944#[derive(Debug, Clone, Serialize, Deserialize)]
948#[serde(deny_unknown_fields)]
949pub struct PortfolioInstanceMsg {
950 pub symbol: String,
951 pub strategy: ConfiguredStrategyRunMsg,
953 #[serde(default)]
954 pub profile: Option<String>,
955 #[serde(default)]
956 pub profile_def: Option<ManagementProfileMsg>,
957 #[serde(default, skip_serializing_if = "Vec::is_empty")]
958 pub entry_profile_routes: Vec<EntryProfileRouteMsg>,
959}
960
961#[derive(Debug, Clone, Serialize, Deserialize)]
963#[serde(deny_unknown_fields)]
964pub struct PortfolioRunSpec {
965 pub instances: Vec<PortfolioInstanceMsg>,
966 pub exchange: String,
967 pub data_type: String,
969 #[serde(default)]
970 pub timeframe: Option<String>,
971 #[serde(default)]
972 pub from: Option<String>,
973 #[serde(default)]
974 pub to: Option<String>,
975 pub config: BacktestConfigMsg,
976 #[serde(default, skip_serializing_if = "Option::is_none")]
978 pub policies: Option<serde_json::Value>,
979 #[serde(default, skip_serializing_if = "Option::is_none")]
981 pub groups: Option<serde_json::Value>,
982}
983
984#[derive(Debug, Clone, Serialize)]
986pub struct RunPortfolioRequest {
987 pub request: PortfolioRunSpec,
988 #[serde(default)]
989 pub future: FutureQuoteConfigMsg,
990 #[serde(default)]
991 pub evaluation: ProviderEvaluationOptionsMsg,
992 #[serde(default)]
993 pub result_delivery: ResultDeliveryMsg,
994}
995
996#[derive(Debug, Clone, Serialize)]
998pub struct SubmitPortfolioRequest {
999 pub request: RunPortfolioRequest,
1000}
1001
1002#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1004pub struct PortfolioInstanceOutputMsg {
1005 pub instance_id: String,
1006 pub symbol: String,
1007 pub strategy: ConfiguredStrategyOutputMsg,
1008}
1009
1010#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1012pub struct PortfolioOutputMsg {
1013 pub instances: Vec<PortfolioInstanceOutputMsg>,
1014 #[serde(default, skip_serializing_if = "Option::is_none")]
1016 pub policies: Option<serde_json::Value>,
1017 #[serde(default, skip_serializing_if = "Option::is_none")]
1019 pub supervisor: Option<serde_json::Value>,
1020}
1021
1022#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1026#[serde(deny_unknown_fields)]
1027pub struct SearchWindowMsg {
1028 pub label: String,
1029 pub from: String,
1030 pub to: String,
1031}
1032
1033#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1035#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1036pub enum SearchWindowsMsg {
1037 Fixed {
1038 in_sample: SearchWindowMsg,
1039 out_of_sample: SearchWindowMsg,
1040 },
1041 RollingWalkForward {
1042 start: String,
1043 end: String,
1044 train_seconds: i64,
1045 test_seconds: i64,
1046 step_seconds: i64,
1047 },
1048}
1049
1050#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1051#[serde(rename_all = "snake_case")]
1052pub enum SearchEvaluationRoleMsg {
1053 Search,
1054 Validation,
1055 Final,
1056}
1057
1058#[derive(Debug, Clone, Serialize, Deserialize)]
1059#[serde(deny_unknown_fields)]
1060pub struct SearchSelectedRerunMsg {
1061 pub experiment_recipe: serde_json::Value,
1062 pub candidate_recipe: serde_json::Value,
1063 pub run_recipe: serde_json::Value,
1064 pub role: SearchEvaluationRoleMsg,
1065 pub caller_revision: String,
1066 #[serde(default)]
1067 pub release_final: bool,
1068 #[serde(default)]
1069 pub future_horizon_millis: Option<u64>,
1070 #[serde(default)]
1071 pub embargo_millis: Option<u64>,
1072}
1073
1074#[derive(Debug, Clone, Serialize, Deserialize)]
1075#[serde(deny_unknown_fields)]
1076pub struct SearchDirectPortfolioInstanceMsg {
1077 pub instance_id: String,
1078 pub symbol: String,
1079 pub factory: SearchDirectFactoryMsg,
1080 #[serde(default)]
1081 pub profile: Option<String>,
1082}
1083
1084#[derive(Debug, Clone, Serialize, Deserialize)]
1085#[serde(deny_unknown_fields)]
1086pub struct SearchPortfolioCandidateMsg {
1087 pub id: String,
1088 #[serde(default)]
1089 pub instances: Vec<PortfolioInstanceMsg>,
1090 #[serde(default)]
1091 pub direct_instances: Vec<SearchDirectPortfolioInstanceMsg>,
1092 #[serde(default, skip_serializing_if = "Option::is_none")]
1093 pub policies: Option<serde_json::Value>,
1094 #[serde(default, skip_serializing_if = "Option::is_none")]
1095 pub groups: Option<serde_json::Value>,
1096}
1097
1098#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1099#[serde(tag = "type", content = "value", rename_all = "snake_case")]
1100pub enum SearchFactoryParameterMsg {
1101 Integer(i64),
1102 Number(f64),
1103 Choice(String),
1104}
1105
1106#[derive(Debug, Clone, Serialize, Deserialize)]
1107#[serde(deny_unknown_fields)]
1108pub struct SearchDirectFactoryPointMsg {
1109 pub parameters: BTreeMap<String, SearchFactoryParameterMsg>,
1110}
1111
1112#[derive(Debug, Clone, Serialize, Deserialize)]
1113#[serde(deny_unknown_fields)]
1114pub struct SearchDirectFactoryMsg {
1115 pub name: String,
1116 pub revision: String,
1117 pub points: Vec<SearchDirectFactoryPointMsg>,
1118}
1119
1120#[derive(Debug, Clone, Serialize, Deserialize)]
1121#[serde(deny_unknown_fields)]
1122pub struct SearchExecutionVariantMsg {
1123 pub id: String,
1124 pub config: BacktestConfigMsg,
1125 #[serde(default)]
1126 pub future: FutureQuoteConfigMsg,
1127 #[serde(default)]
1128 pub profile: Option<String>,
1129}
1130
1131#[derive(Debug, Clone, Serialize, Deserialize)]
1133#[serde(deny_unknown_fields)]
1134pub struct SearchRunSpec {
1135 pub template: serde_json::Value,
1137 pub space: serde_json::Value,
1139 pub symbols: Vec<String>,
1140 pub exchange: String,
1141 pub data_type: String,
1143 #[serde(default)]
1144 pub timeframe: Option<String>,
1145 pub windows: SearchWindowsMsg,
1146 pub config: BacktestConfigMsg,
1147 #[serde(default)]
1149 pub profile: Option<String>,
1150 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1152 pub entry_profile_routes: Vec<EntryProfileRouteMsg>,
1153 #[serde(default)]
1155 pub workers: Option<usize>,
1156 #[serde(default)]
1157 pub decision_latency_ms: u64,
1158 #[serde(default)]
1159 pub structural: Option<serde_json::Value>,
1160 #[serde(default)]
1161 pub resource_limits: Option<serde_json::Value>,
1162 #[serde(default)]
1163 pub variants: Vec<SearchExecutionVariantMsg>,
1164 #[serde(default)]
1166 pub direct_factory: Option<SearchDirectFactoryMsg>,
1167 #[serde(default)]
1169 pub resume_checkpoint: Option<serde_json::Value>,
1170 #[serde(default)]
1172 pub selected_rerun: Option<SearchSelectedRerunMsg>,
1173 #[serde(default)]
1175 pub portfolio_candidates: Vec<SearchPortfolioCandidateMsg>,
1176 #[serde(default)]
1178 pub series_descriptors: Vec<serde_json::Value>,
1179}
1180
1181#[derive(Debug, Clone, Serialize)]
1183pub struct SubmitSearchRequest {
1184 pub request: SearchRunSpec,
1185 #[serde(default)]
1186 pub future: FutureQuoteConfigMsg,
1187 #[serde(default)]
1188 pub evaluation: ProviderEvaluationOptionsMsg,
1189}
1190
1191#[derive(Debug, Clone, Serialize, Deserialize)]
1193#[serde(deny_unknown_fields)]
1194pub struct GetSearchResultRequest {
1195 pub job_id: String,
1196}
1197
1198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1200pub struct SearchSummaryMsg {
1201 pub points_total: usize,
1202 pub rows: usize,
1203 pub completed_rows: usize,
1204 pub failed_rows: usize,
1205 pub data_mode: String,
1207}
1208
1209#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1211pub struct SearchResultMsg {
1212 pub summary: SearchSummaryMsg,
1213 pub table_csv: String,
1215 pub evaluation: serde_json::Value,
1217 pub bound_documents: Vec<serde_json::Value>,
1219 #[serde(default)]
1220 pub experiment_recipe: Option<serde_json::Value>,
1221 #[serde(default)]
1222 pub candidate_recipes: Vec<serde_json::Value>,
1223 #[serde(default)]
1224 pub run_recipes: Vec<serde_json::Value>,
1225 #[serde(default)]
1226 pub generation_dispositions: Option<serde_json::Value>,
1227 #[serde(default)]
1228 pub checkpoint: Option<serde_json::Value>,
1229 #[serde(default)]
1230 pub selected_evidence: Option<serde_json::Value>,
1231 #[serde(default)]
1232 pub trace: Option<serde_json::Value>,
1233}
1234
1235#[derive(Debug, Clone, Serialize, Deserialize)]
1237pub struct GetSearchResultResponse {
1238 pub success: bool,
1239 pub job_id: String,
1240 #[serde(default)]
1241 pub error: Option<String>,
1242 #[serde(default)]
1243 pub summary: Option<SearchSummaryMsg>,
1244 #[serde(default)]
1245 pub artifact: Option<ResultArtifactRefMsg>,
1246 #[serde(default)]
1248 pub checkpoint_artifact: Option<ResultArtifactRefMsg>,
1249}
1250
1251#[derive(Debug, Clone, Serialize, Deserialize)]
1255pub struct BacktestResultMsg {
1256 #[serde(default, skip_serializing_if = "Option::is_none")]
1257 pub admission_report: Option<ReplayAdmissionReportMsg>,
1258 #[serde(default, skip_serializing_if = "Option::is_none")]
1260 pub strategy: Option<ConfiguredStrategyOutputMsg>,
1261 #[serde(default, skip_serializing_if = "Option::is_none")]
1263 pub portfolio: Option<PortfolioOutputMsg>,
1264 pub initial_balance: f64,
1265 pub final_balance: f64,
1266 pub total_pnl: f64,
1267 pub total_trades: usize,
1268 pub winning_trades: usize,
1269 pub losing_trades: usize,
1270 pub win_rate: f64,
1271 pub profit_factor: f64,
1272 pub max_drawdown: f64,
1273 pub max_drawdown_pct: f64,
1274
1275 pub summary: SubsetStatsMsg,
1277 pub per_symbol: HashMap<String, SubsetStatsMsg>,
1278 pub per_group: HashMap<String, SubsetStatsMsg>,
1279 pub long_stats: SubsetStatsMsg,
1280 pub short_stats: SubsetStatsMsg,
1281 pub per_close_reason: Vec<CloseReasonStatsMsg>,
1282 pub streaks: StreakStatsMsg,
1283 pub risk_metrics: RiskMetricsMsg,
1284 pub duration_stats: Option<DurationStatsMsg>,
1285 pub monthly_returns: Vec<MonthlyReturnMsg>,
1286
1287 pub equity_curve: Vec<EquityPoint>,
1288 pub trade_log: Vec<TradeResultMsg>,
1289
1290 pub positions: Vec<PositionSummaryMsg>,
1292 pub total_positions: usize,
1293 pub winning_positions: usize,
1294 pub losing_positions: usize,
1295 pub position_win_rate: f64,
1296
1297 #[serde(default)]
1299 pub future: Option<FutureBacktestResultMsg>,
1300
1301 #[serde(default)]
1303 pub total_commission: f64,
1304 #[serde(default)]
1306 pub total_swap: f64,
1307 #[serde(default)]
1309 pub gross_pnl: Option<f64>,
1310 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1312 pub cost_events: Vec<CostEventMsg>,
1313}
1314
1315#[derive(Debug, Clone, Serialize, Deserialize)]
1317pub struct CostEventMsg {
1318 pub id: String,
1319 pub position_id: String,
1320 pub symbol: String,
1321 pub side: String,
1322 pub ts: String,
1323 pub kind: String,
1325 pub amount: f64,
1327 #[serde(default)]
1328 pub native_amount: Option<f64>,
1329 #[serde(default)]
1330 pub native_currency: Option<String>,
1331 pub size: f64,
1332 #[serde(default)]
1333 pub nights: Option<u32>,
1334}
1335
1336#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1338#[serde(default)]
1339pub struct MtmOutputSummaryMsg {
1340 pub policy: MtmOutputPolicyMsg,
1341 pub observed_points: u64,
1342 pub retained_points: u64,
1343 pub omitted_points: u64,
1344}
1345
1346impl Default for MtmOutputSummaryMsg {
1347 fn default() -> Self {
1348 Self {
1349 policy: MtmOutputPolicyMsg::Full,
1350 observed_points: 0,
1351 retained_points: 0,
1352 omitted_points: 0,
1353 }
1354 }
1355}
1356
1357#[derive(Debug, Clone, Serialize, Deserialize)]
1361pub struct FutureBacktestResultMsg {
1362 pub format_version: u32,
1363 pub execution_metadata: serde_json::Value,
1364 pub recorded_fills: serde_json::Value,
1365 pub action_dispositions: serde_json::Value,
1366 pub close_events: serde_json::Value,
1367 pub completed_positions: serde_json::Value,
1368 pub open_positions: serde_json::Value,
1369 pub pending_orders: serde_json::Value,
1370 #[serde(default)]
1372 pub pending_order_lifecycle: Vec<PendingOrderLifecycleEventMsg>,
1373 pub mtm_equity_curve: serde_json::Value,
1374 #[serde(default)]
1375 pub mtm_output_summary: MtmOutputSummaryMsg,
1376 pub mtm_max_drawdown: Option<f64>,
1377 pub mtm_max_drawdown_pct: Option<f64>,
1378 pub provider_evaluation: serde_json::Value,
1379}
1380
1381#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1383#[serde(rename_all = "snake_case")]
1384pub enum PendingOrderLifecycleStateMsg {
1385 #[default]
1386 Placed,
1387 Filled,
1388 Cancelled,
1389 UnfilledAtEnd,
1390}
1391
1392#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1394#[serde(default)]
1395pub struct PendingOrderLifecycleEventMsg {
1396 pub id: String,
1397 pub sequence: u64,
1398 pub position_id: String,
1399 pub placement_action_id: Option<String>,
1400 pub terminal_action_id: Option<String>,
1401 pub state: PendingOrderLifecycleStateMsg,
1402 pub symbol: String,
1403 pub side: String,
1404 pub order_type: String,
1405 pub requested_size: f64,
1406 pub filled_size: Option<f64>,
1407 pub requested_price: Option<f64>,
1408 pub fill_price: Option<f64>,
1409 pub signal_ts: Option<String>,
1410 pub placed_ts: Option<String>,
1411 pub effective_ts: Option<String>,
1412 pub terminal_ts: Option<String>,
1413 pub wait_latency_ms: Option<i64>,
1414 pub fill_ratio: Option<f64>,
1415}
1416
1417#[derive(Debug, Clone, Serialize, Deserialize)]
1421pub struct SubsetStatsMsg {
1422 pub total_trades: usize,
1423 pub winning_trades: usize,
1424 pub losing_trades: usize,
1425 pub breakeven_trades: usize,
1426 pub total_pnl: f64,
1427 pub gross_profit: f64,
1428 pub gross_loss: f64,
1429 pub win_rate: f64,
1430 pub profit_factor: f64,
1431 pub avg_win: f64,
1432 pub avg_loss: f64,
1433 pub win_loss_ratio: f64,
1434 pub expectancy: f64,
1435 pub largest_win: f64,
1436 pub largest_loss: f64,
1437 #[serde(default)]
1439 pub commission: f64,
1440 #[serde(default)]
1442 pub swap: f64,
1443 #[serde(default)]
1445 pub gross_pnl: Option<f64>,
1446}
1447
1448#[derive(Debug, Clone, Serialize, Deserialize)]
1450pub struct StreakStatsMsg {
1451 pub max_consecutive_wins: u32,
1452 pub max_consecutive_losses: u32,
1453 pub current_streak: i32,
1454}
1455
1456#[derive(Debug, Clone, Serialize, Deserialize)]
1458pub struct RiskMetricsMsg {
1459 pub sharpe_ratio: Option<f64>,
1460 pub sortino_ratio: Option<f64>,
1461 pub calmar_ratio: Option<f64>,
1462 pub return_on_max_drawdown: Option<f64>,
1463 pub max_drawdown: f64,
1464 pub max_drawdown_pct: f64,
1465 pub max_drawdown_duration_secs: Option<i64>,
1466}
1467
1468#[derive(Debug, Clone, Serialize, Deserialize)]
1470pub struct DurationStatsMsg {
1471 pub avg_duration_secs: i64,
1472 pub min_duration_secs: i64,
1473 pub max_duration_secs: i64,
1474 pub avg_winner_duration_secs: i64,
1475 pub avg_loser_duration_secs: i64,
1476}
1477
1478#[derive(Debug, Clone, Serialize, Deserialize)]
1480pub struct MonthlyReturnMsg {
1481 pub year: i32,
1482 pub month: u32,
1483 pub pnl: f64,
1484 pub trade_count: usize,
1485 pub ending_balance: f64,
1486}
1487
1488#[derive(Debug, Clone, Serialize, Deserialize)]
1490pub struct CloseReasonStatsMsg {
1491 pub reason: String,
1492 pub count: usize,
1493 pub total_pnl: f64,
1494 pub avg_pnl: f64,
1495 pub percentage: f64,
1496}
1497
1498#[derive(Debug, Clone, Serialize, Deserialize)]
1500pub struct PositionSummaryMsg {
1501 pub position_id: String,
1502 pub symbol: String,
1503 pub side: String,
1504 pub group: Option<String>,
1505 pub entry_price: f64,
1506 pub avg_exit_price: f64,
1507 pub original_size: f64,
1508 pub close_count: usize,
1509 pub net_pnl: f64,
1510 pub close_reasons: Vec<String>,
1511 pub open_ts: String,
1512 pub final_close_ts: Option<String>,
1513 pub duration_seconds: i64,
1514}
1515
1516#[derive(Debug, Clone, Serialize, Deserialize)]
1518pub struct EquityPoint {
1519 pub ts: String,
1520 pub balance: f64,
1521}
1522
1523#[derive(Debug, Clone, Serialize, Deserialize)]
1525pub struct TradeResultMsg {
1526 pub position_id: String,
1527 pub symbol: String,
1528 pub side: String,
1529 pub entry_price: f64,
1530 pub exit_price: f64,
1531 pub size: f64,
1532 pub pnl: f64,
1534 #[serde(default)]
1538 pub commission: f64,
1539 #[serde(default)]
1541 pub swap: f64,
1542 #[serde(default)]
1544 pub gross_pnl: Option<f64>,
1545 pub open_ts: String,
1546 pub close_ts: String,
1547 pub close_reason: String,
1548 pub group: Option<String>,
1549}
1550
1551#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1555pub enum TargetSelectionMsg {
1556 All,
1558 None,
1560 Selected(Vec<usize>),
1562}
1563
1564#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1566#[serde(tag = "type", deny_unknown_fields)]
1567pub enum TargetSourceMsg {
1568 #[default]
1569 FromSignal,
1570 StopDistanceMultiples {
1571 multiples: Vec<f64>,
1572 },
1573}
1574
1575#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1577#[serde(rename_all = "lowercase")]
1578pub enum EntryGeometryPolicyMsg {
1579 #[default]
1582 Strict,
1583 Permissive,
1586}
1587
1588#[derive(Debug, Clone, Serialize, Deserialize)]
1590pub struct ManagementProfileMsg {
1591 pub name: String,
1592 #[serde(default, skip_serializing_if = "Option::is_none")]
1595 pub target_selection: Option<TargetSelectionMsg>,
1596 pub use_targets: Vec<usize>,
1598 pub close_ratios: Vec<f64>,
1599 #[serde(default, skip_serializing_if = "Option::is_none")]
1600 pub target_source: Option<TargetSourceMsg>,
1601 #[serde(default)]
1602 pub stoploss_mode: Option<StoplossModeMsg>,
1603 #[serde(default)]
1604 pub rules: Vec<RuleConfigDefMsg>,
1605 #[serde(default)]
1606 pub group_override: Option<String>,
1607 #[serde(default)]
1608 pub let_remainder_run: bool,
1609 #[serde(default, skip_serializing_if = "Option::is_none")]
1612 pub entry_geometry: Option<EntryGeometryPolicyMsg>,
1613}
1614
1615#[derive(Debug, Clone, Serialize, Deserialize)]
1617#[serde(tag = "type")]
1618pub enum StoplossModeMsg {
1619 FromSignal,
1620 None,
1621 FixedDistance { distance: f64 },
1622 FixedPrice { price: f64 },
1623 FromSignalDistance { multiplier: f64 },
1624}
1625
1626#[derive(Debug, Clone, Serialize, Deserialize)]
1628#[serde(tag = "type")]
1629pub enum RuleConfigDefMsg {
1630 FixedStoploss { price: f64 },
1631 TrailingStop { distance: f64 },
1632 TakeProfit { price: f64, close_ratio: f64 },
1633 BreakevenWhen { trigger_price: f64 },
1634 BreakevenWhenOffset { trigger_price_offset: f64 },
1635 BreakevenAfterTargets { after_n: u32 },
1636 TimeExit { max_seconds: u64 },
1637}
1638
1639#[derive(Debug, Clone, Serialize, Deserialize)]
1641#[serde(untagged)]
1642pub enum ProfileRef {
1643 Named(String),
1644 Inline(ManagementProfileMsg),
1645}
1646
1647#[derive(Debug, Clone, Serialize, Deserialize)]
1649#[serde(deny_unknown_fields)]
1650pub struct EntryProfileRouteMsg {
1651 pub entry_class: String,
1652 pub profile: ProfileRef,
1653}
1654
1655#[derive(Debug, Clone, Serialize, Deserialize)]
1662#[serde(tag = "action", deny_unknown_fields)]
1663pub enum RawSignalMsg {
1664 Entry {
1666 ts: String,
1667 symbol: String,
1668 side: String,
1669 order_type: String,
1670 price: Option<f64>,
1671 risk: f64,
1672 stoploss: Option<f64>,
1673 #[serde(default)]
1674 targets: Vec<f64>,
1675 #[serde(default)]
1676 group: Option<String>,
1677 #[serde(default)]
1679 trade_id: Option<String>,
1680 #[serde(default, skip_serializing_if = "Option::is_none")]
1682 entry_class: Option<String>,
1683 },
1684 Close {
1686 ts: String,
1687 position: PositionRefMsg,
1688 },
1689 ClosePartial {
1691 ts: String,
1692 position: PositionRefMsg,
1693 ratio: f64,
1694 },
1695 ModifyStoploss {
1697 ts: String,
1698 position: PositionRefMsg,
1699 price: f64,
1700 },
1701 MoveStoplossToEntry {
1703 ts: String,
1704 position: PositionRefMsg,
1705 },
1706 AddTarget {
1708 ts: String,
1709 position: PositionRefMsg,
1710 price: f64,
1711 close_ratio: f64,
1712 },
1713 RemoveTarget {
1715 ts: String,
1716 position: PositionRefMsg,
1717 price: f64,
1718 },
1719 ModifyTarget {
1721 ts: String,
1722 position: PositionRefMsg,
1723 old_price: f64,
1724 new_price: f64,
1725 },
1726 AddRule {
1728 ts: String,
1729 position: PositionRefMsg,
1730 rule: RuleConfigDefMsg,
1731 },
1732 RemoveRule {
1734 ts: String,
1735 position: PositionRefMsg,
1736 rule_name: String,
1737 },
1738 ScaleIn {
1740 ts: String,
1741 position: PositionRefMsg,
1742 price: Option<f64>,
1743 size: f64,
1744 },
1745 CancelPending {
1747 ts: String,
1748 position: PositionRefMsg,
1749 },
1750 CloseAllOf { ts: String, symbol: String },
1752 CloseAll { ts: String },
1754 CancelAllPending { ts: String },
1756 ModifyAllStoploss {
1758 ts: String,
1759 symbol: String,
1760 price: f64,
1761 },
1762 CloseAllInGroup { ts: String, group_id: String },
1764 ModifyAllStoplossInGroup {
1766 ts: String,
1767 group_id: String,
1768 price: f64,
1769 },
1770}
1771
1772impl RawSignalMsg {
1773 pub fn ts(&self) -> &str {
1775 match self {
1776 RawSignalMsg::Entry { ts, .. }
1777 | RawSignalMsg::Close { ts, .. }
1778 | RawSignalMsg::ClosePartial { ts, .. }
1779 | RawSignalMsg::ModifyStoploss { ts, .. }
1780 | RawSignalMsg::MoveStoplossToEntry { ts, .. }
1781 | RawSignalMsg::AddTarget { ts, .. }
1782 | RawSignalMsg::RemoveTarget { ts, .. }
1783 | RawSignalMsg::ModifyTarget { ts, .. }
1784 | RawSignalMsg::AddRule { ts, .. }
1785 | RawSignalMsg::RemoveRule { ts, .. }
1786 | RawSignalMsg::ScaleIn { ts, .. }
1787 | RawSignalMsg::CancelPending { ts, .. }
1788 | RawSignalMsg::CloseAllOf { ts, .. }
1789 | RawSignalMsg::CloseAll { ts }
1790 | RawSignalMsg::CancelAllPending { ts }
1791 | RawSignalMsg::ModifyAllStoploss { ts, .. }
1792 | RawSignalMsg::CloseAllInGroup { ts, .. }
1793 | RawSignalMsg::ModifyAllStoplossInGroup { ts, .. } => ts,
1794 }
1795 }
1796}
1797
1798#[derive(Debug, Clone, Serialize, Deserialize)]
1800#[serde(tag = "type")]
1801pub enum PositionRefMsg {
1802 ByTradeId { trade_id: String },
1804 AllOnSymbol { symbol: String },
1806 AllInGroup { group_id: String },
1808}
1809
1810#[derive(Serialize, Deserialize)]
1813#[serde(tag = "type", deny_unknown_fields)]
1814enum StrictSizingPolicyMsg {
1815 FixedLot { lots: f64 },
1816 FixedRiskAmount { amount: f64 },
1817 BalanceRiskPercent { percent: f64 },
1818}
1819
1820#[derive(Serialize, Deserialize)]
1821#[serde(tag = "type", deny_unknown_fields)]
1822enum StrictCommissionModelMsg {
1823 #[serde(alias = "per_lot_per_side")]
1824 PerLotPerSide { amount: f64, currency: String },
1825 #[serde(alias = "notional_rate_per_side")]
1826 NotionalRatePerSide { buy_rate: f64, sell_rate: f64 },
1827}
1828
1829#[derive(Serialize, Deserialize)]
1830#[serde(tag = "unit", deny_unknown_fields)]
1831enum StrictSwapAmountMsg {
1832 #[serde(alias = "points")]
1833 Points { long: f64, short: f64 },
1834 #[serde(alias = "currency")]
1835 Currency {
1836 long: f64,
1837 short: f64,
1838 currency: String,
1839 },
1840}
1841
1842#[derive(Serialize, Deserialize)]
1843#[serde(deny_unknown_fields)]
1844struct StrictSwapScheduleMsg {
1845 amount: StrictSwapAmountMsg,
1846 rollover: String,
1847 triple_weekday: String,
1848 #[serde(default)]
1849 skipped_weekdays: Vec<String>,
1850}
1851
1852#[derive(Serialize, Deserialize)]
1853#[serde(deny_unknown_fields)]
1854struct StrictInstrumentCostsMsg {
1855 #[serde(default)]
1856 commission: Option<StrictCommissionModelMsg>,
1857 #[serde(default)]
1858 swap: Option<StrictSwapScheduleMsg>,
1859}
1860
1861#[derive(Serialize, Deserialize)]
1862#[serde(deny_unknown_fields)]
1863struct StrictBacktestConfigMsg {
1864 initial_balance: Option<f64>,
1865 close_on_finish: Option<bool>,
1866 fill_model: Option<String>,
1867 #[serde(default)]
1868 sizing: Option<StrictSizingPolicyMsg>,
1869 #[serde(default)]
1870 costs: BTreeMap<String, StrictInstrumentCostsMsg>,
1871}
1872
1873#[derive(Serialize, Deserialize)]
1874#[serde(tag = "type", deny_unknown_fields)]
1875enum StrictStoplossModeMsg {
1876 FromSignal,
1877 None,
1878 FixedDistance { distance: f64 },
1879 FixedPrice { price: f64 },
1880 FromSignalDistance { multiplier: f64 },
1881}
1882
1883#[derive(Serialize, Deserialize)]
1884#[serde(tag = "type", deny_unknown_fields)]
1885enum StrictTargetSourceMsg {
1886 FromSignal,
1887 StopDistanceMultiples { multiples: Vec<f64> },
1888}
1889
1890#[derive(Serialize, Deserialize)]
1891#[serde(tag = "type", deny_unknown_fields)]
1892enum StrictRuleConfigDefMsg {
1893 FixedStoploss { price: f64 },
1894 TrailingStop { distance: f64 },
1895 TakeProfit { price: f64, close_ratio: f64 },
1896 BreakevenWhen { trigger_price: f64 },
1897 BreakevenWhenOffset { trigger_price_offset: f64 },
1898 BreakevenAfterTargets { after_n: u32 },
1899 TimeExit { max_seconds: u64 },
1900}
1901
1902#[derive(Serialize, Deserialize)]
1903#[serde(deny_unknown_fields)]
1904struct StrictManagementProfileMsg {
1905 name: String,
1906 #[serde(default)]
1907 target_selection: Option<TargetSelectionMsg>,
1908 use_targets: Vec<usize>,
1909 close_ratios: Vec<f64>,
1910 #[serde(default)]
1911 target_source: Option<StrictTargetSourceMsg>,
1912 #[serde(default)]
1913 stoploss_mode: Option<StrictStoplossModeMsg>,
1914 #[serde(default)]
1915 rules: Vec<StrictRuleConfigDefMsg>,
1916 #[serde(default)]
1917 group_override: Option<String>,
1918 #[serde(default)]
1919 let_remainder_run: bool,
1920 #[serde(default)]
1921 entry_geometry: Option<EntryGeometryPolicyMsg>,
1922}
1923
1924#[derive(Serialize, Deserialize)]
1925#[serde(tag = "type", deny_unknown_fields)]
1926enum StrictPositionRefMsg {
1927 ByTradeId { trade_id: String },
1928 AllOnSymbol { symbol: String },
1929 AllInGroup { group_id: String },
1930}
1931
1932#[derive(Serialize, Deserialize)]
1933#[serde(tag = "action", deny_unknown_fields)]
1934enum StrictRawSignalMsg {
1935 Entry {
1936 ts: String,
1937 symbol: String,
1938 side: String,
1939 order_type: String,
1940 price: Option<f64>,
1941 risk: f64,
1942 stoploss: Option<f64>,
1943 #[serde(default)]
1944 targets: Vec<f64>,
1945 #[serde(default)]
1946 group: Option<String>,
1947 #[serde(default)]
1948 trade_id: Option<String>,
1949 #[serde(default)]
1950 entry_class: Option<String>,
1951 },
1952 Close {
1953 ts: String,
1954 position: StrictPositionRefMsg,
1955 },
1956 ClosePartial {
1957 ts: String,
1958 position: StrictPositionRefMsg,
1959 ratio: f64,
1960 },
1961 ModifyStoploss {
1962 ts: String,
1963 position: StrictPositionRefMsg,
1964 price: f64,
1965 },
1966 MoveStoplossToEntry {
1967 ts: String,
1968 position: StrictPositionRefMsg,
1969 },
1970 AddTarget {
1971 ts: String,
1972 position: StrictPositionRefMsg,
1973 price: f64,
1974 close_ratio: f64,
1975 },
1976 RemoveTarget {
1977 ts: String,
1978 position: StrictPositionRefMsg,
1979 price: f64,
1980 },
1981 ModifyTarget {
1982 ts: String,
1983 position: StrictPositionRefMsg,
1984 old_price: f64,
1985 new_price: f64,
1986 },
1987 AddRule {
1988 ts: String,
1989 position: StrictPositionRefMsg,
1990 rule: StrictRuleConfigDefMsg,
1991 },
1992 RemoveRule {
1993 ts: String,
1994 position: StrictPositionRefMsg,
1995 rule_name: String,
1996 },
1997 ScaleIn {
1998 ts: String,
1999 position: StrictPositionRefMsg,
2000 price: Option<f64>,
2001 size: f64,
2002 },
2003 CancelPending {
2004 ts: String,
2005 position: StrictPositionRefMsg,
2006 },
2007 CloseAllOf {
2008 ts: String,
2009 symbol: String,
2010 },
2011 CloseAll {
2012 ts: String,
2013 },
2014 CancelAllPending {
2015 ts: String,
2016 },
2017 ModifyAllStoploss {
2018 ts: String,
2019 symbol: String,
2020 price: f64,
2021 },
2022 CloseAllInGroup {
2023 ts: String,
2024 group_id: String,
2025 },
2026 ModifyAllStoplossInGroup {
2027 ts: String,
2028 group_id: String,
2029 price: f64,
2030 },
2031}
2032
2033#[derive(Serialize, Deserialize)]
2034#[serde(deny_unknown_fields)]
2035struct StrictBacktestRunSpec {
2036 #[serde(default)]
2037 on_unavailable: UnavailableInstrumentPolicyMsg,
2038 symbol: String,
2039 #[serde(default)]
2040 symbols: Vec<String>,
2041 #[serde(default)]
2042 all_symbols: bool,
2043 exchange: String,
2044 data_type: String,
2045 timeframe: Option<String>,
2046 from: Option<String>,
2047 to: Option<String>,
2048 #[serde(default)]
2049 raw_signals: Vec<StrictRawSignalMsg>,
2050 profile: Option<String>,
2051 #[serde(default)]
2052 profile_def: Option<StrictManagementProfileMsg>,
2053 #[serde(default)]
2054 entry_profile_routes: Vec<StrictEntryProfileRouteMsg>,
2055 config: StrictBacktestConfigMsg,
2056}
2057
2058#[derive(Serialize, Deserialize)]
2059#[serde(untagged)]
2060enum StrictProfileRef {
2061 Named(String),
2062 Inline(StrictManagementProfileMsg),
2063}
2064
2065#[derive(Serialize, Deserialize)]
2066#[serde(deny_unknown_fields)]
2067struct StrictEntryProfileRouteMsg {
2068 entry_class: String,
2069 profile: StrictProfileRef,
2070}
2071
2072#[derive(Serialize, Deserialize)]
2073#[serde(deny_unknown_fields)]
2074struct StrictBacktestMultiRunSpec {
2075 #[serde(default)]
2076 on_unavailable: UnavailableInstrumentPolicyMsg,
2077 symbol: String,
2078 #[serde(default)]
2079 symbols: Vec<String>,
2080 #[serde(default)]
2081 all_symbols: bool,
2082 exchange: String,
2083 data_type: String,
2084 timeframe: Option<String>,
2085 from: Option<String>,
2086 to: Option<String>,
2087 #[serde(default)]
2088 raw_signals: Vec<StrictRawSignalMsg>,
2089 profiles: Vec<StrictProfileRef>,
2090 #[serde(default)]
2091 entry_profile_routes: Vec<StrictEntryProfileRouteMsg>,
2092 config: StrictBacktestConfigMsg,
2093}
2094
2095#[derive(Deserialize)]
2096#[serde(deny_unknown_fields)]
2097struct StrictRunBacktestRequest {
2098 request: StrictBacktestRunSpec,
2099 #[serde(default)]
2100 future: FutureQuoteConfigMsg,
2101 #[serde(default)]
2102 evaluation: ProviderEvaluationOptionsMsg,
2103 #[serde(default)]
2104 result_delivery: ResultDeliveryMsg,
2105}
2106
2107#[derive(Deserialize)]
2108#[serde(deny_unknown_fields)]
2109struct StrictSubmitBacktestRequest {
2110 request: RunBacktestRequest,
2111}
2112
2113#[derive(Deserialize)]
2114#[serde(deny_unknown_fields)]
2115struct StrictRunBacktestMultiRequest {
2116 request: StrictBacktestMultiRunSpec,
2117 #[serde(default)]
2118 future: FutureQuoteConfigMsg,
2119 #[serde(default)]
2120 evaluation: ProviderEvaluationOptionsMsg,
2121 #[serde(default)]
2122 result_delivery: ResultDeliveryMsg,
2123}
2124
2125fn strict_into_wire<T>(value: impl Serialize) -> Result<T, serde_json::Error>
2126where
2127 T: DeserializeOwned,
2128{
2129 serde_json::from_value(serde_json::to_value(value)?)
2130}
2131
2132impl<'de> Deserialize<'de> for RunBacktestRequest {
2133 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2134 where
2135 D: serde::Deserializer<'de>,
2136 {
2137 let strict = StrictRunBacktestRequest::deserialize(deserializer)?;
2138 Ok(Self {
2139 request: strict_into_wire(strict.request).map_err(serde::de::Error::custom)?,
2140 future: strict.future,
2141 evaluation: strict.evaluation,
2142 result_delivery: strict.result_delivery,
2143 })
2144 }
2145}
2146
2147impl<'de> Deserialize<'de> for SubmitBacktestRequest {
2148 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2149 where
2150 D: serde::Deserializer<'de>,
2151 {
2152 let strict = StrictSubmitBacktestRequest::deserialize(deserializer)?;
2153 Ok(Self {
2154 request: strict.request,
2155 })
2156 }
2157}
2158
2159impl<'de> Deserialize<'de> for RunBacktestMultiRequest {
2160 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2161 where
2162 D: serde::Deserializer<'de>,
2163 {
2164 let strict = StrictRunBacktestMultiRequest::deserialize(deserializer)?;
2165 Ok(Self {
2166 request: strict_into_wire(strict.request).map_err(serde::de::Error::custom)?,
2167 future: strict.future,
2168 evaluation: strict.evaluation,
2169 result_delivery: strict.result_delivery,
2170 })
2171 }
2172}
2173
2174#[derive(Serialize, Deserialize)]
2175#[serde(deny_unknown_fields)]
2176struct StrictConfiguredStrategyRunSpec {
2177 symbol: String,
2178 exchange: String,
2179 data_type: String,
2180 #[serde(default)]
2181 timeframe: Option<String>,
2182 #[serde(default)]
2183 from: Option<String>,
2184 #[serde(default)]
2185 to: Option<String>,
2186 strategy: ConfiguredStrategyRunMsg,
2187 #[serde(default)]
2188 profile: Option<String>,
2189 #[serde(default)]
2190 profile_def: Option<StrictManagementProfileMsg>,
2191 #[serde(default)]
2192 entry_profile_routes: Vec<StrictEntryProfileRouteMsg>,
2193 config: StrictBacktestConfigMsg,
2194}
2195
2196#[derive(Deserialize)]
2197#[serde(deny_unknown_fields)]
2198struct StrictRunConfiguredStrategyRequest {
2199 request: StrictConfiguredStrategyRunSpec,
2200 #[serde(default)]
2201 future: FutureQuoteConfigMsg,
2202 #[serde(default)]
2203 evaluation: ProviderEvaluationOptionsMsg,
2204 #[serde(default)]
2205 result_delivery: ResultDeliveryMsg,
2206}
2207
2208#[derive(Deserialize)]
2209#[serde(deny_unknown_fields)]
2210struct StrictSubmitConfiguredStrategyRequest {
2211 request: RunConfiguredStrategyRequest,
2212}
2213
2214#[derive(Serialize, Deserialize)]
2215#[serde(deny_unknown_fields)]
2216struct StrictSearchExecutionVariantMsg {
2217 id: String,
2218 config: StrictBacktestConfigMsg,
2219 #[serde(default)]
2220 future: FutureQuoteConfigMsg,
2221 #[serde(default)]
2222 profile: Option<String>,
2223}
2224
2225#[derive(Serialize, Deserialize)]
2226#[serde(deny_unknown_fields)]
2227struct StrictSearchPortfolioCandidateMsg {
2228 id: String,
2229 #[serde(default)]
2230 instances: Vec<StrictPortfolioInstanceMsg>,
2231 #[serde(default)]
2232 direct_instances: Vec<SearchDirectPortfolioInstanceMsg>,
2233 #[serde(default)]
2234 policies: Option<serde_json::Value>,
2235 #[serde(default)]
2236 groups: Option<serde_json::Value>,
2237}
2238
2239#[derive(Serialize, Deserialize)]
2240#[serde(deny_unknown_fields)]
2241struct StrictSearchRunSpec {
2242 template: serde_json::Value,
2243 space: serde_json::Value,
2244 symbols: Vec<String>,
2245 exchange: String,
2246 data_type: String,
2247 #[serde(default)]
2248 timeframe: Option<String>,
2249 windows: SearchWindowsMsg,
2250 config: StrictBacktestConfigMsg,
2251 #[serde(default)]
2252 profile: Option<String>,
2253 #[serde(default)]
2254 entry_profile_routes: Vec<StrictEntryProfileRouteMsg>,
2255 #[serde(default)]
2256 workers: Option<usize>,
2257 #[serde(default)]
2258 decision_latency_ms: u64,
2259 #[serde(default)]
2260 structural: Option<serde_json::Value>,
2261 #[serde(default)]
2262 resource_limits: Option<serde_json::Value>,
2263 #[serde(default)]
2264 variants: Vec<StrictSearchExecutionVariantMsg>,
2265 #[serde(default)]
2266 direct_factory: Option<SearchDirectFactoryMsg>,
2267 #[serde(default)]
2268 resume_checkpoint: Option<serde_json::Value>,
2269 #[serde(default)]
2270 selected_rerun: Option<SearchSelectedRerunMsg>,
2271 #[serde(default)]
2272 portfolio_candidates: Vec<StrictSearchPortfolioCandidateMsg>,
2273 #[serde(default)]
2274 series_descriptors: Vec<serde_json::Value>,
2275}
2276
2277#[derive(Deserialize)]
2278#[serde(deny_unknown_fields)]
2279struct StrictSubmitSearchRequest {
2280 request: StrictSearchRunSpec,
2281 #[serde(default)]
2282 future: FutureQuoteConfigMsg,
2283 #[serde(default)]
2284 evaluation: ProviderEvaluationOptionsMsg,
2285}
2286
2287#[derive(Serialize, Deserialize)]
2288#[serde(deny_unknown_fields)]
2289struct StrictPortfolioInstanceMsg {
2290 symbol: String,
2291 strategy: ConfiguredStrategyRunMsg,
2292 #[serde(default)]
2293 profile: Option<String>,
2294 #[serde(default)]
2295 profile_def: Option<StrictManagementProfileMsg>,
2296 #[serde(default)]
2297 entry_profile_routes: Vec<StrictEntryProfileRouteMsg>,
2298}
2299
2300#[derive(Serialize, Deserialize)]
2301#[serde(deny_unknown_fields)]
2302struct StrictPortfolioRunSpec {
2303 instances: Vec<StrictPortfolioInstanceMsg>,
2304 exchange: String,
2305 data_type: String,
2306 #[serde(default)]
2307 timeframe: Option<String>,
2308 #[serde(default)]
2309 from: Option<String>,
2310 #[serde(default)]
2311 to: Option<String>,
2312 config: StrictBacktestConfigMsg,
2313 #[serde(default)]
2314 policies: Option<serde_json::Value>,
2315 #[serde(default)]
2316 groups: Option<serde_json::Value>,
2317}
2318
2319#[derive(Deserialize)]
2320#[serde(deny_unknown_fields)]
2321struct StrictRunPortfolioRequest {
2322 request: StrictPortfolioRunSpec,
2323 #[serde(default)]
2324 future: FutureQuoteConfigMsg,
2325 #[serde(default)]
2326 evaluation: ProviderEvaluationOptionsMsg,
2327 #[serde(default)]
2328 result_delivery: ResultDeliveryMsg,
2329}
2330
2331#[derive(Deserialize)]
2332#[serde(deny_unknown_fields)]
2333struct StrictSubmitPortfolioRequest {
2334 request: RunPortfolioRequest,
2335}
2336
2337impl<'de> Deserialize<'de> for RunPortfolioRequest {
2338 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2339 where
2340 D: serde::Deserializer<'de>,
2341 {
2342 let strict = StrictRunPortfolioRequest::deserialize(deserializer)?;
2343 Ok(Self {
2344 request: strict_into_wire(strict.request).map_err(serde::de::Error::custom)?,
2345 future: strict.future,
2346 evaluation: strict.evaluation,
2347 result_delivery: strict.result_delivery,
2348 })
2349 }
2350}
2351
2352impl<'de> Deserialize<'de> for SubmitPortfolioRequest {
2353 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2354 where
2355 D: serde::Deserializer<'de>,
2356 {
2357 let strict = StrictSubmitPortfolioRequest::deserialize(deserializer)?;
2358 Ok(Self {
2359 request: strict.request,
2360 })
2361 }
2362}
2363
2364impl<'de> Deserialize<'de> for RunConfiguredStrategyRequest {
2365 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2366 where
2367 D: serde::Deserializer<'de>,
2368 {
2369 let strict = StrictRunConfiguredStrategyRequest::deserialize(deserializer)?;
2370 Ok(Self {
2371 request: strict_into_wire(strict.request).map_err(serde::de::Error::custom)?,
2372 future: strict.future,
2373 evaluation: strict.evaluation,
2374 result_delivery: strict.result_delivery,
2375 })
2376 }
2377}
2378
2379impl<'de> Deserialize<'de> for SubmitConfiguredStrategyRequest {
2380 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2381 where
2382 D: serde::Deserializer<'de>,
2383 {
2384 let strict = StrictSubmitConfiguredStrategyRequest::deserialize(deserializer)?;
2385 Ok(Self {
2386 request: strict.request,
2387 })
2388 }
2389}
2390
2391impl<'de> Deserialize<'de> for SubmitSearchRequest {
2392 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2393 where
2394 D: serde::Deserializer<'de>,
2395 {
2396 let strict = StrictSubmitSearchRequest::deserialize(deserializer)?;
2397 Ok(Self {
2398 request: strict_into_wire(strict.request).map_err(serde::de::Error::custom)?,
2399 future: strict.future,
2400 evaluation: strict.evaluation,
2401 })
2402 }
2403}
2404
2405#[derive(Debug, Clone, Serialize, Deserialize)]
2409pub struct AddProfileRequest {
2410 pub profile: ManagementProfileMsg,
2411 #[serde(default)]
2412 pub overwrite: bool,
2413}
2414
2415#[derive(Debug, Clone, Serialize, Deserialize)]
2417pub struct AddProfileResponse {
2418 pub success: bool,
2419 pub error: Option<String>,
2420 pub profile_count: usize,
2421}
2422
2423#[derive(Debug, Clone, Serialize, Deserialize)]
2425pub struct RemoveProfileRequest {
2426 pub name: String,
2427}
2428
2429#[derive(Debug, Clone, Serialize, Deserialize)]
2431pub struct RemoveProfileResponse {
2432 pub success: bool,
2433 pub error: Option<String>,
2434 pub profile_count: usize,
2435}
2436
2437#[derive(Debug, Clone, Serialize, Deserialize)]
2439pub struct ReloadProfilesResponse {
2440 pub success: bool,
2441 pub error: Option<String>,
2442 pub profile_count: usize,
2443 pub loaded_from: String,
2444}
2445
2446#[cfg(test)]
2447mod tests {
2448 use super::*;
2449 use serde_json::json;
2450
2451 fn entry_fields() -> serde_json::Value {
2452 json!({
2453 "action": "Entry",
2454 "ts": "2026-01-02T10:00:00",
2455 "symbol": "eurusd",
2456 "side": "Buy",
2457 "order_type": "Market",
2458 "price": null,
2459 "risk": 1.5,
2460 "stoploss": 1.08
2461 })
2462 }
2463
2464 #[test]
2465 fn entry_requires_risk_and_rejects_size() {
2466 let entry: RawSignalMsg = serde_json::from_value(entry_fields()).unwrap();
2467 assert!(matches!(entry, RawSignalMsg::Entry { risk, .. } if risk == 1.5));
2468
2469 let mut with_size = entry_fields();
2470 with_size.as_object_mut().unwrap().remove("risk");
2471 with_size["size"] = json!(0.1);
2472 let error = serde_json::from_value::<RawSignalMsg>(with_size).unwrap_err();
2473 assert!(error.to_string().contains("unknown field `size`"));
2474
2475 let mut missing_risk = entry_fields();
2476 missing_risk.as_object_mut().unwrap().remove("risk");
2477 let error = serde_json::from_value::<RawSignalMsg>(missing_risk).unwrap_err();
2478 assert!(error.to_string().contains("missing field `risk`"));
2479 }
2480
2481 #[test]
2482 fn scale_in_keeps_size_field() {
2483 let signal: RawSignalMsg = serde_json::from_value(json!({
2484 "action": "ScaleIn",
2485 "ts": "2026-01-02T10:01:00",
2486 "position": { "type": "ByTradeId", "trade_id": "trade-1" },
2487 "price": null,
2488 "size": 0.25
2489 }))
2490 .unwrap();
2491 assert!(matches!(signal, RawSignalMsg::ScaleIn { size, .. } if size == 0.25));
2492 }
2493
2494 #[test]
2495 fn current_sizing_and_future_fields_are_strict() {
2496 let sizing: SizingPolicyMsg = serde_json::from_value(json!({
2497 "type": "FixedRiskAmount",
2498 "amount": 100.0
2499 }))
2500 .unwrap();
2501 assert!(matches!(
2502 sizing,
2503 SizingPolicyMsg::FixedRiskAmount { amount } if amount == 100.0
2504 ));
2505 assert!(
2506 serde_json::from_value::<SizingPolicyMsg>(json!({
2507 "type": "FixedLot",
2508 "qty": "all=0.01"
2509 }))
2510 .is_err()
2511 );
2512
2513 let future: FutureQuoteConfigMsg = serde_json::from_value(json!({
2514 "account_currency": "USD",
2515 "conversion_stale_after_ms": 15_000
2516 }))
2517 .unwrap();
2518 assert_eq!(future.account_currency, "USD");
2519 assert_eq!(future.conversion_stale_after_ms, 15_000);
2520 assert_eq!(
2521 future.mtm_output,
2522 MtmOutputPolicyMsg::Bounded { max_points: 4_096 }
2523 );
2524 assert_eq!(
2525 future.market_entry_sizing_basis,
2526 MarketEntrySizingBasisMsg::FillPrice
2527 );
2528 assert!(
2529 serde_json::from_value::<FutureQuoteConfigMsg>(json!({
2530 "account_currency": "USD",
2531 "conversion_rates": {}
2532 }))
2533 .is_err()
2534 );
2535 }
2536
2537 #[test]
2538 fn market_entry_sizing_basis_uses_strict_snake_case_wire_values() {
2539 for (value, expected) in [
2540 (json!("fill_price"), MarketEntrySizingBasisMsg::FillPrice),
2541 (
2542 json!("signal_entry_price"),
2543 MarketEntrySizingBasisMsg::SignalEntryPrice,
2544 ),
2545 ] {
2546 let basis: MarketEntrySizingBasisMsg = serde_json::from_value(value.clone()).unwrap();
2547 assert_eq!(basis, expected);
2548 assert_eq!(serde_json::to_value(basis).unwrap(), value);
2549 }
2550
2551 assert!(serde_json::from_value::<MarketEntrySizingBasisMsg>(json!("mark_price")).is_err());
2552 assert!(
2553 serde_json::from_value::<FutureQuoteConfigMsg>(json!({
2554 "market_entry_sizing_basis": "mark_price"
2555 }))
2556 .is_err()
2557 );
2558 }
2559
2560 #[test]
2561 fn mtm_output_policy_uses_strict_snake_case_wire_values() {
2562 for (value, expected) in [
2563 (json!("none"), MtmOutputPolicyMsg::None),
2564 (
2565 json!({ "bounded": { "max_points": 512 } }),
2566 MtmOutputPolicyMsg::Bounded { max_points: 512 },
2567 ),
2568 (json!("full"), MtmOutputPolicyMsg::Full),
2569 ] {
2570 let policy: MtmOutputPolicyMsg = serde_json::from_value(value.clone()).unwrap();
2571 assert_eq!(policy, expected);
2572 assert_eq!(serde_json::to_value(policy).unwrap(), value);
2573 }
2574
2575 assert!(
2576 serde_json::from_value::<MtmOutputPolicyMsg>(json!({
2577 "bounded": { "max_points": 512, "unexpected": true }
2578 }))
2579 .is_err()
2580 );
2581 }
2582
2583 #[test]
2584 fn future_result_defaults_missing_mtm_output_summary() {
2585 let result: FutureBacktestResultMsg = serde_json::from_value(json!({
2586 "format_version": 1,
2587 "execution_metadata": null,
2588 "recorded_fills": null,
2589 "action_dispositions": null,
2590 "close_events": null,
2591 "completed_positions": null,
2592 "open_positions": null,
2593 "pending_orders": null,
2594 "pending_order_lifecycle": [],
2595 "mtm_equity_curve": null,
2596 "mtm_max_drawdown": null,
2597 "mtm_max_drawdown_pct": null,
2598 "provider_evaluation": null
2599 }))
2600 .unwrap();
2601
2602 assert_eq!(result.mtm_output_summary, MtmOutputSummaryMsg::default());
2603 assert_eq!(result.mtm_output_summary.policy, MtmOutputPolicyMsg::Full);
2604
2605 let summary: MtmOutputSummaryMsg = serde_json::from_value(json!({
2606 "policy": "full",
2607 "observed_points": 4,
2608 "retained_points": 4,
2609 "omitted_points": 0,
2610 "future_addition": true
2611 }))
2612 .unwrap();
2613 assert_eq!(summary.policy, MtmOutputPolicyMsg::Full);
2614 }
2615
2616 fn minimal_request_json() -> serde_json::Value {
2617 json!({
2618 "request": {
2619 "symbol": "EURUSD",
2620 "exchange": "fixture",
2621 "data_type": "tick",
2622 "timeframe": null,
2623 "from": null,
2624 "to": null,
2625 "raw_signals": [],
2626 "profile": null,
2627 "config": {
2628 "initial_balance": null,
2629 "close_on_finish": null,
2630 "fill_model": null
2631 }
2632 },
2633 "future": {
2634 "account_currency": "USD"
2635 }
2636 })
2637 }
2638
2639 #[test]
2640 fn omitted_delivery_and_mtm_fields_use_current_defaults() {
2641 let request: RunBacktestRequest = serde_json::from_value(minimal_request_json()).unwrap();
2642 assert_eq!(request.result_delivery, ResultDeliveryMsg::Auto);
2643 assert_eq!(
2644 request.future.mtm_output,
2645 MtmOutputPolicyMsg::Bounded { max_points: 4_096 }
2646 );
2647 assert_eq!(
2648 request.future.market_entry_sizing_basis,
2649 MarketEntrySizingBasisMsg::FillPrice
2650 );
2651
2652 let submitted: SubmitBacktestRequest = serde_json::from_value(json!({
2653 "request": minimal_request_json()
2654 }))
2655 .unwrap();
2656 assert_eq!(submitted.request.result_delivery, ResultDeliveryMsg::Auto);
2657 assert_eq!(
2658 submitted.request.future.mtm_output,
2659 MtmOutputPolicyMsg::Bounded { max_points: 4_096 }
2660 );
2661
2662 let mut explicit = minimal_request_json();
2663 explicit["result_delivery"] = json!("auto");
2664 explicit["future"]["mtm_output"] = json!({
2665 "bounded": { "max_points": 4_096 }
2666 });
2667 let explicit: RunBacktestRequest = serde_json::from_value(explicit).unwrap();
2668 assert_eq!(explicit.result_delivery, ResultDeliveryMsg::Auto);
2669 assert_eq!(
2670 explicit.future.mtm_output,
2671 MtmOutputPolicyMsg::Bounded { max_points: 4_096 }
2672 );
2673
2674 let mut multi_request = minimal_request_json()["request"].clone();
2675 let fields = multi_request.as_object_mut().unwrap();
2676 fields.remove("profile");
2677 fields.insert("profiles".into(), json!([]));
2678 let multi: RunBacktestMultiRequest = serde_json::from_value(json!({
2679 "request": multi_request,
2680 "future": { "account_currency": "USD" }
2681 }))
2682 .unwrap();
2683 assert_eq!(multi.result_delivery, ResultDeliveryMsg::Auto);
2684 assert_eq!(
2685 multi.future.mtm_output,
2686 MtmOutputPolicyMsg::Bounded { max_points: 4_096 }
2687 );
2688 }
2689
2690 #[test]
2691 fn older_async_result_responses_default_artifact_consumption_state() {
2692 let response: GetBacktestResultResponse = serde_json::from_value(json!({
2693 "success": true,
2694 "job_id": "job-old",
2695 "result": null,
2696 "error": null,
2697 "inline_complete": true
2698 }))
2699 .unwrap();
2700 assert!(!response.artifact_consumed);
2701 }
2702}