optionchain_simulator 0.2.8

OptionChain-Simulator is a lightweight REST API service that simulates an evolving option chain with every request. It is designed for developers building or testing trading systems, backtesters, and visual tools that depend on option data streams but want to avoid relying on live data feeds.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
//! The `greeks` query parameter and the payload it selects.
//!
//! A chain response carries implied volatility, gamma and per-side delta by
//! default and nothing else. The full set is twelve values per option style,
//! and a chain can be 1 001 strikes wide across 512 expirations, so emitting it
//! unconditionally would multiply the payload a play-loop client pulls every
//! few hundred milliseconds. It is therefore opt-in, and the default is exactly
//! what clients get today.
//!
//! # Levels
//!
//! | `greeks` | What the quote carries |
//! |---|---|
//! | absent or `none` | Today's fields only. No `greeks` key at all |
//! | `first` | `theta`, `vega`, `rho`, `rho_d` |
//! | `all` | The full twelve-value [`GreeksSnapshot`] |
//!
//! # Sign and size
//!
//! Every emitted greek is **per one long contract**, with one exception. For
//! the eleven that scale, upstream builds the snapshot through
//! `get_option(Side::Long, style)`, and since optionstratlib 0.20 the `Greeks`
//! trait applies the `Side` sign in *every* greek rather than only in `delta` —
//! so a consumer that applies the position sign again would double-count it.
//! The client applies position sign and size exactly once, to those values.
//!
//! **`alpha` is not one of them.** It is the ratio `gamma / theta`, so a short
//! position negates both and the ratio is unchanged; upstream says so in as many
//! words. Scaling it by quantity, or flipping it with the position sign, gives a
//! client a number that means nothing. Carry it through as it arrives.
//!
//! An absent `rho`, `rho_d` or `alpha` means **not meaningful for these
//! inputs**, never zero; upstream normalises `alpha` to `None` where it would
//! otherwise be `Decimal::MAX`.

use crate::utils::ChainError;
use optionstratlib::chains::OptionData;
use optionstratlib::greeks::GreeksSnapshot;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

/// How much of the greek set a chain response should carry.
///
/// Parsed from the `greeks` query parameter by [`GreekLevel::parse`]; the
/// default is [`GreekLevel::None`], which is the pre-existing response.
/// Serialised and schema-bearing so the OpenAPI document can publish the
/// parameter as a closed enum rather than an unconstrained string: a generated
/// client then cannot send `?greeks=second` at all, and a reader of the document
/// sees the three values without reading prose. The wire form is still parsed by
/// [`GreekLevel::parse`] from a raw string, so an unknown value stays a typed
/// `400` naming the field rather than actix's untyped rejection.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GreekLevel {
    /// Implied volatility, gamma and per-side delta only: the default, and
    /// byte-identical to the response before the parameter existed.
    None,
    /// Adds the remaining first-order greeks: `theta`, `vega`, `rho`, `rho_d`.
    First,
    /// The full twelve-value snapshot per option style.
    All,
}

impl GreekLevel {
    /// Parses the raw `greeks` query value.
    ///
    /// An absent parameter is [`GreekLevel::None`], so a client that has never
    /// heard of it keeps its current payload.
    ///
    /// # Errors
    ///
    /// Returns [`ChainError::Validation`] naming `greeks` for any other value.
    /// An unrecognised level is rejected rather than silently downgraded: a
    /// client that asked for `all` and quietly received the default would
    /// price a position against greeks it never got.
    pub(crate) fn parse(raw: Option<&str>) -> Result<Self, ChainError> {
        match raw {
            None => Ok(Self::None),
            Some(value) => match value.trim() {
                "none" => Ok(Self::None),
                "first" => Ok(Self::First),
                "all" => Ok(Self::All),
                other => Err(ChainError::Validation {
                    field: "greeks".to_string(),
                    reason: format!("unknown greek level '{other}'; expected none, first or all"),
                }),
            },
        }
    }

    /// Whether this level needs the option's greek snapshots at all.
    #[must_use]
    #[inline]
    pub(crate) fn wants_greeks(self) -> bool {
        !matches!(self, Self::None)
    }
}

