Skip to main content

finance_data/
surface.rs

1//! Library-owned runtime surface for `finance-data`.
2
3use 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
11/// Returns the package surface exposed by every transport wrapper.
12pub 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
58/// Runs one library-owned operation.
59pub 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}