1use runtime_core::{
4 describe_surface_response, structured_surface_response, surface_operation, OperationId,
5 PackageSurface, RuntimeCapabilities, SurfaceRequest, SurfaceResponse,
6};
7use serde::Deserialize;
8
9use crate::{FinanceSeries, FinanceSeriesIndex, RiskSummaryOptions};
10
11pub fn package_surface() -> PackageSurface {
13 PackageSurface {
14 library: env!("CARGO_PKG_NAME").to_string(),
15 version: env!("CARGO_PKG_VERSION").to_string(),
16 capabilities: RuntimeCapabilities::pure_rust(),
17 operations: vec![
18 surface_operation(
19 "describe",
20 "Describe package",
21 "Provider-neutral financial market data validation, indexing, and derived series operations.",
22 serde_json::json!({"includeOperations": true}),
23 ),
24 surface_operation(
25 "financeData.bounds",
26 "Finance data bounds",
27 "Returns the timestamp and price bounds for an inline OHLCV series.",
28 serde_json::json!({"series": example_series()}),
29 ),
30 surface_operation(
31 "financeData.barsInRange",
32 "Finance bars in range",
33 "Returns validated OHLCV bars whose timestamps fall inside an inclusive range.",
34 serde_json::json!({"series": example_series(), "startMs": 2, "endMs": 3}),
35 ),
36 surface_operation(
37 "financeData.downsampleOhlcv",
38 "Downsample OHLCV",
39 "Buckets validated OHLCV bars while preserving open, high, low, close, volume, and adjusted close semantics.",
40 serde_json::json!({"series": example_series(), "startMs": 1, "endMs": 4, "targetCount": 2}),
41 ),
42 surface_operation(
43 "financeData.returns",
44 "Finance data returns",
45 "Computes simple or log returns from close or adjusted close prices.",
46 serde_json::json!({"series": example_series(), "method": "simple", "adjusted": false}),
47 ),
48 surface_operation(
49 "financeData.riskSummary",
50 "Finance data risk summary",
51 "Computes return, volatility, ratio, VaR/CVaR, and drawdown summary values from an inline OHLCV series.",
52 serde_json::json!({"series": example_series(), "adjusted": false, "periodsPerYear": 252.0, "confidence": 0.95}),
53 ),
54 ],
55 }
56}
57
58pub fn run_surface_operation(request: SurfaceRequest) -> Result<SurfaceResponse, String> {
60 let surface = package_surface();
61 match request.operation.as_str() {
62 "describe" => Ok(describe_surface_response(&surface, request)),
63 "financeData.bounds" => {
64 let input: SeriesRequest = parse_input(request.input)?;
65 let index = index(input.series)?;
66 let bounds = index.bounds();
67 Ok(response(
68 request.operation,
69 "Finance data bounds",
70 serde_json::json!({
71 "barCount": index.series().bars.len(),
72 "hasBounds": bounds.is_some()
73 }),
74 serde_json::json!({
75 "instrument": index.series().instrument,
76 "barCount": index.series().bars.len(),
77 "bounds": bounds
78 }),
79 ))
80 }
81 "financeData.barsInRange" => {
82 let input: BarsInRangeRequest = parse_input(request.input)?;
83 let index = index(input.series)?;
84 let bars = index.bars_in_range(input.start_ms, input.end_ms);
85 Ok(response(
86 request.operation,
87 "Finance bars in range",
88 serde_json::json!({
89 "startMs": input.start_ms,
90 "endMs": input.end_ms,
91 "barCount": bars.len()
92 }),
93 serde_json::json!({
94 "startMs": input.start_ms,
95 "endMs": input.end_ms,
96 "bars": bars
97 }),
98 ))
99 }
100 "financeData.downsampleOhlcv" => {
101 let input: DownsampleRequest = parse_input(request.input)?;
102 let index = index(input.series)?;
103 let bars = index
104 .downsample_ohlcv(input.start_ms, input.end_ms, input.target_count)
105 .map_err(|error| error.to_string())?;
106 Ok(response(
107 request.operation,
108 "Downsample OHLCV",
109 serde_json::json!({
110 "startMs": input.start_ms,
111 "endMs": input.end_ms,
112 "targetCount": input.target_count,
113 "barCount": bars.len()
114 }),
115 serde_json::json!({
116 "startMs": input.start_ms,
117 "endMs": input.end_ms,
118 "targetCount": input.target_count,
119 "bars": bars
120 }),
121 ))
122 }
123 "financeData.returns" => {
124 let input: ReturnsRequest = parse_input(request.input)?;
125 let index = index(input.series)?;
126 let returns = match input.method.as_str() {
127 "simple" => index.simple_returns(input.adjusted),
128 "log" => index.log_returns(input.adjusted),
129 method => return Err(format!("unsupported returns method `{method}`")),
130 }
131 .map_err(|error| error.to_string())?;
132 Ok(response(
133 request.operation,
134 "Finance data returns",
135 serde_json::json!({
136 "method": input.method,
137 "adjusted": input.adjusted,
138 "returnCount": returns.len()
139 }),
140 serde_json::json!({
141 "method": input.method,
142 "adjusted": input.adjusted,
143 "returns": returns
144 }),
145 ))
146 }
147 "financeData.riskSummary" => {
148 let input: RiskSummaryRequest = parse_input(request.input)?;
149 let index = index(input.series)?;
150 let options = RiskSummaryOptions {
151 adjusted: input.adjusted,
152 periods_per_year: input.periods_per_year,
153 confidence: input.confidence,
154 risk_free_return_per_period: input.risk_free_return_per_period,
155 };
156 let risk = index
157 .risk_summary(options)
158 .map_err(|error| error.to_string())?;
159 Ok(response(
160 request.operation,
161 "Finance data risk summary",
162 serde_json::json!({
163 "adjusted": options.adjusted,
164 "periodsPerYear": options.periods_per_year,
165 "confidence": options.confidence,
166 "valueAtRisk": risk.value_at_risk,
167 "maxDrawdownDepth": risk.max_drawdown.depth
168 }),
169 serde_json::json!({
170 "options": options,
171 "risk": risk
172 }),
173 ))
174 }
175 operation => Err(format!(
176 "unsupported operation `{operation}` for {}",
177 env!("CARGO_PKG_NAME")
178 )),
179 }
180}
181
182#[derive(Debug, Deserialize)]
183#[serde(rename_all = "camelCase")]
184struct SeriesRequest {
185 series: FinanceSeries,
186}
187
188#[derive(Debug, Deserialize)]
189#[serde(rename_all = "camelCase")]
190struct BarsInRangeRequest {
191 series: FinanceSeries,
192 start_ms: i64,
193 end_ms: i64,
194}
195
196#[derive(Debug, Deserialize)]
197#[serde(rename_all = "camelCase")]
198struct DownsampleRequest {
199 series: FinanceSeries,
200 start_ms: i64,
201 end_ms: i64,
202 target_count: usize,
203}
204
205#[derive(Debug, Deserialize)]
206#[serde(rename_all = "camelCase")]
207struct ReturnsRequest {
208 series: FinanceSeries,
209 #[serde(default)]
210 adjusted: bool,
211 #[serde(default = "default_returns_method")]
212 method: String,
213}
214
215#[derive(Debug, Deserialize)]
216#[serde(rename_all = "camelCase")]
217struct RiskSummaryRequest {
218 series: FinanceSeries,
219 #[serde(default)]
220 adjusted: bool,
221 #[serde(default = "default_periods_per_year")]
222 periods_per_year: f64,
223 #[serde(default = "default_confidence")]
224 confidence: f64,
225 #[serde(default)]
226 risk_free_return_per_period: f64,
227}
228
229fn response(
230 operation: OperationId,
231 title: &str,
232 summary: serde_json::Value,
233 result: serde_json::Value,
234) -> SurfaceResponse {
235 structured_surface_response(
236 operation,
237 title,
238 format!("Ran package-surface operation `{title}`."),
239 summary,
240 result,
241 )
242}
243
244fn index(series: FinanceSeries) -> Result<FinanceSeriesIndex, String> {
245 FinanceSeriesIndex::new(series).map_err(|error| error.to_string())
246}
247
248fn parse_input<T: for<'de> Deserialize<'de>>(input: serde_json::Value) -> Result<T, String> {
249 serde_json::from_value(input).map_err(|error| format!("invalid request: {error}"))
250}
251
252fn default_returns_method() -> String {
253 "simple".to_string()
254}
255
256fn default_periods_per_year() -> f64 {
257 252.0
258}
259
260fn default_confidence() -> f64 {
261 0.95
262}
263
264fn example_series() -> serde_json::Value {
265 serde_json::json!({
266 "instrument": {
267 "id": "aapl",
268 "symbol": "AAPL",
269 "name": "Apple Inc.",
270 "exchange": "NASDAQ",
271 "currency": "USD",
272 "assetClass": "equity"
273 },
274 "bars": [
275 {"timestampMs": 1, "open": 100.0, "high": 110.0, "low": 99.0, "close": 108.0, "volume": 10.0, "adjustedClose": 107.0},
276 {"timestampMs": 2, "open": 108.0, "high": 112.0, "low": 105.0, "close": 106.0, "volume": 11.0, "adjustedClose": 105.0},
277 {"timestampMs": 3, "open": 106.0, "high": 109.0, "low": 101.0, "close": 102.0, "volume": 12.0, "adjustedClose": 101.0},
278 {"timestampMs": 4, "open": 102.0, "high": 120.0, "low": 100.0, "close": 118.0, "volume": 13.0, "adjustedClose": 117.0}
279 ]
280 })
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 fn run(operation: &str) -> SurfaceResponse {
288 let surface = package_surface();
289 let request = surface
290 .operations
291 .iter()
292 .find(|candidate| candidate.id.as_str() == operation)
293 .expect("operation")
294 .example_request
295 .clone();
296 run_surface_operation(SurfaceRequest {
297 operation: OperationId::new(operation),
298 input: request,
299 })
300 .expect("surface operation")
301 }
302
303 #[test]
304 fn package_surface_lists_finance_data_operations() {
305 let ids = package_surface()
306 .operations
307 .into_iter()
308 .map(|operation| operation.id.0)
309 .collect::<Vec<_>>();
310 assert!(ids.contains(&"financeData.bounds".to_string()));
311 assert!(ids.contains(&"financeData.barsInRange".to_string()));
312 assert!(ids.contains(&"financeData.downsampleOhlcv".to_string()));
313 assert!(ids.contains(&"financeData.returns".to_string()));
314 assert!(ids.contains(&"financeData.riskSummary".to_string()));
315 }
316
317 #[test]
318 fn examples_return_structured_values() {
319 for operation in [
320 "describe",
321 "financeData.bounds",
322 "financeData.barsInRange",
323 "financeData.downsampleOhlcv",
324 "financeData.returns",
325 "financeData.riskSummary",
326 ] {
327 let response = run(operation);
328 assert_eq!(response.operation.as_str(), operation);
329 assert_eq!(response.value["operation"], operation);
330 assert!(response.value["title"].is_string());
331 assert!(response.value["summary"].is_object());
332 assert!(response.value["result"].is_object());
333 }
334 }
335}