/// The first-order greeks the default response does not already carry.
///
/// `delta` is deliberately absent: it is already on the quote, and repeating it
/// under a second key would let the two drift. `gamma` is likewise already on
/// the contract.
///
/// Closed with `deny_unknown_fields`, which does two jobs at once: it makes
/// serde's untagged resolution independent of variant order, and utoipa derives
/// `additionalProperties: false` from it. That is what keeps the published
/// `oneOf` satisfiable — without it a full twelve-value payload would satisfy
/// this four-field shape as well, and "exactly one branch" would fail for
/// every `greeks=all` response.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, ToSchema)]
#[serde(deny_unknown_fields)]
pub struct FirstOrderGreeks {
    /// Sensitivity to the passage of time, per one long contract.
    pub theta: Option<f64>,
    /// Sensitivity to implied volatility, per one long contract.
    pub vega: Option<f64>,
    /// Sensitivity to the risk-free rate. `null` means not meaningful for
    /// these inputs, never zero.
    pub rho: Option<f64>,
    /// Sensitivity to the dividend yield. `null` means not meaningful for
    /// these inputs, never zero.
    pub rho_d: Option<f64>,
}

impl From<&GreeksSnapshot> for FirstOrderGreeks {
    fn from(snapshot: &GreeksSnapshot) -> Self {
        Self {
            theta: to_f64(snapshot.theta),
            vega: to_f64(snapshot.vega),
            rho: snapshot.rho.and_then(to_f64),
            rho_d: snapshot.rho_d.and_then(to_f64),
        }
    }
}

/// The full twelve-value greek set of one option style.
///
/// A LOCAL type, not upstream's `GreeksSnapshot`. `CLAUDE.md` is binding that
/// the REST DTOs speak `f64` with the conversion happening exactly once at the
/// boundary; carrying the upstream struct would have made its `Decimal`
/// serialisation — JSON strings, and whatever field set the next release
/// carries — part of this service's public contract by accident rather than by
/// decision.
///
/// The cost is a twelve-field mirror that has to track upstream. That cost is
/// paid deliberately, and [`FullGreeks::from`] destructures the snapshot so a
/// thirteenth greek is a compile error here rather than a field the API
/// silently stops carrying.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, ToSchema)]
#[serde(deny_unknown_fields)]
pub struct FullGreeks {
    /// Sensitivity to the underlying price, per one long contract.
    pub delta: Option<f64>,
    /// Rate of change of delta, per one long contract.
    pub gamma: Option<f64>,
    /// Sensitivity to the passage of time, per one long contract.
    pub theta: Option<f64>,
    /// Sensitivity to implied volatility, per one long contract.
    pub vega: Option<f64>,
    /// Sensitivity to the risk-free rate. `null` means not meaningful.
    pub rho: Option<f64>,
    /// Sensitivity to the dividend yield. `null` means not meaningful.
    pub rho_d: Option<f64>,
    /// The ratio `gamma / theta`. **Does not scale with position sign or
    /// size** — a short position negates both terms and leaves the ratio
    /// unchanged. `null` means not meaningful. See the module docs.
    pub alpha: Option<f64>,
    /// Rate of change of delta with volatility, per one long contract.
    pub vanna: Option<f64>,
    /// Rate of change of vega with volatility, per one long contract.
    pub vomma: Option<f64>,
    /// Rate of change of vega with time, per one long contract.
    pub veta: Option<f64>,
    /// Rate of change of delta with time, per one long contract.
    pub charm: Option<f64>,
    /// Rate of change of gamma with time, per one long contract.
    pub color: Option<f64>,
}

impl From<&GreeksSnapshot> for FullGreeks {
    fn from(snapshot: &GreeksSnapshot) -> Self {
        // Destructured, not field-accessed: a thirteenth upstream greek is then
        // a COMPILE ERROR rather than a value this DTO silently stops carrying.
        // Same discipline `ApiWalkType` uses for a new `WalkType` variant.
        let GreeksSnapshot {
            delta,
            gamma,
            theta,
            vega,
            rho,
            rho_d,
            alpha,
            vanna,
            vomma,
            veta,
            charm,
            color,
        } = snapshot;
        Self {
            delta: to_f64(*delta),
            gamma: to_f64(*gamma),
            theta: to_f64(*theta),
            vega: to_f64(*vega),
            rho: rho.and_then(to_f64),
            rho_d: rho_d.and_then(to_f64),
            alpha: alpha.and_then(to_f64),
            vanna: to_f64(*vanna),
            vomma: to_f64(*vomma),
            veta: to_f64(*veta),
            charm: to_f64(*charm),
            color: to_f64(*color),
        }
    }
}

