From 4e1e290d0ee6c66a80d158fb60307a165257d724 Mon Sep 17 00:00:00 2001 From: Stephen Romano Date: Wed, 4 Nov 2026 21:26:28 -0500 Subject: [PATCH 1/2] feat(sdk): add scenario evaluation and spot-target solving --- README.md | 4 +- contracts/openapi.json | 5472 +++++++++++++-------- contracts/provenance.json | 10 +- docs/API.md | 6 + docs/CONTRACT.md | 22 +- docs/COST.md | 8 + docs/MODELS.md | 5 + docs/README.md | 1 + docs/ROADMAP.md | 10 + docs/SCENARIOS.md | 185 + docs/validation/2026-11-05-scenarios.json | 87 + examples/README.md | 8 + examples/scenario_evaluate.py | 84 + examples/solve_spot.py | 91 + examples/solve_spot_async.py | 92 + notebooks/README.md | 2 + notebooks/scenarios/evaluate.ipynb | 264 + notebooks/scenarios/solve-spot.ipynb | 272 + notebooks/start-here.ipynb | 2 +- scripts/check_notebook_container.py | 13 +- scripts/execute_notebook_fixture.py | 80 + scripts/generate_notebook.py | 4 + src/morphiq_sdk/__init__.py | 52 + src/morphiq_sdk/client.py | 57 +- src/morphiq_sdk/models.py | 188 + tests/conftest.py | 34 +- tests/fixtures/analytics.json | 7 +- tests/fixtures/batch.json | 7 +- tests/fixtures/greeks.json | 7 +- tests/fixtures/implied_volatility.json | 7 +- tests/fixtures/pricing_models.json | 32 +- tests/fixtures/scenario_quotes.json | 1336 +++++ tests/fixtures/scenarios.json | 3710 ++++++++++++++ tests/test_analytics.py | 4 +- tests/test_batch.py | 4 +- tests/test_examples.py | 11 +- tests/test_greeks.py | 4 +- tests/test_implied_volatility.py | 4 +- tests/test_scenario_notebooks.py | 87 + tests/test_scenarios.py | 391 ++ 40 files changed, 10538 insertions(+), 2126 deletions(-) create mode 100644 docs/SCENARIOS.md create mode 100644 docs/validation/2026-11-05-scenarios.json create mode 100644 examples/scenario_evaluate.py create mode 100644 examples/solve_spot.py create mode 100644 examples/solve_spot_async.py create mode 100644 notebooks/scenarios/evaluate.ipynb create mode 100644 notebooks/scenarios/solve-spot.ipynb create mode 100644 tests/fixtures/scenario_quotes.json create mode 100644 tests/fixtures/scenarios.json create mode 100644 tests/test_scenario_notebooks.py create mode 100644 tests/test_scenarios.py diff --git a/README.md b/README.md index 918da90..019f287 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,9 @@ valuation, sensitivity analysis and market-regime identification. See the [documentation index](https://github.com/MorphIQ-Labs/morphiq-sdk-python/blob/main/docs/README.md) for all feature guides and the [Python API reference](https://github.com/MorphIQ-Labs/morphiq-sdk-python/blob/main/docs/API.md) for methods, models and errors. -This client targets API **v0.5.0**, including public model discovery, +This branch includes candidate [scenario evaluation and spot solving](docs/SCENARIOS.md) +for API MR !209; SDK merge/release is held until the API is deployed. +Its existing baseline is API **v0.5.0**, including public model discovery, standard-five Greeks, cost utilities and spend caps. SDK 0.1.x lacks these additions; use the release containing this contract update or install this checkout. See [model capabilities and migration](https://github.com/MorphIQ-Labs/morphiq-sdk-python/blob/main/docs/MODELS.md): BSM/BS1993 support batches diff --git a/contracts/openapi.json b/contracts/openapi.json index 6d19cee..8f34ca3 100644 --- a/contracts/openapi.json +++ b/contracts/openapi.json @@ -4243,1648 +4243,1678 @@ } ] } - } - }, - "components": { - "schemas": { - "BatchFusedAnalyticsItemResult": { - "allOf": [ - { - "$ref": "#/components/schemas/BatchFusedAnalyticsOutcome" - }, + }, + "/v1/scenarios/evaluate": { + "post": { + "tags": [ + "scenarios" + ], + "description": "Reprice one option at explicit points using BSM or BS1993. JSON only. Requires price-batch entitlement. Base plus scenarios counts against the batch limit and is charged as one price batch. Validation rejects the entire request before reservation; numerical failures at individual points are returned in order. Base pricing failure fails the request. No target solver, portfolio aggregation or live market data is included.", + "operationId": "evaluateOptionScenarios", + "parameters": [ { - "type": "object", - "required": [ - "index" - ], - "properties": { - "index": { - "type": "integer", - "format": "int32", - "minimum": 0 - } + "name": "x-morphiq-max-cu", + "in": "header", + "description": "Optional maximum commercial CU charge for this whole request. An actual quote above this ceiling is refused before commitment with 409 cost_limit_exceeded and zero charged CUs. Applies equally to JSON, Protobuf and gRPC metadata. Zero is allowed; duplicate or malformed values are rejected.", + "required": false, + "schema": { + "type": "integer", + "maximum": 18446744073709551615, + "minimum": 0 } } ], - "description": "Ordered result with an explicit per-item failure alternative." - }, - "BatchFusedAnalyticsOutcome": { - "oneOf": [ - { - "type": "object", - "required": [ - "result", - "status" - ], - "properties": { - "result": { - "$ref": "#/components/schemas/FusedAnalyticsResult" - }, - "status": { - "type": "string", - "enum": [ - "success" - ] + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScenarioRequest" } } }, - { - "type": "object", - "required": [ - "error", - "status" - ], - "properties": { - "error": { - "$ref": "#/components/schemas/ErrorBody" - }, - "status": { - "type": "string", - "enum": [ - "error" - ] + "required": true + }, + "responses": { + "200": { + "description": "Base valuation and ordered scenario results", + "headers": { + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScenarioResponse" + } } } - } - ] - }, - "BatchFusedAnalyticsRequest": { - "type": "object", - "description": "Ordered request envelope; runtime applies the pinned plan limit.", - "required": [ - "requests" - ], - "properties": { - "requests": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FusedAnalyticsRequest" + }, + "400": { + "description": "Malformed request", + "headers": { + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" + } }, - "description": "One to 100 contracts; a lower account limit still applies.", - "maxItems": 100, - "minItems": 1 - } - }, - "additionalProperties": false - }, - "BatchFusedAnalyticsResponse": { - "type": "object", - "description": "One metadata block for the entire admitted batch.", - "required": [ - "metadata", - "results" - ], - "properties": { - "metadata": { - "$ref": "#/components/schemas/ResponseMetadata" + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } }, - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BatchFusedAnalyticsItemResult" + "401": { + "description": "Credential missing or invalid", + "headers": { + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" + } + }, + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } } - } - } - }, - "BatchGreeksAllTenItemResult": { - "allOf": [ - { - "$ref": "#/components/schemas/BatchGreeksAllTenOutcome" }, - { - "type": "object", - "required": [ - "index" - ], - "properties": { - "index": { - "type": "integer", - "format": "int32", - "minimum": 0 + "402": { + "description": "Funding exhausted", + "headers": { + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" + } + }, + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } } } - } - ], - "description": "Ordered result with an explicit per-item failure alternative." - }, - "BatchGreeksAllTenOutcome": { - "oneOf": [ - { - "type": "object", - "required": [ - "result", - "status" - ], - "properties": { - "result": { - "$ref": "#/components/schemas/GreeksAllTenResult" - }, - "status": { - "type": "string", - "enum": [ - "success" - ] + }, + "403": { + "description": "Price batch not permitted", + "headers": { + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" + } + }, + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } } } }, - { - "type": "object", - "required": [ - "error", - "status" - ], - "properties": { - "error": { - "$ref": "#/components/schemas/ErrorBody" - }, - "status": { - "type": "string", - "enum": [ - "error" - ] + "406": { + "description": "Response media type not acceptable", + "headers": { + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" + } + }, + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } } } - } - ] - }, - "BatchGreeksAllTenRequest": { - "type": "object", - "description": "Ordered request envelope; runtime applies the pinned plan limit.", - "required": [ - "requests" - ], - "properties": { - "requests": { - "type": "array", - "items": { - "$ref": "#/components/schemas/GreeksAllTenRequest" - }, - "description": "One to 100 contracts; a lower account limit still applies.", - "maxItems": 100, - "minItems": 1 - } - }, - "additionalProperties": false - }, - "BatchGreeksAllTenResponse": { - "type": "object", - "description": "One metadata block for the entire admitted batch.", - "required": [ - "metadata", - "results" - ], - "properties": { - "metadata": { - "$ref": "#/components/schemas/ResponseMetadata" }, - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BatchGreeksAllTenItemResult" + "408": { + "description": "Body deadline exceeded", + "headers": { + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" + } + }, + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } } - } - } - }, - "BatchGreeksStandardItemResult": { - "allOf": [ - { - "$ref": "#/components/schemas/BatchGreeksStandardOutcome" }, - { - "type": "object", - "required": [ - "index" - ], - "properties": { - "index": { - "type": "integer", - "format": "int32", - "minimum": 0 + "409": { + "description": "Reservation expired, or the quote exceeds x-morphiq-max-cu; check error.code. Zero commercial charge.", + "headers": { + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" } - } - } - ], - "description": "Ordered result with an explicit per-item failure alternative." - }, - "BatchGreeksStandardOutcome": { - "oneOf": [ - { - "type": "object", - "required": [ - "result", - "status" - ], - "properties": { - "result": { - "$ref": "#/components/schemas/GreeksStandardResult" - }, - "status": { - "type": "string", - "enum": [ - "success" - ] + }, + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } } } }, - { - "type": "object", - "required": [ - "error", - "status" - ], - "properties": { - "error": { - "$ref": "#/components/schemas/ErrorBody" - }, - "status": { - "type": "string", - "enum": [ - "error" - ] + "413": { + "description": "Request too large", + "headers": { + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" } - } - } - ] - }, - "BatchGreeksStandardRequest": { - "type": "object", - "description": "Ordered request envelope; runtime applies the pinned plan limit.", - "required": [ - "requests" - ], - "properties": { - "requests": { - "type": "array", - "items": { - "$ref": "#/components/schemas/GreeksStandardRequest" }, - "description": "One to 100 contracts; a lower account limit still applies.", - "maxItems": 100, - "minItems": 1 - } - }, - "additionalProperties": false - }, - "BatchGreeksStandardResponse": { - "type": "object", - "description": "One metadata block for the entire admitted batch.", - "required": [ - "metadata", - "results" - ], - "properties": { - "metadata": { - "$ref": "#/components/schemas/ResponseMetadata" + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } }, - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BatchGreeksStandardItemResult" + "415": { + "description": "JSON required", + "headers": { + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" + } + }, + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } } - } - } - }, - "BatchImpliedVolatilityItemResult": { - "allOf": [ - { - "$ref": "#/components/schemas/BatchImpliedVolatilityOutcome" }, - { - "type": "object", - "required": [ - "index" - ], - "properties": { - "index": { - "type": "integer", - "format": "int32", - "minimum": 0 + "422": { + "description": "Invalid contract, scenarios or account limits", + "headers": { + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" + } + }, + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } } } - } - ], - "description": "Ordered result with an explicit per-item failure alternative." - }, - "BatchImpliedVolatilityOutcome": { - "oneOf": [ - { - "type": "object", - "required": [ - "result", - "status" - ], - "properties": { - "result": { - "$ref": "#/components/schemas/ImpliedVolatilityResult" + }, + "429": { + "description": "Rate or concurrency allowance exhausted", + "headers": { + "Retry-After": { + "schema": { + "type": "string" + }, + "description": "Minimum retry delay in seconds" }, - "status": { - "type": "string", - "enum": [ - "success" - ] + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" + } + }, + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } } } }, - { - "type": "object", - "required": [ - "error", - "status" - ], - "properties": { - "error": { - "$ref": "#/components/schemas/ErrorBody" - }, - "status": { - "type": "string", - "enum": [ - "error" - ] + "500": { + "description": "Pricing or encoding failed", + "headers": { + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" + } + }, + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } } } - } - ] - }, - "BatchImpliedVolatilityRequest": { - "type": "object", - "description": "Ordered request envelope; runtime applies the pinned plan limit.", - "required": [ - "requests" - ], - "properties": { - "requests": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ImpliedVolatilityRequest" + }, + "503": { + "description": "Capacity, funding or admission unavailable", + "headers": { + "Retry-After": { + "schema": { + "type": "string" + }, + "description": "Minimum retry delay in seconds" + }, + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" + } }, - "description": "One to 100 contracts; a lower account limit still applies.", - "maxItems": 100, - "minItems": 1 - } - }, - "additionalProperties": false - }, - "BatchImpliedVolatilityResponse": { - "type": "object", - "description": "One metadata block for the entire admitted batch.", - "required": [ - "metadata", - "results" - ], - "properties": { - "metadata": { - "$ref": "#/components/schemas/ResponseMetadata" + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } }, - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BatchImpliedVolatilityItemResult" + "504": { + "description": "Compute deadline exceeded", + "headers": { + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" + } + }, + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } } } } - }, - "BatchItemResult": { - "allOf": [ - { - "$ref": "#/components/schemas/BatchOutcome" - }, + } + }, + "/v1/scenarios/solve-spot": { + "post": { + "tags": [ + "scenarios" + ], + "description": "Bounded engine-owned target solve for explicit BSM/BS1993 options. JSON only. Returns evaluated points, sampled intervals and unresolved coverage, never an assumed unique/global root. The fixed spot_target/1 profile uses 32 scan intervals and 64 refinements per candidate. The approved flat rate is 100 CU per BSM request or 200 CU per BS1993 request, independent of pricing evaluations, target kind or returned boundaries. A free quote determines the exact conditional charge before reservation without running the solver. Requires an account with the matching signed solve schedule; documentation does not imply live availability. Price-batch entitlement alone does not grant solver access. Numerical failure at any trial aborts the solve.", + "operationId": "solveScenarioSpot", + "parameters": [ { - "type": "object", - "required": [ - "index" - ], - "properties": { - "index": { - "type": "integer", - "format": "int32", - "minimum": 0 - } + "name": "x-morphiq-max-cu", + "in": "header", + "description": "Optional maximum commercial CU charge for this whole request. An actual quote above this ceiling is refused before commitment with 409 cost_limit_exceeded and zero charged CUs. Applies equally to JSON, Protobuf and gRPC metadata. Zero is allowed; duplicate or malformed values are rejected.", + "required": false, + "schema": { + "type": "integer", + "maximum": 18446744073709551615, + "minimum": 0 } } ], - "description": "Ordered result for one batch element." - }, - "BatchOutcome": { - "oneOf": [ - { - "type": "object", - "required": [ - "result", - "status" - ], - "properties": { - "result": { - "$ref": "#/components/schemas/PriceResult" - }, - "status": { - "type": "string", - "enum": [ - "success" - ] + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScenarioSpotSolveRequest" } } }, - { - "type": "object", - "required": [ - "error", - "status" - ], - "properties": { - "error": { - "$ref": "#/components/schemas/ErrorBody" - }, - "status": { - "type": "string", - "enum": [ - "error" - ] + "required": true + }, + "responses": { + "200": { + "description": "Boundaries and explicit coverage, possibly no roots or unresolved regions", + "headers": { + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" } - } - } - ], - "description": "Per-item batch outcome. Structural request failures remain request-level errors." - }, - "BatchPriceRequest": { - "type": "object", - "description": "An ordered batch of option-pricing requests.", - "required": [ - "requests" - ], - "properties": { - "requests": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PriceRequest" }, - "description": "One to 100 contracts; a lower account limit still applies.", - "maxItems": 100, - "minItems": 1 - } - }, - "additionalProperties": false - }, - "BatchPriceResponse": { - "type": "object", - "description": "Successful response for a batch, including any per-item pricing failures.", - "required": [ - "metadata", - "results" - ], - "properties": { - "metadata": { - "$ref": "#/components/schemas/ResponseMetadata" - }, - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BatchItemResult" + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScenarioSpotSolveResponse" + } + } } - } - } - }, - "CostBreakdown": { - "type": "object", - "description": "Tollgate's exact quote, conditional on this policy and workload at execution.", - "required": [ - "total_fcu", - "fixed_fcu", - "variable_fcu", - "minimum_charge_fcu", - "lines" - ], - "properties": { - "fixed_fcu": { - "type": "integer", - "format": "int64", - "minimum": 0 }, - "lines": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CostLine" + "400": { + "description": "Malformed request", + "headers": { + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" + } + }, + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } } }, - "minimum_charge_fcu": { - "type": "integer", - "format": "int64", - "minimum": 0 - }, - "total_fcu": { - "type": "integer", - "format": "int64", - "minimum": 0 - }, - "variable_fcu": { - "type": "integer", - "format": "int64", - "minimum": 0 - } - } - }, - "CostLine": { - "type": "object", - "description": "A rate-class aggregate. Model identifies the schedule row, not necessarily\nthe selected numerical algorithm; `resolved_models` reports the latter.", - "required": [ - "operation", - "model", - "billing_unit", - "quantity", - "fcu_per_unit", - "variable_fcu" - ], - "properties": { - "billing_unit": { - "type": "string" - }, - "fcu_per_unit": { - "type": "integer", - "format": "int64", - "minimum": 0 - }, - "model": { - "type": "string" - }, - "operation": { - "type": "string" - }, - "quantity": { - "type": "integer", - "format": "int64", - "minimum": 0 - }, - "variable_fcu": { - "type": "integer", - "format": "int64", - "minimum": 0 - } - } - }, - "CostQuoteRequest": { - "oneOf": [ - { - "type": "object", - "required": [ - "request", - "operation" - ], - "properties": { - "operation": { - "type": "string", - "enum": [ - "price" - ] - }, - "request": { - "$ref": "#/components/schemas/PriceRequest" + "401": { + "description": "Credential missing or invalid", + "headers": { + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" } - } - }, - { - "type": "object", - "required": [ - "request", - "operation" - ], - "properties": { - "operation": { - "type": "string", - "enum": [ - "price_batch" - ] - }, - "request": { - "$ref": "#/components/schemas/BatchPriceRequest" + }, + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } } } }, - { - "type": "object", - "required": [ - "request", - "operation" - ], - "properties": { - "operation": { - "type": "string", - "enum": [ - "implied_volatility" - ] - }, - "request": { - "$ref": "#/components/schemas/ImpliedVolatilityRequest" + "402": { + "description": "Funding exhausted", + "headers": { + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" + } + }, + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } } } }, - { - "type": "object", - "required": [ - "request", - "operation" - ], - "properties": { - "operation": { - "type": "string", - "enum": [ - "implied_volatility_batch" - ] - }, - "request": { - "$ref": "#/components/schemas/BatchImpliedVolatilityRequest" + "403": { + "description": "Solver is not entitled", + "headers": { + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" + } + }, + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } } } }, - { - "type": "object", - "required": [ - "request", - "operation" - ], - "properties": { - "operation": { - "type": "string", - "enum": [ - "greeks_standard" - ] - }, - "request": { - "$ref": "#/components/schemas/GreeksStandardRequest" + "406": { + "description": "Response media type not acceptable", + "headers": { + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" + } + }, + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } } } }, - { - "type": "object", - "required": [ - "request", - "operation" - ], - "properties": { - "operation": { - "type": "string", - "enum": [ - "greeks_standard_batch" - ] - }, - "request": { - "$ref": "#/components/schemas/BatchGreeksStandardRequest" + "408": { + "description": "Body deadline exceeded", + "headers": { + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" + } + }, + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } } } }, - { - "type": "object", - "required": [ - "request", - "operation" - ], - "properties": { - "operation": { - "type": "string", - "enum": [ - "greeks" - ] - }, - "request": { - "$ref": "#/components/schemas/GreeksAllTenRequest" + "409": { + "description": "Reservation expired, or the quote exceeds x-morphiq-max-cu; check error.code. Zero commercial charge.", + "headers": { + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" + } + }, + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } } } }, - { - "type": "object", - "required": [ - "request", - "operation" - ], - "properties": { - "operation": { - "type": "string", - "enum": [ - "greeks_batch" - ] + "413": { + "description": "Request too large", + "headers": { + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" + } + }, + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "415": { + "description": "JSON required", + "headers": { + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" + } + }, + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "422": { + "description": "Invalid inputs, unrepresentable values or numerical refusal", + "headers": { + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" + } + }, + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "429": { + "description": "Rate or concurrency allowance exhausted", + "headers": { + "Retry-After": { + "schema": { + "type": "string" + }, + "description": "Minimum retry delay in seconds" }, - "request": { - "$ref": "#/components/schemas/BatchGreeksAllTenRequest" + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" + } + }, + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "500": { + "description": "Numerical or encoding failure", + "headers": { + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" + } + }, + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "503": { + "description": "Capacity, policy or rollout unavailable", + "headers": { + "Retry-After": { + "schema": { + "type": "string" + }, + "description": "Minimum retry delay in seconds" + }, + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" + } + }, + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "504": { + "description": "Compute deadline exceeded", + "headers": { + "x-request-id": { + "schema": { + "type": "string" + }, + "description": "Unique service-generated request identifier" + } + }, + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } } } + } + } + } + } + }, + "components": { + "schemas": { + "BatchFusedAnalyticsItemResult": { + "allOf": [ + { + "$ref": "#/components/schemas/BatchFusedAnalyticsOutcome" }, { "type": "object", "required": [ - "request", - "operation" + "index" ], "properties": { - "operation": { - "type": "string", - "enum": [ - "analytics" - ] - }, - "request": { - "$ref": "#/components/schemas/FusedAnalyticsRequest" + "index": { + "type": "integer", + "format": "int32", + "minimum": 0 } } - }, + } + ], + "description": "Ordered result with an explicit per-item failure alternative." + }, + "BatchFusedAnalyticsOutcome": { + "oneOf": [ { "type": "object", "required": [ - "request", - "operation" + "result", + "status" ], "properties": { - "operation": { + "result": { + "$ref": "#/components/schemas/FusedAnalyticsResult" + }, + "status": { "type": "string", "enum": [ - "analytics_batch" + "success" ] - }, - "request": { - "$ref": "#/components/schemas/BatchFusedAnalyticsRequest" } } }, { "type": "object", "required": [ - "request", - "operation" + "error", + "status" ], "properties": { - "operation": { + "error": { + "$ref": "#/components/schemas/ErrorBody" + }, + "status": { "type": "string", "enum": [ - "regime" + "error" ] - }, - "request": { - "$ref": "#/components/schemas/RegimeRequest" } } } + ] + }, + "BatchFusedAnalyticsRequest": { + "type": "object", + "description": "Ordered request envelope; runtime applies the pinned plan limit.", + "required": [ + "requests" ], - "description": "OpenAPI's discriminated request union; runtime retains each body verbatim." + "properties": { + "requests": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FusedAnalyticsRequest" + }, + "description": "One to 100 contracts; a lower account limit still applies.", + "maxItems": 100, + "minItems": 1 + } + }, + "additionalProperties": false }, - "CostQuoteResponse": { + "BatchFusedAnalyticsResponse": { "type": "object", - "description": "Quote identity and breakdown, with no reservation or price lock.", + "description": "One metadata block for the entire admitted batch.", "required": [ - "quote", - "resolved_models", - "metadata" + "metadata", + "results" ], "properties": { "metadata": { - "$ref": "#/components/schemas/ServiceErrorMetadata" - }, - "quote": { - "$ref": "#/components/schemas/CostBreakdown" + "$ref": "#/components/schemas/ResponseMetadata" }, - "resolved_models": { + "results": { "type": "array", "items": { - "$ref": "#/components/schemas/ResolvedCostModel" + "$ref": "#/components/schemas/BatchFusedAnalyticsItemResult" } } } }, - "CostRate": { - "type": "object", - "description": "A published rate under the caller's pinned schedule. A rate is not an entitlement.", - "required": [ - "operation", - "model", - "billing_unit", - "fcu_per_unit", - "max_discrete_dividends" - ], - "properties": { - "billing_unit": { - "type": "string" + "BatchGreeksAllTenItemResult": { + "allOf": [ + { + "$ref": "#/components/schemas/BatchGreeksAllTenOutcome" }, - "fcu_per_unit": { - "type": "integer", - "format": "int64", - "minimum": 0 - }, - "max_discrete_dividends": { - "type": "integer", - "format": "int32", - "minimum": 0 - }, - "model": { - "type": "string" + { + "type": "object", + "required": [ + "index" + ], + "properties": { + "index": { + "type": "integer", + "format": "int32", + "minimum": 0 + } + } + } + ], + "description": "Ordered result with an explicit per-item failure alternative." + }, + "BatchGreeksAllTenOutcome": { + "oneOf": [ + { + "type": "object", + "required": [ + "result", + "status" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/GreeksAllTenResult" + }, + "status": { + "type": "string", + "enum": [ + "success" + ] + } + } }, - "operation": { - "type": "string" + { + "type": "object", + "required": [ + "error", + "status" + ], + "properties": { + "error": { + "$ref": "#/components/schemas/ErrorBody" + }, + "status": { + "type": "string", + "enum": [ + "error" + ] + } + } } - } + ] }, - "CostSchedule": { + "BatchGreeksAllTenRequest": { "type": "object", - "description": "Customer-facing projection of one immutable, account-effective cost schedule.", + "description": "Ordered request envelope; runtime applies the pinned plan limit.", "required": [ - "schedule_id", - "schedule_version", - "record_version", - "policy_revision", - "effective_from", - "fixed_request_fcu", - "minimum_charge_fcu", - "rounding", - "failure_charging", - "rates" + "requests" ], "properties": { - "effective_from": { - "type": "string" - }, - "effective_until": { - "type": [ - "string", - "null" - ] - }, - "failure_charging": { - "type": "string" - }, - "fixed_request_fcu": { - "type": "integer", - "format": "int64", - "minimum": 0 - }, - "minimum_charge_fcu": { - "type": "integer", - "format": "int64", - "minimum": 0 - }, - "policy_revision": { - "type": "string" - }, - "rates": { + "requests": { "type": "array", "items": { - "$ref": "#/components/schemas/CostRate" - } - }, - "record_version": { - "type": "integer", - "format": "int32", - "minimum": 0 - }, - "rounding": { - "type": "string" - }, - "schedule_id": { - "type": "string" - }, - "schedule_version": { - "type": "string" + "$ref": "#/components/schemas/GreeksAllTenRequest" + }, + "description": "One to 100 contracts; a lower account limit still applies.", + "maxItems": 100, + "minItems": 1 } - } + }, + "additionalProperties": false }, - "CostScheduleResponse": { + "BatchGreeksAllTenResponse": { "type": "object", - "description": "Full or filtered schedule; metadata describes this non-billable utility call.", + "description": "One metadata block for the entire admitted batch.", "required": [ - "schedule", - "metadata" + "metadata", + "results" ], "properties": { "metadata": { - "$ref": "#/components/schemas/ServiceErrorMetadata" + "$ref": "#/components/schemas/ResponseMetadata" }, - "schedule": { - "$ref": "#/components/schemas/CostSchedule" + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BatchGreeksAllTenItemResult" + } } } }, - "DividendEvent": { + "BatchGreeksStandardItemResult": { + "allOf": [ + { + "$ref": "#/components/schemas/BatchGreeksStandardOutcome" + }, + { + "type": "object", + "required": [ + "index" + ], + "properties": { + "index": { + "type": "integer", + "format": "int32", + "minimum": 0 + } + } + } + ], + "description": "Ordered result with an explicit per-item failure alternative." + }, + "BatchGreeksStandardOutcome": { + "oneOf": [ + { + "type": "object", + "required": [ + "result", + "status" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/GreeksStandardResult" + }, + "status": { + "type": "string", + "enum": [ + "success" + ] + } + } + }, + { + "type": "object", + "required": [ + "error", + "status" + ], + "properties": { + "error": { + "$ref": "#/components/schemas/ErrorBody" + }, + "status": { + "type": "string", + "enum": [ + "error" + ] + } + } + } + ] + }, + "BatchGreeksStandardRequest": { "type": "object", - "description": "One discrete cash dividend.\n\nTime is a year fraction on the same basis as `time_to_expiry_years`, not a\ncalendar date. The contract carries no calendar, day-count convention or\ntimezone, and introducing one here to express a dividend date would add a\nbasis the rest of the contract does not have and cannot validate. The\nengine's own `DividendEvent` is `ex_time_years` for the same reason.", + "description": "Ordered request envelope; runtime applies the pinned plan limit.", "required": [ - "ex_time_years", - "cash_amount" + "requests" ], "properties": { - "cash_amount": { - "type": "number", - "format": "double", - "description": "Cash amount per share, in the same currency as spot and strike." - }, - "ex_time_years": { - "type": "number", - "format": "double", - "description": "Ex-dividend time as a year fraction from valuation." + "requests": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GreeksStandardRequest" + }, + "description": "One to 100 contracts; a lower account limit still applies.", + "maxItems": 100, + "minItems": 1 } }, "additionalProperties": false }, - "EngineIdentity": { + "BatchGreeksStandardResponse": { "type": "object", - "description": "Service-owned engine provenance, independent of any engine's Rust types.", + "description": "One metadata block for the entire admitted batch.", "required": [ - "name", - "version", - "revision" + "metadata", + "results" ], "properties": { - "name": { - "type": "string" - }, - "revision": { - "type": "string" + "metadata": { + "$ref": "#/components/schemas/ResponseMetadata" }, - "version": { - "type": "string" + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BatchGreeksStandardItemResult" + } } } }, - "ErrorBody": { + "BatchImpliedVolatilityItemResult": { + "allOf": [ + { + "$ref": "#/components/schemas/BatchImpliedVolatilityOutcome" + }, + { + "type": "object", + "required": [ + "index" + ], + "properties": { + "index": { + "type": "integer", + "format": "int32", + "minimum": 0 + } + } + } + ], + "description": "Ordered result with an explicit per-item failure alternative." + }, + "BatchImpliedVolatilityOutcome": { + "oneOf": [ + { + "type": "object", + "required": [ + "result", + "status" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/ImpliedVolatilityResult" + }, + "status": { + "type": "string", + "enum": [ + "success" + ] + } + } + }, + { + "type": "object", + "required": [ + "error", + "status" + ], + "properties": { + "error": { + "$ref": "#/components/schemas/ErrorBody" + }, + "status": { + "type": "string", + "enum": [ + "error" + ] + } + } + } + ] + }, + "BatchImpliedVolatilityRequest": { "type": "object", - "description": "Stable, machine-readable error details.", + "description": "Ordered request envelope; runtime applies the pinned plan limit.", "required": [ - "code", - "message" + "requests" ], "properties": { - "code": { - "type": "string" - }, - "field": { - "type": [ - "string", - "null" - ] - }, - "message": { - "type": "string" + "requests": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImpliedVolatilityRequest" + }, + "description": "One to 100 contracts; a lower account limit still applies.", + "maxItems": 100, + "minItems": 1 } - } + }, + "additionalProperties": false }, - "ErrorEnvelope": { + "BatchImpliedVolatilityResponse": { "type": "object", - "description": "Top-level HTTP error envelope.", + "description": "One metadata block for the entire admitted batch.", "required": [ "metadata", - "error" + "results" ], "properties": { - "error": { - "$ref": "#/components/schemas/ErrorBody" - }, "metadata": { "$ref": "#/components/schemas/ResponseMetadata" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BatchImpliedVolatilityItemResult" + } } } }, - "ExerciseStyle": { - "type": "string", - "description": "Contract exercise convention, independent of the pricing algorithm.", - "enum": [ - "european", - "american" - ] + "BatchItemResult": { + "allOf": [ + { + "$ref": "#/components/schemas/BatchOutcome" + }, + { + "type": "object", + "required": [ + "index" + ], + "properties": { + "index": { + "type": "integer", + "format": "int32", + "minimum": 0 + } + } + } + ], + "description": "Ordered result for one batch element." }, - "FusedAnalyticsRequest": { + "BatchOutcome": { + "oneOf": [ + { + "type": "object", + "required": [ + "result", + "status" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/PriceResult" + }, + "status": { + "type": "string", + "enum": [ + "success" + ] + } + } + }, + { + "type": "object", + "required": [ + "error", + "status" + ], + "properties": { + "error": { + "$ref": "#/components/schemas/ErrorBody" + }, + "status": { + "type": "string", + "enum": [ + "error" + ] + } + } + } + ], + "description": "Per-item batch outcome. Structural request failures remain request-level errors." + }, + "BatchPriceRequest": { "type": "object", - "description": "A request for price and Greeks, optionally at a volatility solved from an\nobserved premium first.\n\nWith `market_price`, volatility is solved and the Greeks are evaluated at\nthe solved value — one admission, one solve, and the solved point passed\ninward as verified evidence rather than re-derived. Without it, the\nsupplied `volatility` is used and the result is the all-ten-Greeks\noperation under another name.", + "description": "An ordered batch of option-pricing requests.", "required": [ - "exercise_style", - "pricing_model", - "option_type", - "spot", - "strike", - "time_to_expiry_years", - "risk_free_rate", - "dividend_yield", - "volatility" + "requests" ], "properties": { - "dividend_schedule": { + "requests": { "type": "array", "items": { - "$ref": "#/components/schemas/DividendEvent" + "$ref": "#/components/schemas/PriceRequest" }, - "description": "Cash dividends are supported with supplied or solved volatility.", - "maxItems": 16 - }, - "dividend_yield": { - "type": "number", - "format": "double" - }, - "exercise_style": { - "$ref": "#/components/schemas/ExerciseStyle" - }, - "market_price": { - "type": [ - "number", - "null" - ], - "format": "double", - "description": "The observed premium to invert before evaluating the Greeks." - }, - "model_version": { - "type": [ - "string", - "null" - ] - }, - "option_type": { - "$ref": "#/components/schemas/OptionType" - }, - "pricing_model": { - "$ref": "#/components/schemas/PricingModel" - }, - "risk_free_rate": { - "type": "number", - "format": "double" - }, - "spot": { - "type": "number", - "format": "double" - }, - "strike": { - "type": "number", - "format": "double" - }, - "time_to_expiry_years": { - "type": "number", - "format": "double" - }, - "volatility": { - "type": "number", - "format": "double", - "description": "Used only when `market_price` is absent." + "description": "One to 100 contracts; a lower account limit still applies.", + "maxItems": 100, + "minItems": 1 } }, "additionalProperties": false }, - "FusedAnalyticsResponse": { + "BatchPriceResponse": { "type": "object", - "description": "Successful response for one fused-analytics operation.", + "description": "Successful response for a batch, including any per-item pricing failures.", "required": [ "metadata", - "result" + "results" ], "properties": { "metadata": { "$ref": "#/components/schemas/ResponseMetadata" }, - "result": { - "$ref": "#/components/schemas/FusedAnalyticsResult" - } - } - }, - "FusedAnalyticsResult": { - "type": "object", - "description": "Price, Greeks and — when a premium was supplied — the solve that produced\nthe volatility they were evaluated at.", - "required": [ - "price", - "greeks", - "pricing_model", - "model_version" - ], - "properties": { - "greeks": { - "$ref": "#/components/schemas/GreeksAllTen" - }, - "implied_volatility": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/ImpliedVolatilityResult", - "description": "Present only when the request supplied `market_price`." - } - ] - }, - "model_version": { - "type": "string" - }, - "price": { - "type": "number", - "format": "double" - }, - "pricing_model": { - "$ref": "#/components/schemas/ResolvedPricingModel" - } - } - }, - "GreeksAllTen": { - "type": "object", - "description": "Available Greeks on the contract's stated bases; absent values are explained by `unavailable`.\n\nTime derivatives are **per year**. The engine reports them per calendar\nday, which embeds a 365-day calendar; the contract carries no calendar,\nso the service scales by the engine's own day count at this boundary and\nthe caller applies whichever convention it holds. Vega is raw `dV/dσ` per\nunit volatility, not per point. Nothing else is rescaled.", - "properties": { - "charm": { - "type": "number", - "format": "double", - "description": "`d²V/(dS dt)`, **per year**." - }, - "color": { - "type": "number", - "format": "double", - "description": "`d³V/(dS² dt)`, **per year**." - }, - "delta": { - "type": "number", - "format": "double", - "description": "`dV/dS`." - }, - "gamma": { - "type": "number", - "format": "double", - "description": "`d²V/dS²`." - }, - "rho": { - "type": "number", - "format": "double", - "description": "`dV/dr`, per unit rate." - }, - "theta": { - "type": "number", - "format": "double", - "description": "`dV/dt`, **per year**." - }, - "unavailable": { - "type": "object", - "description": "Each omitted Greek has a reason. A resolved zero is present as 0.0.\nCodes distinguish unsupported quantities, model domain/validity,\naccuracy refusals and unresolved numerical derivatives.", - "additionalProperties": { - "$ref": "#/components/schemas/ErrorBody" - }, - "propertyNames": { - "type": "string" + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BatchItemResult" } - }, - "vanna": { - "type": "number", - "format": "double", - "description": "`d²V/(dS dσ)`, per unit volatility." - }, - "vega": { - "type": "number", - "format": "double", - "description": "`dV/dσ`, per unit volatility." - }, - "veta": { - "type": "number", - "format": "double", - "description": "`d²V/(dσ dt)`, **per year** per unit volatility." - }, - "volga": { - "type": "number", - "format": "double", - "description": "`d²V/dσ²`, per unit volatility squared." } } }, - "GreeksAllTenRequest": { + "CostBreakdown": { "type": "object", - "description": "A request for the price and all ten Greeks at a supplied volatility.\n\nThe same shape as `PriceRequest`: the Greeks are derivatives of the\nprice, so they are a property of the same contract and the same\ncoordinates.", + "description": "Tollgate's exact quote, conditional on this policy and workload at execution.", "required": [ - "exercise_style", - "pricing_model", - "option_type", - "spot", - "strike", - "time_to_expiry_years", - "risk_free_rate", - "dividend_yield", - "volatility" + "total_fcu", + "fixed_fcu", + "variable_fcu", + "minimum_charge_fcu", + "lines" ], "properties": { - "dividend_schedule": { + "fixed_fcu": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "lines": { "type": "array", "items": { - "$ref": "#/components/schemas/DividendEvent" - }, - "description": "Cash dividends select the finite-difference American model with Auto.", - "maxItems": 16 - }, - "dividend_yield": { - "type": "number", - "format": "double" - }, - "exercise_style": { - "$ref": "#/components/schemas/ExerciseStyle" - }, - "model_version": { - "type": [ - "string", - "null" - ] - }, - "option_type": { - "$ref": "#/components/schemas/OptionType" - }, - "pricing_model": { - "$ref": "#/components/schemas/PricingModel" - }, - "risk_free_rate": { - "type": "number", - "format": "double" - }, - "spot": { - "type": "number", - "format": "double" - }, - "strike": { - "type": "number", - "format": "double" + "$ref": "#/components/schemas/CostLine" + } }, - "time_to_expiry_years": { - "type": "number", - "format": "double" + "minimum_charge_fcu": { + "type": "integer", + "format": "int64", + "minimum": 0 }, - "volatility": { - "type": "number", - "format": "double" - } - }, - "additionalProperties": false - }, - "GreeksAllTenResponse": { - "type": "object", - "description": "Successful response for one all-ten-Greeks operation.", - "required": [ - "metadata", - "result" - ], - "properties": { - "metadata": { - "$ref": "#/components/schemas/ResponseMetadata" + "total_fcu": { + "type": "integer", + "format": "int64", + "minimum": 0 }, - "result": { - "$ref": "#/components/schemas/GreeksAllTenResult" + "variable_fcu": { + "type": "integer", + "format": "int64", + "minimum": 0 } } }, - "GreeksAllTenResult": { + "CostLine": { "type": "object", - "description": "The price and Greeks of one contract at the volatility supplied.", + "description": "A rate-class aggregate. Model identifies the schedule row, not necessarily\nthe selected numerical algorithm; `resolved_models` reports the latter.", "required": [ - "price", - "greeks", - "pricing_model", - "model_version" + "operation", + "model", + "billing_unit", + "quantity", + "fcu_per_unit", + "variable_fcu" ], "properties": { - "greeks": { - "$ref": "#/components/schemas/GreeksAllTen" + "billing_unit": { + "type": "string" }, - "model_version": { + "fcu_per_unit": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "model": { "type": "string" }, - "price": { - "type": "number", - "format": "double" + "operation": { + "type": "string" }, - "pricing_model": { - "$ref": "#/components/schemas/ResolvedPricingModel" + "quantity": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "variable_fcu": { + "type": "integer", + "format": "int64", + "minimum": 0 } } }, - "GreeksStandard": { - "type": "object", - "description": "Available Greeks on the contract's stated bases; absent values are explained by `unavailable`.\n\nTime derivatives are **per year**. The engine reports them per calendar\nday, which embeds a 365-day calendar; the contract carries no calendar,\nso the service scales by the engine's own day count at this boundary and\nthe caller applies whichever convention it holds. Vega is raw `dV/dσ` per\nunit volatility, not per point. Nothing else is rescaled.", - "properties": { - "delta": { - "type": "number", - "format": "double", - "description": "`dV/dS`." - }, - "gamma": { - "type": "number", - "format": "double", - "description": "`d²V/dS²`." - }, - "rho": { - "type": "number", - "format": "double", - "description": "`dV/dr`, per unit rate." - }, - "theta": { - "type": "number", - "format": "double", - "description": "`dV/dt`, **per year**." - }, - "unavailable": { + "CostQuoteRequest": { + "oneOf": [ + { "type": "object", - "description": "Each omitted Greek has a reason. A resolved zero is present as 0.0.\nCodes distinguish unsupported quantities, model domain/validity,\naccuracy refusals and unresolved numerical derivatives.", - "additionalProperties": { - "$ref": "#/components/schemas/ErrorBody" - }, - "propertyNames": { - "type": "string" + "required": [ + "request", + "operation" + ], + "properties": { + "operation": { + "type": "string", + "enum": [ + "price" + ] + }, + "request": { + "$ref": "#/components/schemas/PriceRequest" + } } }, - "vega": { - "type": "number", - "format": "double", - "description": "`dV/dσ`, per unit volatility." - } - } - }, - "GreeksStandardRequest": { - "type": "object", - "description": "A request for the price and the standard five Greeks at a supplied volatility.\n\nThe same shape as `PriceRequest`: the Greeks are derivatives of the\nprice, so they are a property of the same contract and the same\ncoordinates.", - "required": [ - "exercise_style", - "pricing_model", - "option_type", - "spot", - "strike", - "time_to_expiry_years", - "risk_free_rate", - "dividend_yield", - "volatility" - ], - "properties": { - "dividend_schedule": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DividendEvent" - }, - "description": "Cash dividends select the finite-difference American model with Auto.", - "maxItems": 16 + { + "type": "object", + "required": [ + "request", + "operation" + ], + "properties": { + "operation": { + "type": "string", + "enum": [ + "price_batch" + ] + }, + "request": { + "$ref": "#/components/schemas/BatchPriceRequest" + } + } }, - "dividend_yield": { - "type": "number", - "format": "double" + { + "type": "object", + "required": [ + "request", + "operation" + ], + "properties": { + "operation": { + "type": "string", + "enum": [ + "scenario_evaluate" + ] + }, + "request": { + "$ref": "#/components/schemas/ScenarioRequest" + } + } }, - "exercise_style": { - "$ref": "#/components/schemas/ExerciseStyle" + { + "type": "object", + "required": [ + "request", + "operation" + ], + "properties": { + "operation": { + "type": "string", + "enum": [ + "scenario_spot_solve" + ] + }, + "request": { + "$ref": "#/components/schemas/ScenarioSpotSolveRequest" + } + } }, - "model_version": { - "type": [ - "string", - "null" - ] + { + "type": "object", + "required": [ + "request", + "operation" + ], + "properties": { + "operation": { + "type": "string", + "enum": [ + "implied_volatility" + ] + }, + "request": { + "$ref": "#/components/schemas/ImpliedVolatilityRequest" + } + } }, - "option_type": { - "$ref": "#/components/schemas/OptionType" + { + "type": "object", + "required": [ + "request", + "operation" + ], + "properties": { + "operation": { + "type": "string", + "enum": [ + "implied_volatility_batch" + ] + }, + "request": { + "$ref": "#/components/schemas/BatchImpliedVolatilityRequest" + } + } }, - "pricing_model": { - "$ref": "#/components/schemas/PricingModel" + { + "type": "object", + "required": [ + "request", + "operation" + ], + "properties": { + "operation": { + "type": "string", + "enum": [ + "greeks_standard" + ] + }, + "request": { + "$ref": "#/components/schemas/GreeksStandardRequest" + } + } }, - "risk_free_rate": { - "type": "number", - "format": "double" + { + "type": "object", + "required": [ + "request", + "operation" + ], + "properties": { + "operation": { + "type": "string", + "enum": [ + "greeks_standard_batch" + ] + }, + "request": { + "$ref": "#/components/schemas/BatchGreeksStandardRequest" + } + } }, - "spot": { - "type": "number", - "format": "double" + { + "type": "object", + "required": [ + "request", + "operation" + ], + "properties": { + "operation": { + "type": "string", + "enum": [ + "greeks" + ] + }, + "request": { + "$ref": "#/components/schemas/GreeksAllTenRequest" + } + } }, - "strike": { - "type": "number", - "format": "double" + { + "type": "object", + "required": [ + "request", + "operation" + ], + "properties": { + "operation": { + "type": "string", + "enum": [ + "greeks_batch" + ] + }, + "request": { + "$ref": "#/components/schemas/BatchGreeksAllTenRequest" + } + } }, - "time_to_expiry_years": { - "type": "number", - "format": "double" + { + "type": "object", + "required": [ + "request", + "operation" + ], + "properties": { + "operation": { + "type": "string", + "enum": [ + "analytics" + ] + }, + "request": { + "$ref": "#/components/schemas/FusedAnalyticsRequest" + } + } }, - "volatility": { - "type": "number", - "format": "double" - } - }, - "additionalProperties": false + { + "type": "object", + "required": [ + "request", + "operation" + ], + "properties": { + "operation": { + "type": "string", + "enum": [ + "analytics_batch" + ] + }, + "request": { + "$ref": "#/components/schemas/BatchFusedAnalyticsRequest" + } + } + }, + { + "type": "object", + "required": [ + "request", + "operation" + ], + "properties": { + "operation": { + "type": "string", + "enum": [ + "regime" + ] + }, + "request": { + "$ref": "#/components/schemas/RegimeRequest" + } + } + } + ], + "description": "OpenAPI's discriminated request union; runtime retains each body verbatim." }, - "GreeksStandardResponse": { + "CostQuoteResponse": { "type": "object", - "description": "Successful response for one standard-five-Greeks operation.", + "description": "Quote identity and breakdown, with no reservation or price lock.", "required": [ - "metadata", - "result" + "quote", + "resolved_models", + "metadata" ], "properties": { "metadata": { - "$ref": "#/components/schemas/ResponseMetadata" + "$ref": "#/components/schemas/ServiceErrorMetadata" }, - "result": { - "$ref": "#/components/schemas/GreeksStandardResult" + "quote": { + "$ref": "#/components/schemas/CostBreakdown" + }, + "resolved_models": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResolvedCostModel" + } } } }, - "GreeksStandardResult": { + "CostRate": { "type": "object", - "description": "The price and Greeks of one contract at the volatility supplied.", + "description": "A published rate under the caller's pinned schedule. A rate is not an entitlement.", "required": [ - "price", - "greeks", - "pricing_model", - "model_version" + "operation", + "model", + "billing_unit", + "fcu_per_unit", + "max_discrete_dividends" ], "properties": { - "greeks": { - "$ref": "#/components/schemas/GreeksStandard" - }, - "model_version": { + "billing_unit": { "type": "string" }, - "price": { - "type": "number", - "format": "double" + "fcu_per_unit": { + "type": "integer", + "format": "int64", + "minimum": 0 }, - "pricing_model": { - "$ref": "#/components/schemas/ResolvedPricingModel" + "max_discrete_dividends": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "model": { + "type": "string" + }, + "operation": { + "type": "string" } } }, - "ImpliedVolatilityRequest": { + "CostSchedule": { "type": "object", - "description": "A request to invert an observed premium into a volatility.\n\nCarries `market_price` and no `volatility`: volatility is the unknown being\nsolved for, so a request that supplied one would be asking the service to\nignore it.", + "description": "Customer-facing projection of one immutable, account-effective cost schedule.", "required": [ - "exercise_style", - "pricing_model", - "option_type", - "spot", - "strike", - "time_to_expiry_years", - "risk_free_rate", - "dividend_yield", - "market_price" + "schedule_id", + "schedule_version", + "record_version", + "policy_revision", + "effective_from", + "fixed_request_fcu", + "minimum_charge_fcu", + "rounding", + "failure_charging", + "rates" ], "properties": { - "dividend_schedule": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DividendEvent" - }, - "maxItems": 16 - }, - "dividend_yield": { - "type": "number", - "format": "double" - }, - "exercise_style": { - "$ref": "#/components/schemas/ExerciseStyle" - }, - "market_price": { - "type": "number", - "format": "double", - "description": "The observed premium to invert." + "effective_from": { + "type": "string" }, - "model_version": { + "effective_until": { "type": [ "string", "null" ] }, - "option_type": { - "$ref": "#/components/schemas/OptionType" + "failure_charging": { + "type": "string" }, - "pricing_model": { - "$ref": "#/components/schemas/PricingModel" + "fixed_request_fcu": { + "type": "integer", + "format": "int64", + "minimum": 0 }, - "risk_free_rate": { - "type": "number", - "format": "double" + "minimum_charge_fcu": { + "type": "integer", + "format": "int64", + "minimum": 0 }, - "spot": { - "type": "number", - "format": "double" + "policy_revision": { + "type": "string" }, - "strike": { - "type": "number", - "format": "double" + "rates": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CostRate" + } }, - "time_to_expiry_years": { - "type": "number", - "format": "double" + "record_version": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "rounding": { + "type": "string" + }, + "schedule_id": { + "type": "string" + }, + "schedule_version": { + "type": "string" } - }, - "additionalProperties": false + } }, - "ImpliedVolatilityResponse": { + "CostScheduleResponse": { "type": "object", - "description": "Successful response for one implied-volatility operation.", + "description": "Full or filtered schedule; metadata describes this non-billable utility call.", "required": [ - "metadata", - "result" + "schedule", + "metadata" ], "properties": { "metadata": { - "$ref": "#/components/schemas/ResponseMetadata" + "$ref": "#/components/schemas/ServiceErrorMetadata" }, - "result": { - "$ref": "#/components/schemas/ImpliedVolatilityResult" + "schedule": { + "$ref": "#/components/schemas/CostSchedule" } } }, - "ImpliedVolatilityResult": { + "DividendEvent": { "type": "object", - "description": "A solved implied volatility and the diagnostics that qualify it.", + "description": "One discrete cash dividend.\n\nTime is a year fraction on the same basis as `time_to_expiry_years`, not a\ncalendar date. The contract carries no calendar, day-count convention or\ntimezone, and introducing one here to express a dividend date would add a\nbasis the rest of the contract does not have and cannot validate. The\nengine's own `DividendEvent` is `ex_time_years` for the same reason.", "required": [ - "implied_volatility", - "iterations", - "used_fallback", - "residual", - "residual_basis", - "pricing_model", - "model_version" + "ex_time_years", + "cash_amount" ], "properties": { - "implied_volatility": { + "cash_amount": { "type": "number", "format": "double", - "description": "Decimal fraction, on the same basis as `PriceRequest.volatility`." - }, - "iterations": { - "type": "integer", - "format": "int32", - "minimum": 0 - }, - "model_version": { - "type": "string" - }, - "pricing_model": { - "$ref": "#/components/schemas/ResolvedPricingModel" + "description": "Cash amount per share, in the same currency as spot and strike." }, - "residual": { + "ex_time_years": { "type": "number", - "format": "double" - }, - "residual_basis": { - "$ref": "#/components/schemas/ResidualBasis" - }, - "used_fallback": { - "type": "boolean", - "description": "Whether the solve left its primary path.\n\nNot a quality signal on its own: an American solve inverts by bisection\nand reports `true` for every live solve, while a Bachelier solve reports\n`false` because bisection is its canonical solver rather than a degraded\nfallback." + "format": "double", + "description": "Ex-dividend time as a year fraction from valuation." } - } + }, + "additionalProperties": false }, - "ModelConstraint": { + "EngineIdentity": { "type": "object", + "description": "Service-owned engine provenance, independent of any engine's Rust types.", "required": [ - "quantity", - "description" + "name", + "version", + "revision" ], "properties": { - "description": { + "name": { "type": "string" }, - "maximum": { - "type": [ - "number", - "null" - ], - "format": "double", - "description": "Inclusive upper bound when present." + "revision": { + "type": "string" }, - "minimum": { + "version": { + "type": "string" + } + } + }, + "ErrorBody": { + "type": "object", + "description": "Stable, machine-readable error details.", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "string" + }, + "field": { "type": [ - "number", + "string", "null" - ], - "format": "double", - "description": "Inclusive lower bound when present." + ] }, - "quantity": { - "type": "string", - "description": "Input field or derived quantity, defined by description." + "message": { + "type": "string" } } }, - "OptionType": { - "type": "string", - "description": "Option payoff direction.", - "enum": [ - "call", - "put" - ] - }, - "PolicyResponseMetadata": { + "ErrorEnvelope": { "type": "object", - "description": "Pinned policy identity and contributing record versions nested in response metadata.", + "description": "Top-level HTTP error envelope.", "required": [ - "snapshot_id", - "revision", - "plan", - "records" + "metadata", + "error" ], "properties": { - "plan": { - "type": "string", - "description": "Versioned plan reference, formatted as id@version." - }, - "records": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Contributing versioned records in resolution precedence order." - }, - "revision": { - "type": "string", - "description": "Full opaque revision, encoded as 64 lowercase hexadecimal digits." + "error": { + "$ref": "#/components/schemas/ErrorBody" }, - "snapshot_id": { - "type": "string" + "metadata": { + "$ref": "#/components/schemas/ResponseMetadata" } } }, - "PriceRequest": { + "ExerciseStyle": { + "type": "string", + "description": "Contract exercise convention, independent of the pricing algorithm.", + "enum": [ + "european", + "american" + ] + }, + "FusedAnalyticsRequest": { "type": "object", - "description": "A single option-pricing request. All numeric fields are required.", + "description": "A request for price and Greeks, optionally at a volatility solved from an\nobserved premium first.\n\nWith `market_price`, volatility is solved and the Greeks are evaluated at\nthe solved value — one admission, one solve, and the solved point passed\ninward as verified evidence rather than re-derived. Without it, the\nsupplied `volatility` is used and the result is the all-ten-Greeks\noperation under another name.", "required": [ "exercise_style", "pricing_model", @@ -5902,7 +5932,7 @@ "items": { "$ref": "#/components/schemas/DividendEvent" }, - "description": "Discrete cash dividends within the option's life. American\nCash-dividend American model only; Auto selects it for a non-empty schedule.", + "description": "Cash dividends are supported with supplied or solved volatility.", "maxItems": 16 }, "dividend_yield": { @@ -5912,12 +5942,19 @@ "exercise_style": { "$ref": "#/components/schemas/ExerciseStyle" }, + "market_price": { + "type": [ + "number", + "null" + ], + "format": "double", + "description": "The observed premium to invert before evaluating the Greeks." + }, "model_version": { "type": [ "string", "null" - ], - "description": "Optional algorithm-version pin. Omit to use the current version." + ] }, "option_type": { "$ref": "#/components/schemas/OptionType" @@ -5943,14 +5980,15 @@ }, "volatility": { "type": "number", - "format": "double" + "format": "double", + "description": "Used only when `market_price` is absent." } }, "additionalProperties": false }, - "PriceResponse": { + "FusedAnalyticsResponse": { "type": "object", - "description": "Successful response for one pricing operation.", + "description": "Successful response for one fused-analytics operation.", "required": [ "metadata", "result" @@ -5960,30 +5998,36 @@ "$ref": "#/components/schemas/ResponseMetadata" }, "result": { - "$ref": "#/components/schemas/PriceResult" + "$ref": "#/components/schemas/FusedAnalyticsResult" } } }, - "PriceResult": { + "FusedAnalyticsResult": { "type": "object", - "description": "The transport-neutral result of one pricing operation.", + "description": "Price, Greeks and — when a premium was supplied — the solve that produced\nthe volatility they were evaluated at.", "required": [ "price", - "exercise_style", - "option_type", + "greeks", "pricing_model", "model_version" ], "properties": { - "exercise_style": { - "$ref": "#/components/schemas/ExerciseStyle" + "greeks": { + "$ref": "#/components/schemas/GreeksAllTen" }, - "model_version": { - "type": "string", - "description": "Stable revision of the selected pricing algorithm." + "implied_volatility": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ImpliedVolatilityResult", + "description": "Present only when the request supplied `market_price`." + } + ] }, - "option_type": { - "$ref": "#/components/schemas/OptionType" + "model_version": { + "type": "string" }, "price": { "type": "number", @@ -5994,204 +6038,1040 @@ } } }, - "PricingModel": { - "type": "string", - "description": "Canonical selectors are listed by GET /v1/pricing/models. qd_fixed_point and cash_dividend_american remain input aliases. BS2002 is retired and returns a migration error.", - "enum": [ - "auto", - "black_scholes_merton", - "bjerksund_stensland_2002", - "bjerksund_stensland_1993", - "black_scholes_merton_qdfp", - "black_scholes_merton_fd", - "qd_fixed_point", - "cash_dividend_american" - ] - }, - "PricingModelCatalog": { + "GreeksAllTen": { "type": "object", - "required": [ - "models", - "engines", - "greek_units" - ], + "description": "Available Greeks on the contract's stated bases; absent values are explained by `unavailable`.\n\nTime derivatives are **per year**. The engine reports them per calendar\nday, which embeds a 365-day calendar; the contract carries no calendar,\nso the service scales by the engine's own day count at this boundary and\nthe caller applies whichever convention it holds. Vega is raw `dV/dσ` per\nunit volatility, not per point. Nothing else is rescaled.", "properties": { - "engines": { - "type": "array", - "items": { - "$ref": "#/components/schemas/EngineIdentity" - } + "charm": { + "type": "number", + "format": "double", + "description": "`d²V/(dS dt)`, **per year**." }, - "greek_units": { + "color": { + "type": "number", + "format": "double", + "description": "`d³V/(dS² dt)`, **per year**." + }, + "delta": { + "type": "number", + "format": "double", + "description": "`dV/dS`." + }, + "gamma": { + "type": "number", + "format": "double", + "description": "`d²V/dS²`." + }, + "rho": { + "type": "number", + "format": "double", + "description": "`dV/dr`, per unit rate." + }, + "theta": { + "type": "number", + "format": "double", + "description": "`dV/dt`, **per year**." + }, + "unavailable": { "type": "object", - "description": "API units, derived from canonical Greek field descriptions.", + "description": "Each omitted Greek has a reason. A resolved zero is present as 0.0.\nCodes distinguish unsupported quantities, model domain/validity,\naccuracy refusals and unresolved numerical derivatives.", "additionalProperties": { - "type": "string" + "$ref": "#/components/schemas/ErrorBody" }, "propertyNames": { "type": "string" } }, - "models": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PricingModelDescriptor" - }, - "description": "All executable pricing models in this build, identical for every caller.\nAuto is a selection rule and retired BS2002 is not an executable model." + "vanna": { + "type": "number", + "format": "double", + "description": "`d²V/(dS dσ)`, per unit volatility." + }, + "vega": { + "type": "number", + "format": "double", + "description": "`dV/dσ`, per unit volatility." + }, + "veta": { + "type": "number", + "format": "double", + "description": "`d²V/(dσ dt)`, **per year** per unit volatility." + }, + "volga": { + "type": "number", + "format": "double", + "description": "`d²V/dσ²`, per unit volatility squared." } } }, - "PricingModelDescriptor": { + "GreeksAllTenRequest": { "type": "object", + "description": "A request for the price and all ten Greeks at a supplied volatility.\n\nThe same shape as `PriceRequest`: the Greeks are derivatives of the\nprice, so they are a property of the same contract and the same\ncoordinates.", "required": [ - "id", - "name", - "version", - "model_version", - "aliases", - "underlying_model", - "method", - "exercise_styles", - "dividend_support", - "operations", - "batch_operations", - "greeks", - "guidance", - "constraints", - "notes" + "exercise_style", + "pricing_model", + "option_type", + "spot", + "strike", + "time_to_expiry_years", + "risk_free_rate", + "dividend_yield", + "volatility" ], "properties": { - "aliases": { + "dividend_schedule": { "type": "array", "items": { - "type": "string" + "$ref": "#/components/schemas/DividendEvent" }, - "description": "Earlier JSON selectors still accepted as aliases of this same algorithm." + "description": "Cash dividends select the finite-difference American model with Auto.", + "maxItems": 16 }, - "batch_operations": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Operations also supported in synchronous batches of at most 100 contracts." + "dividend_yield": { + "type": "number", + "format": "double" }, - "constraints": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ModelConstraint" - }, - "description": "Selected structured limits of the pinned engine for live contracts.\nThese describe capabilities; the engine remains the validation authority." + "exercise_style": { + "$ref": "#/components/schemas/ExerciseStyle" }, - "dividend_support": { - "type": "array", - "items": { - "type": "string" - } + "model_version": { + "type": [ + "string", + "null" + ] }, - "exercise_styles": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ExerciseStyle" - } + "option_type": { + "$ref": "#/components/schemas/OptionType" + }, + "pricing_model": { + "$ref": "#/components/schemas/PricingModel" + }, + "risk_free_rate": { + "type": "number", + "format": "double" + }, + "spot": { + "type": "number", + "format": "double" + }, + "strike": { + "type": "number", + "format": "double" + }, + "time_to_expiry_years": { + "type": "number", + "format": "double" + }, + "volatility": { + "type": "number", + "format": "double" + } + }, + "additionalProperties": false + }, + "GreeksAllTenResponse": { + "type": "object", + "description": "Successful response for one all-ten-Greeks operation.", + "required": [ + "metadata", + "result" + ], + "properties": { + "metadata": { + "$ref": "#/components/schemas/ResponseMetadata" }, + "result": { + "$ref": "#/components/schemas/GreeksAllTenResult" + } + } + }, + "GreeksAllTenResult": { + "type": "object", + "description": "The price and Greeks of one contract at the volatility supplied.", + "required": [ + "price", + "greeks", + "pricing_model", + "model_version" + ], + "properties": { "greeks": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Implemented quantities, not a guarantee they resolve for every input.\nPer-request refusals appear in greeks.unavailable." + "$ref": "#/components/schemas/GreeksAllTen" }, - "guidance": { + "model_version": { "type": "string" }, - "id": { - "$ref": "#/components/schemas/ResolvedPricingModel", - "description": "Canonical JSON pricing_model selector, accepted by pricing operations." + "price": { + "type": "number", + "format": "double" }, - "method": { - "type": "string" + "pricing_model": { + "$ref": "#/components/schemas/ResolvedPricingModel" + } + } + }, + "GreeksStandard": { + "type": "object", + "description": "Available Greeks on the contract's stated bases; absent values are explained by `unavailable`.\n\nTime derivatives are **per year**. The engine reports them per calendar\nday, which embeds a 365-day calendar; the contract carries no calendar,\nso the service scales by the engine's own day count at this boundary and\nthe caller applies whichever convention it holds. Vega is raw `dV/dσ` per\nunit volatility, not per point. Nothing else is rescaled.", + "properties": { + "delta": { + "type": "number", + "format": "double", + "description": "`dV/dS`." }, - "model_version": { - "type": "string", - "description": "Exact compatible model_version pin for requests and response provenance." + "gamma": { + "type": "number", + "format": "double", + "description": "`d²V/dS²`." }, - "name": { - "type": "string" + "rho": { + "type": "number", + "format": "double", + "description": "`dV/dr`, per unit rate." }, - "notes": { - "type": "string" + "theta": { + "type": "number", + "format": "double", + "description": "`dV/dt`, **per year**." }, - "operations": { - "type": "array", - "items": { - "type": "string" + "unavailable": { + "type": "object", + "description": "Each omitted Greek has a reason. A resolved zero is present as 0.0.\nCodes distinguish unsupported quantities, model domain/validity,\naccuracy refusals and unresolved numerical derivatives.", + "additionalProperties": { + "$ref": "#/components/schemas/ErrorBody" }, - "description": "Operation identifiers supported as single requests." - }, - "underlying_model": { - "type": "string" + "propertyNames": { + "type": "string" + } }, - "version": { - "type": "string", - "description": "Algorithm revision, independent of API and engine package versions." + "vega": { + "type": "number", + "format": "double", + "description": "`dV/dσ`, per unit volatility." } } }, - "ProtobufBody": { - "type": "string", - "format": "binary" - }, - "RegimeBasis": { + "GreeksStandardRequest": { "type": "object", - "description": "What the model predicts, not a claim that a reference fit ran on this request.", + "description": "A request for the price and the standard five Greeks at a supplied volatility.\n\nThe same shape as `PriceRequest`: the Greeks are derivatives of the\nprice, so they are a property of the same contract and the same\ncoordinates.", "required": [ - "target" + "exercise_style", + "pricing_model", + "option_type", + "spot", + "strike", + "time_to_expiry_years", + "risk_free_rate", + "dividend_yield", + "volatility" ], "properties": { - "target": { - "$ref": "#/components/schemas/RegimeTarget" + "dividend_schedule": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DividendEvent" + }, + "description": "Cash dividends select the finite-difference American model with Auto.", + "maxItems": 16 + }, + "dividend_yield": { + "type": "number", + "format": "double" + }, + "exercise_style": { + "$ref": "#/components/schemas/ExerciseStyle" + }, + "model_version": { + "type": [ + "string", + "null" + ] + }, + "option_type": { + "$ref": "#/components/schemas/OptionType" + }, + "pricing_model": { + "$ref": "#/components/schemas/PricingModel" + }, + "risk_free_rate": { + "type": "number", + "format": "double" + }, + "spot": { + "type": "number", + "format": "double" + }, + "strike": { + "type": "number", + "format": "double" + }, + "time_to_expiry_years": { + "type": "number", + "format": "double" + }, + "volatility": { + "type": "number", + "format": "double" } - } + }, + "additionalProperties": false }, - "RegimeConsumedPriceRange": { + "GreeksStandardResponse": { "type": "object", - "description": "Contiguous half-open range in the caller's original price array. Only this\nrange enters inference; every supplied price is validated before cropping.", + "description": "Successful response for one standard-five-Greeks operation.", "required": [ - "start_index", - "end_index_exclusive" + "metadata", + "result" ], "properties": { - "end_index_exclusive": { - "type": "integer", - "description": "Exclusive original-array index; equals received_price_count in V1.", - "minimum": 0 + "metadata": { + "$ref": "#/components/schemas/ResponseMetadata" }, - "start_index": { - "type": "integer", - "description": "Inclusive, zero-based original-array index.", - "minimum": 0 + "result": { + "$ref": "#/components/schemas/GreeksStandardResult" } } }, - "RegimeDecisionLabel": { - "type": "string", - "description": "Prediction targets: not_eligible = reference selection is not separate-scale;\nsupported = eligible and separation-test p-value upper bound <= 0.05;\nunsupported = eligible and lower bound > 0.05; unresolved = eligible and\nlower <= 0.05 < upper. These are offline numerical rank bounds, not served\nscores or confidence intervals. Unresolved is never a serving failure.", - "enum": [ - "not_eligible", + "GreeksStandardResult": { + "type": "object", + "description": "The price and Greeks of one contract at the volatility supplied.", + "required": [ + "price", + "greeks", + "pricing_model", + "model_version" + ], + "properties": { + "greeks": { + "$ref": "#/components/schemas/GreeksStandard" + }, + "model_version": { + "type": "string" + }, + "price": { + "type": "number", + "format": "double" + }, + "pricing_model": { + "$ref": "#/components/schemas/ResolvedPricingModel" + } + } + }, + "ImpliedVolatilityRequest": { + "type": "object", + "description": "A request to invert an observed premium into a volatility.\n\nCarries `market_price` and no `volatility`: volatility is the unknown being\nsolved for, so a request that supplied one would be asking the service to\nignore it.", + "required": [ + "exercise_style", + "pricing_model", + "option_type", + "spot", + "strike", + "time_to_expiry_years", + "risk_free_rate", + "dividend_yield", + "market_price" + ], + "properties": { + "dividend_schedule": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DividendEvent" + }, + "maxItems": 16 + }, + "dividend_yield": { + "type": "number", + "format": "double" + }, + "exercise_style": { + "$ref": "#/components/schemas/ExerciseStyle" + }, + "market_price": { + "type": "number", + "format": "double", + "description": "The observed premium to invert." + }, + "model_version": { + "type": [ + "string", + "null" + ] + }, + "option_type": { + "$ref": "#/components/schemas/OptionType" + }, + "pricing_model": { + "$ref": "#/components/schemas/PricingModel" + }, + "risk_free_rate": { + "type": "number", + "format": "double" + }, + "spot": { + "type": "number", + "format": "double" + }, + "strike": { + "type": "number", + "format": "double" + }, + "time_to_expiry_years": { + "type": "number", + "format": "double" + } + }, + "additionalProperties": false + }, + "ImpliedVolatilityResponse": { + "type": "object", + "description": "Successful response for one implied-volatility operation.", + "required": [ + "metadata", + "result" + ], + "properties": { + "metadata": { + "$ref": "#/components/schemas/ResponseMetadata" + }, + "result": { + "$ref": "#/components/schemas/ImpliedVolatilityResult" + } + } + }, + "ImpliedVolatilityResult": { + "type": "object", + "description": "A solved implied volatility and the diagnostics that qualify it.", + "required": [ + "implied_volatility", + "iterations", + "used_fallback", + "residual", + "residual_basis", + "pricing_model", + "model_version" + ], + "properties": { + "implied_volatility": { + "type": "number", + "format": "double", + "description": "Decimal fraction, on the same basis as `PriceRequest.volatility`." + }, + "iterations": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "model_version": { + "type": "string" + }, + "pricing_model": { + "$ref": "#/components/schemas/ResolvedPricingModel" + }, + "residual": { + "type": "number", + "format": "double" + }, + "residual_basis": { + "$ref": "#/components/schemas/ResidualBasis" + }, + "used_fallback": { + "type": "boolean", + "description": "Whether the solve left its primary path.\n\nNot a quality signal on its own: an American solve inverts by bisection\nand reports `true` for every live solve, while a Bachelier solve reports\n`false` because bisection is its canonical solver rather than a degraded\nfallback." + } + } + }, + "ModelConstraint": { + "type": "object", + "required": [ + "quantity", + "description" + ], + "properties": { + "description": { + "type": "string" + }, + "maximum": { + "type": [ + "number", + "null" + ], + "format": "double", + "description": "Inclusive upper bound when present." + }, + "minimum": { + "type": [ + "number", + "null" + ], + "format": "double", + "description": "Inclusive lower bound when present." + }, + "quantity": { + "type": "string", + "description": "Input field or derived quantity, defined by description." + } + } + }, + "OptionType": { + "type": "string", + "description": "Option payoff direction.", + "enum": [ + "call", + "put" + ] + }, + "PolicyResponseMetadata": { + "type": "object", + "description": "Pinned policy identity and contributing record versions nested in response metadata.", + "required": [ + "snapshot_id", + "revision", + "plan", + "records" + ], + "properties": { + "plan": { + "type": "string", + "description": "Versioned plan reference, formatted as id@version." + }, + "records": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Contributing versioned records in resolution precedence order." + }, + "revision": { + "type": "string", + "description": "Full opaque revision, encoded as 64 lowercase hexadecimal digits." + }, + "snapshot_id": { + "type": "string" + } + } + }, + "PriceRequest": { + "type": "object", + "description": "A single option-pricing request. All numeric fields are required.", + "required": [ + "exercise_style", + "pricing_model", + "option_type", + "spot", + "strike", + "time_to_expiry_years", + "risk_free_rate", + "dividend_yield", + "volatility" + ], + "properties": { + "dividend_schedule": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DividendEvent" + }, + "description": "Discrete cash dividends within the option's life. American\nCash-dividend American model only; Auto selects it for a non-empty schedule.", + "maxItems": 16 + }, + "dividend_yield": { + "type": "number", + "format": "double" + }, + "exercise_style": { + "$ref": "#/components/schemas/ExerciseStyle" + }, + "model_version": { + "type": [ + "string", + "null" + ], + "description": "Optional algorithm-version pin. Omit to use the current version." + }, + "option_type": { + "$ref": "#/components/schemas/OptionType" + }, + "pricing_model": { + "$ref": "#/components/schemas/PricingModel" + }, + "risk_free_rate": { + "type": "number", + "format": "double" + }, + "spot": { + "type": "number", + "format": "double" + }, + "strike": { + "type": "number", + "format": "double" + }, + "time_to_expiry_years": { + "type": "number", + "format": "double" + }, + "volatility": { + "type": "number", + "format": "double" + } + }, + "additionalProperties": false + }, + "PriceResponse": { + "type": "object", + "description": "Successful response for one pricing operation.", + "required": [ + "metadata", + "result" + ], + "properties": { + "metadata": { + "$ref": "#/components/schemas/ResponseMetadata" + }, + "result": { + "$ref": "#/components/schemas/PriceResult" + } + } + }, + "PriceResult": { + "type": "object", + "description": "The transport-neutral result of one pricing operation.", + "required": [ + "price", + "exercise_style", + "option_type", + "pricing_model", + "model_version" + ], + "properties": { + "exercise_style": { + "$ref": "#/components/schemas/ExerciseStyle" + }, + "model_version": { + "type": "string", + "description": "Stable revision of the selected pricing algorithm." + }, + "option_type": { + "$ref": "#/components/schemas/OptionType" + }, + "price": { + "type": "number", + "format": "double" + }, + "pricing_model": { + "$ref": "#/components/schemas/ResolvedPricingModel" + } + } + }, + "PricingModel": { + "type": "string", + "description": "Canonical selectors are listed by GET /v1/pricing/models. qd_fixed_point and cash_dividend_american remain input aliases. BS2002 is retired and returns a migration error.", + "enum": [ + "auto", + "black_scholes_merton", + "bjerksund_stensland_2002", + "bjerksund_stensland_1993", + "black_scholes_merton_qdfp", + "black_scholes_merton_fd", + "qd_fixed_point", + "cash_dividend_american" + ] + }, + "PricingModelCatalog": { + "type": "object", + "required": [ + "models", + "engines", + "greek_units" + ], + "properties": { + "engines": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EngineIdentity" + } + }, + "greek_units": { + "type": "object", + "description": "API units, derived from canonical Greek field descriptions.", + "additionalProperties": { + "type": "string" + }, + "propertyNames": { + "type": "string" + } + }, + "models": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PricingModelDescriptor" + }, + "description": "All executable pricing models in this build, identical for every caller.\nAuto is a selection rule and retired BS2002 is not an executable model." + } + } + }, + "PricingModelDescriptor": { + "type": "object", + "required": [ + "id", + "name", + "version", + "model_version", + "aliases", + "underlying_model", + "method", + "exercise_styles", + "dividend_support", + "operations", + "scenario_operations", + "batch_operations", + "greeks", + "guidance", + "constraints", + "notes" + ], + "properties": { + "aliases": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Earlier JSON selectors still accepted as aliases of this same algorithm." + }, + "batch_operations": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Operations also supported in synchronous batches of at most 100 contracts." + }, + "constraints": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ModelConstraint" + }, + "description": "Selected structured limits of the pinned engine for live contracts.\nThese describe capabilities; the engine remains the validation authority." + }, + "dividend_support": { + "type": "array", + "items": { + "type": "string" + } + }, + "exercise_styles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ExerciseStyle" + } + }, + "greeks": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Implemented quantities, not a guarantee they resolve for every input.\nPer-request refusals appear in greeks.unavailable." + }, + "guidance": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/ResolvedPricingModel", + "description": "Canonical JSON pricing_model selector, accepted by pricing operations." + }, + "method": { + "type": "string" + }, + "model_version": { + "type": "string", + "description": "Exact compatible model_version pin for requests and response provenance." + }, + "name": { + "type": "string" + }, + "notes": { + "type": "string" + }, + "operations": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Operation identifiers supported as single requests." + }, + "scenario_operations": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Scenario operations over one contract; HTTP JSON only." + }, + "underlying_model": { + "type": "string" + }, + "version": { + "type": "string", + "description": "Algorithm revision, independent of API and engine package versions." + } + } + }, + "ProtobufBody": { + "type": "string", + "format": "binary" + }, + "RegimeBasis": { + "type": "object", + "description": "What the model predicts, not a claim that a reference fit ran on this request.", + "required": [ + "target" + ], + "properties": { + "target": { + "$ref": "#/components/schemas/RegimeTarget" + } + } + }, + "RegimeConsumedPriceRange": { + "type": "object", + "description": "Contiguous half-open range in the caller's original price array. Only this\nrange enters inference; every supplied price is validated before cropping.", + "required": [ + "start_index", + "end_index_exclusive" + ], + "properties": { + "end_index_exclusive": { + "type": "integer", + "description": "Exclusive original-array index; equals received_price_count in V1.", + "minimum": 0 + }, + "start_index": { + "type": "integer", + "description": "Inclusive, zero-based original-array index.", + "minimum": 0 + } + } + }, + "RegimeDecisionLabel": { + "type": "string", + "description": "Prediction targets: not_eligible = reference selection is not separate-scale;\nsupported = eligible and separation-test p-value upper bound <= 0.05;\nunsupported = eligible and lower bound > 0.05; unresolved = eligible and\nlower <= 0.05 < upper. These are offline numerical rank bounds, not served\nscores or confidence intervals. Unresolved is never a serving failure.", + "enum": [ + "not_eligible", "supported", "unsupported", "unresolved" ] }, - "RegimeDirectionLabel": { - "type": "string", - "enum": [ - "UP", - "DOWN", - "NEUTRAL" - ] + "RegimeDirectionLabel": { + "type": "string", + "enum": [ + "UP", + "DOWN", + "NEUTRAL" + ] + }, + "RegimeHead_RegimeDecisionPrediction": { + "oneOf": [ + { + "type": "object", + "required": [ + "basis", + "prediction", + "status" + ], + "properties": { + "basis": { + "$ref": "#/components/schemas/RegimeBasis" + }, + "prediction": { + "type": "object", + "description": "Tie priority: unresolved, unsupported, supported, not_eligible (first wins).", + "required": [ + "label", + "scores", + "selection" + ], + "properties": { + "label": { + "$ref": "#/components/schemas/RegimeDecisionLabel" + }, + "scores": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RegimeScore_RegimeDecisionLabel" + }, + "description": "Every allowed label occurs exactly once. Array order has no meaning.\nSerialized values satisfy abs(sum(scores[*].value) - 1) <= 1e-12\nseparately for each available prediction. The selected label\nmaximizes the serialized scores; ties use the documented label priority.", + "maxItems": 4, + "minItems": 4 + }, + "selection": { + "$ref": "#/components/schemas/RegimeSelection" + } + } + }, + "status": { + "type": "string", + "enum": [ + "available" + ] + } + } + }, + { + "type": "object", + "required": [ + "basis", + "prediction", + "reason_code", + "status" + ], + "properties": { + "basis": { + "$ref": "#/components/schemas/RegimeBasis" + }, + "prediction": { + "type": "null" + }, + "reason_code": { + "$ref": "#/components/schemas/RegimeNotApplicableReason" + }, + "status": { + "type": "string", + "enum": [ + "not_applicable" + ] + } + } + }, + { + "type": "object", + "required": [ + "basis", + "prediction", + "reason_code", + "status" + ], + "properties": { + "basis": { + "$ref": "#/components/schemas/RegimeBasis" + }, + "prediction": { + "type": "null" + }, + "reason_code": { + "$ref": "#/components/schemas/RegimeUnavailableReason" + }, + "status": { + "type": "string", + "enum": [ + "unavailable" + ] + } + } + } + ], + "description": "Shared envelope for all heads. Available has a prediction and no reason_code.\nBasis is required for every status. Other statuses require prediction:null\nand a stable reason_code. A predicted\nunresolved decision is available, unlike an unavailable head. Whole-request\nruntime failures use the ordinary non-200 error envelope, not a synthetic label." + }, + "RegimeHead_RegimeDirectionPrediction": { + "oneOf": [ + { + "type": "object", + "required": [ + "basis", + "prediction", + "status" + ], + "properties": { + "basis": { + "$ref": "#/components/schemas/RegimeBasis" + }, + "prediction": { + "type": "object", + "description": "Tie priority: NEUTRAL, DOWN, UP (first wins). NEUTRAL is a valid tendency, never missing data.", + "required": [ + "label", + "scores", + "selection" + ], + "properties": { + "label": { + "$ref": "#/components/schemas/RegimeDirectionLabel" + }, + "scores": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RegimeScore_RegimeDirectionLabel" + }, + "description": "Every allowed label occurs exactly once. Array order has no meaning.\nSerialized values satisfy abs(sum(scores[*].value) - 1) <= 1e-12\nseparately for each available prediction. The selected label\nmaximizes the serialized scores; ties use the documented label priority.", + "maxItems": 3, + "minItems": 3 + }, + "selection": { + "$ref": "#/components/schemas/RegimeSelection" + } + } + }, + "status": { + "type": "string", + "enum": [ + "available" + ] + } + } + }, + { + "type": "object", + "required": [ + "basis", + "prediction", + "reason_code", + "status" + ], + "properties": { + "basis": { + "$ref": "#/components/schemas/RegimeBasis" + }, + "prediction": { + "type": "null" + }, + "reason_code": { + "$ref": "#/components/schemas/RegimeNotApplicableReason" + }, + "status": { + "type": "string", + "enum": [ + "not_applicable" + ] + } + } + }, + { + "type": "object", + "required": [ + "basis", + "prediction", + "reason_code", + "status" + ], + "properties": { + "basis": { + "$ref": "#/components/schemas/RegimeBasis" + }, + "prediction": { + "type": "null" + }, + "reason_code": { + "$ref": "#/components/schemas/RegimeUnavailableReason" + }, + "status": { + "type": "string", + "enum": [ + "unavailable" + ] + } + } + } + ], + "description": "Shared envelope for all heads. Available has a prediction and no reason_code.\nBasis is required for every status. Other statuses require prediction:null\nand a stable reason_code. A predicted\nunresolved decision is available, unlike an unavailable head. Whole-request\nruntime failures use the ordinary non-200 error envelope, not a synthetic label." }, - "RegimeHead_RegimeDecisionPrediction": { + "RegimeHead_RegimeStatePrediction": { "oneOf": [ { "type": "object", @@ -6206,7 +7086,7 @@ }, "prediction": { "type": "object", - "description": "Tie priority: unresolved, unsupported, supported, not_eligible (first wins).", + "description": "Tie priority: HIGHER_VOL, LOWER_VOL (first wins). Ordering is relative within the target fit, not historical volatility levels.", "required": [ "label", "scores", @@ -6214,16 +7094,16 @@ ], "properties": { "label": { - "$ref": "#/components/schemas/RegimeDecisionLabel" + "$ref": "#/components/schemas/RegimeStateLabel" }, "scores": { "type": "array", "items": { - "$ref": "#/components/schemas/RegimeScore_RegimeDecisionLabel" + "$ref": "#/components/schemas/RegimeScore_RegimeStateLabel" }, "description": "Every allowed label occurs exactly once. Array order has no meaning.\nSerialized values satisfy abs(sum(scores[*].value) - 1) <= 1e-12\nseparately for each available prediction. The selected label\nmaximizes the serialized scores; ties use the documented label priority.", - "maxItems": 4, - "minItems": 4 + "maxItems": 2, + "minItems": 2 }, "selection": { "$ref": "#/components/schemas/RegimeSelection" @@ -6238,303 +7118,519 @@ } } }, - { - "type": "object", - "required": [ - "basis", - "prediction", - "reason_code", - "status" + { + "type": "object", + "required": [ + "basis", + "prediction", + "reason_code", + "status" + ], + "properties": { + "basis": { + "$ref": "#/components/schemas/RegimeBasis" + }, + "prediction": { + "type": "null" + }, + "reason_code": { + "$ref": "#/components/schemas/RegimeNotApplicableReason" + }, + "status": { + "type": "string", + "enum": [ + "not_applicable" + ] + } + } + }, + { + "type": "object", + "required": [ + "basis", + "prediction", + "reason_code", + "status" + ], + "properties": { + "basis": { + "$ref": "#/components/schemas/RegimeBasis" + }, + "prediction": { + "type": "null" + }, + "reason_code": { + "$ref": "#/components/schemas/RegimeUnavailableReason" + }, + "status": { + "type": "string", + "enum": [ + "unavailable" + ] + } + } + } + ], + "description": "Shared envelope for all heads. Available has a prediction and no reason_code.\nBasis is required for every status. Other statuses require prediction:null\nand a stable reason_code. A predicted\nunresolved decision is available, unlike an unavailable head. Whole-request\nruntime failures use the ordinary non-200 error envelope, not a synthetic label." + }, + "RegimeMetadata": { + "type": "object", + "required": [ + "response_contract_version", + "request_id", + "api_version", + "service_version", + "engines", + "provenance", + "received_price_count", + "window_price_count", + "price_count", + "return_count", + "consumed_price_range", + "observation_index", + "input_price_index", + "as_of", + "return_type", + "return_unit", + "inference_time_us", + "all_prices_consumed" + ], + "properties": { + "all_prices_consumed": { + "type": "boolean", + "description": "True iff consumed_price_range covers [0, received_price_count).\nFalse means intentional contiguous truncation of the oldest prefix only,\nnever invalid-price removal or incomplete processing. Always false in V1." + }, + "api_version": { + "type": "string" + }, + "as_of": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Final price timestamp supplied by the caller, or null when unknown.\nNull never means the HTTP response time." + }, + "budget_period": { + "type": [ + "string", + "null" + ] + }, + "budget_reset_at": { + "type": [ + "string", + "null" + ] + }, + "consumed_price_range": { + "$ref": "#/components/schemas/RegimeConsumedPriceRange" + }, + "engines": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EngineIdentity" + }, + "description": "Library that executed inference. Version and revision come from the\nreleased dependency pin; model weights are identified by provenance.\nDoes not include reference fitting engines that did not execute." + }, + "fcu_charged": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "minimum": 0 + }, + "fcu_quoted": { + "type": [ + "integer", + "null" ], - "properties": { - "basis": { - "$ref": "#/components/schemas/RegimeBasis" - }, - "prediction": { + "format": "int64", + "minimum": 0 + }, + "fcu_schedule_id": { + "type": [ + "string", + "null" + ] + }, + "fcu_schedule_version": { + "type": [ + "string", + "null" + ] + }, + "inference_time_us": { + "type": "integer", + "format": "int64", + "description": "Monotonic elapsed microseconds, truncated to an integer. Starts after\nacquiring the model lock and committing admission, before selecting the\ninput slice; ends immediately after predict returns its heads. Includes\nslice selection, preprocessing, tensors, inference, output extraction,\nsoftmax and head construction/gating. Excludes loading, admission/queue,\nmetadata construction, serialization, HTTP and network time.", + "minimum": 0 + }, + "input_price_index": { + "type": "integer", + "description": "Original-array price index at the selected return's end:\nconsumed_price_range.start_index + observation_index + 1.", + "minimum": 0 + }, + "observation_index": { + "type": "integer", + "description": "Zero-based return index within consumed_price_range: 505. Return i uses\noriginal prices[start_index + i] and prices[start_index + i + 1].", + "minimum": 0 + }, + "policy": { + "oneOf": [ + { "type": "null" }, - "reason_code": { - "$ref": "#/components/schemas/RegimeNotApplicableReason" - }, - "status": { - "type": "string", - "enum": [ - "not_applicable" - ] + { + "$ref": "#/components/schemas/PolicyResponseMetadata" } - } + ] }, - { - "type": "object", - "required": [ - "basis", - "prediction", - "reason_code", - "status" + "price_count": { + "type": "integer", + "description": "Number of prices actually consumed by inference.", + "minimum": 0 + }, + "provenance": { + "$ref": "#/components/schemas/RegimeProvenance" + }, + "received_price_count": { + "type": "integer", + "description": "Number of prices received before cropping.", + "minimum": 0 + }, + "remaining_fcu_instance_estimate": { + "type": [ + "integer", + "null" ], - "properties": { - "basis": { - "$ref": "#/components/schemas/RegimeBasis" - }, - "prediction": { - "type": "null" - }, - "reason_code": { - "$ref": "#/components/schemas/RegimeUnavailableReason" - }, - "status": { - "type": "string", - "enum": [ - "unavailable" - ] - } - } + "format": "int64", + "minimum": 0 + }, + "request_id": { + "type": "string" + }, + "response_contract_version": { + "type": "string" + }, + "return_count": { + "type": "integer", + "description": "Equals consumed_price_range.end_index_exclusive - start_index - 1.", + "minimum": 0 + }, + "return_type": { + "$ref": "#/components/schemas/RegimeReturnType" + }, + "return_unit": { + "$ref": "#/components/schemas/RegimeReturnUnit" + }, + "service_version": { + "type": "string" + }, + "window_price_count": { + "type": "integer", + "description": "Intermediate public window size: always 512, covering original indices\n[received_price_count - 512, received_price_count). Not the consumed count.", + "minimum": 0 } + } + }, + "RegimeNotApplicableReason": { + "type": "string", + "enum": [ + "predicted_not_eligible" + ] + }, + "RegimeProvenance": { + "type": "object", + "description": "Pinned served artifact identity; no request-time reference fitting is implied.", + "required": [ + "model_id", + "model_status", + "model_sha256", + "preprocessing", + "teacher_executed" ], - "description": "Shared envelope for all heads. Available has a prediction and no reason_code.\nBasis is required for every status. Other statuses require prediction:null\nand a stable reason_code. A predicted\nunresolved decision is available, unlike an unavailable head. Whole-request\nruntime failures use the ordinary non-200 error envelope, not a synthetic label." + "properties": { + "model_id": { + "type": "string", + "description": "Immutable identity covering weights, preprocessing, reference procedure\nand label definitions; see the REGIME_MODEL_V1.md identity record." + }, + "model_sha256": { + "type": "string", + "description": "Lowercase SHA-256 of the exact served student.onnx file bytes, not the\ntraining checkpoint, manifest, runtime binary or response.", + "pattern": "^[0-9a-f]{64}$" + }, + "model_status": { + "type": "string" + }, + "preprocessing": { + "type": "string", + "description": "Supplemental preprocessing identity; explicit return semantics are in metadata." + }, + "teacher_executed": { + "type": "boolean" + } + } }, - "RegimeHead_RegimeDirectionPrediction": { - "oneOf": [ - { - "type": "object", - "required": [ - "basis", - "prediction", - "status" + "RegimeRequest": { + "type": "object", + "description": "At least 512 comparable positive prices, ordered oldest to newest.\nNo sampling interval is supplied, inferred or assumed.\nLonger arrays are cropped to the latest 512; inference uses that window's\nlatest 507 prices. Every supplied price must be finite and positive.\nThe encoded request body is limited to 32 KiB (policy may narrow this).\nCorporate-action adjustment is the caller's responsibility; the V1 model\nwas trained on split-adjusted closes, without cash-dividend total-return adjustment.", + "required": [ + "prices" + ], + "properties": { + "as_of": { + "type": [ + "string", + "null" ], - "properties": { - "basis": { - "$ref": "#/components/schemas/RegimeBasis" - }, - "prediction": { - "type": "object", - "description": "Tie priority: NEUTRAL, DOWN, UP (first wins). NEUTRAL is a valid tendency, never missing data.", - "required": [ - "label", - "scores", - "selection" - ], - "properties": { - "label": { - "$ref": "#/components/schemas/RegimeDirectionLabel" - }, - "scores": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RegimeScore_RegimeDirectionLabel" - }, - "description": "Every allowed label occurs exactly once. Array order has no meaning.\nSerialized values satisfy abs(sum(scores[*].value) - 1) <= 1e-12\nseparately for each available prediction. The selected label\nmaximizes the serialized scores; ties use the documented label priority.", - "maxItems": 3, - "minItems": 3 - }, - "selection": { - "$ref": "#/components/schemas/RegimeSelection" - } - } - }, - "status": { - "type": "string", - "enum": [ - "available" - ] - } - } + "format": "date-time", + "description": "Timestamp of the final supplied price, echoed unchanged. Never inferred\nfrom the server clock. RFC 3339 with an explicit UTC offset.", + "maxLength": 64 + }, + "prices": { + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "minItems": 512 + } + }, + "additionalProperties": false + }, + "RegimeResponse": { + "type": "object", + "required": [ + "result", + "metadata" + ], + "properties": { + "metadata": { + "$ref": "#/components/schemas/RegimeMetadata" + }, + "result": { + "$ref": "#/components/schemas/RegimeResult" + } + } + }, + "RegimeResult": { + "type": "object", + "required": [ + "score_contract", + "decision", + "direction", + "state" + ], + "properties": { + "decision": { + "$ref": "#/components/schemas/RegimeHead_RegimeDecisionPrediction" }, - { - "type": "object", - "required": [ - "basis", - "prediction", - "reason_code", - "status" - ], - "properties": { - "basis": { - "$ref": "#/components/schemas/RegimeBasis" - }, - "prediction": { - "type": "null" - }, - "reason_code": { - "$ref": "#/components/schemas/RegimeNotApplicableReason" - }, - "status": { - "type": "string", - "enum": [ - "not_applicable" - ] - } - } + "direction": { + "$ref": "#/components/schemas/RegimeHead_RegimeDirectionPrediction" }, - { - "type": "object", - "required": [ - "basis", - "prediction", - "reason_code", - "status" - ], - "properties": { - "basis": { - "$ref": "#/components/schemas/RegimeBasis" - }, - "prediction": { - "type": "null" - }, - "reason_code": { - "$ref": "#/components/schemas/RegimeUnavailableReason" - }, - "status": { - "type": "string", - "enum": [ - "unavailable" - ] - } - } + "score_contract": { + "$ref": "#/components/schemas/RegimeScoreContract" + }, + "state": { + "$ref": "#/components/schemas/RegimeHead_RegimeStatePrediction" } + } + }, + "RegimeReturnType": { + "type": "string", + "enum": [ + "log" + ] + }, + "RegimeReturnUnit": { + "type": "string", + "enum": [ + "fraction" + ] + }, + "RegimeScoreContract": { + "type": "object", + "description": "Applies separately to every available head; never multiply heads or interpret\ntheir scores as a joint probability. Identifies score semantics, not calibration.", + "required": [ + "kind", + "version", + "sum_tolerance" ], - "description": "Shared envelope for all heads. Available has a prediction and no reason_code.\nBasis is required for every status. Other statuses require prediction:null\nand a stable reason_code. A predicted\nunresolved decision is available, unlike an unavailable head. Whole-request\nruntime failures use the ordinary non-200 error envelope, not a synthetic label." + "properties": { + "kind": { + "$ref": "#/components/schemas/RegimeScoreKind" + }, + "sum_tolerance": { + "type": "number", + "format": "double", + "description": "For each available prediction, using serialized values:\nabs(sum(prediction.scores[*].value) - 1) <= sum_tolerance." + }, + "version": { + "type": "string" + } + } }, - "RegimeHead_RegimeStatePrediction": { - "oneOf": [ - { - "type": "object", - "required": [ - "basis", - "prediction", - "status" - ], - "properties": { - "basis": { - "$ref": "#/components/schemas/RegimeBasis" - }, - "prediction": { - "type": "object", - "description": "Tie priority: HIGHER_VOL, LOWER_VOL (first wins). Ordering is relative within the target fit, not historical volatility levels.", - "required": [ - "label", - "scores", - "selection" - ], - "properties": { - "label": { - "$ref": "#/components/schemas/RegimeStateLabel" - }, - "scores": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RegimeScore_RegimeStateLabel" - }, - "description": "Every allowed label occurs exactly once. Array order has no meaning.\nSerialized values satisfy abs(sum(scores[*].value) - 1) <= 1e-12\nseparately for each available prediction. The selected label\nmaximizes the serialized scores; ties use the documented label priority.", - "maxItems": 2, - "minItems": 2 - }, - "selection": { - "$ref": "#/components/schemas/RegimeSelection" - } - } - }, - "status": { - "type": "string", - "enum": [ - "available" - ] - } - } + "RegimeScoreKind": { + "type": "string", + "enum": [ + "uncalibrated_softmax" + ] + }, + "RegimeScore_RegimeDecisionLabel": { + "type": "object", + "required": [ + "label", + "value" + ], + "properties": { + "label": { + "type": "string", + "description": "Prediction targets: not_eligible = reference selection is not separate-scale;\nsupported = eligible and separation-test p-value upper bound <= 0.05;\nunsupported = eligible and lower bound > 0.05; unresolved = eligible and\nlower <= 0.05 < upper. These are offline numerical rank bounds, not served\nscores or confidence intervals. Unresolved is never a serving failure.", + "enum": [ + "not_eligible", + "supported", + "unsupported", + "unresolved" + ] }, - { - "type": "object", - "required": [ - "basis", - "prediction", - "reason_code", - "status" - ], - "properties": { - "basis": { - "$ref": "#/components/schemas/RegimeBasis" - }, - "prediction": { - "type": "null" - }, - "reason_code": { - "$ref": "#/components/schemas/RegimeNotApplicableReason" - }, - "status": { - "type": "string", - "enum": [ - "not_applicable" - ] - } - } + "value": { + "type": "number", + "format": "double", + "description": "Finite score in [0,1]; not a calibrated probability.", + "maximum": 1, + "minimum": 0 + } + } + }, + "RegimeScore_RegimeDirectionLabel": { + "type": "object", + "required": [ + "label", + "value" + ], + "properties": { + "label": { + "type": "string", + "enum": [ + "UP", + "DOWN", + "NEUTRAL" + ] }, - { - "type": "object", - "required": [ - "basis", - "prediction", - "reason_code", - "status" - ], - "properties": { - "basis": { - "$ref": "#/components/schemas/RegimeBasis" - }, - "prediction": { - "type": "null" - }, - "reason_code": { - "$ref": "#/components/schemas/RegimeUnavailableReason" - }, - "status": { - "type": "string", - "enum": [ - "unavailable" - ] - } - } + "value": { + "type": "number", + "format": "double", + "description": "Finite score in [0,1]; not a calibrated probability.", + "maximum": 1, + "minimum": 0 + } + } + }, + "RegimeScore_RegimeStateLabel": { + "type": "object", + "required": [ + "label", + "value" + ], + "properties": { + "label": { + "type": "string", + "description": "Volatility relative to the other state in the reference Markov model, not\nhistorical norms. Fixed Student-t degrees of freedom make scale ordering\nidentical to standard-deviation ordering. Neither label indicates direction.", + "enum": [ + "LOWER_VOL", + "HIGHER_VOL" + ] + }, + "value": { + "type": "number", + "format": "double", + "description": "Finite score in [0,1]; not a calibrated probability.", + "maximum": 1, + "minimum": 0 } + } + }, + "RegimeSelection": { + "type": "string", + "enum": [ + "argmax" + ] + }, + "RegimeStateLabel": { + "type": "string", + "description": "Volatility relative to the other state in the reference Markov model, not\nhistorical norms. Fixed Student-t degrees of freedom make scale ordering\nidentical to standard-deviation ordering. Neither label indicates direction.", + "enum": [ + "LOWER_VOL", + "HIGHER_VOL" + ] + }, + "RegimeTarget": { + "type": "string", + "enum": [ + "reference_procedure_outcome", + "selected_state_tendency", + "filtered_scale_order_membership" + ] + }, + "RegimeUnavailableReason": { + "type": "string", + "enum": [ + "prediction_unavailable" + ] + }, + "ResidualBasis": { + "type": "string", + "description": "The basis a solved residual is expressed in.\n\nThe residual is not one quantity. European and Black paths report a\nforward-normalised price residual comparable against the engine's\nconvergence threshold; American models report a\nraw option-price residual. Publishing the number without its basis would\ninvite exactly the cross-model comparison it does not support, so the basis\ntravels with it and a client that ignores this field cannot silently get it\nwrong.", + "enum": [ + "forward_normalised_price", + "option_price" + ] + }, + "ResolvedCostModel": { + "type": "object", + "description": "Selected numerical model identity, without executing the numerical kernel.", + "required": [ + "model", + "quantity" ], - "description": "Shared envelope for all heads. Available has a prediction and no reason_code.\nBasis is required for every status. Other statuses require prediction:null\nand a stable reason_code. A predicted\nunresolved decision is available, unlike an unavailable head. Whole-request\nruntime failures use the ordinary non-200 error envelope, not a synthetic label." + "properties": { + "model": { + "type": "string" + }, + "quantity": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + } }, - "RegimeMetadata": { + "ResolvedPricingModel": { + "type": "string", + "description": "Pricing algorithm selected after resolving an input request.\n\nUnlike [`PricingModel`], this output-only type cannot represent `auto`.", + "x-extensible-enum": [ + "black_scholes_merton", + "bjerksund_stensland_1993", + "black_scholes_merton_qdfp", + "black_scholes_merton_fd" + ] + }, + "ResponseMetadata": { "type": "object", + "description": "Metadata shared by every successful response and API error.", "required": [ - "response_contract_version", "request_id", "api_version", - "service_version", + "engine_version", "engines", - "provenance", - "received_price_count", - "window_price_count", - "price_count", - "return_count", - "consumed_price_range", - "observation_index", - "input_price_index", - "as_of", - "return_type", - "return_unit", - "inference_time_us", - "all_prices_consumed" + "units_charged", + "compute_schedule_version" ], "properties": { - "all_prices_consumed": { - "type": "boolean", - "description": "True iff consumed_price_range covers [0, received_price_count).\nFalse means intentional contiguous truncation of the oldest prefix only,\nnever invalid-price removal or incomplete processing. Always false in V1." - }, "api_version": { "type": "string" }, - "as_of": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "Final price timestamp supplied by the caller, or null when unknown.\nNull never means the HTTP response time." - }, "budget_period": { "type": [ "string", @@ -6545,17 +7641,22 @@ "type": [ "string", "null" - ] + ], + "description": "UTC end of the budget period from the pinned account snapshot, when known." }, - "consumed_price_range": { - "$ref": "#/components/schemas/RegimeConsumedPriceRange" + "compute_schedule_version": { + "type": "string" + }, + "engine_version": { + "type": "string", + "description": "Version of the operation's primary engine (legacy compatibility field)." }, "engines": { "type": "array", "items": { "$ref": "#/components/schemas/EngineIdentity" }, - "description": "Library that executed inference. Version and revision come from the\nreleased dependency pin; model weights are identified by provenance.\nDoes not include reference fitting engines that did not execute." + "description": "Engines selected for this operation, including their released Git revisions.\nDoes not assert execution on pre-execution failures." }, "fcu_charged": { "type": [ @@ -6563,6 +7664,7 @@ "null" ], "format": "int64", + "description": "Committed commercial charge, including on post-commit failures.\nAbsent in open development mode; zero for protected pre-commit refusals.", "minimum": 0 }, "fcu_quoted": { @@ -6571,6 +7673,7 @@ "null" ], "format": "int64", + "description": "Commercial quote; absent until reservation succeeds.", "minimum": 0 }, "fcu_schedule_id": { @@ -6585,22 +7688,6 @@ "null" ] }, - "inference_time_us": { - "type": "integer", - "format": "int64", - "description": "Monotonic elapsed microseconds, truncated to an integer. Starts after\nacquiring the model lock and committing admission, before selecting the\ninput slice; ends immediately after predict returns its heads. Includes\nslice selection, preprocessing, tensors, inference, output extraction,\nsoftmax and head construction/gating. Excludes loading, admission/queue,\nmetadata construction, serialization, HTTP and network time.", - "minimum": 0 - }, - "input_price_index": { - "type": "integer", - "description": "Original-array price index at the selected return's end:\nconsumed_price_range.start_index + observation_index + 1.", - "minimum": 0 - }, - "observation_index": { - "type": "integer", - "description": "Zero-based return index within consumed_price_range: 505. Return i uses\noriginal prices[start_index + i] and prices[start_index + i + 1].", - "minimum": 0 - }, "policy": { "oneOf": [ { @@ -6611,433 +7698,644 @@ } ] }, - "price_count": { - "type": "integer", - "description": "Number of prices actually consumed by inference.", - "minimum": 0 - }, - "provenance": { - "$ref": "#/components/schemas/RegimeProvenance" - }, - "received_price_count": { - "type": "integer", - "description": "Number of prices received before cropping.", - "minimum": 0 - }, "remaining_fcu_instance_estimate": { "type": [ "integer", "null" ], "format": "int64", + "description": "Instance estimate, never an authoritative account balance or authorization input.", "minimum": 0 }, "request_id": { "type": "string" }, - "response_contract_version": { - "type": "string" - }, - "return_count": { + "units_charged": { "type": "integer", - "description": "Equals consumed_price_range.end_index_exclusive - start_index - 1.", + "format": "int64", "minimum": 0 + } + } + }, + "ScenarioItem": { + "allOf": [ + { + "$ref": "#/components/schemas/ScenarioOutcome" }, - "return_type": { - "$ref": "#/components/schemas/RegimeReturnType" + { + "type": "object", + "required": [ + "index", + "inputs" + ], + "properties": { + "index": { + "type": "integer", + "format": "int32", + "description": "Zero-based index in the submitted scenarios array, including duplicate points.", + "minimum": 0 + }, + "inputs": { + "$ref": "#/components/schemas/PriceRequest" + } + } + } + ] + }, + "ScenarioOutcome": { + "oneOf": [ + { + "type": "object", + "required": [ + "result", + "price_change", + "status" + ], + "properties": { + "position_pnl": { + "type": [ + "number", + "null" + ], + "format": "double", + "description": "price_change * signed quantity * multiplier; absent without a position." + }, + "price_change": { + "type": "number", + "format": "double", + "description": "Scenario option price minus base option price, per underlying unit." + }, + "result": { + "$ref": "#/components/schemas/PriceResult" + }, + "status": { + "type": "string", + "enum": [ + "success" + ] + } + } }, - "return_unit": { - "$ref": "#/components/schemas/RegimeReturnUnit" + { + "type": "object", + "required": [ + "error", + "status" + ], + "properties": { + "error": { + "$ref": "#/components/schemas/ErrorBody" + }, + "status": { + "type": "string", + "enum": [ + "error" + ] + } + } + } + ] + }, + "ScenarioPoint": { + "type": "object", + "description": "Changes applied independently to the base contract. Monetary values use the\ncontract's quote currency; volatility is annualized decimal volatility.", + "properties": { + "elapsed_time_years": { + "type": "number", + "format": "double", + "description": "Subtract from base time to expiry. Zero holds expiry fixed; cannot exceed it." }, - "service_version": { - "type": "string" + "spot": { + "type": [ + "number", + "null" + ], + "format": "double", + "description": "Absolute underlying price; mutually exclusive with spot_change." }, - "window_price_count": { - "type": "integer", - "description": "Intermediate public window size: always 512, covering original indices\n[received_price_count - 512, received_price_count). Not the consumed count.", - "minimum": 0 + "spot_change": { + "type": [ + "number", + "null" + ], + "format": "double", + "description": "Add this amount to the base underlying price (5 means five currency units)." + }, + "volatility": { + "type": [ + "number", + "null" + ], + "format": "double", + "description": "Absolute annualized volatility; omitted means hold base volatility fixed." } - } - }, - "RegimeNotApplicableReason": { - "type": "string", - "enum": [ - "predicted_not_eligible" - ] + }, + "additionalProperties": false }, - "RegimeProvenance": { + "ScenarioPosition": { "type": "object", - "description": "Pinned served artifact identity; no request-time reference fitting is implied.", + "description": "Optional signed position scaling. P&L is relative to the base model value,\nnot trade entry cost. No currency conversion, fees or financing are applied.", "required": [ - "model_id", - "model_status", - "model_sha256", - "preprocessing", - "teacher_executed" + "quantity", + "multiplier" ], "properties": { - "model_id": { - "type": "string", - "description": "Immutable identity covering weights, preprocessing, reference procedure\nand label definitions; see the REGIME_MODEL_V1.md identity record." - }, - "model_sha256": { - "type": "string", - "description": "Lowercase SHA-256 of the exact served student.onnx file bytes, not the\ntraining checkpoint, manifest, runtime binary or response.", - "pattern": "^[0-9a-f]{64}$" - }, - "model_status": { - "type": "string" - }, - "preprocessing": { - "type": "string", - "description": "Supplemental preprocessing identity; explicit return semantics are in metadata." + "multiplier": { + "type": "number", + "format": "double", + "description": "Positive number of underlying units per contract; e.g. 100 for equity options." }, - "teacher_executed": { - "type": "boolean" + "quantity": { + "type": "number", + "format": "double", + "description": "Signed contract quantity: positive for long, negative for short." } - } + }, + "additionalProperties": false }, - "RegimeRequest": { + "ScenarioRequest": { "type": "object", - "description": "At least 512 comparable positive prices, ordered oldest to newest.\nNo sampling interval is supplied, inferred or assumed.\nLonger arrays are cropped to the latest 512; inference uses that window's\nlatest 507 prices. Every supplied price must be finite and positive.\nThe encoded request body is limited to 32 KiB (policy may narrow this).\nCorporate-action adjustment is the caller's responsibility; the V1 model\nwas trained on split-adjusted closes, without cash-dividend total-return adjustment.", + "description": "Independently reprice a base option at explicit points. Send a curve or a\nflattened grid in the desired order. Only explicit BSM and BS1993 are supported.", "required": [ - "prices" + "contract", + "scenarios" ], "properties": { - "as_of": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "Timestamp of the final supplied price, echoed unchanged. Never inferred\nfrom the server clock. RFC 3339 with an explicit UTC offset.", - "maxLength": 64 + "contract": { + "$ref": "#/components/schemas/PriceRequest" }, - "prices": { + "position": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ScenarioPosition" + } + ] + }, + "scenarios": { "type": "array", "items": { - "type": "number", - "format": "double" + "$ref": "#/components/schemas/ScenarioPoint" }, - "minItems": 512 + "description": "1–99 points; base plus points must fit the account's price batch limit.", + "maxItems": 99, + "minItems": 1 } }, "additionalProperties": false }, - "RegimeResponse": { + "ScenarioResponse": { "type": "object", "required": [ - "result", - "metadata" + "metadata", + "base", + "results" ], "properties": { + "base": { + "$ref": "#/components/schemas/ScenarioValuation" + }, "metadata": { - "$ref": "#/components/schemas/RegimeMetadata" + "$ref": "#/components/schemas/ResponseMetadata" }, - "result": { - "$ref": "#/components/schemas/RegimeResult" + "position": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ScenarioPosition" + } + ] + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScenarioItem" + } } } }, - "RegimeResult": { + "ScenarioSolveBoundary": { + "oneOf": [ + { + "type": "object", + "required": [ + "spot", + "value", + "crossing", + "refinements", + "bracket_width", + "kind" + ], + "properties": { + "bracket_width": { + "type": "number", + "format": "double" + }, + "crossing": { + "type": "boolean" + }, + "kind": { + "type": "string", + "enum": [ + "point" + ] + }, + "refinements": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "spot": { + "type": "number", + "format": "double" + }, + "value": { + "type": "number", + "format": "double" + } + } + }, + { + "type": "object", + "required": [ + "min_spot", + "max_spot", + "value", + "kind" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "interval" + ] + }, + "max_spot": { + "type": "number", + "format": "double" + }, + "min_spot": { + "type": "number", + "format": "double" + }, + "value": { + "type": "number", + "format": "double" + } + } + } + ], + "description": "Evaluated target boundaries. Intervals are certified at half-cell sampling\nresolution, not a proof for every real number in the interval." + }, + "ScenarioSolveCoverage": { + "oneOf": [ + { + "type": "object", + "description": "Every candidate resolved at the declared scan resolution; smaller\nstructures can be missed. Empty boundaries mean no root at this resolution.", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "resolved" + ] + } + } + }, + { + "type": "object", + "required": [ + "cells", + "status" + ], + "properties": { + "cells": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScenarioSolveUnresolvedCell" + } + }, + "status": { + "type": "string", + "enum": [ + "unresolved" + ] + } + } + } + ] + }, + "ScenarioSolveProfile": { "type": "object", + "description": "Fixed bounded work profile, independent of user tolerances and target.", "required": [ - "score_contract", - "decision", - "direction", - "state" + "id", + "scan_intervals", + "max_refinements", + "max_price_evaluations" ], "properties": { - "decision": { - "$ref": "#/components/schemas/RegimeHead_RegimeDecisionPrediction" + "id": { + "type": "string" }, - "direction": { - "$ref": "#/components/schemas/RegimeHead_RegimeDirectionPrediction" + "max_price_evaluations": { + "type": "integer", + "format": "int64", + "minimum": 0 }, - "score_contract": { - "$ref": "#/components/schemas/RegimeScoreContract" + "max_refinements": { + "type": "integer", + "format": "int32", + "minimum": 0 }, - "state": { - "$ref": "#/components/schemas/RegimeHead_RegimeStatePrediction" + "scan_intervals": { + "type": "integer", + "format": "int32", + "minimum": 0 } } }, - "RegimeReturnType": { - "type": "string", - "enum": [ - "log" - ] + "ScenarioSolveRegion": { + "type": "object", + "required": [ + "min_spot", + "max_spot", + "sign" + ], + "properties": { + "max_spot": { + "type": "number", + "format": "double" + }, + "min_spot": { + "type": "number", + "format": "double" + }, + "sign": { + "$ref": "#/components/schemas/ScenarioSolveSign" + } + } }, - "RegimeReturnUnit": { + "ScenarioSolveSign": { "type": "string", "enum": [ - "fraction" + "above_target", + "below_target", + "at_target" ] }, - "RegimeScoreContract": { + "ScenarioSolveTarget": { + "oneOf": [ + { + "type": "object", + "required": [ + "value", + "kind" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "option_price" + ] + }, + "value": { + "type": "number", + "format": "double" + } + } + }, + { + "type": "object", + "required": [ + "value", + "position", + "kind" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "position_pnl" + ] + }, + "position": { + "$ref": "#/components/schemas/ScenarioPosition" + }, + "value": { + "type": "number", + "format": "double" + } + } + } + ], + "description": "Target of a bounded spot solve. Position P&L is relative to the original\nbase model valuation, with signed quantity and explicit multiplier." + }, + "ScenarioSolveUnresolvedCell": { "type": "object", - "description": "Applies separately to every available head; never multiply heads or interpret\ntheir scores as a joint probability. Identifies score semantics, not calibration.", "required": [ - "kind", - "version", - "sum_tolerance" + "lower_spot", + "upper_spot", + "best_spot", + "best_abs_value_error", + "reason" ], "properties": { - "kind": { - "$ref": "#/components/schemas/RegimeScoreKind" + "best_abs_value_error": { + "type": "number", + "format": "double" }, - "sum_tolerance": { + "best_spot": { "type": "number", - "format": "double", - "description": "For each available prediction, using serialized values:\nabs(sum(prediction.scores[*].value) - 1) <= sum_tolerance." + "format": "double" }, - "version": { - "type": "string" + "lower_spot": { + "type": "number", + "format": "double" + }, + "reason": { + "$ref": "#/components/schemas/ScenarioSolveUnresolvedReason" + }, + "upper_spot": { + "type": "number", + "format": "double" } } }, - "RegimeScoreKind": { + "ScenarioSolveUnresolvedReason": { "type": "string", "enum": [ - "uncalibrated_softmax" + "refinement_budget_exhausted", + "bracket_collapsed_above_tolerance", + "spot_tolerance_reached", + "representative_out_of_band" ] }, - "RegimeScore_RegimeDecisionLabel": { + "ScenarioSpotSolveRequest": { "type": "object", "required": [ - "label", - "value" + "contract", + "target", + "min_spot", + "max_spot" ], "properties": { - "label": { - "type": "string", - "description": "Prediction targets: not_eligible = reference selection is not separate-scale;\nsupported = eligible and separation-test p-value upper bound <= 0.05;\nunsupported = eligible and lower bound > 0.05; unresolved = eligible and\nlower <= 0.05 < upper. These are offline numerical rank bounds, not served\nscores or confidence intervals. Unresolved is never a serving failure.", - "enum": [ - "not_eligible", - "supported", - "unsupported", - "unresolved" - ] + "contract": { + "$ref": "#/components/schemas/PriceRequest" }, - "value": { + "elapsed_time_years": { + "type": "number", + "format": "double" + }, + "max_spot": { + "type": "number", + "format": "double" + }, + "min_spot": { "type": "number", "format": "double", - "description": "Finite score in [0,1]; not a calibrated probability.", - "maximum": 1, - "minimum": 0 - } - } - }, - "RegimeScore_RegimeDirectionLabel": { - "type": "object", - "required": [ - "label", - "value" - ], - "properties": { - "label": { - "type": "string", - "enum": [ - "UP", - "DOWN", - "NEUTRAL" - ] + "description": "Positive finite search bounds, min_spot strictly below max_spot." }, - "value": { + "spot_tolerance": { "type": "number", "format": "double", - "description": "Finite score in [0,1]; not a calibrated probability.", - "maximum": 1, - "minimum": 0 - } - } - }, - "RegimeScore_RegimeStateLabel": { - "type": "object", - "required": [ - "label", - "value" - ], - "properties": { - "label": { - "type": "string", - "description": "Volatility relative to the other state in the reference Markov model, not\nhistorical norms. Fixed Student-t degrees of freedom make scale ordering\nidentical to standard-deviation ordering. Neither label indicates direction.", - "enum": [ - "LOWER_VOL", - "HIGHER_VOL" - ] + "description": "Positive absolute spot localization tolerance; default 1e-9." }, - "value": { + "target": { + "$ref": "#/components/schemas/ScenarioSolveTarget" + }, + "value_tolerance": { "type": "number", "format": "double", - "description": "Finite score in [0,1]; not a calibrated probability.", - "maximum": 1, - "minimum": 0 + "description": "Positive absolute tolerance in the target's units; default 1e-6." + }, + "volatility": { + "type": [ + "number", + "null" + ], + "format": "double", + "description": "Absolute volatility; omitted holds base volatility fixed." } - } - }, - "RegimeSelection": { - "type": "string", - "enum": [ - "argmax" - ] - }, - "RegimeStateLabel": { - "type": "string", - "description": "Volatility relative to the other state in the reference Markov model, not\nhistorical norms. Fixed Student-t degrees of freedom make scale ordering\nidentical to standard-deviation ordering. Neither label indicates direction.", - "enum": [ - "LOWER_VOL", - "HIGHER_VOL" - ] - }, - "RegimeTarget": { - "type": "string", - "enum": [ - "reference_procedure_outcome", - "selected_state_tendency", - "filtered_scale_order_membership" - ] - }, - "RegimeUnavailableReason": { - "type": "string", - "enum": [ - "prediction_unavailable" - ] - }, - "ResidualBasis": { - "type": "string", - "description": "The basis a solved residual is expressed in.\n\nThe residual is not one quantity. European and Black paths report a\nforward-normalised price residual comparable against the engine's\nconvergence threshold; American models report a\nraw option-price residual. Publishing the number without its basis would\ninvite exactly the cross-model comparison it does not support, so the basis\ntravels with it and a client that ignores this field cannot silently get it\nwrong.", - "enum": [ - "forward_normalised_price", - "option_price" - ] + }, + "additionalProperties": false }, - "ResolvedCostModel": { + "ScenarioSpotSolveResponse": { "type": "object", - "description": "Selected numerical model identity, without executing the numerical kernel.", "required": [ - "model", - "quantity" + "metadata", + "result" ], "properties": { - "model": { - "type": "string" + "metadata": { + "$ref": "#/components/schemas/ResponseMetadata" }, - "quantity": { - "type": "integer", - "format": "int64", - "minimum": 0 + "result": { + "$ref": "#/components/schemas/ScenarioSpotSolveResult" } } }, - "ResolvedPricingModel": { - "type": "string", - "description": "Pricing algorithm selected after resolving an input request.\n\nUnlike [`PricingModel`], this output-only type cannot represent `auto`.", - "x-extensible-enum": [ - "black_scholes_merton", - "bjerksund_stensland_1993", - "black_scholes_merton_qdfp", - "black_scholes_merton_fd" - ] - }, - "ResponseMetadata": { + "ScenarioSpotSolveResult": { "type": "object", - "description": "Metadata shared by every successful response and API error.", "required": [ - "request_id", - "api_version", - "engine_version", - "engines", - "units_charged", - "compute_schedule_version" + "pricing_model", + "model_version", + "base", + "inputs", + "target", + "min_spot", + "max_spot", + "spot_tolerance", + "value_tolerance", + "profile", + "reference_net_value", + "boundaries", + "regions", + "coverage", + "price_evaluations" ], "properties": { - "api_version": { - "type": "string" + "base": { + "$ref": "#/components/schemas/ScenarioValuation" }, - "budget_period": { - "type": [ - "string", - "null" - ] + "boundaries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScenarioSolveBoundary" + } }, - "budget_reset_at": { - "type": [ - "string", - "null" - ], - "description": "UTC end of the budget period from the pinned account snapshot, when known." + "coverage": { + "$ref": "#/components/schemas/ScenarioSolveCoverage" }, - "compute_schedule_version": { - "type": "string" + "inputs": { + "$ref": "#/components/schemas/PriceRequest", + "description": "Full fixed assumptions for repricing boundaries; replace spot with the boundary spot." }, - "engine_version": { - "type": "string", - "description": "Version of the operation's primary engine (legacy compatibility field)." + "max_spot": { + "type": "number", + "format": "double" }, - "engines": { - "type": "array", - "items": { - "$ref": "#/components/schemas/EngineIdentity" - }, - "description": "Engines selected for this operation, including their released Git revisions.\nDoes not assert execution on pre-execution failures." + "min_spot": { + "type": "number", + "format": "double" }, - "fcu_charged": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "description": "Committed commercial charge, including on post-commit failures.\nAbsent in open development mode; zero for protected pre-commit refusals.", - "minimum": 0 + "model_version": { + "type": "string" }, - "fcu_quoted": { - "type": [ - "integer", - "null" - ], + "price_evaluations": { + "type": "integer", "format": "int64", - "description": "Commercial quote; absent until reservation succeeds.", + "description": "Actual pricing calls, including the base and the engine's reference valuation.", "minimum": 0 }, - "fcu_schedule_id": { - "type": [ - "string", - "null" - ] + "pricing_model": { + "$ref": "#/components/schemas/ResolvedPricingModel" }, - "fcu_schedule_version": { - "type": [ - "string", - "null" - ] + "profile": { + "$ref": "#/components/schemas/ScenarioSolveProfile" }, - "policy": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/PolicyResponseMetadata" - } - ] + "reference_net_value": { + "type": "number", + "format": "double" }, - "remaining_fcu_instance_estimate": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "description": "Instance estimate, never an authoritative account balance or authorization input.", - "minimum": 0 + "regions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScenarioSolveRegion" + } }, - "request_id": { - "type": "string" + "spot_tolerance": { + "type": "number", + "format": "double" }, - "units_charged": { - "type": "integer", - "format": "int64", - "minimum": 0 + "target": { + "$ref": "#/components/schemas/ScenarioSolveTarget" + }, + "value_tolerance": { + "type": "number", + "format": "double" + } + } + }, + "ScenarioValuation": { + "type": "object", + "required": [ + "inputs", + "result" + ], + "properties": { + "inputs": { + "$ref": "#/components/schemas/PriceRequest", + "description": "Full resolved assumptions, in the same units as /v1/price." + }, + "result": { + "$ref": "#/components/schemas/PriceResult" } } }, @@ -7164,6 +8462,10 @@ } }, "tags": [ + { + "name": "scenarios", + "description": "BSM/BS1993 point and grid repricing" + }, { "name": "cost", "description": "Authenticated cost schedules and non-billable request quotes" diff --git a/contracts/provenance.json b/contracts/provenance.json index dab3e19..2678935 100644 --- a/contracts/provenance.json +++ b/contracts/provenance.json @@ -1,10 +1,10 @@ { "repository": "https://gitlab.com/prophetizo/morphiq-labs/sites/morphiq-api", - "source_commit": "2284d802b5ac0b37b43ee269bbf4bfeec3777776", + "source_commit": "7b4f233b2222fbb95b2492d5c57dbde1ac32e571", "source_path": "services/api/openapi.json", "contract_version": "0.1.0", - "sha256": "e206327e13aee32c02dbfa22e1d8e8a80ea2d549083971a1cc87aa2b67f401e7", - "import_method": "Reviewed API v0.5.0 source snapshot copied byte-for-byte from the deployed immutable main commit; the release tag has the identical source tree. Public API and developer-site schema digests verified at adoption.", + "sha256": "38a0c933ea805f132bc4245867c3130c5a1db94e23cb4837872abf8c27e33ccc", + "import_method": "Immutable draft API MR !209 source snapshot copied byte-for-byte. Candidate contract: not yet deployed. SDK merge/release held until deployment of the matching API and verification of live schema, solve quotes and receipts.", "covered_operations": [ "POST /v1/price", "POST /v1/regime", @@ -19,6 +19,8 @@ "POST /v1/cost/quote", "GET /v1/pricing/models", "POST /v1/greeks/standard", - "POST /v1/greeks/standard:batch" + "POST /v1/greeks/standard:batch", + "POST /v1/scenarios/evaluate", + "POST /v1/scenarios/solve-spot" ] } diff --git a/docs/API.md b/docs/API.md index 8cd48e1..8c72119 100644 --- a/docs/API.md +++ b/docs/API.md @@ -37,6 +37,8 @@ All request/response classes below are exported from `morphiq_sdk`. | Method | Request type | Response type | Route | | --- | --- | --- | --- | +| `evaluate_scenarios(request)` | `ScenarioRequest` | `ScenarioResponse` | `/v1/scenarios/evaluate` | +| `solve_spot(request)` | `ScenarioSpotSolveRequest` | `ScenarioSpotSolveResponse` | `/v1/scenarios/solve-spot` | | `price(request)` | `PriceRequest` | `PriceResponse` | `/v1/price` | | `greeks_standard(request)` | `GreeksStandardRequest` | `GreeksStandardResponse` | `/v1/greeks/standard` | | `greeks(request)` | `GreeksRequest` | `GreeksResponse` | `/v1/greeks` | @@ -205,3 +207,7 @@ Standard-five batching uses `BatchGreeksStandardRequest(requests=[...])` and contains `GreeksStandardRequest` items; ordering, error and charge handling match the other batch families. Public `client.pricing_models()` returns a typed `PricingModelCatalog` without authentication; see [model discovery](MODELS.md). + +See [scenarios and spot-target solving](SCENARIOS.md) for BSM/BS1993 inputs, typed +point/interval boundaries, unresolved coverage, flat solve charges and examples. +Both methods accept an optional `max_cu`; `quote()` accepts both request types. diff --git a/docs/CONTRACT.md b/docs/CONTRACT.md index 6f82bc3..906612e 100644 --- a/docs/CONTRACT.md +++ b/docs/CONTRACT.md @@ -9,18 +9,20 @@ coordinates the producer/import workflow with [API #106](https://gitlab.com/prophetizo/morphiq-labs/sites/morphiq-api/-/work_items/106). contracts/openapi.json now contains the reviewed snapshot byte-for-byte. contracts/provenance.json records its digest, API source commit and contract -version. This snapshot is from deployed API **v0.5.0**, commit -`2284d802b5ac0b37b43ee269bbf4bfeec3777776`, SHA-256 -`e206327e13aee32c02dbfa22e1d8e8a80ea2d549083971a1cc87aa2b67f401e7`. +version. This candidate snapshot is from draft API MR !209, commit +`7b4f233b2222fbb95b2492d5c57dbde1ac32e571`, SHA-256 +`38a0c933ea805f132bc4245867c3130c5a1db94e23cb4837872abf8c27e33ccc`. It is not yet deployed; SDK merge/release is held until the +matching API deployment is verified. The beta wire contract version remains `0.1.0`; API and SDK release versions are separate. -It was copied from immutable reviewed main source; automated published +It was copied byte-for-byte from immutable draft source; automated published artifact delivery remains #2. SDK tests and builds use the offline snapshot without private repository or numerical engine access. The SDK covers POST /v1/price, POST /v1/greeks, POST /v1/implied-volatility, POST /v1/analytics, POST /v1/greeks/standard, their five `:batch` routes, POST /v1/regime, GET /v1/pricing/models, GET /v1/cost/schedule and -POST /v1/cost/quote (14 operations). Hand-written model field sets and serialized +POST /v1/cost/quote, POST /v1/scenarios/evaluate and POST +/v1/scenarios/solve-spot (16 operations). Hand-written model field sets and serialized requests/responses are checked against the pinned schema. Digest/version checks detect accidental snapshot changes. Intentional upgrades need reviewed provenance and tests; this is not a complete future-operation compatibility generator. @@ -47,6 +49,14 @@ source identity are unchanged. Optional controlled-service integration checks SDK results against native JSON; it does not expect synthetic fixture scores. Greeks, implied-volatility and analytics fixtures contain synthetic inputs evaluated by the -native service built from the pinned API commit. Tests verify unscaled +native service at their recorded capture commit and revalidated against the +candidate API. Tests verify unscaled Greeks and explicit unavailable-quantity reasons, IV solver diagnostics and residual bases, model identity, metadata, error evidence and sync/async parity against native JSON. + +Historical Greeks/IV/analytics/batch fixtures retain their original capture commit, +schema digest, request IDs and metadata. Their separate `validation` identity +records revalidation against the candidate contract and controlled native service. +Scenario fixtures were captured directly from the candidate protected preview; +all inputs and account keys are synthetic. No historical capture is relabeled as +a response produced by a newer commit. diff --git a/docs/COST.md b/docs/COST.md index ca2687a..d458362 100644 --- a/docs/COST.md +++ b/docs/COST.md @@ -297,3 +297,11 @@ Inspect the error code, the current quote and your budget before choosing a new ceiling. Other admission errors, including funding refusals, can occur before the ceiling check. Malformed or duplicate HTTP ceilings return 422; the SDK rejects invalid `max_cu` values locally before sending a request. + +## Scenario requests + +`quote()` accepts `ScenarioRequest` and `ScenarioSpotSolveRequest`. Evaluation +uses the existing base-plus-points price-batch charge. A complete solve uses the +approved flat request rate: BSM 100 CU or BS1993 200 CU, independent of internal +evaluation count. Quotes perform no solving. See [scenario billing and receipts](SCENARIOS.md#flat-charges-and-receipts). +Solver access requires the matching deployment, signed schedule and permission. diff --git a/docs/MODELS.md b/docs/MODELS.md index 15f831a..95e62c6 100644 --- a/docs/MODELS.md +++ b/docs/MODELS.md @@ -41,3 +41,8 @@ reasons available in the `result.greeks.unavailable` column and full `outcome`. The API owns these capabilities and may evolve them. Query discovery for current capabilities and preserve actionable server errors instead of copying this table into client admission logic. + +The candidate catalog adds `scenario_operations`: BSM and BS1993 expose +`evaluate` and `solve_spot`; other models expose an empty list. This describes +compiled capability, not account entitlement or live deployment. See +[scenario availability and usage](SCENARIOS.md). diff --git a/docs/README.md b/docs/README.md index 784d414..8f6c2e3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,6 +9,7 @@ response types, configuration and errors. | Task | Guide | Executable examples | | --- | --- | --- | +| Explore option scenarios and solve targets | [Scenarios and flat solve charges](SCENARIOS.md) | [Evaluation](../examples/scenario_evaluate.py), [sync solve](../examples/solve_spot.py), [async solve](../examples/solve_spot_async.py) | | Discover models | [Model capabilities](MODELS.md) | [Public discovery](../examples/pricing_models.py) | | Calculate standard-five Greeks | [Greeks and units](GREEKS.md) | [Standard five](../examples/greeks_standard.py) | | Discover rates and quote requests | [Cost utilities](COST.md) | [Quote and optional capped execution](../examples/cost_quote.py) | diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 1f08ac6..3918334 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -40,3 +40,13 @@ evaluates a browser experience built from the cookbook, including runtime choice credentials, CU visibility, session isolation, costs and download/local-run support. It is outside the initial SDK milestone. A prototype and a decision come before an implementation/hosting commitment. + +## Scenario API adoption + +[#45 Scenario evaluation and spot solving](https://github.com/MorphIQ-Labs/morphiq-sdk-python/issues/45) +tracks SDK adoption of API MR !209. The candidate includes typed sync/async +methods, quotes/ceilings, examples and two scenario workspaces. **Owner hold:** +keep the SDK PR draft with auto-merge off; do not merge to main or release until +the matching API has been deployed and its schema, permissions and 100/200-CU +solve quotes/receipts are verified. This work does not authorize API deployment. +See [local validation](validation/2026-11-05-scenarios.json). diff --git a/docs/SCENARIOS.md b/docs/SCENARIOS.md new file mode 100644 index 0000000..2c50e07 --- /dev/null +++ b/docs/SCENARIOS.md @@ -0,0 +1,185 @@ +# Option scenarios and spot-target solving + +This SDK candidate targets the scenario contract in API MR !209. Merge and +release are held until the matching API is deployed and its live schema, +permissions, quotes and receipts are verified. Existing published SDK versions +may not provide these methods. Use a compatible controlled service for testing. + +Both `MorphIQ` and `AsyncMorphIQ` provide `evaluate_scenarios()` and `solve_spot()`. +Each call sends exactly one HTTP JSON request. All numerical work runs on the +server; the SDK does not price, interpolate or find roots locally. There are no +automatic retries, hidden preliminary prices, request splitting or parallel calls. + +## A five-dollar underlying move + +```python +from morphiq_sdk import MorphIQ, PriceRequest, ScenarioPoint, ScenarioPosition, ScenarioRequest + +contract = PriceRequest( + exercise_style="european", + pricing_model="black_scholes_merton", + option_type="call", + spot=100.0, + strike=100.0, + time_to_expiry_years=1.0, + risk_free_rate=0.05, + dividend_yield=0.0, + volatility=0.20, +) +request = ScenarioRequest( + contract=contract, + scenarios=[ScenarioPoint(spot_change=5.0)], + position=ScenarioPosition(quantity=1.0, multiplier=100.0), +) +with MorphIQ() as client: + quote = client.quote(request) # One zero-CU utility request; no execution. + print(quote.quote.total_fcu) + result = client.evaluate_scenarios(request, max_cu=2) # One capped request. + for item in result.results: + if item.status == "success": + print(item.inputs.spot, item.result.price, item.price_change, item.position_pnl) + else: + print(item.index, item.error.code, item.error.message) + print("Commercial CU charged:", result.metadata.fcu_charged) +``` + +The example uses synthetic inputs and an explicit two-CU ceiling. Obtain the +account's active quote before choosing a ceiling; quotes do not reserve funds +or lock policy. Use `bjerksund_stensland_1993` with American exercise for BS1993. +These routes require an explicit model: Auto, QdFp and cash-dividend models are +not supported. Model-domain validation remains server-owned. + +`spot_change=5` means five quote-currency units, not five percent. Alternatively, +`spot` sets an absolute price; do not combine them. Optional `volatility` is an +absolute annualized decimal (0.25 is 25%). `elapsed_time_years` subtracts from +remaining life, defaults to zero and cannot exceed expiry. Exactly expiry is +allowed. Each point starts from the original contract; shocks do not accumulate. +Rates, yield, strike and direction remain fixed. These are conditional model +valuations, not forecasts or market quotes. + +Send 1–99 points in display order for a curve or flattened grid. The base plus +points must fit the account's price-batch limit. Duplicates remain distinct. +The SDK rejects missing, duplicate or reordered response indexes with +`ResponseError`, preserving available billing metadata. It never silently drops +failed points or sorts the server's output. + +Evaluation uses existing price-batch rates for the base and every point. Signed +quantity and positive multiplier scale base-relative P&L; they do not multiply +CU charges. Prices/changes are per underlying unit. P&L excludes fees, financing, +trade-entry cost and currency conversion. A negative quantity is short. + +## Solve for a target price or position P&L + +```python +from morphiq_sdk import MorphIQ, PriceRequest, ScenarioOptionPriceTarget, ScenarioSpotSolveRequest + +request = ScenarioSpotSolveRequest( + contract=PriceRequest( + exercise_style="european", + pricing_model="black_scholes_merton", + option_type="call", + spot=100.0, + strike=100.0, + time_to_expiry_years=1.0, + risk_free_rate=0.05, + dividend_yield=0.0, + volatility=0.20, + ), + target=ScenarioOptionPriceTarget(kind="option_price", value=15.0), + min_spot=50.0, + max_spot=150.0, +) +with MorphIQ() as client: + print("Quoted CU:", client.quote(request).quote.total_fcu) + response = client.solve_spot(request, max_cu=100) + print(response.result.coverage.status) + for boundary in response.result.boundaries: + print(boundary.model_dump()) + print("Commercial CU charged:", response.metadata.fcu_charged) +``` + +For position P&L, replace the target with: + +```python +from morphiq_sdk import ScenarioPosition, ScenarioPositionPnlTarget + +target = ScenarioPositionPnlTarget( + kind="position_pnl", + value=500.0, + position=ScenarioPosition(quantity=2.0, multiplier=100.0), +) +``` + +The P&L target is relative to the original model mark, before optional volatility +or elapsed-time adjustments. Quantity is nonzero and signed; multiplier is +positive. Option-price targets are per underlying unit; P&L is position currency. +Bounds must be finite, positive and ordered. Positive tolerances default to +`spot_tolerance=1e-9` and `value_tolerance=1e-6`; the latter uses target units. +Large positions may require tighter spot tolerances to achieve the value tolerance. + +Async uses the same requests, types and semantics: + +```python +from morphiq_sdk import AsyncMorphIQ + + +async def solve(request): + async with AsyncMorphIQ() as client: + quote = await client.quote(request) + print(quote.quote.total_fcu) + return await client.solve_spot(request, max_cu=100) +``` + +## Flat charges and receipts + +| Model | CU per complete solve request | +| --- | ---: | +| BSM | 100 | +| BS1993 | 200 | + +The approved schedule includes every internal base, reference and search price. +Target kind, position scaling, evaluation count, iterations and returned +boundaries do not change the flat charge. `result.price_evaluations` is a +diagnostic count, not a billing unit. No additional trial-price charge applies. +The SDK never derives a CU amount from the count or internal `units_charged`. + +`client.quote(request)` chooses `scenario_spot_solve` or `scenario_evaluate` +from the request type. A solve quote resolves quantity one without executing +numerical work. The service reserves the full known amount before execution. +`max_cu` forwards `X-MorphIQ-Max-CU` unchanged; a ceiling below the quote refuses +with zero charge, equality is accepted. Solver access needs its own permission +and matching signed schedule; price-batch access alone is insufficient. + +The charge commits at execution start. No-root and unresolved searches, and +numerical failures/timeouts after commitment, retain the full flat charge. +Pre-commit validation/admission refusals or cancellation charge zero. Inspect +`APIError.metadata`, `ResponseError.metadata`, raw bodies and request IDs. +Absent commercial metadata is unknown, never zero. A missing response or async +cancellation does not establish whether the server charged; do not retry blindly. + +## Read every returned outcome + +- `ScenarioSolvePoint`: evaluated spot/value, crossing flag, refinement count and + bracket width. Compare its value with the requested target for the residual. +- `ScenarioSolveInterval`: sampled plateau bounds and evaluated value. It does + not certify every real-valued spot inside the interval. +- `regions`: above-target, below-target or at-target segments. +- `coverage.status == "resolved"`: candidates resolved at the declared scan + resolution; smaller structures can still be missed. Empty boundaries mean no + solution found at that resolution, not global nonexistence. +- `coverage.status == "unresolved"`: inspect every cell's bounds, best spot, + absolute value error and typed reason. The SDK preserves these cells and + does not choose a unique answer or retry with looser tolerances. + +The returned profile identifies 32 scan intervals, 64 refinements per candidate +and a conservative 4,775-price ceiling. This is a computational bound, not a CU +reservation. `base` retains the original valuation; `inputs` contains adjusted +fixed assumptions for repricing a boundary using `client.price()` explicitly. +No repricing is performed automatically. + +Executable examples: [evaluation](../examples/scenario_evaluate.py), +[sync solve](../examples/solve_spot.py), [async solve](../examples/solve_spot_async.py). +They quote only unless you supply `--max-cu`. Interactive workspaces: +[evaluation](../notebooks/scenarios/evaluate.ipynb) and +[spot targeting](../notebooks/scenarios/solve-spot.ipynb). Setup and control edits +send no requests; action buttons state request counts and require an explicit ceiling. diff --git a/docs/validation/2026-11-05-scenarios.json b/docs/validation/2026-11-05-scenarios.json new file mode 100644 index 0000000..e8e4e51 --- /dev/null +++ b/docs/validation/2026-11-05-scenarios.json @@ -0,0 +1,87 @@ +{ + "api_source_commit": "7b4f233b2222fbb95b2492d5c57dbde1ac32e571", + "sdk_base_commit": "043287a", + "platform": "macOS-27.0.1-arm64-arm-64bit-Mach-O", + "merge_hold": "SDK main merge/release requires verified deployment of the matching API. Draft PR, auto-merge off.", + "checks": [ + { + "command": ".venv/bin/python -m pytest -q", + "python": "3.14", + "passed": 647, + "skipped": 19, + "note": "Native integration intentionally opt-in; six subsequently added notebook outcome tests validated separately." + }, + { + "command": ".venv311/bin/python -m pytest -q", + "python": "3.11", + "passed": 647, + "skipped": 19, + "note": "Same scope as Python 3.14." + }, + { + "command": "python -m pytest -q tests/test_scenario_notebooks.py tests/test_examples.py -k \"workspace_preserves or generated_notebook\"", + "python": [ + "3.11", + "3.14" + ], + "passed_each": 14, + "note": "Final notebook code: six additional outcome/error tests plus all eight notebook setup/action cases." + }, + { + "command": "MORPHIQ_TEST_API_URL= python -m pytest -q -m integration", + "passed": 19 + }, + { + "command": "MORPHIQ_TEST_API_URL= MORPHIQ_TEST_API_KEY= python -m pytest -q tests/test_scenarios.py -m integration", + "passed": 1, + "note": "Exact 100/200 CU quote/receipt parity and below-cap refusals; ephemeral ledger." + }, + { + "command": "python -m ruff check .", + "status": "passed" + }, + { + "command": "python -m ruff format --check .", + "status": "passed" + }, + { + "command": "python -m mypy", + "status": "passed" + }, + { + "command": "python scripts/generate_notebook.py --check", + "status": "passed" + }, + { + "command": "python scripts/check_package.py", + "status": "passed", + "note": "Built sdist/wheel and exercised isolated wheel installs outside checkout." + }, + { + "command": "docker build -t morphiq-sdk-scenario:test . && python scripts/check_notebook_container.py --image morphiq-sdk-scenario:test", + "status": "passed", + "note": "All eight workspaces, token authentication and persistence; test container and volume removed." + } + ], + "limitations": [ + "No production calls, deployment, package publication or merge.", + "Initial debug native integration hit the unchanged 10-second deadline on existing cash-dividend IV; optimized build passed all 19 cases.", + "Historical fixtures retain original capture provenance and separate candidate validation identity." + ], + "sha256": { + "contracts/openapi.json": "38a0c933ea805f132bc4245867c3130c5a1db94e23cb4837872abf8c27e33ccc", + "contracts/provenance.json": "4d13d4054a2cde8216a0a82663ecbdfd9428ec7ddccf5992130ed6f319f027ff", + "src/morphiq_sdk/client.py": "65bbbdc601690ea2ee3efab17b10f05def726b2866d4e174ce133c23ff136213", + "src/morphiq_sdk/models.py": "a20f9493c5792cb9cc05458d030523c4851590aa56b68250cb00fd24a5ef451d", + "src/morphiq_sdk/__init__.py": "833ffea18e1e918fb423b655499e88c5003ad7d7d522cf16a6be7fac9ae8e628", + "tests/fixtures/scenarios.json": "c73be677e7387deac573d20bb233d6f2ee68277b1b1143466f46a5ad9fd40fc6", + "tests/fixtures/scenario_quotes.json": "f392059067bb801d810645952ce9ff48ad6a3dc4d5b8f88119e43e2cc3b47275", + "tests/fixtures/pricing_models.json": "9f717398d875d2fe0d04a93e176d6a0412cbc128e693e52a5c9a991765cc572e", + "requirements-dev.txt": "9240428b0b2867b99af7aa790842f79d67591396ffd240f958da741092a5451f", + "notebooks/scenarios/evaluate.ipynb": "6c6fb051ec319d47b43c95911b3a0b2d168863b8056f773c08184acef613ee27", + "notebooks/scenarios/solve-spot.ipynb": "e55a09f267d21e8ea67132dd3d01ea54d9db8ef9a2d6c170c55f2d1e7921e938" + }, + "native_release_binary_sha256": "9f68e648f5fbbcce122cf46d1f685fc57896d37fb823ba93a09c5f4b6d95c543", + "native_preview_binary_sha256": "485ca13ab77fe174443d17620e419af1f6032f95af407304a8393566f2b275d3", + "docker_image_id": "sha256:001b7706e0254876b0f1d54339273b9227b1cd9ceef2553792133658fa1cd04a" +} diff --git a/examples/README.md b/examples/README.md index aa7e79f..a5ab9e4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -110,3 +110,11 @@ python examples/tabular_price.py See [the pandas adapter guide](../docs/PANDAS.md) for partial results, identifiers, units and exact charge totals. Pandas is optional and conversion makes no API calls. + +## Scenario API + +`scenario_evaluate.py` quotes a +5 move or 3×3 grid. `solve_spot.py` and +`solve_spot_async.py` quote a target price or signed position P&L. These scripts +use synthetic inputs and require a compatible scenario deployment. Add +`--max-cu` to explicitly execute once; otherwise they only quote. Use +`--model bs1993` for American options. See [the guide](../docs/SCENARIOS.md). diff --git a/examples/scenario_evaluate.py b/examples/scenario_evaluate.py new file mode 100644 index 0000000..bdbcbba --- /dev/null +++ b/examples/scenario_evaluate.py @@ -0,0 +1,84 @@ +"""Quote a +5 underlying move or a 3×3 grid; --max-cu explicitly executes once. + +Run: python examples/scenario_evaluate.py --mode point --model bsm --max-cu 2 +""" + +import argparse +import sys + +from morphiq_sdk import ( + APIError, + MorphIQ, + PriceRequest, + ResponseError, + ScenarioPoint, + ScenarioPosition, + ScenarioRequest, + TransportError, +) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--mode", choices=["point", "grid"], default="point") + parser.add_argument("--model", choices=["bsm", "bs1993"], default="bsm") + parser.add_argument( + "--max-cu", type=int, help="execute once with this ceiling; otherwise quote only" + ) + args = parser.parse_args() + if args.max_cu is not None and not 0 <= args.max_cu <= 2**64 - 1: + parser.error("--max-cu must be an unsigned 64-bit integer") + contract = PriceRequest( + exercise_style="american" if args.model == "bs1993" else "european", + pricing_model="bjerksund_stensland_1993" + if args.model == "bs1993" + else "black_scholes_merton", + option_type="call", + spot=100.0, + strike=100.0, + time_to_expiry_years=1.0, + risk_free_rate=0.05, + dividend_yield=0.0, + volatility=0.20, + ) + points = [ScenarioPoint(spot_change=5.0)] + if args.mode == "grid": + points = [ + ScenarioPoint(spot=spot, volatility=vol) + for spot in (95.0, 100.0, 105.0) + for vol in (0.15, 0.20, 0.25) + ] + request = ScenarioRequest( + contract=contract, + scenarios=points, + position=ScenarioPosition(quantity=1.0, multiplier=100.0), + ) + try: + with MorphIQ() as client: + quote = client.quote(request) + print("Quoted CU:", quote.quote.total_fcu) + if args.max_cu is None: + return + if quote.quote.total_fcu > args.max_cu: + parser.error("Quote exceeds --max-cu; no execution submitted") + response = client.evaluate_scenarios(request, max_cu=args.max_cu) + except APIError as error: + charged = error.metadata.fcu_charged if error.metadata else None + print( + f"HTTP {error.status_code}: {error.code}; request_id={error.request_id}", + file=sys.stderr, + ) + print( + "Commercial CU charged:", + charged if charged is not None else "not reported", + file=sys.stderr, + ) + raise SystemExit(1) from None + except (ResponseError, TransportError) as error: + raise SystemExit(f"{error}; inspect receipt evidence. No retry.") from None + # Includes ordered successes/errors and one request-level commercial receipt. + print(response.model_dump_json(indent=2, exclude_unset=True)) + + +if __name__ == "__main__": + main() diff --git a/examples/solve_spot.py b/examples/solve_spot.py new file mode 100644 index 0000000..848bbaa --- /dev/null +++ b/examples/solve_spot.py @@ -0,0 +1,91 @@ +"""Quote a synthetic spot-target solve; add --max-cu to explicitly execute once. + +Requires the scenario API deployment and the account's approved solve schedule. +Examples: python examples/solve_spot.py --model bsm --max-cu 100 + python examples/solve_spot.py --model bs1993 --target-pnl 500 --max-cu 200 +""" + +import argparse +import sys + +from morphiq_sdk import ( + APIError, + MorphIQ, + PriceRequest, + ResponseError, + ScenarioOptionPriceTarget, + ScenarioPosition, + ScenarioPositionPnlTarget, + ScenarioSolveTarget, + ScenarioSpotSolveRequest, + TransportError, +) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--model", choices=["bsm", "bs1993"], default="bsm") + targets = parser.add_mutually_exclusive_group() + targets.add_argument("--target-price", type=float, default=15.0) + targets.add_argument("--target-pnl", type=float) + parser.add_argument( + "--max-cu", type=int, help="execute once with this ceiling; otherwise quote only" + ) + args = parser.parse_args() + if args.max_cu is not None and not 0 <= args.max_cu <= 2**64 - 1: + parser.error("--max-cu must be an unsigned 64-bit integer") + contract = PriceRequest( + exercise_style="american" if args.model == "bs1993" else "european", + pricing_model="bjerksund_stensland_1993" + if args.model == "bs1993" + else "black_scholes_merton", + option_type="call", + spot=100.0, + strike=100.0, + time_to_expiry_years=1.0, + risk_free_rate=0.05, + dividend_yield=0.0, + volatility=0.20, + ) + target: ScenarioSolveTarget = ScenarioOptionPriceTarget( + kind="option_price", value=args.target_price + ) + if args.target_pnl is not None: + target = ScenarioPositionPnlTarget( + kind="position_pnl", + value=args.target_pnl, + position=ScenarioPosition(quantity=2.0, multiplier=100.0), + ) + request = ScenarioSpotSolveRequest( + contract=contract, target=target, min_spot=50.0, max_spot=150.0 + ) + try: + with MorphIQ() as client: + quote = client.quote(request) + print("Quoted CU:", quote.quote.total_fcu) + if args.max_cu is None: + return + if quote.quote.total_fcu > args.max_cu: + parser.error("Quote exceeds --max-cu; no execution submitted") + response = client.solve_spot(request, max_cu=args.max_cu) + except APIError as error: + charged = error.metadata.fcu_charged if error.metadata else None + print( + f"HTTP {error.status_code}: {error.code}; request_id={error.request_id}", + file=sys.stderr, + ) + print( + "Commercial CU charged:", + charged if charged is not None else "not reported", + file=sys.stderr, + ) + raise SystemExit(1) from None + except (ResponseError, TransportError) as error: + raise SystemExit(f"{error}; inspect receipt evidence. No retry.") from None + # Preserve every boundary, sampled interval, region and unresolved cell. + # An empty boundary list is not a proof of global nonexistence. + print(response.model_dump_json(indent=2, exclude_unset=True)) + + +if __name__ == "__main__": + main() diff --git a/examples/solve_spot_async.py b/examples/solve_spot_async.py new file mode 100644 index 0000000..96f6a6c --- /dev/null +++ b/examples/solve_spot_async.py @@ -0,0 +1,92 @@ +"""Quote a synthetic spot-target solve; add --max-cu to explicitly execute once. + +Requires the scenario API deployment and the account's approved solve schedule. +Examples: python examples/solve_spot_async.py --model bsm --max-cu 100 + python examples/solve_spot_async.py --model bs1993 --target-pnl 500 --max-cu 200 +""" + +import argparse +import asyncio +import sys + +from morphiq_sdk import ( + APIError, + AsyncMorphIQ, + PriceRequest, + ResponseError, + ScenarioOptionPriceTarget, + ScenarioPosition, + ScenarioPositionPnlTarget, + ScenarioSolveTarget, + ScenarioSpotSolveRequest, + TransportError, +) + + +async def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--model", choices=["bsm", "bs1993"], default="bsm") + targets = parser.add_mutually_exclusive_group() + targets.add_argument("--target-price", type=float, default=15.0) + targets.add_argument("--target-pnl", type=float) + parser.add_argument( + "--max-cu", type=int, help="execute once with this ceiling; otherwise quote only" + ) + args = parser.parse_args() + if args.max_cu is not None and not 0 <= args.max_cu <= 2**64 - 1: + parser.error("--max-cu must be an unsigned 64-bit integer") + contract = PriceRequest( + exercise_style="american" if args.model == "bs1993" else "european", + pricing_model="bjerksund_stensland_1993" + if args.model == "bs1993" + else "black_scholes_merton", + option_type="call", + spot=100.0, + strike=100.0, + time_to_expiry_years=1.0, + risk_free_rate=0.05, + dividend_yield=0.0, + volatility=0.20, + ) + target: ScenarioSolveTarget = ScenarioOptionPriceTarget( + kind="option_price", value=args.target_price + ) + if args.target_pnl is not None: + target = ScenarioPositionPnlTarget( + kind="position_pnl", + value=args.target_pnl, + position=ScenarioPosition(quantity=2.0, multiplier=100.0), + ) + request = ScenarioSpotSolveRequest( + contract=contract, target=target, min_spot=50.0, max_spot=150.0 + ) + try: + async with AsyncMorphIQ() as client: + quote = await client.quote(request) + print("Quoted CU:", quote.quote.total_fcu) + if args.max_cu is None: + return + if quote.quote.total_fcu > args.max_cu: + parser.error("Quote exceeds --max-cu; no execution submitted") + response = await client.solve_spot(request, max_cu=args.max_cu) + except APIError as error: + charged = error.metadata.fcu_charged if error.metadata else None + print( + f"HTTP {error.status_code}: {error.code}; request_id={error.request_id}", + file=sys.stderr, + ) + print( + "Commercial CU charged:", + charged if charged is not None else "not reported", + file=sys.stderr, + ) + raise SystemExit(1) from None + except (ResponseError, TransportError) as error: + raise SystemExit(f"{error}; inspect receipt evidence. No retry.") from None + # Preserve every boundary, sampled interval, region and unresolved cell. + # An empty boundary list is not a proof of global nonexistence. + print(response.model_dump_json(indent=2, exclude_unset=True)) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/notebooks/README.md b/notebooks/README.md index 7e62457..e8c5a19 100644 --- a/notebooks/README.md +++ b/notebooks/README.md @@ -5,6 +5,8 @@ Use the controls to load data and run an analysis; no code edits are required. | Workspace | Tools | API requests per action | | --- | --- | --- | +| [Scenario evaluation](scenarios/evaluate.ipynb) | Absolute underlying move or explicit grid, signed P&L, ordered results/errors and receipts | Quote: 1 free utility; execute: 1 capped request | +| [Spot-target solve](scenarios/solve-spot.ipynb) | BSM/BS1993 target price/P&L, bounded search, all boundaries and unresolved cells | Quote: 1 free utility; execute: 1 capped solve | | [Available models](pricing/models.ipynb) | Public capabilities, aliases, constraints and engine versions; no key needed | Load: 1 free request | | [Standard-five Greeks](pricing/standard-five.ipynb) | BSM/BS1993 inputs, price, five primary sensitivities, unavailable reasons and CU | Calculate: 1 | | [Cost quotation](cost/quotation.ipynb) | Account rates, synthetic option inputs, conditional quote, editable execution ceiling | Schedule: 1 utility; quote: 1 utility; execute: 1 capped pricing request | diff --git a/notebooks/scenarios/evaluate.ipynb b/notebooks/scenarios/evaluate.ipynb new file mode 100644 index 0000000..b827cef --- /dev/null +++ b/notebooks/scenarios/evaluate.ipynb @@ -0,0 +1,264 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "intro", + "metadata": {}, + "source": "# Explore option scenarios\n\nMorphIQ SDK for Python\n\nExplore an absolute underlying move or an explicit spot/volatility grid. Five means\nfive quote-currency units, not five percent. Volatility/rate/yield controls use\npercent; the request sends decimals. Each point starts from the same base.\nThe base and 1–99 points must fit your account's price-batch limit. Grid order\nis spots first, then volatilities, including duplicates. Signed quantity and\nmultiplier scale base-relative P&L; fees, financing and trade-entry cost are excluded.\nEvaluation uses the existing price rate for the base and every point.\n\nRequires the scenario API deployment and matching account permissions. This SDK\ncandidate is held from merge/release until API deployment is verified.\n\nInputs start with synthetic values. Run all cells to display controls: setup\nand changing controls send **zero API requests**. Quote sends one free utility\nrequest. Execute sends one billable request, capped by Max CU, after a current\nquote. Actions run sequentially. Quotes reserve nothing. Changes invalidate the\nquote and displayed result; prior receipts remain in `history`.\n\nA missing charge is **not reported**, never zero. No automatic retries are made.\nEnter a key at the masked prompt or set `MORPHIQ_API_KEY`; set\n`MORPHIQ_API_BASE_URL` to a controlled service when testing. Never save credentials\nor outputs. The widget ceiling is limited to exact JavaScript integers; the\nPython client accepts the full unsigned 64-bit range.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "id": "inputs", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from getpass import getpass\n", + "\n", + "import ipywidgets as widgets\n", + "import pandas as pd\n", + "from IPython.display import display\n", + "\n", + "from morphiq_sdk import (\n", + " APIError,\n", + " MorphIQ,\n", + " PriceRequest,\n", + " ResponseError,\n", + " ScenarioPoint,\n", + " ScenarioPosition,\n", + " ScenarioRequest,\n", + " TransportError,\n", + ")\n", + "\n", + "api_key = os.environ.get(\"MORPHIQ_API_KEY\") or getpass(\"MorphIQ API key: \")\n", + "model = widgets.Dropdown(\n", + " options=[(\"BSM · European\", \"bsm\"), (\"BS1993 · American\", \"bs1993\")], description=\"Model\"\n", + ")\n", + "option_type = widgets.Dropdown(options=[\"call\", \"put\"], description=\"Option\")\n", + "spot = widgets.FloatText(value=100.0, description=\"Spot\")\n", + "strike = widgets.FloatText(value=100.0, description=\"Strike\")\n", + "expiry = widgets.FloatText(value=1.0, description=\"Years left\")\n", + "volatility = widgets.FloatText(value=20.0, description=\"Volatility %\")\n", + "rate = widgets.FloatText(value=5.0, description=\"Rate %\")\n", + "yield_input = widgets.FloatText(value=0.0, description=\"Yield %\")\n", + "multiplier = widgets.FloatText(value=100.0, description=\"Multiplier\")\n", + "quote_button = widgets.Button(\n", + " description=\"Quote · 1 request / 0 CU\", layout=widgets.Layout(width=\"240px\")\n", + ")\n", + "run_button = widgets.Button(\n", + " description=\"Execute · 1 request\", disabled=True, layout=widgets.Layout(width=\"240px\")\n", + ")\n", + "output = widgets.Output()\n", + "\n", + "\n", + "def show_message(*values):\n", + " output.append_stdout(\" \".join(str(value) for value in values) + \"\\n\")\n", + "\n", + "\n", + "usage = widgets.HTML(value=\"No API requests sent.\")\n", + "history = []\n", + "last_quote = None\n", + "last_response = None\n", + "quote_snapshot = None\n", + "result_snapshot = None\n", + "\n", + "\n", + "def contract_inputs():\n", + " return PriceRequest(\n", + " exercise_style=\"american\" if model.value == \"bs1993\" else \"european\",\n", + " pricing_model=\"bjerksund_stensland_1993\"\n", + " if model.value == \"bs1993\"\n", + " else \"black_scholes_merton\",\n", + " option_type=option_type.value,\n", + " spot=spot.value,\n", + " strike=strike.value,\n", + " time_to_expiry_years=expiry.value,\n", + " volatility=volatility.value / 100,\n", + " risk_free_rate=rate.value / 100,\n", + " dividend_yield=yield_input.value / 100,\n", + " )\n", + "\n", + "\n", + "quantity = widgets.FloatText(value=1.0, description=\"Quantity\")\n", + "mode = widgets.Dropdown(\n", + " options=[(\"Single move\", \"point\"), (\"Spot × volatility grid\", \"grid\")], description=\"Scenario\"\n", + ")\n", + "spot_change = widgets.FloatText(value=5.0, description=\"Spot move\")\n", + "grid_spots = widgets.Text(value=\"95,100,105\", description=\"Grid spots\")\n", + "grid_vols = widgets.Text(value=\"15,20,25\", description=\"Grid vols %\")\n", + "max_cu = widgets.BoundedIntText(value=10, min=0, max=2**53 - 1, description=\"Max CU\")\n", + "controls = [\n", + " model,\n", + " option_type,\n", + " spot,\n", + " strike,\n", + " expiry,\n", + " volatility,\n", + " rate,\n", + " yield_input,\n", + " quantity,\n", + " multiplier,\n", + " mode,\n", + " spot_change,\n", + " grid_spots,\n", + " grid_vols,\n", + "]\n", + "\n", + "\n", + "def current_request():\n", + " points = [ScenarioPoint(spot_change=spot_change.value)]\n", + " if mode.value == \"grid\":\n", + " spots = [float(value.strip()) for value in grid_spots.value.split(\",\")]\n", + " vols = [float(value.strip()) / 100 for value in grid_vols.value.split(\",\")]\n", + " points = [ScenarioPoint(spot=s, volatility=v) for s in spots for v in vols]\n", + " return ScenarioRequest(\n", + " contract=contract_inputs(),\n", + " scenarios=points,\n", + " position=ScenarioPosition(quantity=quantity.value, multiplier=multiplier.value),\n", + " )\n", + "\n", + "\n", + "def execute_request(client, request):\n", + " return client.evaluate_scenarios(request, max_cu=max_cu.value)\n", + "\n", + "\n", + "def show_result(response):\n", + " show_message(\"Base option price:\", response.base.result.price)\n", + " rows = []\n", + " for item in response.results:\n", + " row = {\n", + " \"Index\": item.index,\n", + " \"Spot\": item.inputs.spot,\n", + " \"Volatility\": item.inputs.volatility,\n", + " \"Status\": item.status,\n", + " }\n", + " if item.status == \"success\":\n", + " row.update(\n", + " Price=item.result.price, Change=item.price_change, PositionPnL=item.position_pnl\n", + " )\n", + " else:\n", + " row.update(Error=item.error.code)\n", + " rows.append(row)\n", + " output.append_display_data(pd.DataFrame(rows))\n", + " show_message(\"Prices are per underlying unit; position P&L is relative to the base mark.\")\n", + " show_message(\"Errors remain in submitted order; one receipt covers the complete request.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "actions", + "metadata": {}, + "outputs": [], + "source": [ + "def record(action, request, charged, request_id):\n", + " history.append(\n", + " {\"action\": action, \"request\": request, \"cu_charged\": charged, \"request_id\": request_id}\n", + " )\n", + " known = sum(row[\"cu_charged\"] for row in history if row[\"cu_charged\"] is not None)\n", + " unknown = sum(row[\"cu_charged\"] is None for row in history)\n", + " usage.value = (\n", + " f\"Recorded receipts: {known} CU; {unknown} unknown charges; {len(history)} requests.\"\n", + " )\n", + "\n", + "\n", + "def changed(_change):\n", + " global last_quote, last_response, quote_snapshot, result_snapshot\n", + " last_quote = last_response = quote_snapshot = result_snapshot = None\n", + " run_button.disabled = True\n", + " with output:\n", + " output.clear_output(wait=True)\n", + " show_message(\n", + " \"Inputs changed. Quote again before execution. Previous receipts remain in history.\"\n", + " )\n", + "\n", + "\n", + "for control in controls:\n", + " control.observe(changed, names=\"value\")\n", + "\n", + "\n", + "def perform(action):\n", + " global last_quote, last_response, quote_snapshot, result_snapshot\n", + " with output:\n", + " output.clear_output(wait=True)\n", + " try:\n", + " request = current_request()\n", + " wire = request.to_wire()\n", + " if action == \"execute\":\n", + " if last_quote is None or wire != quote_snapshot:\n", + " show_message(\"Quote the current inputs first. No execution submitted.\")\n", + " return\n", + " if last_quote.quote.total_fcu > max_cu.value:\n", + " show_message(\"Quote exceeds Max CU. No execution submitted.\")\n", + " return\n", + " else:\n", + " last_quote = quote_snapshot = None\n", + " last_response = result_snapshot = None\n", + " for control in controls + [max_cu, quote_button, run_button]:\n", + " control.disabled = True\n", + " with MorphIQ(api_key=api_key) as client:\n", + " if action == \"quote\":\n", + " response = client.quote(request)\n", + " last_quote, quote_snapshot = response, wire\n", + " record(\n", + " action, wire, response.metadata.fcu_charged, response.metadata.request_id\n", + " )\n", + " show_message(f\"Quoted execution charge: {response.quote.total_fcu} CU\")\n", + " show_message(\"Quote charge:\", response.metadata.fcu_charged)\n", + " show_message(\n", + " \"Quotes reserve nothing and do not lock policy or guarantee funds/capacity.\"\n", + " )\n", + " else:\n", + " response = execute_request(client, request)\n", + " last_response, result_snapshot = response, wire\n", + " record(\n", + " action, wire, response.metadata.fcu_charged, response.metadata.request_id\n", + " )\n", + " last_quote = quote_snapshot = None\n", + " show_message(\n", + " \"Commercial CU charged:\",\n", + " response.metadata.fcu_charged\n", + " if response.metadata.fcu_charged is not None\n", + " else \"not reported\",\n", + " )\n", + " show_message(\"Request ID:\", response.metadata.request_id)\n", + " show_result(response)\n", + " except (APIError, ResponseError, TransportError) as error:\n", + " metadata = getattr(error, \"metadata\", None)\n", + " charged = 0 if action == \"quote\" else (metadata.fcu_charged if metadata else None)\n", + " record(action, wire, charged, getattr(error, \"request_id\", None))\n", + " last_quote = quote_snapshot = None\n", + " show_message(str(error))\n", + " show_message(\n", + " \"Commercial CU charged:\", charged if charged is not None else \"not reported\"\n", + " )\n", + " show_message(\"No retry. A missing response is not evidence of zero charge.\")\n", + " except ValueError as error:\n", + " show_message(f\"Invalid local inputs/configuration: {error}. No request submitted.\")\n", + " finally:\n", + " for control in controls + [max_cu, quote_button]:\n", + " control.disabled = False\n", + " run_button.disabled = last_quote is None\n", + "\n", + "\n", + "quote_button.on_click(lambda _button: perform(\"quote\"))\n", + "run_button.on_click(lambda _button: perform(\"execute\"))\n", + "display(widgets.VBox(controls + [max_cu, widgets.HBox([quote_button, run_button]), usage, output]))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/scenarios/solve-spot.ipynb b/notebooks/scenarios/solve-spot.ipynb new file mode 100644 index 0000000..3072361 --- /dev/null +++ b/notebooks/scenarios/solve-spot.ipynb @@ -0,0 +1,272 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "intro", + "metadata": {}, + "source": "# Find the underlying price for a target\n\nMorphIQ SDK for Python\n\nFind spot levels that make an option worth a target price or produce target\nposition P&L relative to the original model mark. Enter finite positive ordered\nsearch bounds. Quantity is signed; multiplier is underlying units per contract.\nOption-price targets are per underlying unit; P&L targets are position currency.\nVolatility/rate/yield controls use percent; requests use decimals.\n\nThe approved flat rate is **100 CU per BSM request** or **200 CU per BS1993\nrequest**, covering every internal pricing call. Quote the account's active\nschedule and choose Max CU before executing. No-root or unresolved results and\npost-commit numerical failures still carry the flat charge. This workspace does\nno local root finding. Default tolerances are 1e-9 spot and 1e-6 target value;\nedit the explicit request construction below for other positive tolerances.\n\nRequires the scenario API deployment and matching account permissions. This SDK\ncandidate is held from merge/release until API deployment is verified.\n\nInputs start with synthetic values. Run all cells to display controls: setup\nand changing controls send **zero API requests**. Quote sends one free utility\nrequest. Execute sends one billable request, capped by Max CU, after a current\nquote. Actions run sequentially. Quotes reserve nothing. Changes invalidate the\nquote and displayed result; prior receipts remain in `history`.\n\nA missing charge is **not reported**, never zero. No automatic retries are made.\nEnter a key at the masked prompt or set `MORPHIQ_API_KEY`; set\n`MORPHIQ_API_BASE_URL` to a controlled service when testing. Never save credentials\nor outputs. The widget ceiling is limited to exact JavaScript integers; the\nPython client accepts the full unsigned 64-bit range.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "id": "inputs", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from getpass import getpass\n", + "\n", + "import ipywidgets as widgets\n", + "import pandas as pd\n", + "from IPython.display import display\n", + "\n", + "from morphiq_sdk import (\n", + " APIError,\n", + " MorphIQ,\n", + " PriceRequest,\n", + " ResponseError,\n", + " ScenarioOptionPriceTarget,\n", + " ScenarioPosition,\n", + " ScenarioPositionPnlTarget,\n", + " ScenarioSpotSolveRequest,\n", + " TransportError,\n", + ")\n", + "\n", + "api_key = os.environ.get(\"MORPHIQ_API_KEY\") or getpass(\"MorphIQ API key: \")\n", + "model = widgets.Dropdown(\n", + " options=[(\"BSM · European\", \"bsm\"), (\"BS1993 · American\", \"bs1993\")], description=\"Model\"\n", + ")\n", + "option_type = widgets.Dropdown(options=[\"call\", \"put\"], description=\"Option\")\n", + "spot = widgets.FloatText(value=100.0, description=\"Spot\")\n", + "strike = widgets.FloatText(value=100.0, description=\"Strike\")\n", + "expiry = widgets.FloatText(value=1.0, description=\"Years left\")\n", + "volatility = widgets.FloatText(value=20.0, description=\"Volatility %\")\n", + "rate = widgets.FloatText(value=5.0, description=\"Rate %\")\n", + "yield_input = widgets.FloatText(value=0.0, description=\"Yield %\")\n", + "multiplier = widgets.FloatText(value=100.0, description=\"Multiplier\")\n", + "quote_button = widgets.Button(\n", + " description=\"Quote · 1 request / 0 CU\", layout=widgets.Layout(width=\"240px\")\n", + ")\n", + "run_button = widgets.Button(\n", + " description=\"Execute · 1 request\", disabled=True, layout=widgets.Layout(width=\"240px\")\n", + ")\n", + "output = widgets.Output()\n", + "\n", + "\n", + "def show_message(*values):\n", + " output.append_stdout(\" \".join(str(value) for value in values) + \"\\n\")\n", + "\n", + "\n", + "usage = widgets.HTML(value=\"No API requests sent.\")\n", + "history = []\n", + "last_quote = None\n", + "last_response = None\n", + "quote_snapshot = None\n", + "result_snapshot = None\n", + "\n", + "\n", + "def contract_inputs():\n", + " return PriceRequest(\n", + " exercise_style=\"american\" if model.value == \"bs1993\" else \"european\",\n", + " pricing_model=\"bjerksund_stensland_1993\"\n", + " if model.value == \"bs1993\"\n", + " else \"black_scholes_merton\",\n", + " option_type=option_type.value,\n", + " spot=spot.value,\n", + " strike=strike.value,\n", + " time_to_expiry_years=expiry.value,\n", + " volatility=volatility.value / 100,\n", + " risk_free_rate=rate.value / 100,\n", + " dividend_yield=yield_input.value / 100,\n", + " )\n", + "\n", + "\n", + "quantity = widgets.FloatText(value=2.0, description=\"Quantity\")\n", + "target_kind = widgets.Dropdown(\n", + " options=[(\"Option price\", \"option_price\"), (\"Position P&L\", \"position_pnl\")],\n", + " description=\"Target kind\",\n", + ")\n", + "target_value = widgets.FloatText(value=15.0, description=\"Target value\")\n", + "min_spot = widgets.FloatText(value=50.0, description=\"Min spot\")\n", + "max_spot = widgets.FloatText(value=150.0, description=\"Max spot\")\n", + "max_cu = widgets.BoundedIntText(value=100, min=0, max=2**53 - 1, description=\"Max CU\")\n", + "controls = [\n", + " model,\n", + " option_type,\n", + " spot,\n", + " strike,\n", + " expiry,\n", + " volatility,\n", + " rate,\n", + " yield_input,\n", + " target_kind,\n", + " target_value,\n", + " quantity,\n", + " multiplier,\n", + " min_spot,\n", + " max_spot,\n", + "]\n", + "\n", + "\n", + "def current_request():\n", + " target = ScenarioOptionPriceTarget(kind=\"option_price\", value=target_value.value)\n", + " if target_kind.value == \"position_pnl\":\n", + " target = ScenarioPositionPnlTarget(\n", + " kind=\"position_pnl\",\n", + " value=target_value.value,\n", + " position=ScenarioPosition(quantity=quantity.value, multiplier=multiplier.value),\n", + " )\n", + " return ScenarioSpotSolveRequest(\n", + " contract=contract_inputs(), target=target, min_spot=min_spot.value, max_spot=max_spot.value\n", + " )\n", + "\n", + "\n", + "def execute_request(client, request):\n", + " return client.solve_spot(request, max_cu=max_cu.value)\n", + "\n", + "\n", + "def show_result(response):\n", + " result = response.result\n", + " show_message(\"Coverage:\", result.coverage.status)\n", + " show_message(\"Pricing evaluations (diagnostic, not a billing unit):\", result.price_evaluations)\n", + " if result.boundaries:\n", + " display(\n", + " pd.DataFrame(\n", + " [boundary.model_dump(exclude_unset=True) for boundary in result.boundaries]\n", + " )\n", + " )\n", + " else:\n", + " show_message(\n", + " \"No boundaries found at the declared scan resolution. This is not a global proof.\"\n", + " )\n", + " output.append_display_data(pd.DataFrame([region.model_dump() for region in result.regions]))\n", + " if result.coverage.status == \"unresolved\":\n", + " show_message(\"Unresolved cells: inspect the bounds, best candidate, error and reason.\")\n", + " output.append_display_data(\n", + " pd.DataFrame([cell.model_dump() for cell in result.coverage.cells])\n", + " )\n", + " show_message(\n", + " \"Point values and intervals are evaluated evidence. \"\n", + " \"Resolved coverage can miss smaller structures.\"\n", + " )\n", + " show_message(\"Fixed assumptions for repricing a boundary:\")\n", + " output.append_display_data(pd.DataFrame([result.inputs.model_dump(exclude_unset=True)]))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "actions", + "metadata": {}, + "outputs": [], + "source": [ + "def record(action, request, charged, request_id):\n", + " history.append(\n", + " {\"action\": action, \"request\": request, \"cu_charged\": charged, \"request_id\": request_id}\n", + " )\n", + " known = sum(row[\"cu_charged\"] for row in history if row[\"cu_charged\"] is not None)\n", + " unknown = sum(row[\"cu_charged\"] is None for row in history)\n", + " usage.value = (\n", + " f\"Recorded receipts: {known} CU; {unknown} unknown charges; {len(history)} requests.\"\n", + " )\n", + "\n", + "\n", + "def changed(_change):\n", + " global last_quote, last_response, quote_snapshot, result_snapshot\n", + " last_quote = last_response = quote_snapshot = result_snapshot = None\n", + " run_button.disabled = True\n", + " with output:\n", + " output.clear_output(wait=True)\n", + " show_message(\n", + " \"Inputs changed. Quote again before execution. Previous receipts remain in history.\"\n", + " )\n", + "\n", + "\n", + "for control in controls:\n", + " control.observe(changed, names=\"value\")\n", + "\n", + "\n", + "def perform(action):\n", + " global last_quote, last_response, quote_snapshot, result_snapshot\n", + " with output:\n", + " output.clear_output(wait=True)\n", + " try:\n", + " request = current_request()\n", + " wire = request.to_wire()\n", + " if action == \"execute\":\n", + " if last_quote is None or wire != quote_snapshot:\n", + " show_message(\"Quote the current inputs first. No execution submitted.\")\n", + " return\n", + " if last_quote.quote.total_fcu > max_cu.value:\n", + " show_message(\"Quote exceeds Max CU. No execution submitted.\")\n", + " return\n", + " else:\n", + " last_quote = quote_snapshot = None\n", + " last_response = result_snapshot = None\n", + " for control in controls + [max_cu, quote_button, run_button]:\n", + " control.disabled = True\n", + " with MorphIQ(api_key=api_key) as client:\n", + " if action == \"quote\":\n", + " response = client.quote(request)\n", + " last_quote, quote_snapshot = response, wire\n", + " record(\n", + " action, wire, response.metadata.fcu_charged, response.metadata.request_id\n", + " )\n", + " show_message(f\"Quoted execution charge: {response.quote.total_fcu} CU\")\n", + " show_message(\"Quote charge:\", response.metadata.fcu_charged)\n", + " show_message(\n", + " \"Quotes reserve nothing and do not lock policy or guarantee funds/capacity.\"\n", + " )\n", + " else:\n", + " response = execute_request(client, request)\n", + " last_response, result_snapshot = response, wire\n", + " record(\n", + " action, wire, response.metadata.fcu_charged, response.metadata.request_id\n", + " )\n", + " last_quote = quote_snapshot = None\n", + " show_message(\n", + " \"Commercial CU charged:\",\n", + " response.metadata.fcu_charged\n", + " if response.metadata.fcu_charged is not None\n", + " else \"not reported\",\n", + " )\n", + " show_message(\"Request ID:\", response.metadata.request_id)\n", + " show_result(response)\n", + " except (APIError, ResponseError, TransportError) as error:\n", + " metadata = getattr(error, \"metadata\", None)\n", + " charged = 0 if action == \"quote\" else (metadata.fcu_charged if metadata else None)\n", + " record(action, wire, charged, getattr(error, \"request_id\", None))\n", + " last_quote = quote_snapshot = None\n", + " show_message(str(error))\n", + " show_message(\n", + " \"Commercial CU charged:\", charged if charged is not None else \"not reported\"\n", + " )\n", + " show_message(\"No retry. A missing response is not evidence of zero charge.\")\n", + " except ValueError as error:\n", + " show_message(f\"Invalid local inputs/configuration: {error}. No request submitted.\")\n", + " finally:\n", + " for control in controls + [max_cu, quote_button]:\n", + " control.disabled = False\n", + " run_button.disabled = last_quote is None\n", + "\n", + "\n", + "quote_button.on_click(lambda _button: perform(\"quote\"))\n", + "run_button.on_click(lambda _button: perform(\"execute\"))\n", + "display(widgets.VBox(controls + [max_cu, widgets.HBox([quote_button, run_button]), usage, output]))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/start-here.ipynb b/notebooks/start-here.ipynb index 330317e..7cceb6a 100644 --- a/notebooks/start-here.ipynb +++ b/notebooks/start-here.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "id": "welcome", "metadata": {}, - "source": "# MorphIQ SDK for Python — Start Here\n\n## Choose an analysis\n\n- [Available models](pricing/models.ipynb): inspect public model capabilities and constraints.\n- [Standard-five Greeks](pricing/standard-five.ipynb): price an option and inspect\n its five primary sensitivities.\n- [Cost quotation](cost/quotation.ipynb): inspect active rates, quote a synthetic\n option, and explicitly execute with a maximum CU charge.\n- [Option pricing](pricing/options.ipynb): value European or American calls and puts,\n load a daily or intraday underlying price from Yahoo, inspect Greeks, solve implied\n volatility, analyze a premium with Greeks, track session CU, and chart sensitivity.\n- [Regime analysis](regime/identification.ipynb): load daily or intraday price history\n and inspect the latest regime assessment, scores and model provenance.\n\nOpen a workspace and select **Run → Run All Cells** once to display its controls.\nThen use its buttons. Running the setup makes no MorphIQ requests; each action button\nstates how many requests it sends. All market data stays in memory.\n\n## API access\n\nGet your API key from [the developer portal](https://developers.morphiqlabs.com).\nSet `MORPHIQ_API_KEY` before starting Docker or Jupyter, or enter it at the masked\nprompt in a workspace. The SDK connects to `https://api.morphiqlabs.com`.\n\nDocker includes the SDK, widgets, plotting and Yahoo dependencies. Outside Docker,\ninstall this repository with `pip install -e '.[notebooks]'`, then open JupyterLab.\n\nWorkspaces retain results in the current kernel. Clear outputs before sharing and\nshut down the kernel when finished. Notebook edits persist across Docker restarts.\nThe executable Python scripts in `examples/` demonstrate SDK usage separately.\n" + "source": "# MorphIQ SDK for Python — Start Here\n\n## Choose an analysis\n\n- [Scenario evaluation](scenarios/evaluate.ipynb): reprice a move or a spot/volatility grid.\n- [Spot-target solving](scenarios/solve-spot.ipynb): quote and solve a target option price or P&L.\n- [Available models](pricing/models.ipynb): inspect public model capabilities and constraints.\n- [Standard-five Greeks](pricing/standard-five.ipynb): price an option and inspect\n its five primary sensitivities.\n- [Cost quotation](cost/quotation.ipynb): inspect active rates, quote a synthetic\n option, and explicitly execute with a maximum CU charge.\n- [Option pricing](pricing/options.ipynb): value European or American calls and puts,\n load a daily or intraday underlying price from Yahoo, inspect Greeks, solve implied\n volatility, analyze a premium with Greeks, track session CU, and chart sensitivity.\n- [Regime analysis](regime/identification.ipynb): load daily or intraday price history\n and inspect the latest regime assessment, scores and model provenance.\n\nOpen a workspace and select **Run → Run All Cells** once to display its controls.\nThen use its buttons. Running the setup makes no MorphIQ requests; each action button\nstates how many requests it sends. All market data stays in memory.\n\n## API access\n\nGet your API key from [the developer portal](https://developers.morphiqlabs.com).\nSet `MORPHIQ_API_KEY` before starting Docker or Jupyter, or enter it at the masked\nprompt in a workspace. The SDK connects to `https://api.morphiqlabs.com`.\n\nDocker includes the SDK, widgets, plotting and Yahoo dependencies. Outside Docker,\ninstall this repository with `pip install -e '.[notebooks]'`, then open JupyterLab.\n\nWorkspaces retain results in the current kernel. Clear outputs before sharing and\nshut down the kernel when finished. Notebook edits persist across Docker restarts.\nThe executable Python scripts in `examples/` demonstrate SDK usage separately.\n" } ], "metadata": { diff --git a/scripts/check_notebook_container.py b/scripts/check_notebook_container.py index 240329a..2496035 100644 --- a/scripts/check_notebook_container.py +++ b/scripts/check_notebook_container.py @@ -109,7 +109,16 @@ def contents( "Local Jupyter startup, token authentication and all seeded notebooks verified.", flush=True, ) - for name in ("pricing", "regime", "greeks", "implied_volatility", "analytics", "batch"): + for name in ( + "pricing", + "regime", + "greeks", + "implied_volatility", + "analytics", + "batch", + "scenarios", + "scenario_quotes", + ): docker( "cp", str(ROOT / f"tests/fixtures/{name}.json"), identifier + f":/tmp/{name}.json" ) @@ -137,6 +146,8 @@ def contents( "/tmp/implied_volatility.json", "/tmp/analytics.json", "/tmp/batch.json", + "/tmp/scenarios.json", + "/tmp/scenario_quotes.json", ), flush=True, ) diff --git a/scripts/execute_notebook_fixture.py b/scripts/execute_notebook_fixture.py index f69b73f..02a7359 100644 --- a/scripts/execute_notebook_fixture.py +++ b/scripts/execute_notebook_fixture.py @@ -15,6 +15,22 @@ from nbclient import NotebookClient NOTEBOOK_CASES = { + "scenarios/evaluate.ipynb": ( + "scenario-bsm-point-quote", + "scenario-bsm-point", + "scenario-bsm-grid-quote", + "scenario-bsm-grid", + "scenario-bs1993-point-quote", + "scenario-bs1993-point", + ), + "scenarios/solve-spot.ipynb": ( + "solve-bsm-price-quote", + "solve-bsm-price", + "solve-bsm-pnl-quote", + "solve-bsm-pnl", + "solve-bs1993-pnl-quote", + "solve-bs1993-pnl", + ), "start-here.ipynb": (), "pricing/models.ipynb": (), # Explicit actions tested in test_api_v050. "pricing/standard-five.ipynb": (), @@ -36,6 +52,70 @@ def workspace_exercise(filename: str, prices: list[float]) -> str: """Exercise real widget actions with explicit synthetic market data in offline checks.""" + if filename == "scenarios/evaluate.ipynb": + return """ +assert history == [] and last_response is None and run_button.disabled +quote_button.click() +assert last_quote.quote.total_fcu == 2 and len(history) == 1 +max_cu.value = 1 +run_button.click() +assert len(history) == 1 and last_response is None +max_cu.value = 2 +run_button.click() +assert last_response.results[0].status == 'success' +assert last_response.results[0].inputs.spot == 105 +assert len(history) == 2 and history[-1]['cu_charged'] == 2 +spot_change.value = 10 +assert last_response is None and last_quote is None and run_button.disabled +assert history[-1]['request']['scenarios'][0]['spot_change'] == 5 +mode.value = 'grid' +quote_button.click() +max_cu.value = 10 +run_button.click() +assert len(last_response.results) == 9 and history[-1]['cu_charged'] == 10 +model.value = 'bs1993' +mode.value = 'point' +spot_change.value = 5 +quote_button.click() +max_cu.value = 4 +run_button.click() +assert last_response.base.result.pricing_model == 'bjerksund_stensland_1993' +assert len(history) == 6 and history[-1]['cu_charged'] == 4 +mode.value = 'grid' +grid_spots.value = 'invalid' +quote_button.click() +assert len(history) == 6 and last_response is None +assert not quote_button.disabled and run_button.disabled +""" + if filename == "scenarios/solve-spot.ipynb": + return """ +assert history == [] and last_response is None and run_button.disabled +quote_button.click() +assert last_quote.quote.total_fcu == 100 and history[-1]['cu_charged'] == 0 +run_button.click() +assert last_response.result.coverage.status == 'resolved' +assert last_response.result.boundaries[0].kind == 'point' +assert last_response.metadata.fcu_charged == 100 +assert history[-1]['request']['target']['kind'] == 'option_price' +target_kind.value = 'position_pnl' +target_value.value = 500 +assert last_response is None and run_button.disabled +quote_button.click() +run_button.click() +assert last_response.result.target.kind == 'position_pnl' +assert history[-1]['cu_charged'] == 100 +model.value = 'bs1993' +quote_button.click() +max_cu.value = 199 +run_button.click() +assert len(history) == 5 and last_response is None +max_cu.value = 200 +run_button.click() +assert len(history) == 6 and history[-1]['cu_charged'] == 200 +assert last_response.result.pricing_model == 'bjerksund_stensland_1993' +assert '400 CU' in usage.value and '0 unknown' in usage.value +assert not quote_button.disabled and run_button.disabled +""" if filename == "pricing/options.ipynb": return """ assert price_history == [] diff --git a/scripts/generate_notebook.py b/scripts/generate_notebook.py index 69cde7e..c4f87a2 100644 --- a/scripts/generate_notebook.py +++ b/scripts/generate_notebook.py @@ -72,6 +72,8 @@ def notebook() -> dict[str, Any]: ## Choose an analysis +- [Scenario evaluation](scenarios/evaluate.ipynb): reprice a move or a spot/volatility grid. +- [Spot-target solving](scenarios/solve-spot.ipynb): quote and solve a target option price or P&L. - [Available models](pricing/models.ipynb): inspect public model capabilities and constraints. - [Standard-five Greeks](pricing/standard-five.ipynb): price an option and inspect its five primary sensitivities. @@ -120,6 +122,8 @@ def notebooks() -> dict[str, dict[str, Any]]: "pricing/standard-five.ipynb", "regime/identification.ipynb", "cost/quotation.ipynb", + "scenarios/evaluate.ipynb", + "scenarios/solve-spot.ipynb", ): content = json.loads((ROOT / "notebooks" / filename).read_text()) if "widgets" in content["metadata"]: diff --git a/src/morphiq_sdk/__init__.py b/src/morphiq_sdk/__init__.py index 8f85402..4fdcd66 100644 --- a/src/morphiq_sdk/__init__.py +++ b/src/morphiq_sdk/__init__.py @@ -55,12 +55,64 @@ RegimeRequest, RegimeResponse, ResolvedCostModel, + ScenarioFailure, + ScenarioItem, + ScenarioOptionPriceTarget, + ScenarioPoint, + ScenarioPosition, + ScenarioPositionPnlTarget, + ScenarioPriceInputs, + ScenarioRequest, + ScenarioResponse, + ScenarioSolveBoundary, + ScenarioSolveCoverage, + ScenarioSolveInterval, + ScenarioSolvePoint, + ScenarioSolveProfile, + ScenarioSolveRegion, + ScenarioSolveResolved, + ScenarioSolveSign, + ScenarioSolveTarget, + ScenarioSolveUnresolved, + ScenarioSolveUnresolvedCell, + ScenarioSolveUnresolvedReason, + ScenarioSpotSolveRequest, + ScenarioSpotSolveResponse, + ScenarioSpotSolveResult, + ScenarioSuccess, + ScenarioValuation, ) from .workflows import AsyncBatchWorkflow, BatchWorkflow, ChunkAttempt __version__ = version("morphiq-sdk") __all__ = [ + "ScenarioPosition", + "ScenarioPoint", + "ScenarioRequest", + "ScenarioOptionPriceTarget", + "ScenarioPositionPnlTarget", + "ScenarioSolveTarget", + "ScenarioSpotSolveRequest", + "ScenarioPriceInputs", + "ScenarioValuation", + "ScenarioSuccess", + "ScenarioFailure", + "ScenarioItem", + "ScenarioResponse", + "ScenarioSolvePoint", + "ScenarioSolveInterval", + "ScenarioSolveBoundary", + "ScenarioSolveSign", + "ScenarioSolveUnresolvedReason", + "ScenarioSolveRegion", + "ScenarioSolveUnresolvedCell", + "ScenarioSolveResolved", + "ScenarioSolveUnresolved", + "ScenarioSolveCoverage", + "ScenarioSolveProfile", + "ScenarioSpotSolveResult", + "ScenarioSpotSolveResponse", "GreeksStandard", "GreeksStandardRequest", "GreeksStandardResult", diff --git a/src/morphiq_sdk/client.py b/src/morphiq_sdk/client.py index 1d23f2c..ead7dee 100644 --- a/src/morphiq_sdk/client.py +++ b/src/morphiq_sdk/client.py @@ -40,12 +40,18 @@ RegimeRequest, RegimeResponse, ResponseMetadata, + ScenarioRequest, + ScenarioResponse, + ScenarioSpotSolveRequest, + ScenarioSpotSolveResponse, _BatchResponse, ) from .workflows import AsyncBatchWorkflow, BatchWorkflow, _prepare_chunks _ResponseType = TypeVar( "_ResponseType", + ScenarioResponse, + ScenarioSpotSolveResponse, BatchPriceResponse, BatchGreeksResponse, BatchGreeksStandardResponse, @@ -78,6 +84,17 @@ def _check_batch(response: _BatchResponseType, expected: int) -> _BatchResponseT return response +def _check_scenarios(response: ScenarioResponse, expected: int) -> ScenarioResponse: + if [item.index for item in response.results] != list(range(expected)): + raise ResponseError( + request_id=response.metadata.request_id, + body=response.raw_body, + headers=response.headers, + metadata=response.metadata, + ) + return response + + def _redact_body(body: bytes, key: str) -> bytes: """Handle both literal and JSON-escaped reflections without rewriting clean bodies.""" try: @@ -103,7 +120,9 @@ def visit(value: Any) -> Any: QuoteInput = ( - PriceRequest + ScenarioRequest + | ScenarioSpotSolveRequest + | PriceRequest | RegimeRequest | GreeksRequest | GreeksStandardRequest @@ -116,6 +135,8 @@ def visit(value: Any) -> Any: | BatchAnalyticsRequest ) _QUOTE_OPERATIONS = { + ScenarioRequest: "scenario_evaluate", + ScenarioSpotSolveRequest: "scenario_spot_solve", PriceRequest: "price", RegimeRequest: "regime", GreeksRequest: "greeks", @@ -270,6 +291,22 @@ def __init__( trust_env=False, ) + def evaluate_scenarios( + self, request: ScenarioRequest, *, max_cu: int | None = None + ) -> ScenarioResponse: + """Reprice ordered points once; preserve partial errors and one request charge.""" + payload = request.to_wire() + response = self._post("v1/scenarios/evaluate", payload, ScenarioResponse, max_cu=max_cu) + return _check_scenarios(response, len(payload["scenarios"])) + + def solve_spot( + self, request: ScenarioSpotSolveRequest, *, max_cu: int | None = None + ) -> ScenarioSpotSolveResponse: + """Solve on the server; retain all boundaries, coverage and the flat charge receipt.""" + return self._post( + "v1/scenarios/solve-spot", request.to_wire(), ScenarioSpotSolveResponse, max_cu=max_cu + ) + def price(self, request: PriceRequest, *, max_cu: int | None = None) -> PriceResponse: """Price one option; preserve server model selection and charge evidence.""" return self._post("v1/price", request.to_wire(), PriceResponse, max_cu=max_cu) @@ -495,6 +532,24 @@ def __init__( trust_env=False, ) + async def evaluate_scenarios( + self, request: ScenarioRequest, *, max_cu: int | None = None + ) -> ScenarioResponse: + """Reprice ordered points once; preserve partial errors and one request charge.""" + payload = request.to_wire() + response = await self._post( + "v1/scenarios/evaluate", payload, ScenarioResponse, max_cu=max_cu + ) + return _check_scenarios(response, len(payload["scenarios"])) + + async def solve_spot( + self, request: ScenarioSpotSolveRequest, *, max_cu: int | None = None + ) -> ScenarioSpotSolveResponse: + """Solve on the server; retain all boundaries, coverage and the flat charge receipt.""" + return await self._post( + "v1/scenarios/solve-spot", request.to_wire(), ScenarioSpotSolveResponse, max_cu=max_cu + ) + async def price(self, request: PriceRequest, *, max_cu: int | None = None) -> PriceResponse: """Price one option; preserve server model selection and charge evidence.""" return await self._post("v1/price", request.to_wire(), PriceResponse, max_cu=max_cu) diff --git a/src/morphiq_sdk/models.py b/src/morphiq_sdk/models.py index eac4798..61b7faf 100644 --- a/src/morphiq_sdk/models.py +++ b/src/morphiq_sdk/models.py @@ -588,6 +588,7 @@ class PricingModelDescriptor(_Response): dividend_support: list[str] operations: list[str] batch_operations: list[str] + scenario_operations: list[str] greeks: list[str] guidance: str constraints: list[ModelConstraint] @@ -600,3 +601,190 @@ class PricingModelCatalog(_EvidenceResponse): models: list[PricingModelDescriptor] engines: list[EngineIdentity] greek_units: dict[str, str] + + +class ScenarioPosition(_Request): + """Signed contracts and positive underlying units per contract; no currency conversion.""" + + quantity: FiniteFloat + multiplier: FiniteFloat + + +class ScenarioPoint(_Request): + """Independent changes from the base; spot_change=5 means five currency units.""" + + spot: FiniteFloat | None = None + spot_change: FiniteFloat | None = None + volatility: FiniteFloat | None = None + elapsed_time_years: FiniteFloat = 0.0 + + +class _ScenarioRequest(_Request): + contract: PriceRequest + + def to_wire(self) -> dict[str, Any]: + """Revalidate nested edits and retain explicit nulls and omitted defaults.""" + values = self.model_dump(exclude={"contract"}, exclude_unset=True) + values["contract"] = self.contract.to_wire() + validated = type(self).model_validate(values) + return validated.model_dump(mode="json", exclude_unset=True) + + +class ScenarioRequest(_ScenarioRequest): + """Reprice 1–99 ordered points; account limits and model domains are server-owned.""" + + scenarios: list[ScenarioPoint] = Field(min_length=1, max_length=99) + position: ScenarioPosition | None = None + + +class ScenarioOptionPriceTarget(_Request): + kind: Literal["option_price"] + value: FiniteFloat + + +class ScenarioPositionPnlTarget(_Request): + kind: Literal["position_pnl"] + value: FiniteFloat + position: ScenarioPosition + + +ScenarioSolveTarget = Annotated[ + ScenarioOptionPriceTarget | ScenarioPositionPnlTarget, Field(discriminator="kind") +] + + +class ScenarioSpotSolveRequest(_ScenarioRequest): + """Bounded server-owned spot solve for an option price or base-relative position P&L.""" + + target: ScenarioSolveTarget + min_spot: FiniteFloat + max_spot: FiniteFloat + volatility: FiniteFloat | None = None + elapsed_time_years: FiniteFloat = 0.0 + spot_tolerance: FiniteFloat = 1e-9 + value_tolerance: FiniteFloat = 1e-6 + + +class ScenarioPriceInputs(PriceRequest): + """Echoed price inputs, retaining additive server fields without inventing values.""" + + model_config = ConfigDict(extra="allow", strict=True, allow_inf_nan=False, frozen=True) + + +class ScenarioValuation(_Response): + inputs: ScenarioPriceInputs + result: PriceResult + + +class ScenarioSuccess(_Response): + index: int = Field(ge=0, le=2**32 - 1) + inputs: ScenarioPriceInputs + status: Literal["success"] + result: PriceResult + price_change: FiniteFloat + position_pnl: FiniteFloat | None = None + + +class ScenarioFailure(_Response): + index: int = Field(ge=0, le=2**32 - 1) + inputs: ScenarioPriceInputs + status: Literal["error"] + error: ErrorDetails + + +ScenarioItem = Annotated[ScenarioSuccess | ScenarioFailure, Field(discriminator="status")] + + +class ScenarioResponse(_EvidenceResponse): + base: ScenarioValuation + results: list[ScenarioItem] + position: ScenarioPosition | None = None + metadata: ResponseMetadata + + +class ScenarioSolvePoint(_Response): + kind: Literal["point"] + spot: FiniteFloat + value: FiniteFloat + crossing: bool + refinements: int = Field(ge=0, le=2**32 - 1) + bracket_width: FiniteFloat + + +class ScenarioSolveInterval(_Response): + kind: Literal["interval"] + min_spot: FiniteFloat + max_spot: FiniteFloat + value: FiniteFloat + + +ScenarioSolveBoundary = Annotated[ + ScenarioSolvePoint | ScenarioSolveInterval, Field(discriminator="kind") +] +ScenarioSolveSign = Literal["above_target", "below_target", "at_target"] +ScenarioSolveUnresolvedReason = Literal[ + "refinement_budget_exhausted", + "bracket_collapsed_above_tolerance", + "spot_tolerance_reached", + "representative_out_of_band", +] + + +class ScenarioSolveRegion(_Response): + min_spot: FiniteFloat + max_spot: FiniteFloat + sign: ScenarioSolveSign + + +class ScenarioSolveUnresolvedCell(_Response): + lower_spot: FiniteFloat + upper_spot: FiniteFloat + best_spot: FiniteFloat + best_abs_value_error: FiniteFloat + reason: ScenarioSolveUnresolvedReason + + +class ScenarioSolveResolved(_Response): + """Resolved at the declared scan resolution; not a global-completeness guarantee.""" + + status: Literal["resolved"] + + +class ScenarioSolveUnresolved(_Response): + status: Literal["unresolved"] + cells: list[ScenarioSolveUnresolvedCell] + + +ScenarioSolveCoverage = Annotated[ + ScenarioSolveResolved | ScenarioSolveUnresolved, Field(discriminator="status") +] + + +class ScenarioSolveProfile(_Response): + id: str + scan_intervals: int = Field(ge=0, le=2**32 - 1) + max_refinements: int = Field(ge=0, le=2**32 - 1) + max_price_evaluations: int = Field(ge=0, le=2**64 - 1) + + +class ScenarioSpotSolveResult(_Response): + pricing_model: str + model_version: str + base: ScenarioValuation + inputs: ScenarioPriceInputs + target: ScenarioSolveTarget + min_spot: FiniteFloat + max_spot: FiniteFloat + spot_tolerance: FiniteFloat + value_tolerance: FiniteFloat + profile: ScenarioSolveProfile + reference_net_value: FiniteFloat + boundaries: list[ScenarioSolveBoundary] + regions: list[ScenarioSolveRegion] + coverage: ScenarioSolveCoverage + price_evaluations: int = Field(ge=0, le=2**64 - 1) + + +class ScenarioSpotSolveResponse(_EvidenceResponse): + result: ScenarioSpotSolveResult + metadata: ResponseMetadata diff --git a/tests/conftest.py b/tests/conftest.py index 9ecaeb9..53ae9f0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -38,6 +38,16 @@ def batch_cases(): return json.loads((ROOT / "tests/fixtures/batch.json").read_text())["cases"] +@pytest.fixture +def scenario_cases(): + return json.loads((ROOT / "tests/fixtures/scenarios.json").read_text())["cases"] + + +@pytest.fixture +def scenario_quote_cases(): + return json.loads((ROOT / "tests/fixtures/scenario_quotes.json").read_text())["cases"] + + @pytest.fixture(autouse=True) def isolate_credentials(monkeypatch): monkeypatch.delenv("MORPHIQ_API_KEY", raising=False) @@ -45,10 +55,28 @@ def isolate_credentials(monkeypatch): @pytest.fixture -def fixture_api(cases, regime_cases, greeks_cases, iv_cases, analytics_cases, batch_cases): +def fixture_api( + cases, + regime_cases, + greeks_cases, + iv_cases, + analytics_cases, + batch_cases, + scenario_cases, + scenario_quote_cases, +): """Loopback replay of contract fixtures; no numerical implementation or external calls.""" received = [] - cases = cases + regime_cases + greeks_cases + iv_cases + analytics_cases + batch_cases + cases = ( + cases + + regime_cases + + greeks_cases + + iv_cases + + analytics_cases + + batch_cases + + scenario_cases + + scenario_quote_cases + ) class Handler(BaseHTTPRequestHandler): def do_POST(self): @@ -65,7 +93,7 @@ def do_POST(self): if match is None or self.path != match.get("path", "/v1/price"): self.send_error(422) return - self.send_response(200) + self.send_response(match.get("status", 200)) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write(json.dumps(match["response"]).encode()) diff --git a/tests/fixtures/analytics.json b/tests/fixtures/analytics.json index d93a337..608ad2a 100644 --- a/tests/fixtures/analytics.json +++ b/tests/fixtures/analytics.json @@ -216,5 +216,10 @@ } } } - ] + ], + "validation": { + "source_commit": "7b4f233b2222fbb95b2492d5c57dbde1ac32e571", + "contract_sha256": "38a0c933ea805f132bc4245867c3130c5a1db94e23cb4837872abf8c27e33ccc", + "method": "Historical native responses retained with original capture identity; requests/results revalidated against this candidate in controlled native integration. Original request IDs and metadata are not rewritten." + } } diff --git a/tests/fixtures/batch.json b/tests/fixtures/batch.json index 5427d52..27d85db 100644 --- a/tests/fixtures/batch.json +++ b/tests/fixtures/batch.json @@ -747,5 +747,10 @@ ] } } - ] + ], + "validation": { + "source_commit": "7b4f233b2222fbb95b2492d5c57dbde1ac32e571", + "contract_sha256": "38a0c933ea805f132bc4245867c3130c5a1db94e23cb4837872abf8c27e33ccc", + "method": "Historical native responses retained with original capture identity; requests/results revalidated against this candidate in controlled native integration. Original request IDs and metadata are not rewritten." + } } diff --git a/tests/fixtures/greeks.json b/tests/fixtures/greeks.json index 4a8ae4c..6c2e5e1 100644 --- a/tests/fixtures/greeks.json +++ b/tests/fixtures/greeks.json @@ -100,5 +100,10 @@ } } } - ] + ], + "validation": { + "source_commit": "7b4f233b2222fbb95b2492d5c57dbde1ac32e571", + "contract_sha256": "38a0c933ea805f132bc4245867c3130c5a1db94e23cb4837872abf8c27e33ccc", + "method": "Historical native responses retained with original capture identity; requests/results revalidated against this candidate in controlled native integration. Original request IDs and metadata are not rewritten." + } } diff --git a/tests/fixtures/implied_volatility.json b/tests/fixtures/implied_volatility.json index bd46ca2..c5b25cd 100644 --- a/tests/fixtures/implied_volatility.json +++ b/tests/fixtures/implied_volatility.json @@ -84,5 +84,10 @@ } } } - ] + ], + "validation": { + "source_commit": "7b4f233b2222fbb95b2492d5c57dbde1ac32e571", + "contract_sha256": "38a0c933ea805f132bc4245867c3130c5a1db94e23cb4837872abf8c27e33ccc", + "method": "Historical native responses retained with original capture identity; requests/results revalidated against this candidate in controlled native integration. Original request IDs and metadata are not rewritten." + } } diff --git a/tests/fixtures/pricing_models.json b/tests/fixtures/pricing_models.json index 2c8352e..28bc8ed 100644 --- a/tests/fixtures/pricing_models.json +++ b/tests/fixtures/pricing_models.json @@ -2,7 +2,7 @@ "models": [ { "id": "black_scholes_merton", - "name": "Black\u2013Scholes\u2013Merton", + "name": "Black–Scholes–Merton", "version": "1", "model_version": "black_scholes_merton/1", "aliases": [], @@ -21,6 +21,10 @@ "greeks", "analytics" ], + "scenario_operations": [ + "evaluate", + "solve_spot" + ], "batch_operations": [ "price", "implied_volatility", @@ -46,7 +50,7 @@ }, { "id": "black_scholes_merton_qdfp", - "name": "American Black\u2013Scholes\u2013Merton \u2014 QdFp", + "name": "American Black–Scholes–Merton — QdFp", "version": "1", "model_version": "qd_fixed_point/1", "aliases": [ @@ -64,6 +68,7 @@ "price", "implied_volatility" ], + "scenario_operations": [], "batch_operations": [], "greeks": [], "guidance": "American calls and puts with a continuous dividend yield.", @@ -72,7 +77,7 @@ }, { "id": "black_scholes_merton_fd", - "name": "Black\u2013Scholes finite difference (cash dividends)", + "name": "Black–Scholes finite difference (cash dividends)", "version": "1", "model_version": "cash_dividend_american/1", "aliases": [ @@ -91,6 +96,7 @@ "price", "implied_volatility" ], + "scenario_operations": [], "batch_operations": [], "greeks": [], "guidance": "American options with scheduled cash dividends.", @@ -140,7 +146,7 @@ }, { "id": "bjerksund_stensland_1993", - "name": "Bjerksund\u2013Stensland 1993", + "name": "Bjerksund–Stensland 1993", "version": "1", "model_version": "bjerksund_stensland_1993/1", "aliases": [], @@ -159,6 +165,10 @@ "greeks", "analytics" ], + "scenario_operations": [ + "evaluate", + "solve_spot" + ], "batch_operations": [ "price", "implied_volatility", @@ -191,15 +201,15 @@ } ], "greek_units": { - "charm": "`d\u00b2V/(dS dt)`, **per year**.", - "color": "`d\u00b3V/(dS\u00b2 dt)`, **per year**.", + "charm": "`d²V/(dS dt)`, **per year**.", + "color": "`d³V/(dS² dt)`, **per year**.", "delta": "`dV/dS`.", - "gamma": "`d\u00b2V/dS\u00b2`.", + "gamma": "`d²V/dS²`.", "rho": "`dV/dr`, per unit rate.", "theta": "`dV/dt`, **per year**.", - "vanna": "`d\u00b2V/(dS d\u03c3)`, per unit volatility.", - "vega": "`dV/d\u03c3`, per unit volatility.", - "veta": "`d\u00b2V/(d\u03c3 dt)`, **per year** per unit volatility.", - "volga": "`d\u00b2V/d\u03c3\u00b2`, per unit volatility squared." + "vanna": "`d²V/(dS dσ)`, per unit volatility.", + "vega": "`dV/dσ`, per unit volatility.", + "veta": "`d²V/(dσ dt)`, **per year** per unit volatility.", + "volga": "`d²V/dσ²`, per unit volatility squared." } } diff --git a/tests/fixtures/scenario_quotes.json b/tests/fixtures/scenario_quotes.json new file mode 100644 index 0000000..ccfd7a4 --- /dev/null +++ b/tests/fixtures/scenario_quotes.json @@ -0,0 +1,1336 @@ +{ + "source": { + "api_commit": "7b4f233b2222fbb95b2492d5c57dbde1ac32e571", + "schema_sha256": "38a0c933ea805f132bc4245867c3130c5a1db94e23cb4837872abf8c27e33ccc", + "binary_sha256": "485ca13ab77fe174443d17620e419af1f6032f95af407304a8393566f2b275d3", + "inputs": "Synthetic local protected preview, plans.pro@8, schedule v5; no production account or key." + }, + "cases": [ + { + "name": "scenario-bsm-point-quote", + "path": "/v1/cost/quote", + "request": { + "operation": "scenario_evaluate", + "request": { + "contract": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "scenarios": [ + { + "spot_change": 5.0 + } + ], + "position": { + "quantity": 1.0, + "multiplier": 100.0 + } + } + }, + "response": { + "quote": { + "total_fcu": 2, + "fixed_fcu": 0, + "variable_fcu": 2, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "price", + "model": "black_scholes_merton/1", + "billing_unit": "contract", + "quantity": 2, + "fcu_per_unit": 1, + "variable_fcu": 2 + } + ] + }, + "resolved_models": [ + { + "model": "black_scholes_merton/1", + "quantity": 2 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000002", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 100000, + "budget_period": "utc_calendar_month" + } + } + }, + { + "name": "scenario-bsm-grid-quote", + "path": "/v1/cost/quote", + "request": { + "operation": "scenario_evaluate", + "request": { + "contract": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "scenarios": [ + { + "spot": 95.0, + "volatility": 0.15 + }, + { + "spot": 95.0, + "volatility": 0.2 + }, + { + "spot": 95.0, + "volatility": 0.25 + }, + { + "spot": 100.0, + "volatility": 0.15 + }, + { + "spot": 100.0, + "volatility": 0.2 + }, + { + "spot": 100.0, + "volatility": 0.25 + }, + { + "spot": 105.0, + "volatility": 0.15 + }, + { + "spot": 105.0, + "volatility": 0.2 + }, + { + "spot": 105.0, + "volatility": 0.25 + } + ], + "position": { + "quantity": 1.0, + "multiplier": 100.0 + } + } + }, + "response": { + "quote": { + "total_fcu": 10, + "fixed_fcu": 0, + "variable_fcu": 10, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "price", + "model": "black_scholes_merton/1", + "billing_unit": "contract", + "quantity": 10, + "fcu_per_unit": 1, + "variable_fcu": 10 + } + ] + }, + "resolved_models": [ + { + "model": "black_scholes_merton/1", + "quantity": 10 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000004", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99998, + "budget_period": "utc_calendar_month" + } + } + }, + { + "name": "scenario-bsm-partial-quote", + "path": "/v1/cost/quote", + "request": { + "operation": "scenario_evaluate", + "request": { + "contract": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "scenarios": [ + { + "spot_change": 5.0 + }, + { + "volatility": -1.0 + }, + { + "spot_change": 5.0 + } + ], + "position": { + "quantity": 1.0, + "multiplier": 100.0 + } + } + }, + "response": { + "quote": { + "total_fcu": 4, + "fixed_fcu": 0, + "variable_fcu": 4, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "price", + "model": "black_scholes_merton/1", + "billing_unit": "contract", + "quantity": 4, + "fcu_per_unit": 1, + "variable_fcu": 4 + } + ] + }, + "resolved_models": [ + { + "model": "black_scholes_merton/1", + "quantity": 4 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000006", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99988, + "budget_period": "utc_calendar_month" + } + } + }, + { + "name": "solve-bsm-price-quote", + "path": "/v1/cost/quote", + "request": { + "operation": "scenario_spot_solve", + "request": { + "contract": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 15.0 + }, + "min_spot": 50.0, + "max_spot": 150.0 + } + }, + "response": { + "quote": { + "total_fcu": 100, + "fixed_fcu": 0, + "variable_fcu": 100, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "black_scholes_merton/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 100, + "variable_fcu": 100 + } + ] + }, + "resolved_models": [ + { + "model": "black_scholes_merton/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000008", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99984, + "budget_period": "utc_calendar_month" + } + } + }, + { + "name": "solve-bsm-pnl-quote", + "path": "/v1/cost/quote", + "request": { + "operation": "scenario_spot_solve", + "request": { + "contract": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "position_pnl", + "value": 500.0, + "position": { + "quantity": 2.0, + "multiplier": 100.0 + } + }, + "min_spot": 50.0, + "max_spot": 150.0 + } + }, + "response": { + "quote": { + "total_fcu": 100, + "fixed_fcu": 0, + "variable_fcu": 100, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "black_scholes_merton/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 100, + "variable_fcu": 100 + } + ] + }, + "resolved_models": [ + { + "model": "black_scholes_merton/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-000000000000000a", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99884, + "budget_period": "utc_calendar_month" + } + } + }, + { + "name": "solve-bsm-no-root-quote", + "path": "/v1/cost/quote", + "request": { + "operation": "scenario_spot_solve", + "request": { + "contract": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 1000.0 + }, + "min_spot": 50.0, + "max_spot": 150.0 + } + }, + "response": { + "quote": { + "total_fcu": 100, + "fixed_fcu": 0, + "variable_fcu": 100, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "black_scholes_merton/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 100, + "variable_fcu": 100 + } + ] + }, + "resolved_models": [ + { + "model": "black_scholes_merton/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-000000000000000c", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99784, + "budget_period": "utc_calendar_month" + } + } + }, + { + "name": "solve-bsm-interval-quote", + "path": "/v1/cost/quote", + "request": { + "operation": "scenario_spot_solve", + "request": { + "contract": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 0.0 + }, + "min_spot": 50.0, + "max_spot": 150.0, + "elapsed_time_years": 1.0 + } + }, + "response": { + "quote": { + "total_fcu": 100, + "fixed_fcu": 0, + "variable_fcu": 100, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "black_scholes_merton/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 100, + "variable_fcu": 100 + } + ] + }, + "resolved_models": [ + { + "model": "black_scholes_merton/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-000000000000000e", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99684, + "budget_period": "utc_calendar_month" + } + } + }, + { + "name": "solve-bsm-unresolved-quote", + "path": "/v1/cost/quote", + "request": { + "operation": "scenario_spot_solve", + "request": { + "contract": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 15.0 + }, + "min_spot": 50.0, + "max_spot": 150.0, + "spot_tolerance": 1000.0, + "value_tolerance": 1e-15 + } + }, + "response": { + "quote": { + "total_fcu": 100, + "fixed_fcu": 0, + "variable_fcu": 100, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "black_scholes_merton/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 100, + "variable_fcu": 100 + } + ] + }, + "resolved_models": [ + { + "model": "black_scholes_merton/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000010", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99584, + "budget_period": "utc_calendar_month" + } + } + }, + { + "name": "scenario-bs1993-point-quote", + "path": "/v1/cost/quote", + "request": { + "operation": "scenario_evaluate", + "request": { + "contract": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "scenarios": [ + { + "spot_change": 5.0 + } + ], + "position": { + "quantity": 1.0, + "multiplier": 100.0 + } + } + }, + "response": { + "quote": { + "total_fcu": 4, + "fixed_fcu": 0, + "variable_fcu": 4, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "price", + "model": "bjerksund_stensland_1993/1", + "billing_unit": "contract", + "quantity": 2, + "fcu_per_unit": 2, + "variable_fcu": 4 + } + ] + }, + "resolved_models": [ + { + "model": "bjerksund_stensland_1993/1", + "quantity": 2 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000014", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99384, + "budget_period": "utc_calendar_month" + } + } + }, + { + "name": "scenario-bs1993-grid-quote", + "path": "/v1/cost/quote", + "request": { + "operation": "scenario_evaluate", + "request": { + "contract": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "scenarios": [ + { + "spot": 95.0, + "volatility": 0.15 + }, + { + "spot": 95.0, + "volatility": 0.2 + }, + { + "spot": 95.0, + "volatility": 0.25 + }, + { + "spot": 100.0, + "volatility": 0.15 + }, + { + "spot": 100.0, + "volatility": 0.2 + }, + { + "spot": 100.0, + "volatility": 0.25 + }, + { + "spot": 105.0, + "volatility": 0.15 + }, + { + "spot": 105.0, + "volatility": 0.2 + }, + { + "spot": 105.0, + "volatility": 0.25 + } + ], + "position": { + "quantity": 1.0, + "multiplier": 100.0 + } + } + }, + "response": { + "quote": { + "total_fcu": 20, + "fixed_fcu": 0, + "variable_fcu": 20, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "price", + "model": "bjerksund_stensland_1993/1", + "billing_unit": "contract", + "quantity": 10, + "fcu_per_unit": 2, + "variable_fcu": 20 + } + ] + }, + "resolved_models": [ + { + "model": "bjerksund_stensland_1993/1", + "quantity": 10 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000016", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99380, + "budget_period": "utc_calendar_month" + } + } + }, + { + "name": "scenario-bs1993-partial-quote", + "path": "/v1/cost/quote", + "request": { + "operation": "scenario_evaluate", + "request": { + "contract": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "scenarios": [ + { + "spot_change": 5.0 + }, + { + "volatility": -1.0 + }, + { + "spot_change": 5.0 + } + ], + "position": { + "quantity": 1.0, + "multiplier": 100.0 + } + } + }, + "response": { + "quote": { + "total_fcu": 8, + "fixed_fcu": 0, + "variable_fcu": 8, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "price", + "model": "bjerksund_stensland_1993/1", + "billing_unit": "contract", + "quantity": 4, + "fcu_per_unit": 2, + "variable_fcu": 8 + } + ] + }, + "resolved_models": [ + { + "model": "bjerksund_stensland_1993/1", + "quantity": 4 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000018", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99360, + "budget_period": "utc_calendar_month" + } + } + }, + { + "name": "solve-bs1993-price-quote", + "path": "/v1/cost/quote", + "request": { + "operation": "scenario_spot_solve", + "request": { + "contract": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 15.0 + }, + "min_spot": 50.0, + "max_spot": 150.0 + } + }, + "response": { + "quote": { + "total_fcu": 200, + "fixed_fcu": 0, + "variable_fcu": 200, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "bjerksund_stensland_1993/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 200, + "variable_fcu": 200 + } + ] + }, + "resolved_models": [ + { + "model": "bjerksund_stensland_1993/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-000000000000001a", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99352, + "budget_period": "utc_calendar_month" + } + } + }, + { + "name": "solve-bs1993-pnl-quote", + "path": "/v1/cost/quote", + "request": { + "operation": "scenario_spot_solve", + "request": { + "contract": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "position_pnl", + "value": 500.0, + "position": { + "quantity": 2.0, + "multiplier": 100.0 + } + }, + "min_spot": 50.0, + "max_spot": 150.0 + } + }, + "response": { + "quote": { + "total_fcu": 200, + "fixed_fcu": 0, + "variable_fcu": 200, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "bjerksund_stensland_1993/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 200, + "variable_fcu": 200 + } + ] + }, + "resolved_models": [ + { + "model": "bjerksund_stensland_1993/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-000000000000001c", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99152, + "budget_period": "utc_calendar_month" + } + } + }, + { + "name": "solve-bs1993-no-root-quote", + "path": "/v1/cost/quote", + "request": { + "operation": "scenario_spot_solve", + "request": { + "contract": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 1000.0 + }, + "min_spot": 50.0, + "max_spot": 150.0 + } + }, + "response": { + "quote": { + "total_fcu": 200, + "fixed_fcu": 0, + "variable_fcu": 200, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "bjerksund_stensland_1993/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 200, + "variable_fcu": 200 + } + ] + }, + "resolved_models": [ + { + "model": "bjerksund_stensland_1993/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-000000000000001e", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 98952, + "budget_period": "utc_calendar_month" + } + } + }, + { + "name": "solve-bs1993-interval-quote", + "path": "/v1/cost/quote", + "request": { + "operation": "scenario_spot_solve", + "request": { + "contract": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 0.0 + }, + "min_spot": 50.0, + "max_spot": 150.0, + "elapsed_time_years": 1.0 + } + }, + "response": { + "quote": { + "total_fcu": 200, + "fixed_fcu": 0, + "variable_fcu": 200, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "bjerksund_stensland_1993/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 200, + "variable_fcu": 200 + } + ] + }, + "resolved_models": [ + { + "model": "bjerksund_stensland_1993/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000020", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 98752, + "budget_period": "utc_calendar_month" + } + } + }, + { + "name": "solve-bs1993-unresolved-quote", + "path": "/v1/cost/quote", + "request": { + "operation": "scenario_spot_solve", + "request": { + "contract": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 15.0 + }, + "min_spot": 50.0, + "max_spot": 150.0, + "spot_tolerance": 1000.0, + "value_tolerance": 1e-15 + } + }, + "response": { + "quote": { + "total_fcu": 200, + "fixed_fcu": 0, + "variable_fcu": 200, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "bjerksund_stensland_1993/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 200, + "variable_fcu": 200 + } + ] + }, + "resolved_models": [ + { + "model": "bjerksund_stensland_1993/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000022", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 98552, + "budget_period": "utc_calendar_month" + } + } + } + ] +} diff --git a/tests/fixtures/scenarios.json b/tests/fixtures/scenarios.json new file mode 100644 index 0000000..d4f6dba --- /dev/null +++ b/tests/fixtures/scenarios.json @@ -0,0 +1,3710 @@ +{ + "source": { + "api_commit": "7b4f233b2222fbb95b2492d5c57dbde1ac32e571", + "schema_sha256": "38a0c933ea805f132bc4245867c3130c5a1db94e23cb4837872abf8c27e33ccc", + "binary_sha256": "485ca13ab77fe174443d17620e419af1f6032f95af407304a8393566f2b275d3", + "inputs": "Synthetic local protected preview, plans.pro@8, schedule v5; no production account or key." + }, + "cases": [ + { + "name": "scenario-bsm-point", + "path": "/v1/scenarios/evaluate", + "request": { + "contract": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "scenarios": [ + { + "spot_change": 5.0 + } + ], + "position": { + "quantity": 1.0, + "multiplier": 100.0 + } + }, + "response": { + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000003", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 2, + "compute_schedule_version": "2026-09-11", + "fcu_quoted": 2, + "fcu_charged": 2, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99998, + "budget_period": "utc_calendar_month" + }, + "base": { + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "result": { + "price": 10.45058357218557, + "exercise_style": "european", + "option_type": "call", + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1" + } + }, + "position": { + "quantity": 1.0, + "multiplier": 100.0 + }, + "results": [ + { + "index": 0, + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 105.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "status": "success", + "result": { + "price": 13.85790626707311, + "exercise_style": "european", + "option_type": "call", + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1" + }, + "price_change": 3.40732269488754, + "position_pnl": 340.732269488754 + } + ] + }, + "quote": { + "quote": { + "total_fcu": 2, + "fixed_fcu": 0, + "variable_fcu": 2, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "price", + "model": "black_scholes_merton/1", + "billing_unit": "contract", + "quantity": 2, + "fcu_per_unit": 1, + "variable_fcu": 2 + } + ] + }, + "resolved_models": [ + { + "model": "black_scholes_merton/1", + "quantity": 2 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000002", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 100000, + "budget_period": "utc_calendar_month" + } + }, + "expected_cu": 2, + "status": 200 + }, + { + "name": "scenario-bsm-grid", + "path": "/v1/scenarios/evaluate", + "request": { + "contract": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "scenarios": [ + { + "spot": 95.0, + "volatility": 0.15 + }, + { + "spot": 95.0, + "volatility": 0.2 + }, + { + "spot": 95.0, + "volatility": 0.25 + }, + { + "spot": 100.0, + "volatility": 0.15 + }, + { + "spot": 100.0, + "volatility": 0.2 + }, + { + "spot": 100.0, + "volatility": 0.25 + }, + { + "spot": 105.0, + "volatility": 0.15 + }, + { + "spot": 105.0, + "volatility": 0.2 + }, + { + "spot": 105.0, + "volatility": 0.25 + } + ], + "position": { + "quantity": 1.0, + "multiplier": 100.0 + } + }, + "response": { + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000005", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 10, + "compute_schedule_version": "2026-09-11", + "fcu_quoted": 10, + "fcu_charged": 10, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99988, + "budget_period": "utc_calendar_month" + }, + "base": { + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "result": { + "price": 10.45058357218557, + "exercise_style": "european", + "option_type": "call", + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1" + } + }, + "position": { + "quantity": 1.0, + "multiplier": 100.0 + }, + "results": [ + { + "index": 0, + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 95.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.15 + }, + "status": "success", + "result": { + "price": 5.622017065205754, + "exercise_style": "european", + "option_type": "call", + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1" + }, + "price_change": -4.828566506979816, + "position_pnl": -482.8566506979816 + }, + { + "index": 1, + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 95.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "status": "success", + "result": { + "price": 7.5108721783527015, + "exercise_style": "european", + "option_type": "call", + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1" + }, + "price_change": -2.9397113938328685, + "position_pnl": -293.97113938328687 + }, + { + "index": 2, + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 95.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.25 + }, + "status": "success", + "result": { + "price": 9.395032308625938, + "exercise_style": "european", + "option_type": "call", + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1" + }, + "price_change": -1.0555512635596322, + "position_pnl": -105.55512635596322 + }, + { + "index": 3, + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.15 + }, + "status": "success", + "result": { + "price": 8.59165831208917, + "exercise_style": "european", + "option_type": "call", + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1" + }, + "price_change": -1.8589252600964006, + "position_pnl": -185.89252600964005 + }, + { + "index": 4, + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "status": "success", + "result": { + "price": 10.45058357218557, + "exercise_style": "european", + "option_type": "call", + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1" + }, + "price_change": 0.0, + "position_pnl": 0.0 + }, + { + "index": 5, + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.25 + }, + "status": "success", + "result": { + "price": 12.335998930368723, + "exercise_style": "european", + "option_type": "call", + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1" + }, + "price_change": 1.8854153581831525, + "position_pnl": 188.54153581831525 + }, + { + "index": 6, + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 105.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.15 + }, + "status": "success", + "result": { + "price": 12.169589961941401, + "exercise_style": "european", + "option_type": "call", + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1" + }, + "price_change": 1.7190063897558314, + "position_pnl": 171.90063897558315 + }, + { + "index": 7, + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 105.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "status": "success", + "result": { + "price": 13.85790626707311, + "exercise_style": "european", + "option_type": "call", + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1" + }, + "price_change": 3.40732269488754, + "position_pnl": 340.732269488754 + }, + { + "index": 8, + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 105.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.25 + }, + "status": "success", + "result": { + "price": 15.654719726823581, + "exercise_style": "european", + "option_type": "call", + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1" + }, + "price_change": 5.204136154638011, + "position_pnl": 520.4136154638011 + } + ] + }, + "quote": { + "quote": { + "total_fcu": 10, + "fixed_fcu": 0, + "variable_fcu": 10, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "price", + "model": "black_scholes_merton/1", + "billing_unit": "contract", + "quantity": 10, + "fcu_per_unit": 1, + "variable_fcu": 10 + } + ] + }, + "resolved_models": [ + { + "model": "black_scholes_merton/1", + "quantity": 10 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000004", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99998, + "budget_period": "utc_calendar_month" + } + }, + "expected_cu": 10, + "status": 200 + }, + { + "name": "scenario-bsm-partial", + "path": "/v1/scenarios/evaluate", + "request": { + "contract": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "scenarios": [ + { + "spot_change": 5.0 + }, + { + "volatility": -1.0 + }, + { + "spot_change": 5.0 + } + ], + "position": { + "quantity": 1.0, + "multiplier": 100.0 + } + }, + "response": { + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000007", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 4, + "compute_schedule_version": "2026-09-11", + "fcu_quoted": 4, + "fcu_charged": 4, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99984, + "budget_period": "utc_calendar_month" + }, + "base": { + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "result": { + "price": 10.45058357218557, + "exercise_style": "european", + "option_type": "call", + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1" + } + }, + "position": { + "quantity": 1.0, + "multiplier": 100.0 + }, + "results": [ + { + "index": 0, + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 105.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "status": "success", + "result": { + "price": 13.85790626707311, + "exercise_style": "european", + "option_type": "call", + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1" + }, + "price_change": 3.40732269488754, + "position_pnl": 340.732269488754 + }, + { + "index": 1, + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": -1.0 + }, + "status": "error", + "error": { + "code": "invalid_field", + "message": "invalid input: volatility = -1", + "field": "volatility" + } + }, + { + "index": 2, + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 105.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "status": "success", + "result": { + "price": 13.85790626707311, + "exercise_style": "european", + "option_type": "call", + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1" + }, + "price_change": 3.40732269488754, + "position_pnl": 340.732269488754 + } + ] + }, + "quote": { + "quote": { + "total_fcu": 4, + "fixed_fcu": 0, + "variable_fcu": 4, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "price", + "model": "black_scholes_merton/1", + "billing_unit": "contract", + "quantity": 4, + "fcu_per_unit": 1, + "variable_fcu": 4 + } + ] + }, + "resolved_models": [ + { + "model": "black_scholes_merton/1", + "quantity": 4 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000006", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99988, + "budget_period": "utc_calendar_month" + } + }, + "expected_cu": 4, + "status": 200 + }, + { + "name": "solve-bsm-price", + "path": "/v1/scenarios/solve-spot", + "request": { + "contract": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 15.0 + }, + "min_spot": 50.0, + "max_spot": 150.0 + }, + "response": { + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000009", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 4775, + "compute_schedule_version": "2026-09-11", + "fcu_quoted": 100, + "fcu_charged": 100, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99884, + "budget_period": "utc_calendar_month" + }, + "result": { + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1", + "base": { + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "result": { + "price": 10.45058357218557, + "exercise_style": "european", + "option_type": "call", + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1" + } + }, + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 15.0 + }, + "min_spot": 50.0, + "max_spot": 150.0, + "spot_tolerance": 1e-09, + "value_tolerance": 1e-06, + "profile": { + "id": "spot_target/1", + "scan_intervals": 32, + "max_refinements": 64, + "max_price_evaluations": 4775 + }, + "reference_net_value": 0.0, + "boundaries": [ + { + "kind": "point", + "spot": 106.55208796524676, + "value": 15.000000000254582, + "crossing": true, + "refinements": 32, + "bracket_width": 7.275957614183426e-10 + } + ], + "regions": [ + { + "min_spot": 50.0, + "max_spot": 106.55208796524676, + "sign": "below_target" + }, + { + "min_spot": 106.55208796524676, + "max_spot": 150.0, + "sign": "above_target" + } + ], + "coverage": { + "status": "resolved" + }, + "price_evaluations": 67 + } + }, + "quote": { + "quote": { + "total_fcu": 100, + "fixed_fcu": 0, + "variable_fcu": 100, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "black_scholes_merton/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 100, + "variable_fcu": 100 + } + ] + }, + "resolved_models": [ + { + "model": "black_scholes_merton/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000008", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99984, + "budget_period": "utc_calendar_month" + } + }, + "expected_cu": 100, + "status": 200 + }, + { + "name": "solve-bsm-pnl", + "path": "/v1/scenarios/solve-spot", + "request": { + "contract": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "position_pnl", + "value": 500.0, + "position": { + "quantity": 2.0, + "multiplier": 100.0 + } + }, + "min_spot": 50.0, + "max_spot": 150.0 + }, + "response": { + "metadata": { + "request_id": "fr-18e50409e8d4af06-000000000000000b", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 4775, + "compute_schedule_version": "2026-09-11", + "fcu_quoted": 100, + "fcu_charged": 100, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99784, + "budget_period": "utc_calendar_month" + }, + "result": { + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1", + "base": { + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "result": { + "price": 10.45058357218557, + "exercise_style": "european", + "option_type": "call", + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1" + } + }, + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "position_pnl", + "value": 500.0, + "position": { + "quantity": 2.0, + "multiplier": 100.0 + } + }, + "min_spot": 50.0, + "max_spot": 150.0, + "spot_tolerance": 1e-09, + "value_tolerance": 1e-06, + "profile": { + "id": "spot_target/1", + "scan_intervals": 32, + "max_refinements": 64, + "max_price_evaluations": 4775 + }, + "reference_net_value": 2090.116714437114, + "boundaries": [ + { + "kind": "point", + "spot": 103.72824138539727, + "value": 499.9999999804477, + "crossing": true, + "refinements": 32, + "bracket_width": 7.275957614183426e-10 + } + ], + "regions": [ + { + "min_spot": 50.0, + "max_spot": 103.72824138539727, + "sign": "below_target" + }, + { + "min_spot": 103.72824138539727, + "max_spot": 150.0, + "sign": "above_target" + } + ], + "coverage": { + "status": "resolved" + }, + "price_evaluations": 67 + } + }, + "quote": { + "quote": { + "total_fcu": 100, + "fixed_fcu": 0, + "variable_fcu": 100, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "black_scholes_merton/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 100, + "variable_fcu": 100 + } + ] + }, + "resolved_models": [ + { + "model": "black_scholes_merton/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-000000000000000a", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99884, + "budget_period": "utc_calendar_month" + } + }, + "expected_cu": 100, + "status": 200 + }, + { + "name": "solve-bsm-no-root", + "path": "/v1/scenarios/solve-spot", + "request": { + "contract": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 1000.0 + }, + "min_spot": 50.0, + "max_spot": 150.0 + }, + "response": { + "metadata": { + "request_id": "fr-18e50409e8d4af06-000000000000000d", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 4775, + "compute_schedule_version": "2026-09-11", + "fcu_quoted": 100, + "fcu_charged": 100, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99684, + "budget_period": "utc_calendar_month" + }, + "result": { + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1", + "base": { + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "result": { + "price": 10.45058357218557, + "exercise_style": "european", + "option_type": "call", + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1" + } + }, + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 1000.0 + }, + "min_spot": 50.0, + "max_spot": 150.0, + "spot_tolerance": 1e-09, + "value_tolerance": 1e-06, + "profile": { + "id": "spot_target/1", + "scan_intervals": 32, + "max_refinements": 64, + "max_price_evaluations": 4775 + }, + "reference_net_value": 0.0, + "boundaries": [], + "regions": [ + { + "min_spot": 50.0, + "max_spot": 150.0, + "sign": "below_target" + } + ], + "coverage": { + "status": "resolved" + }, + "price_evaluations": 35 + } + }, + "quote": { + "quote": { + "total_fcu": 100, + "fixed_fcu": 0, + "variable_fcu": 100, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "black_scholes_merton/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 100, + "variable_fcu": 100 + } + ] + }, + "resolved_models": [ + { + "model": "black_scholes_merton/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-000000000000000c", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99784, + "budget_period": "utc_calendar_month" + } + }, + "expected_cu": 100, + "status": 200 + }, + { + "name": "solve-bsm-interval", + "path": "/v1/scenarios/solve-spot", + "request": { + "contract": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 0.0 + }, + "min_spot": 50.0, + "max_spot": 150.0, + "elapsed_time_years": 1.0 + }, + "response": { + "metadata": { + "request_id": "fr-18e50409e8d4af06-000000000000000f", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 4775, + "compute_schedule_version": "2026-09-11", + "fcu_quoted": 100, + "fcu_charged": 100, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99584, + "budget_period": "utc_calendar_month" + }, + "result": { + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1", + "base": { + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "result": { + "price": 10.45058357218557, + "exercise_style": "european", + "option_type": "call", + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1" + } + }, + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 0.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 0.0 + }, + "min_spot": 50.0, + "max_spot": 150.0, + "spot_tolerance": 1e-09, + "value_tolerance": 1e-06, + "profile": { + "id": "spot_target/1", + "scan_intervals": 32, + "max_refinements": 64, + "max_price_evaluations": 4775 + }, + "reference_net_value": 0.0, + "boundaries": [ + { + "kind": "interval", + "min_spot": 50.0, + "max_spot": 100.00000099971658, + "value": 0.0 + } + ], + "regions": [ + { + "min_spot": 50.0, + "max_spot": 100.00000099971658, + "sign": "at_target" + }, + { + "min_spot": 100.00000099971658, + "max_spot": 150.0, + "sign": "above_target" + } + ], + "coverage": { + "status": "resolved" + }, + "price_evaluations": 85 + } + }, + "quote": { + "quote": { + "total_fcu": 100, + "fixed_fcu": 0, + "variable_fcu": 100, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "black_scholes_merton/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 100, + "variable_fcu": 100 + } + ] + }, + "resolved_models": [ + { + "model": "black_scholes_merton/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-000000000000000e", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99684, + "budget_period": "utc_calendar_month" + } + }, + "expected_cu": 100, + "status": 200 + }, + { + "name": "solve-bsm-unresolved", + "path": "/v1/scenarios/solve-spot", + "request": { + "contract": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 15.0 + }, + "min_spot": 50.0, + "max_spot": 150.0, + "spot_tolerance": 1000.0, + "value_tolerance": 1e-15 + }, + "response": { + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000011", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 4775, + "compute_schedule_version": "2026-09-11", + "fcu_quoted": 100, + "fcu_charged": 100, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99484, + "budget_period": "utc_calendar_month" + }, + "result": { + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1", + "base": { + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "result": { + "price": 10.45058357218557, + "exercise_style": "european", + "option_type": "call", + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1" + } + }, + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 15.0 + }, + "min_spot": 50.0, + "max_spot": 150.0, + "spot_tolerance": 1000.0, + "value_tolerance": 1e-15, + "profile": { + "id": "spot_target/1", + "scan_intervals": 32, + "max_refinements": 64, + "max_price_evaluations": 4775 + }, + "reference_net_value": 0.0, + "boundaries": [], + "regions": [ + { + "min_spot": 50.0, + "max_spot": 106.25, + "sign": "below_target" + }, + { + "min_spot": 109.375, + "max_spot": 150.0, + "sign": "above_target" + } + ], + "coverage": { + "status": "unresolved", + "cells": [ + { + "lower_spot": 106.25, + "upper_spot": 109.375, + "best_spot": 106.25, + "best_abs_value_error": 0.22518944410185782, + "reason": "spot_tolerance_reached" + } + ] + }, + "price_evaluations": 35 + } + }, + "quote": { + "quote": { + "total_fcu": 100, + "fixed_fcu": 0, + "variable_fcu": 100, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "black_scholes_merton/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 100, + "variable_fcu": 100 + } + ] + }, + "resolved_models": [ + { + "model": "black_scholes_merton/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000010", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99584, + "budget_period": "utc_calendar_month" + } + }, + "expected_cu": 100, + "status": 200 + }, + { + "name": "solve-bsm-numerical-error", + "path": "/v1/scenarios/solve-spot", + "request": { + "contract": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": -1.0 + }, + "target": { + "kind": "option_price", + "value": 15.0 + }, + "min_spot": 50.0, + "max_spot": 150.0 + }, + "response": { + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000012", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 0, + "compute_schedule_version": "2026-09-11", + "fcu_quoted": 100, + "fcu_charged": 100, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99384, + "budget_period": "utc_calendar_month" + }, + "error": { + "code": "invalid_field", + "message": "invalid input: volatility = -1", + "field": "volatility" + } + }, + "quote": null, + "expected_cu": 100, + "status": 422 + }, + { + "name": "solve-bsm-invalid", + "path": "/v1/scenarios/solve-spot", + "request": { + "contract": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 15.0 + }, + "min_spot": 0.0, + "max_spot": 150.0 + }, + "response": { + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000013", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 0, + "compute_schedule_version": "2026-09-11", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99384, + "budget_period": "utc_calendar_month" + }, + "error": { + "code": "invalid_field", + "message": "finite bounds must satisfy 0 < min_spot < max_spot", + "field": "min_spot" + } + }, + "quote": null, + "expected_cu": 0, + "status": 422 + }, + { + "name": "scenario-bs1993-point", + "path": "/v1/scenarios/evaluate", + "request": { + "contract": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "scenarios": [ + { + "spot_change": 5.0 + } + ], + "position": { + "quantity": 1.0, + "multiplier": 100.0 + } + }, + "response": { + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000015", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 100, + "compute_schedule_version": "2026-09-11", + "fcu_quoted": 4, + "fcu_charged": 4, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99380, + "budget_period": "utc_calendar_month" + }, + "base": { + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "result": { + "price": 10.45058357218557, + "exercise_style": "american", + "option_type": "call", + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1" + } + }, + "position": { + "quantity": 1.0, + "multiplier": 100.0 + }, + "results": [ + { + "index": 0, + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 105.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "status": "success", + "result": { + "price": 13.85790626707311, + "exercise_style": "american", + "option_type": "call", + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1" + }, + "price_change": 3.40732269488754, + "position_pnl": 340.732269488754 + } + ] + }, + "quote": { + "quote": { + "total_fcu": 4, + "fixed_fcu": 0, + "variable_fcu": 4, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "price", + "model": "bjerksund_stensland_1993/1", + "billing_unit": "contract", + "quantity": 2, + "fcu_per_unit": 2, + "variable_fcu": 4 + } + ] + }, + "resolved_models": [ + { + "model": "bjerksund_stensland_1993/1", + "quantity": 2 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000014", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99384, + "budget_period": "utc_calendar_month" + } + }, + "expected_cu": 4, + "status": 200 + }, + { + "name": "scenario-bs1993-grid", + "path": "/v1/scenarios/evaluate", + "request": { + "contract": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "scenarios": [ + { + "spot": 95.0, + "volatility": 0.15 + }, + { + "spot": 95.0, + "volatility": 0.2 + }, + { + "spot": 95.0, + "volatility": 0.25 + }, + { + "spot": 100.0, + "volatility": 0.15 + }, + { + "spot": 100.0, + "volatility": 0.2 + }, + { + "spot": 100.0, + "volatility": 0.25 + }, + { + "spot": 105.0, + "volatility": 0.15 + }, + { + "spot": 105.0, + "volatility": 0.2 + }, + { + "spot": 105.0, + "volatility": 0.25 + } + ], + "position": { + "quantity": 1.0, + "multiplier": 100.0 + } + }, + "response": { + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000017", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 500, + "compute_schedule_version": "2026-09-11", + "fcu_quoted": 20, + "fcu_charged": 20, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99360, + "budget_period": "utc_calendar_month" + }, + "base": { + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "result": { + "price": 10.45058357218557, + "exercise_style": "american", + "option_type": "call", + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1" + } + }, + "position": { + "quantity": 1.0, + "multiplier": 100.0 + }, + "results": [ + { + "index": 0, + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 95.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.15 + }, + "status": "success", + "result": { + "price": 5.622017065205754, + "exercise_style": "american", + "option_type": "call", + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1" + }, + "price_change": -4.828566506979816, + "position_pnl": -482.8566506979816 + }, + { + "index": 1, + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 95.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "status": "success", + "result": { + "price": 7.5108721783527015, + "exercise_style": "american", + "option_type": "call", + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1" + }, + "price_change": -2.9397113938328685, + "position_pnl": -293.97113938328687 + }, + { + "index": 2, + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 95.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.25 + }, + "status": "success", + "result": { + "price": 9.395032308625938, + "exercise_style": "american", + "option_type": "call", + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1" + }, + "price_change": -1.0555512635596322, + "position_pnl": -105.55512635596322 + }, + { + "index": 3, + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.15 + }, + "status": "success", + "result": { + "price": 8.59165831208917, + "exercise_style": "american", + "option_type": "call", + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1" + }, + "price_change": -1.8589252600964006, + "position_pnl": -185.89252600964005 + }, + { + "index": 4, + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "status": "success", + "result": { + "price": 10.45058357218557, + "exercise_style": "american", + "option_type": "call", + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1" + }, + "price_change": 0.0, + "position_pnl": 0.0 + }, + { + "index": 5, + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.25 + }, + "status": "success", + "result": { + "price": 12.335998930368723, + "exercise_style": "american", + "option_type": "call", + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1" + }, + "price_change": 1.8854153581831525, + "position_pnl": 188.54153581831525 + }, + { + "index": 6, + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 105.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.15 + }, + "status": "success", + "result": { + "price": 12.169589961941401, + "exercise_style": "american", + "option_type": "call", + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1" + }, + "price_change": 1.7190063897558314, + "position_pnl": 171.90063897558315 + }, + { + "index": 7, + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 105.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "status": "success", + "result": { + "price": 13.85790626707311, + "exercise_style": "american", + "option_type": "call", + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1" + }, + "price_change": 3.40732269488754, + "position_pnl": 340.732269488754 + }, + { + "index": 8, + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 105.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.25 + }, + "status": "success", + "result": { + "price": 15.654719726823581, + "exercise_style": "american", + "option_type": "call", + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1" + }, + "price_change": 5.204136154638011, + "position_pnl": 520.4136154638011 + } + ] + }, + "quote": { + "quote": { + "total_fcu": 20, + "fixed_fcu": 0, + "variable_fcu": 20, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "price", + "model": "bjerksund_stensland_1993/1", + "billing_unit": "contract", + "quantity": 10, + "fcu_per_unit": 2, + "variable_fcu": 20 + } + ] + }, + "resolved_models": [ + { + "model": "bjerksund_stensland_1993/1", + "quantity": 10 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000016", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99380, + "budget_period": "utc_calendar_month" + } + }, + "expected_cu": 20, + "status": 200 + }, + { + "name": "scenario-bs1993-partial", + "path": "/v1/scenarios/evaluate", + "request": { + "contract": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "scenarios": [ + { + "spot_change": 5.0 + }, + { + "volatility": -1.0 + }, + { + "spot_change": 5.0 + } + ], + "position": { + "quantity": 1.0, + "multiplier": 100.0 + } + }, + "response": { + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000019", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 200, + "compute_schedule_version": "2026-09-11", + "fcu_quoted": 8, + "fcu_charged": 8, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99352, + "budget_period": "utc_calendar_month" + }, + "base": { + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "result": { + "price": 10.45058357218557, + "exercise_style": "american", + "option_type": "call", + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1" + } + }, + "position": { + "quantity": 1.0, + "multiplier": 100.0 + }, + "results": [ + { + "index": 0, + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 105.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "status": "success", + "result": { + "price": 13.85790626707311, + "exercise_style": "american", + "option_type": "call", + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1" + }, + "price_change": 3.40732269488754, + "position_pnl": 340.732269488754 + }, + { + "index": 1, + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": -1.0 + }, + "status": "error", + "error": { + "code": "invalid_field", + "message": "invalid input: volatility = -1", + "field": "volatility" + } + }, + { + "index": 2, + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 105.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "status": "success", + "result": { + "price": 13.85790626707311, + "exercise_style": "american", + "option_type": "call", + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1" + }, + "price_change": 3.40732269488754, + "position_pnl": 340.732269488754 + } + ] + }, + "quote": { + "quote": { + "total_fcu": 8, + "fixed_fcu": 0, + "variable_fcu": 8, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "price", + "model": "bjerksund_stensland_1993/1", + "billing_unit": "contract", + "quantity": 4, + "fcu_per_unit": 2, + "variable_fcu": 8 + } + ] + }, + "resolved_models": [ + { + "model": "bjerksund_stensland_1993/1", + "quantity": 4 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000018", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99360, + "budget_period": "utc_calendar_month" + } + }, + "expected_cu": 8, + "status": 200 + }, + { + "name": "solve-bs1993-price", + "path": "/v1/scenarios/solve-spot", + "request": { + "contract": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 15.0 + }, + "min_spot": 50.0, + "max_spot": 150.0 + }, + "response": { + "metadata": { + "request_id": "fr-18e50409e8d4af06-000000000000001b", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 238750, + "compute_schedule_version": "2026-09-11", + "fcu_quoted": 200, + "fcu_charged": 200, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99152, + "budget_period": "utc_calendar_month" + }, + "result": { + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1", + "base": { + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "result": { + "price": 10.45058357218557, + "exercise_style": "american", + "option_type": "call", + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1" + } + }, + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 15.0 + }, + "min_spot": 50.0, + "max_spot": 150.0, + "spot_tolerance": 1e-09, + "value_tolerance": 1e-06, + "profile": { + "id": "spot_target/1", + "scan_intervals": 32, + "max_refinements": 64, + "max_price_evaluations": 4775 + }, + "reference_net_value": 0.0, + "boundaries": [ + { + "kind": "point", + "spot": 106.55208796524676, + "value": 15.000000000254582, + "crossing": true, + "refinements": 32, + "bracket_width": 7.275957614183426e-10 + } + ], + "regions": [ + { + "min_spot": 50.0, + "max_spot": 106.55208796524676, + "sign": "below_target" + }, + { + "min_spot": 106.55208796524676, + "max_spot": 150.0, + "sign": "above_target" + } + ], + "coverage": { + "status": "resolved" + }, + "price_evaluations": 67 + } + }, + "quote": { + "quote": { + "total_fcu": 200, + "fixed_fcu": 0, + "variable_fcu": 200, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "bjerksund_stensland_1993/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 200, + "variable_fcu": 200 + } + ] + }, + "resolved_models": [ + { + "model": "bjerksund_stensland_1993/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-000000000000001a", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99352, + "budget_period": "utc_calendar_month" + } + }, + "expected_cu": 200, + "status": 200 + }, + { + "name": "solve-bs1993-pnl", + "path": "/v1/scenarios/solve-spot", + "request": { + "contract": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "position_pnl", + "value": 500.0, + "position": { + "quantity": 2.0, + "multiplier": 100.0 + } + }, + "min_spot": 50.0, + "max_spot": 150.0 + }, + "response": { + "metadata": { + "request_id": "fr-18e50409e8d4af06-000000000000001d", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 238750, + "compute_schedule_version": "2026-09-11", + "fcu_quoted": 200, + "fcu_charged": 200, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 98952, + "budget_period": "utc_calendar_month" + }, + "result": { + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1", + "base": { + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "result": { + "price": 10.45058357218557, + "exercise_style": "american", + "option_type": "call", + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1" + } + }, + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "position_pnl", + "value": 500.0, + "position": { + "quantity": 2.0, + "multiplier": 100.0 + } + }, + "min_spot": 50.0, + "max_spot": 150.0, + "spot_tolerance": 1e-09, + "value_tolerance": 1e-06, + "profile": { + "id": "spot_target/1", + "scan_intervals": 32, + "max_refinements": 64, + "max_price_evaluations": 4775 + }, + "reference_net_value": 2090.116714437114, + "boundaries": [ + { + "kind": "point", + "spot": 103.72824138539727, + "value": 499.9999999804477, + "crossing": true, + "refinements": 32, + "bracket_width": 7.275957614183426e-10 + } + ], + "regions": [ + { + "min_spot": 50.0, + "max_spot": 103.72824138539727, + "sign": "below_target" + }, + { + "min_spot": 103.72824138539727, + "max_spot": 150.0, + "sign": "above_target" + } + ], + "coverage": { + "status": "resolved" + }, + "price_evaluations": 67 + } + }, + "quote": { + "quote": { + "total_fcu": 200, + "fixed_fcu": 0, + "variable_fcu": 200, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "bjerksund_stensland_1993/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 200, + "variable_fcu": 200 + } + ] + }, + "resolved_models": [ + { + "model": "bjerksund_stensland_1993/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-000000000000001c", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99152, + "budget_period": "utc_calendar_month" + } + }, + "expected_cu": 200, + "status": 200 + }, + { + "name": "solve-bs1993-no-root", + "path": "/v1/scenarios/solve-spot", + "request": { + "contract": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 1000.0 + }, + "min_spot": 50.0, + "max_spot": 150.0 + }, + "response": { + "metadata": { + "request_id": "fr-18e50409e8d4af06-000000000000001f", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 238750, + "compute_schedule_version": "2026-09-11", + "fcu_quoted": 200, + "fcu_charged": 200, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 98752, + "budget_period": "utc_calendar_month" + }, + "result": { + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1", + "base": { + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "result": { + "price": 10.45058357218557, + "exercise_style": "american", + "option_type": "call", + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1" + } + }, + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 1000.0 + }, + "min_spot": 50.0, + "max_spot": 150.0, + "spot_tolerance": 1e-09, + "value_tolerance": 1e-06, + "profile": { + "id": "spot_target/1", + "scan_intervals": 32, + "max_refinements": 64, + "max_price_evaluations": 4775 + }, + "reference_net_value": 0.0, + "boundaries": [], + "regions": [ + { + "min_spot": 50.0, + "max_spot": 150.0, + "sign": "below_target" + } + ], + "coverage": { + "status": "resolved" + }, + "price_evaluations": 35 + } + }, + "quote": { + "quote": { + "total_fcu": 200, + "fixed_fcu": 0, + "variable_fcu": 200, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "bjerksund_stensland_1993/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 200, + "variable_fcu": 200 + } + ] + }, + "resolved_models": [ + { + "model": "bjerksund_stensland_1993/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-000000000000001e", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 98952, + "budget_period": "utc_calendar_month" + } + }, + "expected_cu": 200, + "status": 200 + }, + { + "name": "solve-bs1993-interval", + "path": "/v1/scenarios/solve-spot", + "request": { + "contract": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 0.0 + }, + "min_spot": 50.0, + "max_spot": 150.0, + "elapsed_time_years": 1.0 + }, + "response": { + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000021", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 238750, + "compute_schedule_version": "2026-09-11", + "fcu_quoted": 200, + "fcu_charged": 200, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 98552, + "budget_period": "utc_calendar_month" + }, + "result": { + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1", + "base": { + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "result": { + "price": 10.45058357218557, + "exercise_style": "american", + "option_type": "call", + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1" + } + }, + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 0.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 0.0 + }, + "min_spot": 50.0, + "max_spot": 150.0, + "spot_tolerance": 1e-09, + "value_tolerance": 1e-06, + "profile": { + "id": "spot_target/1", + "scan_intervals": 32, + "max_refinements": 64, + "max_price_evaluations": 4775 + }, + "reference_net_value": 0.0, + "boundaries": [ + { + "kind": "interval", + "min_spot": 50.0, + "max_spot": 100.00000099971658, + "value": 0.0 + } + ], + "regions": [ + { + "min_spot": 50.0, + "max_spot": 100.00000099971658, + "sign": "at_target" + }, + { + "min_spot": 100.00000099971658, + "max_spot": 150.0, + "sign": "above_target" + } + ], + "coverage": { + "status": "resolved" + }, + "price_evaluations": 85 + } + }, + "quote": { + "quote": { + "total_fcu": 200, + "fixed_fcu": 0, + "variable_fcu": 200, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "bjerksund_stensland_1993/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 200, + "variable_fcu": 200 + } + ] + }, + "resolved_models": [ + { + "model": "bjerksund_stensland_1993/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000020", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 98752, + "budget_period": "utc_calendar_month" + } + }, + "expected_cu": 200, + "status": 200 + }, + { + "name": "solve-bs1993-unresolved", + "path": "/v1/scenarios/solve-spot", + "request": { + "contract": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 15.0 + }, + "min_spot": 50.0, + "max_spot": 150.0, + "spot_tolerance": 1000.0, + "value_tolerance": 1e-15 + }, + "response": { + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000023", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 238750, + "compute_schedule_version": "2026-09-11", + "fcu_quoted": 200, + "fcu_charged": 200, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 98352, + "budget_period": "utc_calendar_month" + }, + "result": { + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1", + "base": { + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "result": { + "price": 10.45058357218557, + "exercise_style": "american", + "option_type": "call", + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1" + } + }, + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 15.0 + }, + "min_spot": 50.0, + "max_spot": 150.0, + "spot_tolerance": 1000.0, + "value_tolerance": 1e-15, + "profile": { + "id": "spot_target/1", + "scan_intervals": 32, + "max_refinements": 64, + "max_price_evaluations": 4775 + }, + "reference_net_value": 0.0, + "boundaries": [], + "regions": [ + { + "min_spot": 50.0, + "max_spot": 106.25, + "sign": "below_target" + }, + { + "min_spot": 109.375, + "max_spot": 150.0, + "sign": "above_target" + } + ], + "coverage": { + "status": "unresolved", + "cells": [ + { + "lower_spot": 106.25, + "upper_spot": 109.375, + "best_spot": 106.25, + "best_abs_value_error": 0.22518944410185782, + "reason": "spot_tolerance_reached" + } + ] + }, + "price_evaluations": 35 + } + }, + "quote": { + "quote": { + "total_fcu": 200, + "fixed_fcu": 0, + "variable_fcu": 200, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "bjerksund_stensland_1993/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 200, + "variable_fcu": 200 + } + ] + }, + "resolved_models": [ + { + "model": "bjerksund_stensland_1993/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000022", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 98552, + "budget_period": "utc_calendar_month" + } + }, + "expected_cu": 200, + "status": 200 + }, + { + "name": "solve-bs1993-numerical-error", + "path": "/v1/scenarios/solve-spot", + "request": { + "contract": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": -1.0 + }, + "target": { + "kind": "option_price", + "value": 15.0 + }, + "min_spot": 50.0, + "max_spot": 150.0 + }, + "response": { + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000024", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 0, + "compute_schedule_version": "2026-09-11", + "fcu_quoted": 200, + "fcu_charged": 200, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 98152, + "budget_period": "utc_calendar_month" + }, + "error": { + "code": "invalid_field", + "message": "invalid input: volatility = -1", + "field": "volatility" + } + }, + "quote": null, + "expected_cu": 200, + "status": 422 + }, + { + "name": "solve-bs1993-invalid", + "path": "/v1/scenarios/solve-spot", + "request": { + "contract": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 15.0 + }, + "min_spot": 0.0, + "max_spot": 150.0 + }, + "response": { + "metadata": { + "request_id": "fr-18e50409e8d4af06-0000000000000025", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 0, + "compute_schedule_version": "2026-09-11", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 98152, + "budget_period": "utc_calendar_month" + }, + "error": { + "code": "invalid_field", + "message": "finite bounds must satisfy 0 < min_spot < max_spot", + "field": "min_spot" + } + }, + "quote": null, + "expected_cu": 0, + "status": 422 + } + ] +} diff --git a/tests/test_analytics.py b/tests/test_analytics.py index 0492ec3..c83fab3 100644 --- a/tests/test_analytics.py +++ b/tests/test_analytics.py @@ -43,8 +43,8 @@ def test_analytics_contract_and_native_identity(analytics_cases): schema = json.loads((ROOT / "contracts/openapi.json").read_text()) fixture = json.loads((ROOT / "tests/fixtures/analytics.json").read_text()) pin = json.loads((ROOT / "contracts/provenance.json").read_text()) - assert fixture["source_commit"] == pin["source_commit"] - assert fixture["contract_sha256"] == pin["sha256"] + assert fixture["validation"]["source_commit"] == pin["source_commit"] + assert fixture["validation"]["contract_sha256"] == pin["sha256"] assert schema["paths"]["/v1/analytics"]["post"]["operationId"] == "fusedAnalytics" for model, name in [ (AnalyticsRequest, "FusedAnalyticsRequest"), diff --git a/tests/test_batch.py b/tests/test_batch.py index 31a694d..e4cda2b 100644 --- a/tests/test_batch.py +++ b/tests/test_batch.py @@ -56,8 +56,8 @@ def test_batch_contract_identity_and_native_fixtures(batch_cases): schema = json.loads((ROOT / "contracts/openapi.json").read_text()) fixture = json.loads((ROOT / "tests/fixtures/batch.json").read_text()) pin = json.loads((ROOT / "contracts/provenance.json").read_text()) - assert fixture["source_commit"] == pin["source_commit"] - assert fixture["contract_sha256"] == pin["sha256"] + assert fixture["validation"]["source_commit"] == pin["source_commit"] + assert fixture["validation"]["contract_sha256"] == pin["sha256"] for case, (_, request, response, wire) in zip(batch_cases, OPERATIONS, strict=False): for name, value in [ (wire + "Request", request(**case["request"]).to_wire()), diff --git a/tests/test_examples.py b/tests/test_examples.py index eb2feb8..881f5cf 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -152,6 +152,8 @@ def test_generated_notebook_executes_without_source_tree( iv_cases, analytics_cases, batch_cases, + scenario_cases, + scenario_quote_cases, tmp_path, filename, ): @@ -189,7 +191,14 @@ def test_generated_notebook_executes_without_source_tree( ) by_name = { case["name"]: case - for case in cases + regime_cases + greeks_cases + iv_cases + analytics_cases + batch_cases + for case in cases + + regime_cases + + greeks_cases + + iv_cases + + analytics_cases + + batch_cases + + scenario_cases + + scenario_quote_cases } assert [item[2] for item in received] == [ by_name[name]["request"] for name in NOTEBOOK_CASES[filename] diff --git a/tests/test_greeks.py b/tests/test_greeks.py index 5510983..c6f62f6 100644 --- a/tests/test_greeks.py +++ b/tests/test_greeks.py @@ -44,8 +44,8 @@ def test_greeks_contract_and_native_fixture_identity(greeks_cases): schema = json.loads((ROOT / "contracts/openapi.json").read_text()) fixture = json.loads((ROOT / "tests/fixtures/greeks.json").read_text()) pin = json.loads((ROOT / "contracts/provenance.json").read_text()) - assert fixture["source_commit"] == pin["source_commit"] - assert fixture["contract_sha256"] == pin["sha256"] + assert fixture["validation"]["source_commit"] == pin["source_commit"] + assert fixture["validation"]["contract_sha256"] == pin["sha256"] assert schema["paths"]["/v1/greeks"]["post"]["operationId"] == "greeksAllTen" for model, name in [ (GreeksRequest, "GreeksAllTenRequest"), diff --git a/tests/test_implied_volatility.py b/tests/test_implied_volatility.py index be672f0..1b96343 100644 --- a/tests/test_implied_volatility.py +++ b/tests/test_implied_volatility.py @@ -45,8 +45,8 @@ def test_implied_volatility_contract_and_native_fixture_identity(iv_cases): schema = json.loads((ROOT / "contracts/openapi.json").read_text()) fixture = json.loads((ROOT / "tests/fixtures/implied_volatility.json").read_text()) pin = json.loads((ROOT / "contracts/provenance.json").read_text()) - assert fixture["source_commit"] == pin["source_commit"] - assert fixture["contract_sha256"] == pin["sha256"] + assert fixture["validation"]["source_commit"] == pin["source_commit"] + assert fixture["validation"]["contract_sha256"] == pin["sha256"] assert schema["paths"]["/v1/implied-volatility"]["post"]["operationId"] == "impliedVolatility" for model, name in [ (ImpliedVolatilityRequest, "ImpliedVolatilityRequest"), diff --git a/tests/test_scenario_notebooks.py b/tests/test_scenario_notebooks.py new file mode 100644 index 0000000..ea62c20 --- /dev/null +++ b/tests/test_scenario_notebooks.py @@ -0,0 +1,87 @@ +"""Exercise scenario notebook outcomes and receipt handling through real widget actions.""" + +import copy +import json +import threading +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path + +import pytest +from test_analysis_notebooks import run_workspace + +CASES = { + c["name"]: c + for c in json.loads((Path(__file__).parent / "fixtures/scenarios.json").read_text())["cases"] +} + + +@pytest.mark.parametrize( + "outcome", + ["interval", "no-root", "unresolved", "numerical-error", "invalid-response", "disconnect"], +) +def test_solver_workspace_preserves_outcomes_or_receipts(outcome, tmp_path): + received = [] + case = CASES.get(f"solve-bsm-{outcome}", CASES["solve-bsm-price"]) + payload = copy.deepcopy(case["response"]) + if outcome == "invalid-response": + del payload["result"]["boundaries"][0]["value"] + + class Handler(BaseHTTPRequestHandler): + def do_POST(self): + received.append( + (self.path, json.loads(self.rfile.read(int(self.headers["Content-Length"])))) + ) + if self.path == "/v1/cost/quote": + status, response = 200, CASES["solve-bsm-price"]["quote"] + else: + assert self.path == "/v1/scenarios/solve-spot" + assert self.headers["x-morphiq-max-cu"] == "100" + if outcome == "disconnect": + self.close_connection = True + return + status, response = case["status"], payload + self.send_response(status) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(json.dumps(response).encode()) + + def log_message(self, *_args): + pass + + server = ThreadingHTTPServer(("127.0.0.1", 0), Handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + checks = """ +assert history == [] +quote_button.click() +run_button.click() +assert len(history) == 2 +assert not quote_button.disabled and run_button.disabled +assert not any(control.disabled for control in controls) +""" + if outcome in ("numerical-error", "invalid-response", "disconnect"): + checks += "assert last_response is None\n" + if outcome == "disconnect": + checks += "assert history[-1]['cu_charged'] is None\n" + else: + checks += "assert history[-1]['cu_charged'] == 100\n" + checks += "assert 'No retry' in str(output.outputs)\n" + else: + checks += "assert history[-1]['cu_charged'] == 100\n" + checks += ( + "assert last_response.result.model_dump(exclude_unset=True) == " + f"{payload['result']!r}\n" + ) + if outcome == "unresolved": + checks += "assert 'Unresolved cells' in str(output.outputs)\n" + elif outcome == "no-root": + checks += "assert 'No boundaries found' in str(output.outputs)\n" + run_workspace( + "scenarios/solve-spot.ipynb", checks, f"http://127.0.0.1:{server.server_port}", tmp_path + ) + assert [path for path, _ in received] == ["/v1/cost/quote", "/v1/scenarios/solve-spot"] + finally: + server.shutdown() + server.server_close() + thread.join() diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py new file mode 100644 index 0000000..80bc137 --- /dev/null +++ b/tests/test_scenarios.py @@ -0,0 +1,391 @@ +"""Scenario wire, transport and receipt parity against the pinned native API.""" + +import asyncio +import copy +import json +from pathlib import Path + +import httpx +import pytest +from jsonschema import Draft202012Validator +from pydantic import ValidationError + +from morphiq_sdk import ( + APIError, + AsyncMorphIQ, + MorphIQ, + RateLimitError, + RequestTimeout, + ResponseError, + ScenarioFailure, + ScenarioOptionPriceTarget, + ScenarioPoint, + ScenarioPosition, + ScenarioPositionPnlTarget, + ScenarioPriceInputs, + ScenarioRequest, + ScenarioResponse, + ScenarioSolveInterval, + ScenarioSolvePoint, + ScenarioSolveProfile, + ScenarioSolveRegion, + ScenarioSolveResolved, + ScenarioSolveUnresolved, + ScenarioSolveUnresolvedCell, + ScenarioSpotSolveRequest, + ScenarioSpotSolveResponse, + ScenarioSpotSolveResult, + ScenarioSuccess, + ScenarioValuation, +) + +ROOT = Path(__file__).parents[1] +ARTIFACT = json.loads((ROOT / "tests/fixtures/scenarios.json").read_text()) +CASES = ARTIFACT["cases"] +SUCCESS = [case for case in CASES if case["status"] == 200] +SOLVES = [case for case in SUCCESS if case["name"].startswith("solve-")] +SCHEMA = json.loads((ROOT / "contracts/openapi.json").read_text()) + + +def request_for(case): + model = ScenarioSpotSolveRequest if case["path"].endswith("solve-spot") else ScenarioRequest + return model.model_validate(case["request"]) + + +def method_for(case): + return "solve_spot" if case["path"].endswith("solve-spot") else "evaluate_scenarios" + + +def invoke(mode, handler, action): + transport = httpx.MockTransport(handler) + if mode == "sync": + with MorphIQ(api_key="scenario-test-key", transport=transport) as client: + return action(client) + + async def run(): + async with AsyncMorphIQ(api_key="scenario-test-key", transport=transport) as client: + return await action(client) + + return asyncio.run(run()) + + +@pytest.mark.parametrize("mode", ["sync", "async"]) +@pytest.mark.parametrize("case", SUCCESS, ids=lambda case: case["name"]) +def test_native_response_and_request_parity(mode, case): + calls = [] + request = request_for(case) + + def handle(incoming): + calls.append(incoming) + assert incoming.method == "POST" and incoming.url.path == case["path"] + assert incoming.headers["authorization"] == "Bearer scenario-test-key" + assert incoming.headers["x-morphiq-max-cu"] == str(case["expected_cu"]) + assert json.loads(incoming.content) == case["request"] + return httpx.Response(200, json=case["response"], headers={"x-request-id": "fixture-id"}) + + result = invoke( + mode, handle, lambda c: getattr(c, method_for(case))(request, max_cu=case["expected_cu"]) + ) + assert len(calls) == 1 + assert result.model_dump(exclude_unset=True) == case["response"] + assert json.loads(result.raw_body) == case["response"] + assert result.headers["x-request-id"] == "fixture-id" + assert result.metadata.fcu_charged == case["expected_cu"] + + +@pytest.mark.parametrize("mode", ["sync", "async"]) +@pytest.mark.parametrize("case", SUCCESS, ids=lambda case: case["name"]) +def test_quotes_send_one_free_utility_call_with_the_right_operation(mode, case): + calls = [] + + def handle(incoming): + calls.append(incoming) + assert incoming.url.path == "/v1/cost/quote" + assert "x-morphiq-max-cu" not in incoming.headers + assert json.loads(incoming.content) == { + "operation": "scenario_spot_solve" + if method_for(case) == "solve_spot" + else "scenario_evaluate", + "request": case["request"], + } + return httpx.Response(200, json=case["quote"]) + + result = invoke(mode, handle, lambda c: c.quote(request_for(case))) + assert len(calls) == 1 + assert result.quote.total_fcu == case["expected_cu"] + assert result.metadata.fcu_charged == 0 + if case in SOLVES: + assert result.resolved_models[0].quantity == 1 + + +def test_fixture_and_wire_model_coverage_matches_exact_api_snapshot(): + provenance = json.loads((ROOT / "contracts/provenance.json").read_text()) + assert ARTIFACT["source"]["api_commit"] == provenance["source_commit"] + assert ARTIFACT["source"]["schema_sha256"] == provenance["sha256"] + schemas = SCHEMA["components"]["schemas"] + for model, name in [ + (ScenarioPosition, "ScenarioPosition"), + (ScenarioPoint, "ScenarioPoint"), + (ScenarioRequest, "ScenarioRequest"), + (ScenarioSpotSolveRequest, "ScenarioSpotSolveRequest"), + (ScenarioPriceInputs, "PriceRequest"), + (ScenarioValuation, "ScenarioValuation"), + (ScenarioResponse, "ScenarioResponse"), + (ScenarioSpotSolveResponse, "ScenarioSpotSolveResponse"), + (ScenarioSpotSolveResult, "ScenarioSpotSolveResult"), + (ScenarioSolveProfile, "ScenarioSolveProfile"), + (ScenarioSolveRegion, "ScenarioSolveRegion"), + (ScenarioSolveUnresolvedCell, "ScenarioSolveUnresolvedCell"), + ]: + assert set(model.model_fields) == set(schemas[name]["properties"]) + for models, name in [ + ([ScenarioOptionPriceTarget, ScenarioPositionPnlTarget], "ScenarioSolveTarget"), + ([ScenarioSolvePoint, ScenarioSolveInterval], "ScenarioSolveBoundary"), + ([ScenarioSolveResolved, ScenarioSolveUnresolved], "ScenarioSolveCoverage"), + ([ScenarioSuccess, ScenarioFailure], "ScenarioOutcome"), + ]: + for model, variant in zip(models, schemas[name]["oneOf"], strict=True): + extra = {"index", "inputs"} if name == "ScenarioOutcome" else set() + assert set(model.model_fields) == set(variant["properties"]) | extra + for case in CASES: + solving = method_for(case) == "solve_spot" + pairs = [ + ( + "ScenarioSpotSolveRequest" if solving else "ScenarioRequest", + request_for(case).to_wire(), + ), + ( + ("ScenarioSpotSolveResponse" if solving else "ScenarioResponse") + if case["status"] == 200 + else "ErrorEnvelope", + case["response"], + ), + ] + if case["quote"]: + pairs.append(("CostQuoteResponse", case["quote"])) + for name, value in pairs: + Draft202012Validator( + {"$ref": f"#/components/schemas/{name}", "components": SCHEMA["components"]} + ).validate(value) + + +def test_omission_null_zero_and_nested_list_edits_are_preserved_or_revalidated(): + case = SOLVES[0] + request = request_for(case) + assert request.to_wire() == case["request"] + assert "spot_tolerance" not in request.to_wire() + changed = ScenarioSpotSolveRequest.model_validate( + {**case["request"], "volatility": None, "elapsed_time_years": 0.0} + ) + assert changed.to_wire()["volatility"] is None + assert changed.to_wire()["elapsed_time_years"] == 0.0 + evaluate = request_for(SUCCESS[0]) + evaluate.scenarios.append(ScenarioPoint(spot_change=0.0, volatility=None)) + assert evaluate.to_wire()["scenarios"][-1] == {"spot_change": 0.0, "volatility": None} + evaluate.scenarios.append({"spot_change": float("nan")}) + with pytest.raises(ValidationError): + evaluate.to_wire() + request.contract.dividend_schedule.append({"ex_time_years": "wrong", "cash_amount": 1}) + with pytest.raises(ValidationError): + request.to_wire() + for count in (0, 100): + with pytest.raises(ValidationError): + ScenarioRequest(contract=request.contract, scenarios=[ScenarioPoint()] * count) + with pytest.raises(ValidationError): + ScenarioSpotSolveRequest.model_validate({**case["request"], "max_spot": float("inf")}) + with pytest.raises(ValidationError): + ScenarioSpotSolveRequest.model_validate( + {**case["request"], "target": {"kind": "position_pnl", "value": 0.0}} + ) + + +@pytest.mark.parametrize("mode", ["sync", "async"]) +@pytest.mark.parametrize("case", [c for c in CASES if c["status"] != 200], ids=lambda c: c["name"]) +def test_errors_preserve_zero_or_committed_charge_and_never_retry(mode, case): + calls = [] + + def handle(incoming): + calls.append(incoming) + return httpx.Response(case["status"], json=case["response"]) + + with pytest.raises(APIError) as error: + invoke(mode, handle, lambda c: c.solve_spot(request_for(case))) + assert len(calls) == 1 + assert error.value.metadata.fcu_charged == case["expected_cu"] + assert error.value.body == json.dumps(case["response"], separators=(",", ":")).encode() + + +@pytest.mark.parametrize("mode", ["sync", "async"]) +@pytest.mark.parametrize("failure", ["missing_boundary_value", "invalid_coverage", "bad_count"]) +def test_malformed_solve_response_keeps_charge_evidence(mode, failure): + case = SOLVES[0] + payload = copy.deepcopy(case["response"]) + if failure == "missing_boundary_value": + del payload["result"]["boundaries"][0]["value"] + elif failure == "invalid_coverage": + payload["result"]["coverage"] = {"status": "unresolved"} + else: + payload["result"]["price_evaluations"] = 2**64 + with pytest.raises(ResponseError) as error: + invoke( + mode, + lambda r: httpx.Response(200, json=payload), + lambda c: c.solve_spot(request_for(case)), + ) + assert error.value.metadata.fcu_charged == 100 + assert json.loads(error.value.body) == payload + + +@pytest.mark.parametrize("mode", ["sync", "async"]) +@pytest.mark.parametrize("failure", ["reordered", "missing", "duplicate"]) +def test_scenario_results_cannot_be_silently_misassociated(mode, failure): + case = next(c for c in SUCCESS if c["name"] == "scenario-bsm-partial") + payload = copy.deepcopy(case["response"]) + if failure == "reordered": + payload["results"].reverse() + elif failure == "missing": + payload["results"].pop() + else: + payload["results"][1]["index"] = 0 + with pytest.raises(ResponseError) as error: + invoke( + mode, + lambda r: httpx.Response(200, json=payload), + lambda c: c.evaluate_scenarios(request_for(case)), + ) + assert error.value.metadata.fcu_charged == case["expected_cu"] + + +@pytest.mark.parametrize("mode", ["sync", "async"]) +def test_additive_fields_and_large_diagnostic_counts_do_not_recalculate_charges(mode): + case = SOLVES[0] + payload = copy.deepcopy(case["response"]) + payload["result"]["future_diagnostic"] = {"value": 0} + payload["result"]["boundaries"][0]["future_boundary"] = None + payload["result"]["inputs"]["future_input"] = 0 + payload["result"]["price_evaluations"] = 2**53 + 1 + result = invoke( + mode, lambda r: httpx.Response(200, json=payload), lambda c: c.solve_spot(request_for(case)) + ) + assert result.model_dump(exclude_unset=True) == payload + assert result.result.price_evaluations == 2**53 + 1 + assert result.metadata.fcu_charged == 100 + + +@pytest.mark.parametrize("mode", ["sync", "async"]) +@pytest.mark.parametrize("failure", ["timeout", "rate_limit"]) +@pytest.mark.parametrize("case", [SUCCESS[0], SOLVES[0]], ids=lambda c: c["name"]) +def test_scenarios_never_retry_after_uncertain_execution_or_rate_limit(mode, failure, case): + calls = [] + + def handle(request): + calls.append(request) + if failure == "timeout": + raise httpx.ReadTimeout("late response") + return httpx.Response(429, json={"message": "slow down"}, headers={"Retry-After": "3"}) + + with pytest.raises(RequestTimeout if failure == "timeout" else RateLimitError): + invoke(mode, handle, lambda c: getattr(c, method_for(case))(request_for(case))) + assert len(calls) == 1 + + +@pytest.mark.parametrize( + "module,mode", + [ + ("scenario_evaluate", "point"), + ("scenario_evaluate", "grid"), + ("solve_spot", "price"), + ("solve_spot", "pnl"), + ("solve_spot_async", "price"), + ("solve_spot_async", "pnl"), + ], +) +@pytest.mark.parametrize("model", ["bsm", "bs1993"]) +def test_executable_examples_quote_then_optionally_execute_once( + fixture_api, tmp_path, module, mode, model +): + import os + import shutil + import subprocess + import sys + + endpoint, received = fixture_api + script = tmp_path / f"{module}.py" + shutil.copyfile(ROOT / "examples" / script.name, script) + args = [sys.executable, str(script), "--model", model] + if module == "scenario_evaluate": + args += ["--mode", mode] + cost = (2 if mode == "point" else 10) * (1 if model == "bsm" else 2) + else: + cost = 100 if model == "bsm" else 200 + if mode == "pnl": + args += ["--target-pnl", "500"] + env = {**os.environ, "MORPHIQ_API_BASE_URL": endpoint, "MORPHIQ_API_KEY": "fixture-key"} + quoted = subprocess.run(args, cwd=tmp_path, env=env, capture_output=True, text=True) + assert quoted.returncode == 0, quoted.stderr + assert len(received) == 1 and received[0][0] == "/v1/cost/quote" + assert f"Quoted CU: {cost}" in quoted.stdout + refused = subprocess.run( + args + ["--max-cu", str(cost - 1)], cwd=tmp_path, env=env, capture_output=True, text=True + ) + assert refused.returncode == 2 and len(received) == 2 + assert "no execution submitted" in refused.stderr + executed = subprocess.run( + args + ["--max-cu", str(cost)], cwd=tmp_path, env=env, capture_output=True, text=True + ) + assert executed.returncode == 0, executed.stderr + assert len(received) == 4 + assert f'"fcu_charged": {cost}' in executed.stdout + assert "fixture-key" not in executed.stdout + executed.stderr + + +@pytest.mark.integration +def test_native_scenarios_sync_async_quotes_and_receipts(): + import hashlib + import os + + endpoint = os.environ.get("MORPHIQ_TEST_API_URL") + if not endpoint: + pytest.skip("controlled API not set") + key = os.environ.get("MORPHIQ_TEST_API_KEY") + with httpx.Client( + base_url=endpoint, + trust_env=False, + headers={"Authorization": f"Bearer {key}"} if key else {}, + ) as raw: + provenance = json.loads((ROOT / "contracts/provenance.json").read_text()) + assert hashlib.sha256(raw.get("/openapi.json").content).hexdigest() == provenance["sha256"] + + async def run_async(case): + async with AsyncMorphIQ( + base_url=endpoint, api_key=key, allow_unauthenticated=key is None + ) as client: + return await getattr(client, method_for(case))( + request_for(case), max_cu=case["expected_cu"] + ) + + with MorphIQ(base_url=endpoint, api_key=key, allow_unauthenticated=key is None) as client: + for case in SUCCESS: + response = getattr(client, method_for(case))( + request_for(case), max_cu=case["expected_cu"] + ) + asynchronous = asyncio.run(run_async(case)) + direct = raw.post(case["path"], json=case["request"]) + direct.raise_for_status() + for candidate in (response, asynchronous): + assert { + k: v + for k, v in candidate.model_dump(exclude_unset=True).items() + if k != "metadata" + } == {k: v for k, v in direct.json().items() if k != "metadata"} + assert candidate.metadata.fcu_charged == (case["expected_cu"] if key else None) + if key: + quote = client.quote(request_for(case)) + assert quote.quote.total_fcu == case["expected_cu"] + assert quote.metadata.fcu_charged == 0 + with pytest.raises(APIError) as refused: + getattr(client, method_for(case))( + request_for(case), max_cu=case["expected_cu"] - 1 + ) + assert refused.value.code == "cost_limit_exceeded" + assert refused.value.metadata.fcu_charged == 0 From e512801ee19d23def1beacb95b53ee1f6d71884b Mon Sep 17 00:00:00 2001 From: Stephen Romano Date: Wed, 4 Nov 2026 22:05:21 -0500 Subject: [PATCH 2/2] feat(examples): add SDK solver acceptance app --- docs/SCENARIOS.md | 5 + docs/validation/2026-11-05-solver-app.json | 1376 ++++++++++++++++++++ examples/README.md | 6 + examples/solver_app/README.md | 100 ++ examples/solver_app/app.js | 121 ++ examples/solver_app/app.py | 215 +++ examples/solver_app/index.html | 72 + examples/solver_app/style.css | 2 + scripts/check_package.py | 20 + tests/test_examples.py | 2 +- tests/test_solver_app.py | 216 +++ 11 files changed, 2134 insertions(+), 1 deletion(-) create mode 100644 docs/validation/2026-11-05-solver-app.json create mode 100644 examples/solver_app/README.md create mode 100644 examples/solver_app/app.js create mode 100644 examples/solver_app/app.py create mode 100644 examples/solver_app/index.html create mode 100644 examples/solver_app/style.css create mode 100644 tests/test_solver_app.py diff --git a/docs/SCENARIOS.md b/docs/SCENARIOS.md index 2c50e07..2f2ef1c 100644 --- a/docs/SCENARIOS.md +++ b/docs/SCENARIOS.md @@ -183,3 +183,8 @@ They quote only unless you supply `--max-cu`. Interactive workspaces: [evaluation](../notebooks/scenarios/evaluate.ipynb) and [spot targeting](../notebooks/scenarios/solve-spot.ipynb). Setup and control edits send no requests; action buttons state request counts and require an explicit ceiling. + +For a complete client demonstration, run the [solver example app](../examples/solver_app/README.md). +It supplies a browser form, a Python SDK adapter, explicit quote/solve actions, +boundary/coverage tables and session receipts. Its acceptance walkthrough covers +both models, both target types, cap refusal and incomplete/non-unique outcomes. diff --git a/docs/validation/2026-11-05-solver-app.json b/docs/validation/2026-11-05-solver-app.json new file mode 100644 index 0000000..918db96 --- /dev/null +++ b/docs/validation/2026-11-05-solver-app.json @@ -0,0 +1,1376 @@ +{ + "date": "2026-11-05", + "sdk_parent_commit": "4e1e290d0ee6c66a80d158fb60307a165257d724", + "api_commit": "7b4f233b2222fbb95b2492d5c57dbde1ac32e571", + "schema_sha256": "38a0c933ea805f132bc4245867c3130c5a1db94e23cb4837872abf8c27e33ccc", + "api_binary_sha256": "485ca13ab77fe174443d17620e419af1f6032f95af407304a8393566f2b275d3", + "app_files_sha256": { + "examples/solver_app/app.py": "3ca0af8fbd9053956f5b8ea0f917bf480a082649d579d0f445a955b7f4ed32ce", + "examples/solver_app/index.html": "3699b6619d943daf2ef6d9eefb2592a53c66b741c19d97cf90c41342c9299177", + "examples/solver_app/app.js": "e6848b8b9b01ba0e663255ebe4f31f7cda11c80967048005e557f76db412f462", + "examples/solver_app/style.css": "262b8ce9b6ebba2ed62a6d03da6ba642e568b330261bf6833318ee9917ac10eb", + "examples/solver_app/README.md": "222840c3a453af6ae8bde194c5873a2e213d6b640672f9d30eba06702bd221c0", + "tests/test_solver_app.py": "32db7d0fc6f2e350932361a8f47e899a6c137927e2cb046acf422858f07d7d01", + "scripts/check_package.py": "9e9907b564e8c38edaea65bc97a80158b4c8cbe7bf17fd2da6712d5414c3769b" + }, + "validation": [ + { + "command": ".venv/bin/python -m pytest -q", + "result": "672 passed, 20 native tests skipped; before three additional receipt representation tests were added. One pre-existing Pydantic warning from deliberate malformed scenario input." + }, + { + "command": ".venv/bin/python -m pytest -q tests/test_solver_app.py", + "result": "22 passed, 1 protected native test skipped" + }, + { + "command": ".venv311/bin/python -m pytest -q tests/test_solver_app.py", + "result": "22 passed, 1 protected native test skipped" + }, + { + "command": "MORPHIQ_TEST_API_URL= MORPHIQ_TEST_API_KEY= .venv/bin/python -m pytest -q tests/test_solver_app.py -m integration", + "result": "1 passed, 22 deselected; served digest verified; all 10 successful native solve cases through app HTTP -> SDK -> API matched retained results and exact 100/200-CU receipts" + }, + { + "command": ".venv/bin/python scripts/check_package.py", + "result": "passed: sdist/wheel, isolated installation, exact app assets in sdist, standalone app --help with wheel outside checkout, optional pandas checks" + }, + { + "command": ".venv/bin/python -m ruff check .; .venv/bin/python -m ruff format --check .; .venv/bin/python -m mypy; .venv/bin/python scripts/generate_notebook.py --check; node --check examples/solver_app/app.js; git diff --check", + "result": "passed" + } + ], + "walkthrough_method": "HTTP actions against the running example app and protected native candidate, using the README synthetic inputs. Each case first checks local below-cap refusal, then explicitly executes once. This is app adapter evidence, not browser rendering evidence.", + "walkthrough_cases": [ + { + "name": "bsm-option_price", + "request": { + "contract": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "volatility": 0.2, + "risk_free_rate": 0.05, + "dividend_yield": 0.0 + }, + "target": { + "kind": "option_price", + "value": 15.0 + }, + "min_spot": 50.0, + "max_spot": 150.0, + "elapsed_time_years": 0.0, + "spot_tolerance": 1e-09, + "value_tolerance": 1e-06 + }, + "quote": { + "quote": { + "total_fcu": 100, + "fixed_fcu": 0, + "variable_fcu": 100, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "black_scholes_merton/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 100, + "variable_fcu": 100 + } + ] + }, + "resolved_models": [ + { + "model": "black_scholes_merton/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50701ab43fc5c-0000000000000001", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 100000, + "budget_period": "utc_calendar_month" + } + }, + "response": { + "metadata": { + "request_id": "fr-18e50701ab43fc5c-0000000000000002", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 4775, + "compute_schedule_version": "2026-09-11", + "fcu_quoted": 100, + "fcu_charged": 100, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99900, + "budget_period": "utc_calendar_month" + }, + "result": { + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1", + "base": { + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "result": { + "price": 10.45058357218557, + "exercise_style": "european", + "option_type": "call", + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1" + } + }, + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 15.0 + }, + "min_spot": 50.0, + "max_spot": 150.0, + "spot_tolerance": 1e-09, + "value_tolerance": 1e-06, + "profile": { + "id": "spot_target/1", + "scan_intervals": 32, + "max_refinements": 64, + "max_price_evaluations": 4775 + }, + "reference_net_value": 0.0, + "boundaries": [ + { + "kind": "point", + "spot": 106.55208796524676, + "value": 15.000000000254582, + "crossing": true, + "refinements": 32, + "bracket_width": 7.275957614183426e-10 + } + ], + "regions": [ + { + "min_spot": 50.0, + "max_spot": 106.55208796524676, + "sign": "below_target" + }, + { + "min_spot": 106.55208796524676, + "max_spot": 150.0, + "sign": "above_target" + } + ], + "coverage": { + "status": "resolved" + }, + "price_evaluations": 67 + } + }, + "below_cap_local_refusal": true + }, + { + "name": "bsm-position_pnl", + "request": { + "contract": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "volatility": 0.2, + "risk_free_rate": 0.05, + "dividend_yield": 0.0 + }, + "target": { + "kind": "position_pnl", + "value": 500.0, + "position": { + "quantity": 2.0, + "multiplier": 100.0 + } + }, + "min_spot": 50.0, + "max_spot": 150.0, + "elapsed_time_years": 0.0, + "spot_tolerance": 1e-09, + "value_tolerance": 1e-06 + }, + "quote": { + "quote": { + "total_fcu": 100, + "fixed_fcu": 0, + "variable_fcu": 100, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "black_scholes_merton/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 100, + "variable_fcu": 100 + } + ] + }, + "resolved_models": [ + { + "model": "black_scholes_merton/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50701ab43fc5c-0000000000000003", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99900, + "budget_period": "utc_calendar_month" + } + }, + "response": { + "metadata": { + "request_id": "fr-18e50701ab43fc5c-0000000000000004", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 4775, + "compute_schedule_version": "2026-09-11", + "fcu_quoted": 100, + "fcu_charged": 100, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99800, + "budget_period": "utc_calendar_month" + }, + "result": { + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1", + "base": { + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "result": { + "price": 10.45058357218557, + "exercise_style": "european", + "option_type": "call", + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1" + } + }, + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "position_pnl", + "value": 500.0, + "position": { + "quantity": 2.0, + "multiplier": 100.0 + } + }, + "min_spot": 50.0, + "max_spot": 150.0, + "spot_tolerance": 1e-09, + "value_tolerance": 1e-06, + "profile": { + "id": "spot_target/1", + "scan_intervals": 32, + "max_refinements": 64, + "max_price_evaluations": 4775 + }, + "reference_net_value": 2090.116714437114, + "boundaries": [ + { + "kind": "point", + "spot": 103.72824138539727, + "value": 499.9999999804477, + "crossing": true, + "refinements": 32, + "bracket_width": 7.275957614183426e-10 + } + ], + "regions": [ + { + "min_spot": 50.0, + "max_spot": 103.72824138539727, + "sign": "below_target" + }, + { + "min_spot": 103.72824138539727, + "max_spot": 150.0, + "sign": "above_target" + } + ], + "coverage": { + "status": "resolved" + }, + "price_evaluations": 67 + } + }, + "below_cap_local_refusal": true + }, + { + "name": "bs1993-option_price", + "request": { + "contract": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "volatility": 0.2, + "risk_free_rate": 0.05, + "dividend_yield": 0.0 + }, + "target": { + "kind": "option_price", + "value": 15.0 + }, + "min_spot": 50.0, + "max_spot": 150.0, + "elapsed_time_years": 0.0, + "spot_tolerance": 1e-09, + "value_tolerance": 1e-06 + }, + "quote": { + "quote": { + "total_fcu": 200, + "fixed_fcu": 0, + "variable_fcu": 200, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "bjerksund_stensland_1993/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 200, + "variable_fcu": 200 + } + ] + }, + "resolved_models": [ + { + "model": "bjerksund_stensland_1993/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50701ab43fc5c-0000000000000005", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99800, + "budget_period": "utc_calendar_month" + } + }, + "response": { + "metadata": { + "request_id": "fr-18e50701ab43fc5c-0000000000000006", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 238750, + "compute_schedule_version": "2026-09-11", + "fcu_quoted": 200, + "fcu_charged": 200, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99600, + "budget_period": "utc_calendar_month" + }, + "result": { + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1", + "base": { + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "result": { + "price": 10.45058357218557, + "exercise_style": "american", + "option_type": "call", + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1" + } + }, + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 15.0 + }, + "min_spot": 50.0, + "max_spot": 150.0, + "spot_tolerance": 1e-09, + "value_tolerance": 1e-06, + "profile": { + "id": "spot_target/1", + "scan_intervals": 32, + "max_refinements": 64, + "max_price_evaluations": 4775 + }, + "reference_net_value": 0.0, + "boundaries": [ + { + "kind": "point", + "spot": 106.55208796524676, + "value": 15.000000000254582, + "crossing": true, + "refinements": 32, + "bracket_width": 7.275957614183426e-10 + } + ], + "regions": [ + { + "min_spot": 50.0, + "max_spot": 106.55208796524676, + "sign": "below_target" + }, + { + "min_spot": 106.55208796524676, + "max_spot": 150.0, + "sign": "above_target" + } + ], + "coverage": { + "status": "resolved" + }, + "price_evaluations": 67 + } + }, + "below_cap_local_refusal": true + }, + { + "name": "bs1993-position_pnl", + "request": { + "contract": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "volatility": 0.2, + "risk_free_rate": 0.05, + "dividend_yield": 0.0 + }, + "target": { + "kind": "position_pnl", + "value": 500.0, + "position": { + "quantity": 2.0, + "multiplier": 100.0 + } + }, + "min_spot": 50.0, + "max_spot": 150.0, + "elapsed_time_years": 0.0, + "spot_tolerance": 1e-09, + "value_tolerance": 1e-06 + }, + "quote": { + "quote": { + "total_fcu": 200, + "fixed_fcu": 0, + "variable_fcu": 200, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "bjerksund_stensland_1993/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 200, + "variable_fcu": 200 + } + ] + }, + "resolved_models": [ + { + "model": "bjerksund_stensland_1993/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50701ab43fc5c-0000000000000007", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99600, + "budget_period": "utc_calendar_month" + } + }, + "response": { + "metadata": { + "request_id": "fr-18e50701ab43fc5c-0000000000000008", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 238750, + "compute_schedule_version": "2026-09-11", + "fcu_quoted": 200, + "fcu_charged": 200, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99400, + "budget_period": "utc_calendar_month" + }, + "result": { + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1", + "base": { + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "result": { + "price": 10.45058357218557, + "exercise_style": "american", + "option_type": "call", + "pricing_model": "bjerksund_stensland_1993", + "model_version": "bjerksund_stensland_1993/1" + } + }, + "inputs": { + "exercise_style": "american", + "pricing_model": "bjerksund_stensland_1993", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "position_pnl", + "value": 500.0, + "position": { + "quantity": 2.0, + "multiplier": 100.0 + } + }, + "min_spot": 50.0, + "max_spot": 150.0, + "spot_tolerance": 1e-09, + "value_tolerance": 1e-06, + "profile": { + "id": "spot_target/1", + "scan_intervals": 32, + "max_refinements": 64, + "max_price_evaluations": 4775 + }, + "reference_net_value": 2090.116714437114, + "boundaries": [ + { + "kind": "point", + "spot": 103.72824138539727, + "value": 499.9999999804477, + "crossing": true, + "refinements": 32, + "bracket_width": 7.275957614183426e-10 + } + ], + "regions": [ + { + "min_spot": 50.0, + "max_spot": 103.72824138539727, + "sign": "below_target" + }, + { + "min_spot": 103.72824138539727, + "max_spot": 150.0, + "sign": "above_target" + } + ], + "coverage": { + "status": "resolved" + }, + "price_evaluations": 67 + } + }, + "below_cap_local_refusal": true + }, + { + "name": "no-boundary", + "request": { + "contract": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "volatility": 0.2, + "risk_free_rate": 0.05, + "dividend_yield": 0.0 + }, + "target": { + "kind": "option_price", + "value": 1000.0 + }, + "min_spot": 50.0, + "max_spot": 150.0, + "elapsed_time_years": 0.0, + "spot_tolerance": 1e-09, + "value_tolerance": 1e-06 + }, + "quote": { + "quote": { + "total_fcu": 100, + "fixed_fcu": 0, + "variable_fcu": 100, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "black_scholes_merton/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 100, + "variable_fcu": 100 + } + ] + }, + "resolved_models": [ + { + "model": "black_scholes_merton/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50701ab43fc5c-0000000000000009", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99400, + "budget_period": "utc_calendar_month" + } + }, + "response": { + "metadata": { + "request_id": "fr-18e50701ab43fc5c-000000000000000a", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 4775, + "compute_schedule_version": "2026-09-11", + "fcu_quoted": 100, + "fcu_charged": 100, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99300, + "budget_period": "utc_calendar_month" + }, + "result": { + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1", + "base": { + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "result": { + "price": 10.45058357218557, + "exercise_style": "european", + "option_type": "call", + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1" + } + }, + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 1000.0 + }, + "min_spot": 50.0, + "max_spot": 150.0, + "spot_tolerance": 1e-09, + "value_tolerance": 1e-06, + "profile": { + "id": "spot_target/1", + "scan_intervals": 32, + "max_refinements": 64, + "max_price_evaluations": 4775 + }, + "reference_net_value": 0.0, + "boundaries": [], + "regions": [ + { + "min_spot": 50.0, + "max_spot": 150.0, + "sign": "below_target" + } + ], + "coverage": { + "status": "resolved" + }, + "price_evaluations": 35 + } + }, + "below_cap_local_refusal": true + }, + { + "name": "interval", + "request": { + "contract": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "volatility": 0.2, + "risk_free_rate": 0.05, + "dividend_yield": 0.0 + }, + "target": { + "kind": "option_price", + "value": 0.0 + }, + "min_spot": 50.0, + "max_spot": 90.0, + "elapsed_time_years": 1.0, + "spot_tolerance": 1e-09, + "value_tolerance": 1e-06 + }, + "quote": { + "quote": { + "total_fcu": 100, + "fixed_fcu": 0, + "variable_fcu": 100, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "black_scholes_merton/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 100, + "variable_fcu": 100 + } + ] + }, + "resolved_models": [ + { + "model": "black_scholes_merton/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50701ab43fc5c-000000000000000b", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99300, + "budget_period": "utc_calendar_month" + } + }, + "response": { + "metadata": { + "request_id": "fr-18e50701ab43fc5c-000000000000000c", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 4775, + "compute_schedule_version": "2026-09-11", + "fcu_quoted": 100, + "fcu_charged": 100, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99200, + "budget_period": "utc_calendar_month" + }, + "result": { + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1", + "base": { + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "result": { + "price": 10.45058357218557, + "exercise_style": "european", + "option_type": "call", + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1" + } + }, + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 0.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 0.0 + }, + "min_spot": 50.0, + "max_spot": 90.0, + "spot_tolerance": 1e-09, + "value_tolerance": 1e-06, + "profile": { + "id": "spot_target/1", + "scan_intervals": 32, + "max_refinements": 64, + "max_price_evaluations": 4775 + }, + "reference_net_value": 0.0, + "boundaries": [ + { + "kind": "interval", + "min_spot": 50.0, + "max_spot": 90.0, + "value": 0.0 + } + ], + "regions": [ + { + "min_spot": 50.0, + "max_spot": 90.0, + "sign": "at_target" + } + ], + "coverage": { + "status": "resolved" + }, + "price_evaluations": 68 + } + }, + "below_cap_local_refusal": true + }, + { + "name": "unresolved", + "request": { + "contract": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "volatility": 0.2, + "risk_free_rate": 0.05, + "dividend_yield": 0.0 + }, + "target": { + "kind": "option_price", + "value": 15.0 + }, + "min_spot": 50.0, + "max_spot": 150.0, + "elapsed_time_years": 0.0, + "spot_tolerance": 100.0, + "value_tolerance": 1e-16 + }, + "quote": { + "quote": { + "total_fcu": 100, + "fixed_fcu": 0, + "variable_fcu": 100, + "minimum_charge_fcu": 0, + "lines": [ + { + "operation": "scenario_spot_solve", + "model": "black_scholes_merton/1", + "billing_unit": "solve", + "quantity": 1, + "fcu_per_unit": 100, + "variable_fcu": 100 + } + ] + }, + "resolved_models": [ + { + "model": "black_scholes_merton/1", + "quantity": 1 + } + ], + "metadata": { + "request_id": "fr-18e50701ab43fc5c-000000000000000d", + "api_version": "v1", + "service_version": "0.5.0", + "fcu_charged": 0, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99200, + "budget_period": "utc_calendar_month" + } + }, + "response": { + "metadata": { + "request_id": "fr-18e50701ab43fc5c-000000000000000e", + "api_version": "v1", + "engine_version": "0.43.5", + "engines": [ + { + "name": "morphiq-risk", + "version": "0.43.5", + "revision": "43ca7f93b9edda6078b49950de6a8b7d51a3e3ef" + } + ], + "units_charged": 4775, + "compute_schedule_version": "2026-09-11", + "fcu_quoted": 100, + "fcu_charged": 100, + "fcu_schedule_id": "fcu-qdfp-v1", + "fcu_schedule_version": "2026-11-05-flat-solves", + "policy": { + "snapshot_id": "safety.envelope@1+plans.pro@8+entitlements.qdfp-v1@4+limits.pro@1+schedules.fcu-qdfp-v1@5+budgets.pro-monthly@1+overage.hard-stop@1+auth.standard-api-key@1+data-policy.standard@1", + "revision": "0daf1a62614196b2089f51acdbf1b5c3d1ce6fe828b663cb1e395f7e23313ef0", + "plan": "plans.pro@8", + "records": [ + "safety.envelope@1", + "plans.pro@8", + "entitlements.qdfp-v1@4", + "limits.pro@1", + "schedules.fcu-qdfp-v1@5", + "budgets.pro-monthly@1", + "overage.hard-stop@1", + "auth.standard-api-key@1", + "data-policy.standard@1" + ] + }, + "remaining_fcu_instance_estimate": 99100, + "budget_period": "utc_calendar_month" + }, + "result": { + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1", + "base": { + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "result": { + "price": 10.45058357218557, + "exercise_style": "european", + "option_type": "call", + "pricing_model": "black_scholes_merton", + "model_version": "black_scholes_merton/1" + } + }, + "inputs": { + "exercise_style": "european", + "pricing_model": "black_scholes_merton", + "option_type": "call", + "spot": 100.0, + "strike": 100.0, + "time_to_expiry_years": 1.0, + "risk_free_rate": 0.05, + "dividend_yield": 0.0, + "volatility": 0.2 + }, + "target": { + "kind": "option_price", + "value": 15.0 + }, + "min_spot": 50.0, + "max_spot": 150.0, + "spot_tolerance": 100.0, + "value_tolerance": 1e-16, + "profile": { + "id": "spot_target/1", + "scan_intervals": 32, + "max_refinements": 64, + "max_price_evaluations": 4775 + }, + "reference_net_value": 0.0, + "boundaries": [], + "regions": [ + { + "min_spot": 50.0, + "max_spot": 106.25, + "sign": "below_target" + }, + { + "min_spot": 109.375, + "max_spot": 150.0, + "sign": "above_target" + } + ], + "coverage": { + "status": "unresolved", + "cells": [ + { + "lower_spot": 106.25, + "upper_spot": 109.375, + "best_spot": 106.25, + "best_abs_value_error": 0.22518944410185782, + "reason": "spot_tolerance_reached" + } + ] + }, + "price_evaluations": 35 + } + }, + "below_cap_local_refusal": true + } + ], + "limitations": [ + "Computer-use browser visual verification was blocked by the tool URL policy; browser walkthrough remains open in API #34/#146 and SDK #45.", + "No production API requests, deployment, policy activation, merge or SDK publication. SDK PR #46 remains draft with auto-merge off until matching API deployment and live verification.", + "This is the solver client slice; it does not complete portfolio scenarios or attribution. Engine numerical reference/performance qualification remains separately owned by the API/engine.", + "GitHub CI is a separate required gate; refer to the PR checks for final commit evidence." + ] +} diff --git a/examples/README.md b/examples/README.md index a5ab9e4..9dc57b2 100644 --- a/examples/README.md +++ b/examples/README.md @@ -118,3 +118,9 @@ units and exact charge totals. Pandas is optional and conversion makes no API ca use synthetic inputs and require a compatible scenario deployment. Add `--max-cu` to explicitly execute once; otherwise they only quote. Use `--model bs1993` for American options. See [the guide](../docs/SCENARIOS.md). + +The [solver example app](solver_app/README.md) provides a local browser UI for +BSM/BS1993 target price and position P&L solves. Run +`python examples/solver_app/app.py` after configuring the same environment. +Get a quote, choose a CU limit, then explicitly solve. All pricing and searching +run through the SDK against the API; the app shows coverage and request receipts. diff --git a/examples/solver_app/README.md b/examples/solver_app/README.md new file mode 100644 index 0000000..d600349 --- /dev/null +++ b/examples/solver_app/README.md @@ -0,0 +1,100 @@ +# Spot solver example app + +A small local browser app that uses the Python SDK to solve for an underlying +spot at a target option price or position P&L. It supports **BSM (European)** and +**BS1993 (American)**, calls `MorphIQ.quote()` and `MorphIQ.solve_spot()`, and +renders the API's boundaries, sign regions, coverage and receipts. There is no +local option-pricing formula, root finder or hidden verification price call. + +Requires the compatible scenario API and account permission. This candidate SDK +is held for the matching API deployment; existing published versions may not +contain the solver. See the [scenario contract](../../docs/SCENARIOS.md). + +## Run + +From this SDK checkout with Python 3.11 or newer: + +```sh +python -m pip install -e . +export MORPHIQ_API_KEY="YOUR_API_KEY" +# Set this only for a compatible controlled service; the SDK default is production. +export MORPHIQ_API_BASE_URL="http://127.0.0.1:18080" +python examples/solver_app/app.py +``` + +Open the printed **http://127.0.0.1:8765** URL. Use `--port 0` to choose a free +port, or `--port 8766` to select another. Stop with Ctrl+C and unset the environment +variables when finished. The app adds no dependencies beyond the SDK and Python's +standard library. You may copy this entire folder outside the checkout and run +it with the matching SDK installed. + +The key stays in the Python process. Only the printed loopback origin can submit +actions. This is a single-user development example with one current quote, not a +hosted multiuser service; multiple tabs replace each other's current quote. +Responses/receipts stay in the page session and are cleared on reload. + +## Use + +1. Enter the base contract, model, target and positive ordered spot bounds. + Inputs are synthetic by default. Volatility/rates/yield are percentages in + the form and become decimal fractions in the request; times are years. +2. Click **Get quote**: one free API request, no numerical solve or reservation. +3. Set the maximum CU. The approved flat rate is **100 CU for BSM** or **200 CU + for BS1993** per complete solve. The authenticated quote is authoritative; + switching models does not increase your CU limit automatically. +4. Click **Solve**: one capped API request, with all internal pricing included. + Each execution consumes its quote in the app; another solve needs a new quote. +5. Inspect all boundaries, values/residuals, regions, unresolved cells and the + complete API response. Receipts retain request IDs, charges and policy/engine + provenance. Results never imply global completeness beyond the reported scan. + +Changing inputs clears the current quote/results and sends no API calls; previous +receipts remain visible. P&L is relative to the original base model mark and uses +signed quantity and a positive contract multiplier. It excludes trade-entry cost, +fees, financing and currency conversion. Strike/rates/yield remain fixed; optional +scenario volatility and elapsed years are shown under assumptions. An interval +represents a sampled plateau, not proof about every spot inside it. + +The app refuses below-quote caps before sending a solve; the API independently +enforces the same ceiling. No-root and incomplete searches still carry the full +flat charge. API failures display reported charge evidence. Missing responses or +missing charge metadata mean **unknown**, not zero; there are no automatic retries. +Reconcile an unknown outcome before deliberately submitting again. + +## Acceptance walkthrough + +Use an explicitly configured controlled API with the pinned contract and approved +solve schedule. Never point automated tests at a billed production account. + +| Demonstration | Inputs/action | Expected observation | +| --- | --- | --- | +| Target option price | Defaults: call, spot/strike 100, one year, 20% vol, 5% rate, target 15, range 50–150 | BSM quote/receipt 100 CU; returned point value matches target within reported tolerance | +| American target | Switch to BS1993, quote, keep cap 100 then click Solve | Local refusal without a solve; set cap 200 and explicitly solve for a 200-CU receipt | +| Position P&L | Each model, P&L target 500, quantity 2, multiplier 100 | Base-relative position target and boundaries returned; same model flat rate | +| No boundary in scan | BSM call, target price 1000, range 50–150 | Empty boundaries with actual coverage/sign regions; no claim of global nonexistence; 100 CU | +| Sampled interval | BSM call, elapsed time 1 year, target 0, range 50–90 | Interval boundary at expiry; all returned intervals retained; 100 CU | +| Incomplete search | Defaults, spot tolerance 100, value tolerance 1e-16 | Unresolved cells/reasons remain visible, not presented as a unique solved answer; 100 CU | +| Refusal | Minimum spot greater than maximum spot, then Get quote | Typed API refusal/receipt; no solve automatically submitted | +| Stale result | Edit spot after a successful solve | Current result/quote invalidated; receipt retained; no request from editing | + +The app is the **solver slice** of API #34's client acceptance. Point/curve/grid +examples and notebooks remain separate; this app does not complete portfolio +scenarios or attribution. API #146's billing acceptance uses the same app's +quote/cap/receipt flow. Source tests also replay post-commit numerical failure, +zero-charge API refusal, malformed responses and transport disconnects without +retry or invented zero charges. + +```sh +python -m pytest -q tests/test_solver_app.py +# Against the controlled protected native candidate only: +MORPHIQ_TEST_API_URL=http://127.0.0.1:18080 \ +MORPHIQ_TEST_API_KEY="LOCAL_TEST_KEY" \ +python -m pytest -q tests/test_solver_app.py -m integration +``` + +The native test checks the served OpenAPI digest before executing both models' +price/P&L, no-boundary, interval and unresolved fixtures through the app's HTTP +routes and the installed SDK. Results must exactly match retained native API +fixtures, and quotes/receipts must be 100/200 CU. This verifies adapter parity; +numerical reference accuracy and engine speed remain the API/engine's existing +qualification gates. It is not a production deployment or visual-browser sign-off. diff --git a/examples/solver_app/app.js b/examples/solver_app/app.js new file mode 100644 index 0000000..9973377 --- /dev/null +++ b/examples/solver_app/app.js @@ -0,0 +1,121 @@ +/* UI only: request construction and rendering, no local pricing/root finding. */ +const $ = id => document.getElementById(id); +const form = $('controls'); +let quote = null; +let busy = false; +const n = id => Number($(id).value); +const text = value => value == null ? 'Not reported' : String(value); +const pretty = value => JSON.stringify(value, null, 2); +function request() { + const american = $('model').value === 'bs1993'; + const contract = { + pricing_model: american ? 'bjerksund_stensland_1993' : 'black_scholes_merton', + exercise_style: american ? 'american' : 'european', option_type: $('option_type').value, + spot: n('spot'), strike: n('strike'), time_to_expiry_years: n('time_to_expiry_years'), + volatility: n('volatility') / 100, risk_free_rate: n('risk_free_rate') / 100, + dividend_yield: n('dividend_yield') / 100, + }; + const target = {kind: $('target_kind').value, value: n('target_value')}; + if (target.kind === 'position_pnl') target.position = {quantity: n('quantity'), multiplier: n('multiplier')}; + const body = {contract, target, min_spot: n('min_spot'), max_spot: n('max_spot'), + elapsed_time_years: n('elapsed_time_years'), spot_tolerance: n('spot_tolerance'), value_tolerance: n('value_tolerance')}; + if ($('scenario_volatility').value !== '') body.volatility = n('scenario_volatility') / 100; + return body; +} +function refresh() { + for (const input of form.querySelectorAll('input,select,button')) input.disabled = busy; + $('solve').disabled = busy || !quote; +} +form.addEventListener('submit', event => event.preventDefault()); +form.addEventListener('input', event => { + if (event.target.id === 'max_cu') return; + quote = null; + $('quoted-cu').textContent = '—'; + $('outcome').hidden = true; + $('result-title').textContent = 'Inputs changed.'; + $('status').textContent = 'Get a new quote. Earlier responses remain in request receipts.'; + const pnl = $('target_kind').value === 'position_pnl'; + $('position').hidden = !pnl; + $('target-help').textContent = pnl ? 'Total position P&L relative to its base model mark, in the contract’s currency.' : 'Price is per underlying unit in the contract’s currency.'; + refresh(); +}); +function table(id, rows, columns, empty) { + const host = $(id); host.replaceChildren(); + if (!rows.length) { const p = document.createElement('p'); p.className = 'empty'; p.textContent = empty; host.append(p); return; } + const wrap = document.createElement('div'); wrap.className = 'table-wrap'; + const table = document.createElement('table'); + const head = table.createTHead().insertRow(); + for (const [label] of columns) { const th = document.createElement('th'); th.textContent = label; head.append(th); } + const body = table.createTBody(); + for (const row of rows) { const tr = body.insertRow(); for (const [, value] of columns) tr.insertCell().textContent = text(value(row)); } + wrap.append(table); host.append(wrap); +} +function outcome(result) { + $('outcome').hidden = false; + $('result-title').textContent = result.coverage.status === 'unresolved' ? 'Search incomplete.' : result.boundaries.length ? 'Target boundaries returned.' : 'No boundary found in this scan.'; + $('status').textContent = `API coverage: ${result.coverage.status}. Model: ${result.pricing_model}. Results apply to the submitted inputs.`; + $('metrics').replaceChildren(); + for (const [label, value] of [['Search range', `${result.min_spot} – ${result.max_spot}`], ['Target', `${result.target.kind}: ${result.target.value}`], ['Base option price', result.base.result.price], ['Price evaluations', result.price_evaluations]]) { + const card = document.createElement('div'); card.className = 'metric'; + const caption = document.createElement('span'); caption.textContent = label; + const content = document.createElement('strong'); content.textContent = text(value); + card.append(caption, content); $('metrics').append(card); + } + table('boundaries', result.boundaries, [['Kind', r => r.kind], ['Spot / interval', r => r.kind === 'point' ? r.spot : `${r.min_spot} – ${r.max_spot}`], ['Value', r => r.value], ['Value − target', r => r.value - result.target.value]], 'No boundaries returned. Review coverage and sign regions.'); + table('regions', result.regions, [['From', r => r.min_spot], ['To', r => r.max_spot], ['Sign', r => r.sign]], 'No sign regions returned.'); + table('cells', result.coverage.cells || [], [['From', r => r.lower_spot], ['To', r => r.upper_spot], ['Best spot', r => r.best_spot], ['Absolute error', r => r.best_abs_value_error], ['Reason', r => r.reason]], 'No unresolved cells reported.'); + $('assumptions').textContent = pretty({base: result.base, inputs: result.inputs, target: result.target, reference_net_value: result.reference_net_value, profile: result.profile, spot_tolerance: result.spot_tolerance, value_tolerance: result.value_tolerance}); +} +function receipt(action, payload, submitted) { + const host = $('history'); host.querySelector('.empty')?.remove(); + const entry = document.createElement('div'); entry.className = 'receipt'; + const title = document.createElement('strong'); title.textContent = `${action === 'quote' ? 'Quote' : 'Solve'} · ${new Date().toLocaleTimeString()} · ${payload.ok ? 'response received' : 'refused / failed'}`; + let body; + try { body = JSON.parse(payload.raw_response); } catch { body = null; } + const metadata = payload.metadata || body?.metadata; + // The Python adapter sends the charge as a decimal string, preserving large + // integers exactly. Full raw responses remain available as source records. + const charge = payload.charged_cu; + const chargeLine = document.createElement('p'); chargeLine.textContent = `Charged CU: ${payload.local_refusal ? '0 (no upstream request)' : charge ?? 'unknown / not reported'} · Request ID: ${text(payload.request_id || metadata?.request_id)}`; + const message = document.createElement('p'); message.textContent = payload.message || `Quoted CU: ${payload.quoted_cu ?? metadata?.fcu_quoted ?? 'not reported'} · Schedule: ${text(metadata?.fcu_schedule_id)} / ${text(metadata?.fcu_schedule_version)}`; + const details = document.createElement('details'); + const summary = document.createElement('summary'); summary.textContent = 'Submitted inputs & complete API response'; + const raw = document.createElement('pre'); raw.textContent = pretty(submitted) + '\n\n' + (payload.raw_response || pretty(payload)); + details.append(summary, raw); entry.append(title, chargeLine, message, details); host.prepend(entry); + return body; +} +async function act(action) { + if (busy || !form.reportValidity()) return; + const submitted = {request: request()}; + if (action === 'solve') { + if (!quote) return; + submitted.quote_id = quote.quote_id; submitted.max_cu = $('max_cu').value; + if (BigInt(submitted.max_cu) < BigInt(quote.quoted_cu)) { + $('status').textContent = 'Quote exceeds the CU limit. No solve submitted; increase the limit to proceed.'; + return; + } + } + busy = true; refresh(); $('outcome').hidden = true; + $('result-title').textContent = action === 'quote' ? 'Requesting quote…' : 'Solving through the API…'; + $('status').textContent = 'One request in progress. No automatic retries.'; + quote = null; $('quoted-cu').textContent = '—'; + try { + const response = await fetch(`/api/${action}`, {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(submitted)}); + const payload = await response.json(); + const body = receipt(action, payload, submitted); + if (!payload.ok) { + $('result-title').textContent = 'Request did not complete successfully.'; + $('status').textContent = payload.message + ' Review the receipt before submitting again.'; + } else if (action === 'quote') { + quote = payload; $('quoted-cu').textContent = payload.quoted_cu; + $('result-title').textContent = 'Quote ready.'; + $('status').textContent = `${payload.quoted_cu} CU for one complete solve. Check the limit, then click Solve to execute.`; + } else outcome(body.result); + } catch { + const message = 'No usable app response. Execution and charge unknown. No retry attempted; reconcile before submitting again.'; + receipt(action, {ok: false, message}, submitted); + $('result-title').textContent = 'Outcome unknown.'; $('status').textContent = message; + } finally { busy = false; refresh(); } +} +$('quote').addEventListener('click', () => act('quote')); +$('solve').addEventListener('click', () => act('solve')); diff --git a/examples/solver_app/app.py b/examples/solver_app/app.py new file mode 100644 index 0000000..96ba97f --- /dev/null +++ b/examples/solver_app/app.py @@ -0,0 +1,215 @@ +"""Local solver example: browser controls -> Python SDK -> MorphIQ API. + +Run from a checkout with the SDK installed: python examples/solver_app/app.py +Credentials stay in this process. No requests on startup, no local option math, +no automatic execution/retry, and one upstream request per explicit action. +""" + +from __future__ import annotations + +import argparse +import json +import secrets +from http.server import BaseHTTPRequestHandler, HTTPServer +from pathlib import Path +from typing import Any + +from pydantic import ValidationError + +from morphiq_sdk import ( + APIError, + MorphIQ, + ResponseError, + ScenarioSpotSolveRequest, + TransportError, +) + +ASSETS = { + "/": ("index.html", "text/html; charset=utf-8"), + "/app.js": ("app.js", "text/javascript; charset=utf-8"), + "/style.css": ("style.css", "text/css; charset=utf-8"), +} + + +class SolverApp(HTTPServer): + """One local user, sequential actions, one current single-use quote.""" + + def __init__(self, client: MorphIQ, port: int = 8765) -> None: + super().__init__(("127.0.0.1", port), Handler) + self.client = client + self.quote: tuple[str, dict[str, Any], int] | None = None + self.origin = f"http://127.0.0.1:{self.server_port}" + + def action(self, path: str, payload: dict[str, Any]) -> dict[str, Any]: + if path == "/api/quote": + self.quote = None + request = ScenarioSpotSolveRequest.model_validate(payload["request"]) + wire = request.to_wire() + if path == "/api/quote": + # Quoting does not execute the solver or reserve funds. + quote = self.client.quote(request) + token = secrets.token_urlsafe(24) + self.quote = (token, wire, quote.quote.total_fcu) + return { + "ok": True, + "quote_id": token, + "quoted_cu": str(quote.quote.total_fcu), + "raw_response": quote.raw_body.decode(errors="replace"), + "charged_cu": str(quote.metadata.fcu_charged) + if quote.metadata.fcu_charged is not None + else None, + } + current = self.quote + if current is None or payload.get("quote_id") != current[0] or wire != current[1]: + raise ValueError("Get a new quote for these inputs before solving.") + # Accept a decimal string so browser number rounding cannot change a CU cap. + cap_text = payload.get("max_cu") + if not isinstance(cap_text, str) or not cap_text.isascii() or not cap_text.isdecimal(): + raise ValueError("The CU limit must be a nonnegative whole number.") + cap = int(cap_text) + if cap > 2**64 - 1: + raise ValueError("The CU limit must fit an unsigned 64-bit integer.") + if cap < current[2]: + raise ValueError("Quote exceeds the CU limit; no solve submitted.") + # Consume before sending: even an unknown outcome cannot be replayed by + # a double-click. A deliberate new quote is required for another solve. + self.quote = None + response = self.client.solve_spot(request, max_cu=cap) + return { + "ok": True, + "raw_response": response.raw_body.decode(errors="replace"), + "charged_cu": str(response.metadata.fcu_charged) + if response.metadata.fcu_charged is not None + else None, + } + + +class Handler(BaseHTTPRequestHandler): + server: SolverApp + + def setup(self) -> None: + super().setup() + self.connection.settimeout(15) + + def log_message(self, format: str, *args: Any) -> None: + # Do not log credentials, inputs, query strings or response bodies. + pass + + def reply(self, status: int, body: bytes, content_type: str) -> None: + self.send_response(status) + self.send_header("Content-Type", content_type) + self.send_header("Content-Length", str(len(body))) + self.send_header("Cache-Control", "no-store") + self.send_header("X-Content-Type-Options", "nosniff") + self.send_header("Referrer-Policy", "no-referrer") + self.send_header( + "Content-Security-Policy", + "default-src 'self'; script-src 'self'; style-src 'self'; " + "connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'", + ) + self.end_headers() + self.wfile.write(body) + + def json_reply(self, status: int, payload: dict[str, Any]) -> None: + self.reply(status, json.dumps(payload).encode(), "application/json") + + def local_request(self) -> bool: + if self.headers.get("Host") != self.server.origin.removeprefix("http://"): + self.json_reply(403, {"ok": False, "message": "Use the printed local URL."}) + return False + return True + + def do_GET(self) -> None: + if not self.local_request(): + return + asset = ASSETS.get(self.path) + if asset is None: + self.json_reply(404, {"ok": False, "message": "Not found"}) + return + name, content_type = asset + self.reply(200, Path(__file__).with_name(name).read_bytes(), content_type) + + def do_POST(self) -> None: + if not self.local_request(): + return + # No cross-origin billing through this local process, including requests + # from unrelated localhost ports. There is no CORS or generic proxy route. + if self.headers.get("Origin") != self.server.origin: + self.json_reply(403, {"ok": False, "message": "Only this app may submit actions."}) + return + if self.path not in ("/api/quote", "/api/solve"): + self.json_reply(404, {"ok": False, "message": "Not found"}) + return + try: + length = int(self.headers.get("Content-Length", "0")) + if not 0 < length <= 32768 or self.headers.get("Content-Type") != "application/json": + raise ValueError("Expected a JSON request of at most 32 KiB.") + payload = json.loads(self.rfile.read(length)) + allowed = ( + {"request"} if self.path == "/api/quote" else {"request", "quote_id", "max_cu"} + ) + if not isinstance(payload, dict) or set(payload) != allowed: + raise ValueError("Invalid action fields.") + result = self.server.action(self.path, payload) + except ValidationError: + self.json_reply( + 400, + { + "ok": False, + "message": "Invalid solver inputs; no API call.", + "local_refusal": True, + }, + ) + return + except (ValueError, KeyError, UnicodeError) as error: + self.json_reply(400, {"ok": False, "message": str(error), "local_refusal": True}) + return + except (APIError, ResponseError) as error: + self.json_reply( + 502, + { + "ok": False, + "message": str(error), + "upstream_status": error.status_code if isinstance(error, APIError) else None, + "request_id": error.request_id, + "metadata": error.metadata.model_dump(mode="json", exclude_unset=True) + if error.metadata + else None, + "raw_response": error.body.decode(errors="replace"), + "charged_cu": str(error.metadata.fcu_charged) + if error.metadata and error.metadata.fcu_charged is not None + else None, + }, + ) + return + except TransportError: + self.json_reply( + 502, + { + "ok": False, + "message": "No complete API response. Execution and charge unknown. " + "No retry attempted; reconcile before submitting again.", + }, + ) + return + self.json_reply(200, result) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--port", type=int, default=8765, help="loopback port (0 chooses a free port)" + ) + args = parser.parse_args() + if not 0 <= args.port <= 65535: + parser.error("Port must be between 0 and 65535.") + with MorphIQ() as client, SolverApp(client, args.port) as server: + print(f"Solver app: {server.origin} (Ctrl+C to stop)", flush=True) + try: + server.serve_forever() + except KeyboardInterrupt: + pass + + +if __name__ == "__main__": + main() diff --git a/examples/solver_app/index.html b/examples/solver_app/index.html new file mode 100644 index 0000000..2641a9e --- /dev/null +++ b/examples/solver_app/index.html @@ -0,0 +1,72 @@ + + + + + MorphIQ · Spot solver + + + +
MORPHIQ SDK EXAMPLESLOCAL APP · API COMPUTE
+
+

OPTIONS / INVERSE PRICING

Find the underlying price.

+

Choose a target option price or position P&L. The API searches your spot range and returns boundaries, coverage and a charge receipt.

+
+
+
+

01 Base option

+
+ + + + + + + + +
+
+

02 Target & search range

+
+ + + + +
+ +

Price is per underlying unit in the contract's currency.

+
Scenario assumptions & tolerances
+ + + + +
+

Spot varies; strike, rates and yield stay fixed. Volatility stays fixed at the base or explicit scenario value. Time advances only by the elapsed years above. P&L uses the base model mark, signed quantity and multiplier.

+
+

03 Quote, then solve

+

Flat rate per complete solve: BSM 100 CU · BS1993 200 CU. Your authenticated quote is authoritative and does not reserve funds.

+
QUOTED CU—
+
+

Loading or changing inputs sends no API requests. Each solve requires a fresh quote and an explicit click. No automatic retries.

+
+
+
+

API RESULT

Ready when you are.

Start with the synthetic option, or change the inputs. Get a quote to begin.

+ +
+

Request receipts

Kept for this page session. Missing charge evidence means unknown, never zero. Full API responses preserve exact numbers and provenance.

No requests submitted.

+
+
+ +
+ + diff --git a/examples/solver_app/style.css b/examples/solver_app/style.css new file mode 100644 index 0000000..6e7e606 --- /dev/null +++ b/examples/solver_app/style.css @@ -0,0 +1,2 @@ +:root{color-scheme:light;--ink:#182d35;--muted:#526970;--line:#dce4e4;--accent:#176752;--paper:#f3f6f5} +*{box-sizing:border-box}body{margin:0;background:var(--paper);color:var(--ink);font:15px/1.5 system-ui,sans-serif}header{padding:24px 5vw;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;gap:16px;align-items:center;background:#fff}.brand{font-weight:800;letter-spacing:.14em}.brand span{font-size:11px;letter-spacing:.1em;color:var(--muted);margin-left:16px}.badge{font-size:10px;letter-spacing:.1em;border:1px solid var(--line);border-radius:30px;padding:6px 10px}main{max-width:1320px;margin:auto;padding:36px 4vw}.intro{max-width:750px;margin-bottom:30px}.eyebrow{font-size:11px;font-weight:700;letter-spacing:.15em;color:var(--accent);margin:0 0 10px}h1{font-size:clamp(30px,4vw,44px);line-height:1.12;letter-spacing:-.035em;margin:0 0 16px}.intro>p:last-child{font-size:17px;color:var(--muted)}.layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.12fr);gap:24px;align-items:start}.card{background:#fff;border:1px solid var(--line);border-radius:12px;padding:24px;margin-bottom:20px}h2{font-size:18px;margin:0 0 20px}h3{font-size:13px;margin:24px 0 10px}.step{font-size:12px;color:var(--accent);margin-right:12px}.fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px;margin:18px 0}label{display:flex;flex-direction:column;gap:7px;font-size:12px;font-weight:600;color:var(--muted)}input,select{min-width:0;width:100%;border:1px solid #bfcecd;border-radius:6px;background:#fff;color:var(--ink);font:15px system-ui;padding:10px}input:focus,select:focus,button:focus-visible,summary:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.hint,footer{font-size:12px;color:var(--muted)}.actions{display:flex;gap:10px;flex-wrap:wrap;margin:20px 0 12px}button{border:1px solid var(--accent);color:var(--accent);background:#fff;border-radius:7px;padding:12px;font:600 12px system-ui;cursor:pointer}button.primary{color:#fff;background:var(--accent)}button:disabled{opacity:.45;cursor:not-allowed}.quoted{display:flex;flex-direction:column;justify-content:center;gap:2px;padding-left:12px}.quoted span{font-size:10px;color:var(--muted);letter-spacing:.1em}.quoted strong{font-size:28px;line-height:1}.metrics{display:grid;grid-template-columns:1fr 1fr;gap:12px}.metric{border:1px solid var(--line);border-radius:6px;padding:12px}.metric span{display:block;font-size:11px;color:var(--muted)}.metric strong{font-size:16px;overflow-wrap:anywhere}details{margin-top:14px}summary{font-size:12px;font-weight:600;cursor:pointer}pre{font:11px/1.6 ui-monospace,monospace;white-space:pre-wrap;overflow-wrap:anywhere;background:#f4f7f6;padding:12px;border-radius:6px;max-height:480px;overflow:auto}.table-wrap{overflow-x:auto}table{width:100%;border-collapse:collapse;font-size:11px;text-align:left}th,td{padding:9px 7px;border-bottom:1px solid var(--line);white-space:nowrap}th{color:var(--muted);font-weight:600}.empty{font-size:13px;color:var(--muted)}.receipt{border-top:1px solid var(--line);padding:14px 0;overflow-wrap:anywhere}.receipt p{font-size:12px;margin:4px 0}.receipt strong{font-size:13px}footer{padding:8px 0 20px}[hidden]{display:none!important}@media(max-width:800px){.layout{grid-template-columns:1fr}header{padding:20px}.brand span{display:block;margin:5px 0 0}main{padding:28px 18px}.card{padding:20px}.badge{font-size:8px}} diff --git a/scripts/check_package.py b/scripts/check_package.py index 300e6b0..6d30e97 100644 --- a/scripts/check_package.py +++ b/scripts/check_package.py @@ -3,6 +3,7 @@ from __future__ import annotations import os +import shutil import subprocess import sys import tarfile @@ -99,6 +100,25 @@ def main() -> None: check=True, ) + # The browser example is shipped in the sdist, uses no optional extras, + # and can run outside the checkout with only the installed SDK wheel. + app_path = temporary / "solver_app" + shutil.copytree( + root / "examples/solver_app", app_path, ignore=shutil.ignore_patterns("__pycache__") + ) + with tarfile.open(sources[0]) as source: + for name in ("app.py", "index.html", "app.js", "style.css", "README.md"): + member = source.extractfile( + f"morphiq_sdk-{expected_version}/examples/solver_app/{name}" + ) + assert member is not None and member.read() == (app_path / name).read_bytes() + subprocess.run( + [str(python), "-I", str(app_path / "app.py"), "--help"], + cwd=temporary, + check=True, + ) + print("Standalone solver app and sdist assets verified with installed wheel") + # Check the optional extra independently of the notebook/Yahoo environment. subprocess.run( [str(python), "-m", "pip", "install", f"{wheels[0]}[pandas]"], diff --git a/tests/test_examples.py b/tests/test_examples.py index 881f5cf..0634b56 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -26,7 +26,7 @@ def test_examples_import_without_credentials_or_requests(): sys.executable, "-c", "import examples.cost_quote, examples.price_european, examples.price_american, " - "examples.price_async, examples.quickstart, " + "examples.price_async, examples.quickstart, examples.solver_app.app, " "examples.identify_regime, examples.identify_regime_async, " "examples.greeks, examples.greeks_async, " "examples.implied_volatility, examples.implied_volatility_async, " diff --git a/tests/test_solver_app.py b/tests/test_solver_app.py new file mode 100644 index 0000000..4921f7e --- /dev/null +++ b/tests/test_solver_app.py @@ -0,0 +1,216 @@ +"""The runnable example's HTTP -> SDK -> API boundary and billing controls.""" + +import copy +import hashlib +import importlib.util +import json +import os +import threading +from contextlib import contextmanager +from pathlib import Path + +import httpx +import pytest + +from morphiq_sdk import MorphIQ + +ROOT = Path(__file__).parents[1] +SPEC = importlib.util.spec_from_file_location("solver_example", ROOT / "examples/solver_app/app.py") +APP = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(APP) +CASES = json.loads((ROOT / "tests/fixtures/scenarios.json").read_text())["cases"] +SOLVES = [case for case in CASES if case["name"].startswith("solve-")] +SUCCESS = [case for case in SOLVES if case["status"] == 200] + + +@contextmanager +def running_app(client): + with APP.SolverApp(client, port=0) as server: + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + with httpx.Client(base_url=server.origin, headers={"Origin": server.origin}) as browser: + yield browser + finally: + server.shutdown() + thread.join(timeout=5) + assert not thread.is_alive() + + +@contextmanager +def fixture_app(case, *, fault=None): + calls = [] + + def upstream(request): + calls.append(request) + assert request.headers["authorization"] == "Bearer app-test-key" + if request.url.path == "/v1/cost/quote": + return httpx.Response(200, json=case["quote"]) + assert request.url.path == "/v1/scenarios/solve-spot" + assert json.loads(request.content) == case["request"] + assert request.headers["x-morphiq-max-cu"] == str(case["expected_cu"]) + if fault: + return fault(request) + return httpx.Response(case["status"], json=case["response"]) + + with MorphIQ(api_key="app-test-key", transport=httpx.MockTransport(upstream)) as client: + with running_app(client) as browser: + yield browser, calls + + +def quote_and_solve(browser, case): + quote = browser.post("/api/quote", json={"request": case["request"]}) + assert quote.status_code == 200, quote.text + payload = { + "request": case["request"], + "quote_id": quote.json()["quote_id"], + "max_cu": str(case["expected_cu"]), + } + return quote.json(), browser.post("/api/solve", json=payload), payload + + +@pytest.mark.parametrize("case", SUCCESS, ids=lambda case: case["name"]) +def test_app_preserves_native_results_receipts_and_has_no_hidden_calls(case): + with fixture_app(case) as (browser, calls): + for path, content in [ + ("/", "Find the underlying price"), + ("/app.js", "solve"), + ("/style.css", "@media"), + ]: + result = browser.get(path) + assert result.status_code == 200 and content in result.text + assert "frame-ancestors 'none'" in result.headers["content-security-policy"] + assert "app-test-key" not in result.text + assert not calls + quote, result, payload = quote_and_solve(browser, case) + assert quote["quoted_cu"] == str(case["expected_cu"]) + assert json.loads(quote["raw_response"]) == case["quote"] + assert result.status_code == 200 + assert result.json()["charged_cu"] == str(case["expected_cu"]) + assert json.loads(result.json()["raw_response"]) == case["response"] + assert len(calls) == 2 + # A stale click/replay cannot submit another billable solve. + assert browser.post("/api/solve", json=payload).status_code == 400 + assert len(calls) == 2 + + +def test_app_refuses_below_cap_changed_inputs_and_unquoted_requests_locally(): + case = SUCCESS[0] + with fixture_app(case) as (browser, calls): + payload = {"request": case["request"], "quote_id": "none", "max_cu": "100"} + assert browser.post("/api/solve", json=payload).status_code == 400 + assert not calls + quote = browser.post("/api/quote", json={"request": case["request"]}).json() + payload["quote_id"] = quote["quote_id"] + for cap in ("99", "-1", "1.2", "NaN", str(2**64), 100, True, None): + payload["max_cu"] = cap + result = browser.post("/api/solve", json=payload) + assert result.status_code == 400 and result.json()["local_refusal"] + payload["max_cu"] = "100" + payload["request"] = copy.deepcopy(case["request"]) + payload["request"]["target"]["value"] += 1 + assert browser.post("/api/solve", json=payload).status_code == 400 + assert len(calls) == 1 + + +@pytest.mark.parametrize( + "headers", [{"Origin": "https://example.com"}, {"Origin": "null"}, {"Host": "example.com"}] +) +def test_other_origins_cannot_bill_through_local_process(headers): + with fixture_app(SUCCESS[0]) as (browser, calls): + result = browser.post( + "/api/quote", json={"request": SUCCESS[0]["request"]}, headers=headers + ) + assert result.status_code == 403 + assert not calls + + +def test_local_route_and_body_validation_never_call_api(): + with fixture_app(SUCCESS[0]) as (browser, calls): + assert browser.get("/../app.py").status_code == 404 + assert browser.post("/v1/pricing/price", json={}).status_code == 404 + for body in ("[]", "{}", '{"request": {}}', "!", "x" * 32769): + assert ( + browser.post( + "/api/quote", content=body, headers={"Content-Type": "application/json"} + ).status_code + == 400 + ) + assert browser.post("/api/quote", data={"request": "bad"}).status_code == 400 + assert not calls + + +@pytest.mark.parametrize("name", ["solve-bsm-numerical-error", "solve-bsm-invalid"]) +def test_api_errors_preserve_known_charge_and_request_id(name): + error = next(case for case in SOLVES if case["name"] == name) + case = SUCCESS[0] + with fixture_app( + case, fault=lambda _: httpx.Response(error["status"], json=error["response"]) + ) as (browser, calls): + _, result, payload = quote_and_solve(browser, case) + assert result.status_code == 502 + body = result.json() + assert body["upstream_status"] == error["status"] + assert json.loads(body["raw_response"]) == error["response"] + assert body["metadata"] == error["response"]["metadata"] + assert body["charged_cu"] == str(error["response"]["metadata"]["fcu_charged"]) + assert browser.post("/api/solve", json=payload).status_code == 400 + assert len(calls) == 2 + + +def test_invalid_api_result_preserves_receipt_without_retry(): + case = SUCCESS[0] + malformed = copy.deepcopy(case["response"]) + malformed.pop("result") + with fixture_app(case, fault=lambda _: httpx.Response(200, json=malformed)) as (browser, calls): + _, result, _ = quote_and_solve(browser, case) + assert result.status_code == 502 + assert result.json()["charged_cu"] == str(case["expected_cu"]) + assert result.json()["metadata"] == malformed["metadata"] + assert len(calls) == 2 + + +def test_disconnected_response_has_unknown_charge_and_cannot_replay(): + def disconnect(request): + raise httpx.ReadTimeout("fixture disconnect", request=request) + + with fixture_app(SUCCESS[0], fault=disconnect) as (browser, calls): + _, result, payload = quote_and_solve(browser, SUCCESS[0]) + assert result.status_code == 502 + assert "charge unknown" in result.json()["message"] + assert "charged_cu" not in result.json() + assert browser.post("/api/solve", json=payload).status_code == 400 + assert len(calls) == 2 + + +@pytest.mark.integration +def test_example_app_against_protected_native_api(): + url, key = os.getenv("MORPHIQ_TEST_API_URL"), os.getenv("MORPHIQ_TEST_API_KEY") + if not url or not key: + pytest.skip("Requires an explicitly configured protected candidate API") + provenance = json.loads((ROOT / "contracts/provenance.json").read_text()) + schema = httpx.get(f"{url}/openapi.json", trust_env=False) + assert hashlib.sha256(schema.content).hexdigest() == provenance["sha256"] + with MorphIQ(base_url=url, api_key=key) as client, running_app(client) as browser: + for case in SUCCESS: + quote, response, _ = quote_and_solve(browser, case) + assert response.status_code == 200, response.text + result = json.loads(response.json()["raw_response"]) + assert ( + int(quote["quoted_cu"]) == result["metadata"]["fcu_charged"] == case["expected_cu"] + ) + assert result["result"] == case["response"]["result"] + + +@pytest.mark.parametrize("charge", [None, 0, 2**100]) +def test_app_keeps_missing_zero_and_large_receipts_distinct(charge): + case = copy.deepcopy(SUCCESS[0]) + if charge is None: + case["response"]["metadata"].pop("fcu_charged") + else: + case["response"]["metadata"]["fcu_charged"] = charge + with fixture_app(case) as (browser, calls): + _, response, _ = quote_and_solve(browser, case) + assert response.status_code == 200 + assert response.json()["charged_cu"] == (None if charge is None else str(charge)) + assert len(calls) == 2