1use serde::Deserialize;
6
7use crate::data::{Candle, CrossSection, DerivativesTick, OrderBook, TradePrint};
8use crate::engine::{Feeds, StreamingBacktest, DEFAULT_CAPITAL};
9use crate::error::{BacktestError, Result};
10use crate::report::BacktestReport;
11use crate::spec::StrategySpec;
12
13fn default_capital() -> f64 {
14 DEFAULT_CAPITAL
15}
16
17#[derive(Debug, Clone, Deserialize)]
21pub struct RunRequest {
22 pub spec: StrategySpec,
24 pub candles: Vec<Candle>,
26 #[serde(default = "default_capital")]
28 pub capital: f64,
29 #[serde(default)]
31 pub reference: Option<Vec<Candle>>,
32 #[serde(default)]
34 pub derivs: Option<Vec<DerivativesTick>>,
35 #[serde(default)]
37 pub books: Option<Vec<OrderBook>>,
38 #[serde(default)]
40 pub trades: Option<Vec<Vec<TradePrint>>>,
41 #[serde(default)]
43 pub sections: Option<Vec<CrossSection>>,
44}
45
46impl RunRequest {
47 pub fn run(&self) -> Result<BacktestReport> {
49 self.spec.validate()?;
50 crate::engine::require_feeds(&self.spec, self.books.is_some(), self.derivs.is_some())?;
54 let n = self.candles.len();
55 if n == 0 {
56 return Err(BacktestError::InvalidData("no candles".into()));
57 }
58 let check = |name: &str, len: Option<usize>| -> Result<()> {
59 match len {
60 Some(l) if l != n => Err(BacktestError::InvalidData(format!(
61 "{name} feed length {l} does not match {n} candles"
62 ))),
63 _ => Ok(()),
64 }
65 };
66 check("reference", self.reference.as_ref().map(Vec::len))?;
67 check("derivs", self.derivs.as_ref().map(Vec::len))?;
68 check("books", self.books.as_ref().map(Vec::len))?;
69 check("trades", self.trades.as_ref().map(Vec::len))?;
70 check("sections", self.sections.as_ref().map(Vec::len))?;
71
72 let mut bt = StreamingBacktest::new(&self.spec, self.capital)?;
73 for (i, candle) in self.candles.iter().enumerate() {
74 let feeds = Feeds {
75 reference: self.reference.as_ref().map(|r| r[i].close),
76 deriv: self.derivs.as_ref().map(|d| &d[i]),
77 orderbook: self.books.as_ref().map(|b| &b[i]),
78 trades: self.trades.as_ref().map(|t| t[i].as_slice()),
79 cross_section: self.sections.as_ref().map(|s| &s[i]),
80 };
81 bt.step_with_feeds(candle, &feeds)?;
82 }
83 Ok(bt.finish())
84 }
85}
86
87pub fn run_json(request_json: &str) -> Result<String> {
90 let req: RunRequest = serde_json::from_str(request_json)
91 .map_err(|e| BacktestError::InvalidSpec(e.to_string()))?;
92 let report = req.run()?;
93 serde_json::to_string(&report).map_err(|e| BacktestError::InvalidData(e.to_string()))
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn run_json_matches_plain_run() {
102 let request = r#"{
103 "capital": 1000.0,
104 "spec": {"symbol":"x","timeframe":"1h","indicators":{},
105 "entry":{"gt":[{"price":"close"},100]},
106 "exit":{"lt":[{"price":"close"},100]},
107 "sizing":{"type":"fixed_qty","qty":1}},
108 "candles": [
109 {"time":0,"open":100,"high":101,"low":100,"close":101},
110 {"time":1,"open":102,"high":103,"low":102,"close":103},
111 {"time":2,"open":104,"high":104,"low":99,"close":99},
112 {"time":3,"open":98,"high":98,"low":97,"close":97}
113 ]
114 }"#;
115 let json = run_json(request).unwrap();
116 assert!(json.contains("\"num_trades\":1"));
117 assert!(json.contains("\"entry_price\":102.0"));
118 assert!(json.contains("\"exit_price\":98.0"));
119 }
120
121 #[test]
122 fn run_json_threads_a_derivatives_feed() {
123 let request = r#"{
124 "spec": {"symbol":"x","timeframe":"1h",
125 "indicators":{"f":{"type":"FundingRate","params":[]}},
126 "entry":{"gt":["f",0.0]},"exit":{"lt":["f",-1.0]},
127 "sizing":{"type":"fixed_qty","qty":1}},
128 "candles": [
129 {"time":0,"open":100,"high":100,"low":100,"close":100},
130 {"time":1,"open":100,"high":100,"low":100,"close":100},
131 {"time":2,"open":100,"high":100,"low":100,"close":100}
132 ],
133 "derivs": [
134 {"funding_rate":0.01,"mark_price":100,"index_price":100,"futures_price":100,"open_interest":1000,"long_size":600,"short_size":400,"taker_buy_volume":50,"taker_sell_volume":40,"long_liquidation":0,"short_liquidation":0},
135 {"funding_rate":0.01,"mark_price":100,"index_price":100,"futures_price":100,"open_interest":1000,"long_size":600,"short_size":400,"taker_buy_volume":50,"taker_sell_volume":40,"long_liquidation":0,"short_liquidation":0},
136 {"funding_rate":0.01,"mark_price":100,"index_price":100,"futures_price":100,"open_interest":1000,"long_size":600,"short_size":400,"taker_buy_volume":50,"taker_sell_volume":40,"long_liquidation":0,"short_liquidation":0}
137 ]
138 }"#;
139 let report = run_json(request).unwrap();
140 assert!(report.contains("\"num_trades\":1"));
141 }
142
143 #[test]
144 fn run_json_rejects_feed_length_mismatch() {
145 let request = r#"{
146 "spec": {"symbol":"x","timeframe":"1h","indicators":{},
147 "entry":{"gt":[{"price":"close"},0]},"exit":{"in_position":true},
148 "sizing":{"type":"fixed_qty","qty":1}},
149 "candles": [{"time":0,"open":1,"high":1,"low":1,"close":1}],
150 "trades": []
151 }"#;
152 assert!(run_json(request).is_err());
153 }
154}
155
156#[derive(Debug, Clone, Default, Deserialize)]
164pub struct StepFeeds {
165 #[serde(default)]
167 pub reference: Option<f64>,
168 #[serde(default)]
170 pub deriv: Option<DerivativesTick>,
171 #[serde(default)]
173 pub orderbook: Option<OrderBook>,
174 #[serde(default)]
176 pub trades: Option<Vec<TradePrint>>,
177 #[serde(default)]
179 pub cross_section: Option<CrossSection>,
180}
181
182impl StepFeeds {
183 #[must_use]
185 pub fn as_feeds(&self) -> Feeds<'_> {
186 Feeds {
187 reference: self.reference,
188 deriv: self.deriv.as_ref(),
189 orderbook: self.orderbook.as_ref(),
190 trades: self.trades.as_deref(),
191 cross_section: self.cross_section.as_ref(),
192 }
193 }
194}
195
196#[derive(Debug, Clone, Deserialize)]
203pub struct StepRequest {
204 pub candle: Candle,
206 #[serde(default)]
208 pub feeds: StepFeeds,
209}