feat(DATA-003): calculate live option greeks

This commit is contained in:
Bu5hm4nn
2026-03-23 23:46:40 +01:00
parent 46ce81d2d6
commit de03bd0064
5 changed files with 218 additions and 37 deletions

View File

@@ -5,9 +5,10 @@ from __future__ import annotations
import asyncio
import logging
import math
from datetime import UTC, datetime
from datetime import datetime, timezone
from typing import Any
from app.core.calculations import option_row_greeks
from app.services.cache import CacheService
from app.strategies.engine import StrategySelectionEngine
@@ -41,7 +42,7 @@ class DataService:
"portfolio_value": round(quote["price"] * 1000, 2),
"loan_amount": 600_000.0,
"ltv_ratio": round(600_000.0 / max(quote["price"] * 1000, 1), 4),
"updated_at": datetime.now(UTC).isoformat(),
"updated_at": datetime.now(timezone.utc).isoformat(),
"source": quote["source"],
}
await self.cache.set_json(cache_key, portfolio)
@@ -91,7 +92,7 @@ class DataService:
payload = {
"symbol": ticker_symbol,
"updated_at": datetime.now(UTC).isoformat(),
"updated_at": datetime.now(timezone.utc).isoformat(),
"expirations": expirations,
"underlying_price": quote["price"],
"source": "yfinance",
@@ -146,8 +147,8 @@ class DataService:
try:
ticker = yf.Ticker(ticker_symbol)
chain = await asyncio.to_thread(ticker.option_chain, target_expiry)
calls = self._normalize_option_rows(chain.calls, ticker_symbol, target_expiry, "call")
puts = self._normalize_option_rows(chain.puts, ticker_symbol, target_expiry, "put")
calls = self._normalize_option_rows(chain.calls, ticker_symbol, target_expiry, "call", quote["price"])
puts = self._normalize_option_rows(chain.puts, ticker_symbol, target_expiry, "put", quote["price"])
if not calls and not puts:
payload = self._fallback_options_chain(
@@ -164,7 +165,7 @@ class DataService:
payload = {
"symbol": ticker_symbol,
"selected_expiry": target_expiry,
"updated_at": datetime.now(UTC).isoformat(),
"updated_at": datetime.now(timezone.utc).isoformat(),
"expirations": expirations,
"calls": calls,
"puts": puts,
@@ -210,7 +211,7 @@ class DataService:
return {
"symbol": ticker,
"updated_at": datetime.now(UTC).isoformat(),
"updated_at": datetime.now(timezone.utc).isoformat(),
"paper_parameters": {
"portfolio_value": engine.portfolio_value,
"loan_amount": engine.loan_amount,
@@ -247,7 +248,7 @@ class DataService:
"price": round(last, 4),
"change": change,
"change_percent": change_percent,
"updated_at": datetime.now(UTC).isoformat(),
"updated_at": datetime.now(timezone.utc).isoformat(),
"source": "yfinance",
}
except Exception as exc: # pragma: no cover - network dependent
@@ -264,7 +265,7 @@ class DataService:
) -> dict[str, Any]:
payload = {
"symbol": symbol,
"updated_at": datetime.now(UTC).isoformat(),
"updated_at": datetime.now(timezone.utc).isoformat(),
"expirations": [],
"underlying_price": quote["price"],
"source": source,
@@ -286,7 +287,7 @@ class DataService:
options_chain = {
"symbol": symbol,
"selected_expiry": selected_expiry,
"updated_at": datetime.now(UTC).isoformat(),
"updated_at": datetime.now(timezone.utc).isoformat(),
"expirations": expirations,
"calls": [],
"puts": [],
@@ -298,7 +299,14 @@ class DataService:
options_chain["error"] = error
return options_chain
def _normalize_option_rows(self, frame: Any, symbol: str, expiry: str, option_type: str) -> list[dict[str, Any]]:
def _normalize_option_rows(
self,
frame: Any,
symbol: str,
expiry: str,
option_type: str,
underlying_price: float,
) -> list[dict[str, Any]]:
if frame is None or getattr(frame, "empty", True):
return []
@@ -314,27 +322,22 @@ class DataService:
implied_volatility = self._safe_float(item.get("impliedVolatility"))
contract_symbol = str(item.get("contractSymbol") or "").strip()
rows.append(
{
"contractSymbol": contract_symbol,
"symbol": contract_symbol or f"{symbol} {expiry} {option_type.upper()} {strike:.2f}",
"strike": strike,
"bid": bid,
"ask": ask,
"premium": last_price or self._midpoint(bid, ask),
"lastPrice": last_price,
"impliedVolatility": implied_volatility,
"expiry": expiry,
"type": option_type,
"openInterest": int(self._safe_float(item.get("openInterest"))),
"volume": int(self._safe_float(item.get("volume"))),
"delta": 0.0,
"gamma": 0.0,
"theta": 0.0,
"vega": 0.0,
"rho": 0.0,
}
)
row = {
"contractSymbol": contract_symbol,
"symbol": contract_symbol or f"{symbol} {expiry} {option_type.upper()} {strike:.2f}",
"strike": strike,
"bid": bid,
"ask": ask,
"premium": last_price or self._midpoint(bid, ask),
"lastPrice": last_price,
"impliedVolatility": implied_volatility,
"expiry": expiry,
"type": option_type,
"openInterest": int(self._safe_float(item.get("openInterest"))),
"volume": int(self._safe_float(item.get("volume"))),
}
row.update(option_row_greeks(row, underlying_price))
rows.append(row)
return rows
@staticmethod
@@ -358,6 +361,6 @@ class DataService:
"price": 215.0,
"change": 0.0,
"change_percent": 0.0,
"updated_at": datetime.now(UTC).isoformat(),
"updated_at": datetime.now(timezone.utc).isoformat(),
"source": source,
}