/// The one `Decimal` to `f64` conversion on this boundary.
///
/// `None` only if a value were outside `f64`'s range, which no `Decimal` is —
/// its maximum is about `7.9e28`. Written as an `Option` rather than an
/// `unwrap` because `rules/global_rules.md` allows neither on a request path.
#[must_use]
#[inline]
fn to_f64(value: Decimal) -> Option<f64> {
    use rust_decimal::prelude::ToPrimitive;
    value.to_f64()
}

/// The greek payload one quoted side carries, shaped by the requested level.
///
/// Serialised untagged, so the `greeks` key is a plain object at both levels
/// and a client parses it by the fields it finds rather than by a discriminant.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, ToSchema)]
#[serde(untagged)]
pub enum GreeksResponse {
    /// `greeks=all`: the full twelve-value set.
    ///
    /// Listed first so deserialisation prefers it — an untagged enum takes the
    /// first variant that matches, and the four-field variant would otherwise
    /// swallow a complete payload by ignoring the rest.
    Full(FullGreeks),
    /// `greeks=first`: the remaining first-order greeks.
    FirstOrder(FirstOrderGreeks),
}

impl GreeksResponse {
    /// Shapes an upstream snapshot to the requested level.
    ///
    /// Returns `None` at [`GreekLevel::None`], which is what keeps the default
    /// response free of the key entirely.
    #[must_use]
    fn from_snapshot(snapshot: &GreeksSnapshot, level: GreekLevel) -> Option<Self> {
        match level {
            GreekLevel::None => None,
            GreekLevel::First => Some(Self::FirstOrder(FirstOrderGreeks::from(snapshot))),
            GreekLevel::All => Some(Self::Full(FullGreeks::from(snapshot))),
        }
    }
}

/// Runs a rendering job off the runtime and under the shared pricing bound when
/// the level makes it expensive.
///
/// At [`GreekLevel::None`] there is nothing to price, so the work stays inline
/// and takes no permit — the default request must not queue behind a burst of
/// greek requests.
///
/// The bound itself lives in [`crate::utils::admission`], because the v2 session
/// manager prices snapshots against the same cores and a bound only one of them
/// respects is not a bound.
///
/// # Errors
///
/// As [`crate::utils::admission::admit_blocking`].
pub(crate) async fn admit_render<T, F>(level: GreekLevel, job: F) -> Result<T, ChainError>
where
    F: FnOnce() -> Result<T, ChainError> + Send + 'static,
    T: Send + 'static,
{
    if !level.wants_greeks() {
        return job();
    }
    crate::utils::admission::admit_blocking(job).await
}

/// Renders one response body under [`admit_render`].
///
/// # Errors
///
/// As [`admit_render`], plus [`ChainError::Internal`] when the response cannot
/// be serialised.
pub(crate) async fn render_body<T, F>(level: GreekLevel, render: F) -> Result<Vec<u8>, ChainError>
where
    F: FnOnce() -> T + Send + 'static,
    T: serde::Serialize + Send + 'static,
{
    admit_render(level, move || serialize_body(&render())).await
}

/// Serialises a response body, reporting a failure as an internal error rather
/// than panicking on a request path.
///
/// # Errors
///
/// Returns [`ChainError::Internal`] when the value cannot be serialised.
pub(crate) fn serialize_body<T: serde::Serialize>(value: &T) -> Result<Vec<u8>, ChainError> {
    serde_json::to_vec(value)
        .map_err(|error| ChainError::Internal(format!("failed to encode the response: {error}")))
}

/// The greek payloads for both sides of one strike, at the requested level.
///
/// Either branch can be the normal one, depending on the deployment, and the
/// difference is issue #74's:
///
/// * **v2 with a warehouse registered** builds its chains with the snapshots
///   already on, because a filed step has to carry what a replayed one does.
///   This function then just *reads* them, and costs nothing.
/// * **v2 without a warehouse, and v1 always**, build without them, so this
///   function computes them through upstream's own `calculate_greeks` — and
///   only for the requests that ask.
///
/// No greek mathematics lives in this crate either way.
///
/// # An absent payload is not a zero
///
/// Upstream returns no snapshot for a strike whose option cannot be built, and
/// only logs it at `debug`. Such a strike arrives with **no `greeks` key at
/// all**, which on the wire is indistinguishable from the default level. A
/// client that asked for a level and found the key missing on some strikes is
/// looking at degenerate strikes, not at a downgraded response — the existing
/// `implied_volatility`, `gamma` and `delta` mirrors are still there, because
/// they are defined where the full set is not.
///
/// # Cost
///
/// Nothing, when the chain already carries the snapshots. Roughly 40 µs per
/// contract in a release build when it does not. `first` and `all` cost the
/// same either way: upstream builds the snapshot whole and the level only
/// decides what is written out.
///
/// Computing here is measurably more expensive per contract than asking the
/// builder for it — `calculate_greeks` recomputes the delta and gamma this
/// response then reads from the mirrors instead, where a build with snapshots
/// on costs about 1.54x a plain one. Which is cheaper overall depends entirely
/// on how often the payload is actually read, which is why the decision sits
/// with the caller that knows: `SeriesBuilder::with_greek_snapshots`.
///
/// Callers must keep this off the async runtime. Both versions render above the
/// default level inside `spawn_blocking`, because
/// `DEFAULT_MAX_SNAPSHOT_CONTRACTS` is 200 000 and seconds of uninterrupted CPU
/// on a worker would stall every other request that worker holds.
///
/// # Why a clone
///
/// `calculate_greeks` takes `&mut self` and this function only has `&OptionData`
/// — the DTO layer has no business mutating the snapshot it was handed to
/// render. The clone is one option, not the chain, and is unmeasurable next to
/// the pricing itself.
#[must_use]
pub(crate) fn greeks_for(
    data: &OptionData,
    level: GreekLevel,
) -> (Option<GreeksResponse>, Option<GreeksResponse>) {
    if !level.wants_greeks() {
        return (None, None);
    }

    if data.greeks_call.is_some() || data.greeks_put.is_some() {
        return (
            data.greeks_call
                .as_ref()
                .and_then(|snapshot| GreeksResponse::from_snapshot(snapshot, level)),
            data.greeks_put
                .as_ref()
                .and_then(|snapshot| GreeksResponse::from_snapshot(snapshot, level)),
        );
    }

    let mut priced = data.clone();
    priced.calculate_greeks();
    (
        priced
            .greeks_call
            .as_ref()
            .and_then(|snapshot| GreeksResponse::from_snapshot(snapshot, level)),
        priced
            .greeks_put
            .as_ref()
            .and_then(|snapshot| GreeksResponse::from_snapshot(snapshot, level)),
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    use optionstratlib::ExpirationDate;
    use optionstratlib::chains::chain::OptionChain;
    use optionstratlib::chains::{OptionChainBuildParams, utils::OptionDataPriceParams};
    use positive::{Positive, pos_or_panic};
    use rust_decimal_macros::dec;

    /// A one-strike chain, optionally built with the greek snapshots already
    /// populated. `with_greek_snapshots(true)` is the path no handler takes
    /// today, which is exactly why the branch that reads it needs a test.
    fn fixture_chain(prepopulated: bool) -> OptionChain {
        let price_params = OptionDataPriceParams::new(
            Some(Box::new(pos_or_panic!(100.0))),
            Some(ExpirationDate::Days(pos_or_panic!(30.0))),
            Some(dec!(0.04)),
            Some(pos_or_panic!(0.015)),
            Some("AAPL".to_string()),
        );
        let build_params = OptionChainBuildParams::new(
            "AAPL".to_string(),
            Some(Positive::ONE),
            1,
            Some(pos_or_panic!(5.0)),
            dec!(-0.2),
            dec!(0.5),
            pos_or_panic!(0.02),
            2,
            price_params,
            pos_or_panic!(0.2),
        )
        .with_greek_snapshots(prepopulated);

        match OptionChain::build_chain(&build_params) {
            Ok(chain) => chain,
            Err(error) => panic!("the fixture chain must build: {error}"),
        }
    }

    /// The first strike of a fixture chain.
    fn fixture_option(prepopulated: bool) -> optionstratlib::chains::OptionData {
        let chain = fixture_chain(prepopulated);
        match chain.iter().next() {
            Some(data) => data.clone(),
            None => panic!("the fixture chain must carry a strike"),
        }
    }

    /// The default level does no work at all: no clone, no pricing, no key.
    #[test]
    fn test_greeks_for_returns_nothing_at_the_default_level() {
        let data = fixture_option(false);

        assert_eq!(greeks_for(&data, GreekLevel::None), (None, None));
    }

    /// The path every request actually takes: the chain carries no snapshots,
    /// so this function prices them.
    #[test]
    fn test_greeks_for_prices_an_option_that_carries_no_snapshots() {
        let data = fixture_option(false);
        assert!(
            data.greeks_call.is_none() && data.greeks_put.is_none(),
            "the fixture must start without snapshots, or this tests nothing"
        );

        let (call, put) = greeks_for(&data, GreekLevel::All);

        assert!(matches!(call, Some(GreeksResponse::Full(_))));
        assert!(matches!(put, Some(GreeksResponse::Full(_))));
        // Pricing happens on a clone: the caller's option is untouched, which
        // is what lets a shared snapshot be rendered at different levels by
        // different clients.
        assert!(data.greeks_call.is_none() && data.greeks_put.is_none());
    }

    /// The other branch: a chain built WITH snapshots is read rather than
    /// repriced, and yields the same values.
    #[test]
    fn test_greeks_for_reads_snapshots_a_chain_already_carries() {
        let prepopulated = fixture_option(true);
        assert!(
            prepopulated.greeks_call.is_some(),
            "with_greek_snapshots must populate the call snapshot"
        );

        let (read_call, read_put) = greeks_for(&prepopulated, GreekLevel::All);
        let (priced_call, priced_put) = greeks_for(&fixture_option(false), GreekLevel::All);

        assert_eq!(read_call, priced_call, "reading must equal pricing");
        assert_eq!(read_put, priced_put, "reading must equal pricing");
    }

    /// The level selects the shape, not the computation: `first` projects the
    /// same snapshot the `all` payload carries in full.
    #[test]
    fn test_greeks_for_projects_the_first_order_subset() {
        let data = fixture_option(false);

        let (first, _) = greeks_for(&data, GreekLevel::First);
        let (all, _) = greeks_for(&data, GreekLevel::All);

        match (first, all) {
            (Some(GreeksResponse::FirstOrder(subset)), Some(GreeksResponse::Full(full))) => {
                assert_eq!(subset.theta, full.theta);
                assert_eq!(subset.vega, full.vega);
                assert_eq!(subset.rho, full.rho);
                assert_eq!(subset.rho_d, full.rho_d);
            }
            other => panic!("each level must yield its own variant, got {other:?}"),
        }
    }

    #[test]
    fn test_parse_absent_parameter_is_none() {
        match GreekLevel::parse(None) {
            Ok(level) => assert_eq!(level, GreekLevel::None),
            Err(error) => panic!("an absent parameter must parse: {error}"),
        }
    }

    #[test]
    fn test_parse_accepts_the_three_documented_levels() {
        for (raw, expected) in [
            ("none", GreekLevel::None),
            ("first", GreekLevel::First),
            ("all", GreekLevel::All),
        ] {
            match GreekLevel::parse(Some(raw)) {
                Ok(level) => assert_eq!(level, expected, "for {raw}"),
                Err(error) => panic!("{raw} must parse: {error}"),
            }
        }
    }

    #[test]
    fn test_parse_trims_surrounding_whitespace() {
        match GreekLevel::parse(Some("  all  ")) {
            Ok(level) => assert_eq!(level, GreekLevel::All),
            Err(error) => panic!("a padded value must parse: {error}"),
        }
    }

    /// An unknown level is a rejection, not a silent downgrade: a client that
    /// asked for `all` and got the default would price against greeks it never
    /// received.
    #[test]
    fn test_parse_rejects_an_unknown_level_naming_the_field() {
        match GreekLevel::parse(Some("second")) {
            Ok(level) => panic!("an unknown level must be rejected, got {level:?}"),
            Err(ChainError::Validation { field, reason }) => {
                assert_eq!(field, "greeks");
                assert!(
                    reason.contains("second"),
                    "the reason must quote the offending value, got {reason}"
                );
            }
            Err(other) => panic!("expected a validation failure, got {other:?}"),
        }
    }

    /// Case matters: the parameter is a fixed vocabulary, not a free-form
    /// string, and accepting `ALL` here would leave `All` and `all` to diverge
    /// the day the set grows.
    #[test]
    fn test_parse_rejects_a_differently_cased_level() {
        assert!(GreekLevel::parse(Some("ALL")).is_err());
        assert!(GreekLevel::parse(Some("First")).is_err());
    }

    #[test]
    fn test_wants_greeks_is_false_only_for_none() {
        assert!(!GreekLevel::None.wants_greeks());
        assert!(GreekLevel::First.wants_greeks());
        assert!(GreekLevel::All.wants_greeks());
    }
}