diff --git a/app/api/hurst.py b/app/api/hurst.py index 2082bb2f..65c83a10 100644 --- a/app/api/hurst.py +++ b/app/api/hurst.py @@ -52,7 +52,7 @@ def _range_std(pdf: Any, range_seconds: float, seconds_in_day: int) -> float: def _ohlc_hurst(df: pd.DataFrame, serie: str, periods: tuple) -> dict[str, float]: template = OHLC( - serie=serie, + series=serie, period="10m", rogers_satchell_variance=True, parkinson_variance=True, @@ -97,7 +97,7 @@ async def hurst_wiener( gk_vals = [] rs_vals = [] template = OHLC( - serie="0", + series="0", period="10m", rogers_satchell_variance=True, parkinson_variance=True, diff --git a/docs/api/options/moneyness.md b/docs/api/options/moneyness.md new file mode 100644 index 00000000..7c0b7148 --- /dev/null +++ b/docs/api/options/moneyness.md @@ -0,0 +1,10 @@ +# Moneyness + +Conversion utilities between strike, log-strike, and +[moneyness](../../glossary.md#moneyness) representations. + +These functions provide a single authoritative source for the relationships +between absolute strikes, log-strikes, and time-scaled moneyness used +throughout the library. + +::: quantflow.options.moneyness diff --git a/docs/api/options/vol_surface.md b/docs/api/options/vol_surface.md index 30a7abea..074e2534 100644 --- a/docs/api/options/vol_surface.md +++ b/docs/api/options/vol_surface.md @@ -17,8 +17,6 @@ ## Bid/Ask Prices -::: quantflow.options.surface.Price - ::: quantflow.options.surface.SpotPrice ::: quantflow.options.surface.FwdPrice @@ -27,8 +25,6 @@ ::: quantflow.options.surface.OptionArrays -::: quantflow.options.surface.OptionMetadata - ::: quantflow.options.surface.OptionPrice ::: quantflow.options.surface.OptionPrices @@ -41,6 +37,8 @@ ::: quantflow.options.inputs.OptionType +::: quantflow.options.inputs.OptionMetadata + ::: quantflow.options.inputs.VolSurfaceInputs ::: quantflow.options.inputs.VolSurfaceInput diff --git a/docs/api/ta/ewma.md b/docs/api/ta/ewma.md index adec4a1b..0b171dfe 100644 --- a/docs/api/ta/ewma.md +++ b/docs/api/ta/ewma.md @@ -1,4 +1,4 @@ # EWMA -::: quantflow.ta.EWMA +::: quantflow.ta.ewma.EWMA diff --git a/docs/api/ta/kalman.md b/docs/api/ta/kalman.md index 20c8495e..4f1d395d 100644 --- a/docs/api/ta/kalman.md +++ b/docs/api/ta/kalman.md @@ -1,3 +1,3 @@ # Kalman Filter -::: quantflow.ta.KalmanFilter +::: quantflow.ta.kalman.KalmanFilter diff --git a/docs/api/ta/supersmoother.md b/docs/api/ta/supersmoother.md index e3d3b1f8..a7295791 100644 --- a/docs/api/ta/supersmoother.md +++ b/docs/api/ta/supersmoother.md @@ -11,4 +11,4 @@ Reference: Ehlers, J. (2013). "Cycle Analytics for Traders" -::: quantflow.ta.SuperSmoother +::: quantflow.ta.supersmoother.SuperSmoother diff --git a/docs/api/utils/price.md b/docs/api/utils/price.md new file mode 100644 index 00000000..34e7549c --- /dev/null +++ b/docs/api/utils/price.md @@ -0,0 +1,5 @@ +## Bid/Ask Prices + +::: quantflow.utils.price.Price + +::: quantflow.utils.price.PriceVolume diff --git a/mkdocs.yml b/mkdocs.yml index 815743a3..9f59caa0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -74,6 +74,7 @@ nav: - Black-Scholes: api/options/black.md - Calibration: api/options/calibration.md - Deep IV Factor Model: api/options/divfm.md + - Moneyness: api/options/moneyness.md - Put-Call Parity: api/options/parity.md - Pricer: api/options/pricer.md - SVI Smile: api/options/svi.md @@ -90,7 +91,7 @@ nav: - Poisson Process: api/sp/poisson.md - Wiener Process: api/sp/wiener.md - Copulas: api/sp/copula.md - - Technical Analysis: + - Timeseries Analysis: - api/ta/index.md - EWMA: api/ta/ewma.md - Kalman Filter: api/ta/kalman.md @@ -109,6 +110,7 @@ nav: - Distributions: api/utils/distributions.md - Marginal 1D: api/utils/marginal1d.md - Numbers: api/utils/numbers.md + - Price: api/utils/price.md - Types: api/utils/types.md - Tutorials: - tutorials/index.md diff --git a/pyproject.toml b/pyproject.toml index 048d7b3c..13c61f12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,10 +33,10 @@ readme = "readme.md" requires-python = ">=3.11,<3.15" dependencies = [ "scipy>=1.14.1", + "pandas>=2.0.0", "pydantic>=2.0.2", "ccy>=2.0.0", "python-dotenv>=1.0.1", - "polars[pandas,pyarrow]>=1.11.0", "statsmodels>=0.14.6,<0.15.0", ] diff --git a/quantflow/options/calibration/base.py b/quantflow/options/calibration/base.py index 49ad4658..f3ed0a4c 100644 --- a/quantflow/options/calibration/base.py +++ b/quantflow/options/calibration/base.py @@ -15,6 +15,7 @@ from quantflow.utils import plot from ..bs import implied_black_volatility +from ..moneyness import log_strike_from_moneyness, moneyness_from_log_strike from ..pricer import OptionPricerBase from ..surface import OptionPrice, VolSurface @@ -150,7 +151,9 @@ def model_post_init(self, _ctx: Any) -> None: entries = tuple(self.options.values()) self._log_strikes = np.asarray([e.log_strike for e in entries]) self._ttms = np.asarray([e.ttm for e in entries]) - self._moneyness = self._log_strikes / np.sqrt(self._ttms) + self._moneyness = np.asarray( + moneyness_from_log_strike(self._log_strikes, self._ttms) + ) self._mid_prices = np.asarray([e.mid_price() for e in entries]) self._mid_ivs = np.asarray([e.mid_iv() for e in entries]) @@ -223,7 +226,7 @@ def cost_weight(self, ttm: float, log_strike: float) -> float: Up-weights wing options via `exp(moneyness_weight * moneyness**2)`, capped at `max_cost_weight`. The quadratic form mimics `1/vega`. """ - moneyness = log_strike / np.sqrt(ttm) + moneyness = float(moneyness_from_log_strike(log_strike, ttm)) weight = np.exp(self.moneyness_weight * moneyness * moneyness) return float(min(weight, self.max_cost_weight)) @@ -281,7 +284,11 @@ def cost_function(self, params: np.ndarray) -> float: def _model_grid( self, ttm: float, max_moneyness: float, support: int ) -> pd.DataFrame: - log_strikes = np.linspace(-max_moneyness, max_moneyness, support) * np.sqrt(ttm) + log_strikes = np.asarray( + log_strike_from_moneyness( + np.linspace(-max_moneyness, max_moneyness, support), ttm + ) + ) return self.pricer.maturity(ttm).prices(log_strikes) def plot( diff --git a/quantflow/options/inputs.py b/quantflow/options/inputs.py index fbaf2da4..dc4e1709 100644 --- a/quantflow/options/inputs.py +++ b/quantflow/options/inputs.py @@ -2,12 +2,15 @@ import enum from datetime import datetime +from decimal import Decimal from typing import Self, TypeVar from pydantic import BaseModel, Field +from typing_extensions import Annotated, Doc from quantflow.rates import AnyYieldCurve -from quantflow.utils.numbers import ZERO, DecimalNumber +from quantflow.utils.numbers import DecimalNumber +from quantflow.utils.price import PriceVolume P = TypeVar("P") @@ -36,6 +39,29 @@ def call_put(self) -> int: return 1 if self is OptionType.call else -1 +class OptionMetadata(BaseModel): + """Represents the metadata of an option, including its strike, type, maturity, + and other relevant information.""" + + strike: DecimalNumber = Field(description="Strike price of the option") + option_type: OptionType = Field(description="Type of the option, call or put") + maturity: datetime = Field(description="Maturity date of the option") + inverse: bool = Field( + default=True, + description=( + "Whether the option is an inverse option (i.e. quoted in terms of the " + "underlying) or not (i.e. quoted in terms of the quote currency)" + ), + ) + + def is_in_the_money(self, forward: Decimal) -> bool: + """Check if the option is in the money given the forward price""" + if self.option_type.is_call(): + return self.strike < forward + else: + return self.strike > forward + + class VolSecurityType(enum.StrEnum): """Type of security for the volatility surface""" @@ -78,18 +104,7 @@ def option(cls) -> Self: return cls(security_type=VolSecurityType.option) -class VolSurfaceInput(BaseModel): - """Base class for volatility surface inputs""" - - bid: DecimalNumber = Field(description="Bid price of the security") - ask: DecimalNumber = Field(description="Ask price of the security") - open_interest: DecimalNumber = Field( - default=ZERO, description="Open interest of the security" - ) - volume: DecimalNumber = Field(default=ZERO, description="Volume of the security") - - -class SpotInput(VolSurfaceInput): +class SpotInput(PriceVolume): """Input data for a spot contract in the volatility surface""" security_type: VolSecurityType = Field( @@ -98,7 +113,7 @@ class SpotInput(VolSurfaceInput): ) -class ForwardInput(VolSurfaceInput): +class ForwardInput(PriceVolume): """Input data for a forward contract in the volatility surface""" maturity: datetime = Field(description="Expiry date of the forward contract") @@ -108,12 +123,9 @@ class ForwardInput(VolSurfaceInput): ) -class OptionInput(VolSurfaceInput): +class OptionInput(PriceVolume, OptionMetadata): """Input data for an option in the volatility surface""" - strike: DecimalNumber = Field(description="Strike price of the option") - maturity: datetime = Field(description="Expiry date of the option") - option_type: OptionType = Field(description="Type of the option - call or put") security_type: VolSecurityType = Field( default=VolSecurityType.option, description="Type of security for the volatility surface", @@ -132,13 +144,12 @@ class OptionInput(VolSurfaceInput): "(e.g. 0.2 for 20%)" ), ) - inverse: bool = Field( - default=True, - description=( - "Whether the security is inverse (i.e. quoted in terms of the underlying) " - "or not (i.e. quoted in terms of the quote currency)" - ), - ) + + +VolSurfaceInput = Annotated[ + SpotInput | ForwardInput | OptionInput, + Doc("Input data for a security in the volatility surface"), +] class VolSurfaceInputs(BaseModel): diff --git a/quantflow/options/moneyness.py b/quantflow/options/moneyness.py new file mode 100644 index 00000000..8edf31d1 --- /dev/null +++ b/quantflow/options/moneyness.py @@ -0,0 +1,84 @@ +"""Moneyness and log-strike conversion utilities. + +This module provides the canonical conversions between absolute strikes, +log-strikes, and time-scaled moneyness used throughout the library. + +See the [glossary](../glossary.md#moneyness) for definitions. +""" + +from __future__ import annotations + +import math +from decimal import Decimal + +import numpy as np +from typing_extensions import Annotated, Doc + +from quantflow.utils.numbers import to_decimal +from quantflow.utils.types import FloatArrayLike + + +def moneyness_from_log_strike( + log_strike: Annotated[FloatArrayLike, Doc("Log-strike")], + ttm: Annotated[FloatArrayLike, Doc("Time to maturity in years")], +) -> FloatArrayLike: + r"""Convert log-strike $k$ to moneyness $m$. + + \begin{equation} + m = \frac{k}{\sqrt{\tau}} + \end{equation} + """ + return log_strike / np.sqrt(ttm) + + +def log_strike_from_moneyness( + moneyness: Annotated[FloatArrayLike, Doc("Time-scaled moneyness")], + ttm: Annotated[FloatArrayLike, Doc("Time to maturity in years")], +) -> FloatArrayLike: + r"""Convert time-scaled moneyness to log-strike. + + \begin{equation} + k = m \sqrt{\tau} + \end{equation} + """ + return moneyness * np.sqrt(ttm) + + +def strike_from_moneyness( + moneyness: Annotated[float, Doc("Time-scaled moneyness")], + ttm: Annotated[float, Doc("Time to maturity in years")], + forward: Annotated[float, Doc("Forward price of the underlying")], +) -> Decimal: + r"""Convert time-scaled moneyness to an absolute strike price. + + \begin{equation} + K = F \exp\left(m \sqrt{\tau}\right) + \end{equation} + """ + return to_decimal(forward * math.exp(moneyness * math.sqrt(ttm))) + + +def strike_from_log_strike( + log_strike: Annotated[float, Doc("Log-strike")], + forward: Annotated[float, Doc("Forward price of the underlying")], +) -> Decimal: + r"""Convert log-strike to an absolute strike price. + + \begin{equation} + K = F \exp(k) + \end{equation} + """ + return to_decimal(forward * math.exp(log_strike)) + + +def log_strike_from_strike( + strike: Annotated[float | Decimal, Doc("Absolute strike price")], + forward: Annotated[float | Decimal, Doc("Forward price of the underlying")], +) -> float: + r"""Convert an absolute strike to log-strike. + + \begin{equation} + k = \ln\frac{K}{F} + \end{equation} + """ + return math.log(float(strike) / float(forward)) diff --git a/quantflow/options/pricer.py b/quantflow/options/pricer.py index 19c53456..f3a65910 100644 --- a/quantflow/options/pricer.py +++ b/quantflow/options/pricer.py @@ -17,6 +17,7 @@ from ..utils.types import FloatArray, FloatArrayLike from .bs import BlackSensitivities, implied_black_volatility from .inputs import OptionType +from .moneyness import log_strike_from_moneyness, moneyness_from_log_strike M = TypeVar("M", bound=StochasticProcess1D) @@ -143,7 +144,7 @@ class MaturityPricer(BaseModel, arbitrary_types_allowed=True): def moneyness(self, log_strikes: FloatArrayLike) -> FloatArrayLike: """Time-adjusted moneyness for one or many log-strikes""" - return log_strikes / np.sqrt(self.ttm) + return moneyness_from_log_strike(log_strikes, self.ttm) def price( self, @@ -237,8 +238,8 @@ def price( self, option_type: Annotated[OptionType, Doc("Type of the option (call or put)")], ttm: Annotated[float, Doc("Time to maturity")], - strike: Annotated[float, Doc("Strike price of the option")], - forward: Annotated[float, Doc("Forward price of the underlying")], + strike: Annotated[float | Decimal, Doc("Strike price of the option")], + forward: Annotated[float | Decimal, Doc("Forward price of the underlying")], ) -> ModelOptionPrice: """Price a single option @@ -283,7 +284,9 @@ def plot3d( implied = np.zeros((len(ttm), len(moneyness))) for i, t in enumerate(ttm): maturity = self.maturity(cast(float, t)) - implied[i, :] = maturity.prices(moneyness * np.sqrt(t))["iv"] + implied[i, :] = maturity.prices( + np.asarray(log_strike_from_moneyness(moneyness, t)) + )["iv"] properties: dict = dict( xaxis_title="moneyness", yaxis_title="TTM", diff --git a/quantflow/options/strategies/__init__.py b/quantflow/options/strategies/__init__.py index 3935e216..b26eb223 100644 --- a/quantflow/options/strategies/__init__.py +++ b/quantflow/options/strategies/__init__.py @@ -1,4 +1,4 @@ -from .base import Strategy, StrategyLeg, StrategyPrice +from .base import Strategy, StrategyError, StrategyLeg, StrategyPrice from .butterfly import Butterfly from .calendar_spread import CalendarSpread from .spread import Spread @@ -10,6 +10,7 @@ "CalendarSpread", "Spread", "Strategy", + "StrategyError", "StrategyLeg", "StrategyPrice", "Straddle", diff --git a/quantflow/options/strategies/base.py b/quantflow/options/strategies/base.py index 1845c4e0..cb53a1d0 100644 --- a/quantflow/options/strategies/base.py +++ b/quantflow/options/strategies/base.py @@ -1,6 +1,5 @@ from __future__ import annotations -import math from datetime import datetime from pathlib import Path from typing import ClassVar @@ -9,9 +8,10 @@ from pydantic import BaseModel, Field from typing_extensions import Annotated, Doc -from quantflow.options.inputs import OptionType +from quantflow.options.inputs import OptionMetadata from quantflow.options.pricer import ModelOptionPrice, OptionPricer # noqa: TC001 from quantflow.options.surface import default_day_counter +from quantflow.utils.numbers import DecimalNumber OPTIONS_DOCS_PATH = Path(__file__).parent.parent / "docs" @@ -21,32 +21,17 @@ def load_description(filename: str) -> str: return (OPTIONS_DOCS_PATH / filename).read_text(encoding="utf-8") +class StrategyError(ValueError): + """Custom error for invalid strategy construction.""" + + class StrategyLeg(BaseModel, frozen=True): """A single leg of an option strategy.""" - option_type: OptionType = Field(description="Call or put") - quantity: float = Field( + meta: OptionMetadata = Field(description="Option metadata for this leg") + quantity: DecimalNumber = Field( description="Signed quantity: positive for long, negative for short" ) - strike: float = Field(description="Absolute strike price") - maturity: datetime = Field(description="Expiry date of the option") - - @classmethod - def from_moneyness( - cls, - option_type: OptionType, - moneyness: float, - forward: float, - maturity: datetime, - quantity: float = 1.0, - ) -> StrategyLeg: - """Create a leg from a log-strike moneyness offset and forward price.""" - return cls( - option_type=option_type, - quantity=quantity, - strike=forward * math.exp(moneyness), - maturity=maturity, - ) class StrategyPrice(BaseModel, frozen=True): @@ -91,17 +76,18 @@ def price( total_gamma = 0.0 for leg in self.legs: - ttm = day_counter.dcf(ref_date, leg.maturity) + ttm = day_counter.dcf(ref_date, leg.meta.maturity) leg_price = pricer.price( - option_type=leg.option_type, - strike=leg.strike, + option_type=leg.meta.option_type, + strike=leg.meta.strike, forward=forward, ttm=ttm, ) priced.append(leg_price) - total_price += leg.quantity * leg_price.price - total_delta += leg.quantity * leg_price.delta - total_gamma += leg.quantity * leg_price.gamma + q = float(leg.quantity) + total_price += q * leg_price.price + total_delta += q * leg_price.delta + total_gamma += q * leg_price.gamma return StrategyPrice( legs=tuple(priced), diff --git a/quantflow/options/strategies/butterfly.py b/quantflow/options/strategies/butterfly.py index 0b73582e..5b7bfb1d 100644 --- a/quantflow/options/strategies/butterfly.py +++ b/quantflow/options/strategies/butterfly.py @@ -5,96 +5,83 @@ from typing_extensions import Self -from quantflow.options.inputs import OptionType +from quantflow.options.inputs import OptionMetadata, OptionType +from quantflow.options.moneyness import log_strike_from_strike +from quantflow.utils.numbers import Number, to_decimal -from .base import Strategy, StrategyLeg, load_description +from .base import Strategy, StrategyError, StrategyLeg, load_description -def _option_type_for_moneyness(mid_moneyness: float) -> OptionType: - """Select option type based on body moneyness for best liquidity. +def _option_type_for_log_strike(mid_log_strike: float) -> OptionType: + """Select option type based on body position relative to ATM. Calls for body above ATM, puts for body below ATM, calls at ATM. """ - return OptionType.put if mid_moneyness < 0 else OptionType.call + return OptionType.put if mid_log_strike < 0 else OptionType.call class Butterfly(Strategy, frozen=True): """Three-strike strategy: long wings, short body. Long butterfly when quantity > 0, short butterfly when quantity < 0. - Can be constructed with calls or puts — both are equivalent by put-call parity. + Can be constructed with calls or puts, both are equivalent by put-call parity. """ description: ClassVar[str] = load_description("butterfly.md") - @classmethod - def from_moneyness( - cls, - forward: float, - maturity: datetime, - wing_moneyness: float = 0.05, - mid_moneyness: float = 0.0, - quantity: float = 1.0, - option_type: OptionType | None = None, - ) -> Self: - """Create a butterfly from a wing offset and body moneyness. - - If option_type is not specified, it is selected automatically based on - the body moneyness for best liquidity. - """ - ot = option_type or _option_type_for_moneyness(mid_moneyness) - return cls( - legs=( - StrategyLeg.from_moneyness( - ot, mid_moneyness - wing_moneyness, forward, maturity, quantity - ), - StrategyLeg.from_moneyness( - ot, mid_moneyness, forward, maturity, -2.0 * quantity - ), - StrategyLeg.from_moneyness( - ot, mid_moneyness + wing_moneyness, forward, maturity, quantity - ), - ) - ) - @classmethod def from_strikes( cls, - low_strike: float, - mid_strike: float, - high_strike: float, + low_strike: Number, + mid_strike: Number, + high_strike: Number, maturity: datetime, - forward: float, - quantity: float = 1.0, + forward: Number, + quantity: Number = 1.0, option_type: OptionType | None = None, ) -> Self: """Create a butterfly from absolute strikes. If option_type is not specified, it is selected automatically based on - the body moneyness for best liquidity. + the body position relative to the forward for best liquidity. """ - import math - - ot = option_type or _option_type_for_moneyness(math.log(mid_strike / forward)) + low = to_decimal(low_strike) + mid = to_decimal(mid_strike) + high = to_decimal(high_strike) + fwd = to_decimal(forward) + if not (low < mid < high): + raise StrategyError( + "Strikes must be strictly increasing: low < mid < high." + ) + q = to_decimal(quantity) + ot = option_type or _option_type_for_log_strike( + log_strike_from_strike(mid, fwd) + ) return cls( legs=( StrategyLeg( - option_type=ot, - quantity=quantity, - strike=low_strike, - maturity=maturity, + meta=OptionMetadata( + option_type=ot, + strike=low, + maturity=maturity, + ), + quantity=q, ), StrategyLeg( - option_type=ot, - quantity=-2.0 * quantity, - strike=mid_strike, - maturity=maturity, + meta=OptionMetadata( + option_type=ot, + strike=mid, + maturity=maturity, + ), + quantity=to_decimal(-2) * q, ), StrategyLeg( - option_type=ot, - quantity=quantity, - strike=high_strike, - maturity=maturity, + meta=OptionMetadata( + option_type=ot, + strike=high, + maturity=maturity, + ), + quantity=q, ), ) ) diff --git a/quantflow/options/strategies/calendar_spread.py b/quantflow/options/strategies/calendar_spread.py index 123572e2..9af0146c 100644 --- a/quantflow/options/strategies/calendar_spread.py +++ b/quantflow/options/strategies/calendar_spread.py @@ -5,9 +5,10 @@ from typing_extensions import Self -from quantflow.options.inputs import OptionType +from quantflow.options.inputs import OptionMetadata, OptionType +from quantflow.utils.numbers import Number, to_decimal -from .base import Strategy, StrategyLeg, load_description +from .base import Strategy, StrategyError, StrategyLeg, load_description class CalendarSpread(Strategy, frozen=True): @@ -21,33 +22,39 @@ class CalendarSpread(Strategy, frozen=True): @property def option_type(self) -> OptionType: """Option type of the calendar spread.""" - return self.legs[0].option_type + return self.legs[0].meta.option_type @classmethod def create( cls, - strike: float, + strike: Number, near_maturity: datetime, far_maturity: datetime, option_type: OptionType, - quantity: float = 1.0, + quantity: Number = 1.0, ) -> Self: - """Long far call, short near call at the same strike.""" + """Long far option, short near option at the same strike.""" if near_maturity >= far_maturity: - raise ValueError("Near maturity must be before far maturity.") + raise StrategyError("Near maturity must be before far maturity.") + strike_ = to_decimal(strike) + q = to_decimal(quantity) return cls( legs=( StrategyLeg( - option_type=option_type, - quantity=quantity, - strike=strike, - maturity=far_maturity, + meta=OptionMetadata( + option_type=option_type, + strike=strike_, + maturity=far_maturity, + ), + quantity=q, ), StrategyLeg( - option_type=option_type, - quantity=-quantity, - strike=strike, - maturity=near_maturity, + meta=OptionMetadata( + option_type=option_type, + strike=strike_, + maturity=near_maturity, + ), + quantity=-q, ), ) ) @@ -55,10 +62,10 @@ def create( @classmethod def call( cls, - strike: float, + strike: Number, near_maturity: datetime, far_maturity: datetime, - quantity: float = 1.0, + quantity: Number = 1.0, ) -> Self: return cls.create( strike, near_maturity, far_maturity, OptionType.call, quantity @@ -67,9 +74,9 @@ def call( @classmethod def put( cls, - strike: float, + strike: Number, near_maturity: datetime, far_maturity: datetime, - quantity: float = 1.0, + quantity: Number = 1.0, ) -> Self: return cls.create(strike, near_maturity, far_maturity, OptionType.put, quantity) diff --git a/quantflow/options/strategies/spread.py b/quantflow/options/strategies/spread.py index 8c771724..50c43ab9 100644 --- a/quantflow/options/strategies/spread.py +++ b/quantflow/options/strategies/spread.py @@ -5,9 +5,10 @@ from typing_extensions import Self -from quantflow.options.inputs import OptionType +from quantflow.options.inputs import OptionMetadata, OptionType +from quantflow.utils.numbers import Number, to_decimal -from .base import Strategy, StrategyLeg, load_description +from .base import Strategy, StrategyError, StrategyLeg, load_description class Spread(Strategy, frozen=True): @@ -23,25 +24,34 @@ class Spread(Strategy, frozen=True): @classmethod def call( cls, - low_strike: float, - high_strike: float, + low_strike: Number, + high_strike: Number, maturity: datetime, - quantity: float = 1.0, + quantity: Number = 1.0, ) -> Self: """Long call at low_strike, short call at high_strike.""" + low = to_decimal(low_strike) + high = to_decimal(high_strike) + if low >= high: + raise StrategyError("low_strike must be less than high_strike.") + q = to_decimal(quantity) return cls( legs=( StrategyLeg( - option_type=OptionType.call, - quantity=quantity, - strike=low_strike, - maturity=maturity, + meta=OptionMetadata( + option_type=OptionType.call, + strike=low, + maturity=maturity, + ), + quantity=q, ), StrategyLeg( - option_type=OptionType.call, - quantity=-quantity, - strike=high_strike, - maturity=maturity, + meta=OptionMetadata( + option_type=OptionType.call, + strike=high, + maturity=maturity, + ), + quantity=-q, ), ) ) @@ -49,25 +59,34 @@ def call( @classmethod def put( cls, - low_strike: float, - high_strike: float, + low_strike: Number, + high_strike: Number, maturity: datetime, - quantity: float = 1.0, + quantity: Number = 1.0, ) -> Self: """Long put at high_strike, short put at low_strike.""" + low = to_decimal(low_strike) + high = to_decimal(high_strike) + if low >= high: + raise StrategyError("low_strike must be less than high_strike.") + q = to_decimal(quantity) return cls( legs=( StrategyLeg( - option_type=OptionType.put, - quantity=quantity, - strike=high_strike, - maturity=maturity, + meta=OptionMetadata( + option_type=OptionType.put, + strike=high, + maturity=maturity, + ), + quantity=q, ), StrategyLeg( - option_type=OptionType.put, - quantity=-quantity, - strike=low_strike, - maturity=maturity, + meta=OptionMetadata( + option_type=OptionType.put, + strike=low, + maturity=maturity, + ), + quantity=-q, ), ) ) diff --git a/quantflow/options/strategies/straddle.py b/quantflow/options/strategies/straddle.py index e98cf036..d47e0902 100644 --- a/quantflow/options/strategies/straddle.py +++ b/quantflow/options/strategies/straddle.py @@ -5,7 +5,8 @@ from typing_extensions import Self -from quantflow.options.inputs import OptionType +from quantflow.options.inputs import OptionMetadata, OptionType +from quantflow.utils.numbers import Number, to_decimal from .base import Strategy, StrategyLeg, load_description @@ -19,21 +20,27 @@ class Straddle(Strategy, frozen=True): description: ClassVar[str] = load_description("straddle.md") @classmethod - def create(cls, strike: float, maturity: datetime, quantity: float = 1.0) -> Self: + def create(cls, strike: Number, maturity: datetime, quantity: Number = 1.0) -> Self: """Create a straddle at a given absolute strike.""" + strike_ = to_decimal(strike) + q = to_decimal(quantity) return cls( legs=( StrategyLeg( - option_type=OptionType.call, - quantity=quantity, - strike=strike, - maturity=maturity, + meta=OptionMetadata( + option_type=OptionType.call, + strike=strike_, + maturity=maturity, + ), + quantity=q, ), StrategyLeg( - option_type=OptionType.put, - quantity=quantity, - strike=strike, - maturity=maturity, + meta=OptionMetadata( + option_type=OptionType.put, + strike=strike_, + maturity=maturity, + ), + quantity=q, ), ) ) diff --git a/quantflow/options/strategies/strangle.py b/quantflow/options/strategies/strangle.py index 8fef1470..6f0d97aa 100644 --- a/quantflow/options/strategies/strangle.py +++ b/quantflow/options/strategies/strangle.py @@ -5,9 +5,10 @@ from typing_extensions import Self -from quantflow.options.inputs import OptionType +from quantflow.options.inputs import OptionMetadata, OptionType +from quantflow.utils.numbers import Number, to_decimal -from .base import Strategy, StrategyLeg, load_description +from .base import Strategy, StrategyError, StrategyLeg, load_description class Strangle(Strategy, frozen=True): @@ -18,51 +19,37 @@ class Strangle(Strategy, frozen=True): description: ClassVar[str] = load_description("strangle.md") - @classmethod - def from_moneyness( - cls, - forward: float, - maturity: datetime, - put_moneyness: float = -0.05, - call_moneyness: float = 0.05, - quantity: float = 1.0, - ) -> Self: - """Create a strangle from log-strike offsets from forward.""" - return cls( - legs=( - StrategyLeg.from_moneyness( - OptionType.put, put_moneyness, forward, maturity, quantity - ), - StrategyLeg.from_moneyness( - OptionType.call, call_moneyness, forward, maturity, quantity - ), - ) - ) - @classmethod def from_strikes( cls, - put_strike: float, - call_strike: float, + put_strike: Number, + call_strike: Number, maturity: datetime, - quantity: float = 1.0, + quantity: Number = 1.0, ) -> Self: """Create a strangle from absolute strikes.""" - if put_strike >= call_strike: - raise ValueError("Put strike must be less than call strike.") + put = to_decimal(put_strike) + call = to_decimal(call_strike) + if put >= call: + raise StrategyError("Put strike must be less than call strike.") + q = to_decimal(quantity) return cls( legs=( StrategyLeg( - option_type=OptionType.put, - quantity=quantity, - strike=put_strike, - maturity=maturity, + meta=OptionMetadata( + option_type=OptionType.put, + strike=put, + maturity=maturity, + ), + quantity=q, ), StrategyLeg( - option_type=OptionType.call, - quantity=quantity, - strike=call_strike, - maturity=maturity, + meta=OptionMetadata( + option_type=OptionType.call, + strike=call, + maturity=maturity, + ), + quantity=q, ), ) ) diff --git a/quantflow/options/surface.py b/quantflow/options/surface.py index fe2d2b9b..e628af23 100644 --- a/quantflow/options/surface.py +++ b/quantflow/options/surface.py @@ -33,7 +33,7 @@ to_decimal, to_decimal_or_none, ) -from quantflow.utils.price import Price +from quantflow.utils.price import PriceVolume from quantflow.utils.types import FloatArray from .bs import black_price, implied_black_volatility @@ -41,6 +41,7 @@ DefaultVolSecurity, ForwardInput, OptionInput, + OptionMetadata, OptionType, Side, SpotInput, @@ -49,6 +50,7 @@ VolSurfaceInputs, VolSurfaceSecurity, ) +from .moneyness import moneyness_from_log_strike from .parity import PutCallParities, PutCallParity from .svi import SVI @@ -81,16 +83,12 @@ class OptionSelection(enum.Enum): """Select all options regardless of their moneyness""" -class SecurityPrice(Price, Generic[S]): +class SecurityPrice(PriceVolume, Generic[S]): """Represents the bid/ask price of a security, which can be a spot price, forward price or option price """ security: S = Field(description="The underlying security of the price") - open_interest: DecimalNumber = Field( - default=ZERO, description="Open interest of the spot price" - ) - volume: DecimalNumber = Field(default=ZERO, description="Total volume traded") def is_valid(self) -> bool: """Check if the forward price is valid, which means that the bid and ask @@ -134,29 +132,6 @@ def inputs(self) -> ForwardInput: ) -class OptionMetadata(BaseModel): - """Represents the metadata of an option, including its strike, type, maturity, - and other relevant information.""" - - strike: DecimalNumber = Field(description="Strike price of the option") - option_type: OptionType = Field(description="Type of the option, call or put") - maturity: datetime = Field(description="Maturity date of the option") - inverse: bool = Field( - default=True, - description=( - "Whether the option is an inverse option (i.e. quoted in terms of the " - "underlying) or not (i.e. quoted in terms of the quote currency)" - ), - ) - - def is_in_the_money(self, forward: Decimal) -> bool: - """Check if the option is in the money given the forward price""" - if self.option_type.is_call(): - return self.strike < forward - else: - return self.strike > forward - - class OptionPrice(BaseModel): """Represents the price of an option quoted in the market along with its metadata and implied volatility information.""" @@ -282,7 +257,7 @@ def info_dict( forward=float(self.forward), maturity=self.maturity, log_strike=self.log_strike, - moneyness=self.log_strike / np.sqrt(self.ttm), + moneyness=moneyness_from_log_strike(self.log_strike, self.ttm), ttm=self.ttm, iv=self.iv, price=float(self.price_in_forward_space), @@ -306,7 +281,9 @@ def info( forward=self.forward, maturity=self.maturity, log_strike=to_decimal(self.log_strike), - moneyness=to_decimal(self.log_strike / np.sqrt(self.ttm)), + moneyness=to_decimal( + float(moneyness_from_log_strike(self.log_strike, self.ttm)) + ), ttm=to_decimal(self.ttm), iv=to_decimal(self.iv), price=self.price_in_forward_space, @@ -395,9 +372,14 @@ def spread(self) -> Decimal: """Calculate the bid-ask spread""" return self.ask.price - self.bid.price - def price(self) -> Price: - """Convert the option prices to a Price object""" - return Price(bid=self.bid.price, ask=self.ask.price) + def price(self) -> PriceVolume: + """Convert the option prices to a PriceVolume object""" + return PriceVolume( + bid=self.bid.price, + ask=self.ask.price, + volume=self.volume, + open_interest=self.open_interest, + ) def iv_bid_ask_spread(self) -> float: """Calculate the bid-ask spread of the implied volatility""" diff --git a/quantflow/rates/__init__.py b/quantflow/rates/__init__.py index 5d83b577..ca45f0a6 100644 --- a/quantflow/rates/__init__.py +++ b/quantflow/rates/__init__.py @@ -2,6 +2,7 @@ from pydantic import Field +from .cir import CIRCurve from .interest_rate import Rate from .nelson_siegel import NelsonSiegel from .options import YieldCurveCalibration @@ -12,6 +13,7 @@ "YieldCurve", "YieldCurveCalibration", "NoDiscount", + "CIRCurve", "NelsonSiegel", "VasicekCurve", "AnyYieldCurve", @@ -19,8 +21,8 @@ ] AnyYieldCurve = Annotated[ - Union[NoDiscount, NelsonSiegel, VasicekCurve], + Union[NoDiscount, CIRCurve, NelsonSiegel, VasicekCurve], Field(discriminator="curve_type"), ] -YieldCurve.register_curve_types(NoDiscount, NelsonSiegel, VasicekCurve) +YieldCurve.register_curve_types(NoDiscount, CIRCurve, NelsonSiegel, VasicekCurve) diff --git a/quantflow/rates/cir.py b/quantflow/rates/cir.py new file mode 100644 index 00000000..6d8da9fe --- /dev/null +++ b/quantflow/rates/cir.py @@ -0,0 +1,158 @@ +from __future__ import annotations + +from typing import Literal + +import numpy as np +from numpy.typing import ArrayLike +from pydantic import Field +from scipy.optimize import least_squares +from typing_extensions import Self + +from quantflow.sp.cir import CIR +from quantflow.utils.numbers import ZERO, DecimalNumber +from quantflow.utils.types import FloatArray, FloatArrayLike, maybe_float + +from .yield_curve import YieldCurve + + +class CIRCurve(YieldCurve): + r"""Yield curve derived from the Cox-Ingersoll-Ross short-rate model. + + The CIR model describes the short rate as a mean-reverting square-root + diffusion: + + \begin{equation} + dr_t = \kappa(\theta - r_t)\, dt + \sigma\sqrt{r_t}\, dW_t + \end{equation} + + The closed-form discount factor is: + + \begin{equation} + D(\tau) = A(\tau)\, e^{-B(\tau)\, r_0} + \end{equation} + + where + + \begin{equation} + \begin{aligned} + \gamma &= \sqrt{\kappa^2 + 2\sigma^2} \\ + B(\tau) &= \frac{2(e^{\gamma\tau} - 1)} + {2\gamma + (\gamma + \kappa)(e^{\gamma\tau} - 1)} \\ + A(\tau) &= \left(\frac{2\gamma\, e^{(\gamma+\kappa)\tau/2}} + {2\gamma + (\gamma + \kappa)(e^{\gamma\tau} - 1)} + \right)^{2\kappa\theta/\sigma^2} + \end{aligned} + \end{equation} + """ + + curve_type: Literal["cir_curve"] = "cir_curve" + rate: DecimalNumber = Field(description=r"Initial short rate $r_0$") + kappa: DecimalNumber = Field(gt=ZERO, description=r"Mean reversion speed $\kappa$") + theta: DecimalNumber = Field(gt=ZERO, description=r"Long-run mean $\theta$") + sigma: DecimalNumber = Field(gt=ZERO, description=r"Volatility $\sigma$") + + def process(self) -> CIR: + """Return the underlying CIR stochastic process.""" + return CIR( + rate=float(self.rate), + kappa=float(self.kappa), + theta=float(self.theta), + sigma=float(self.sigma), + ) + + def instantaneous_forward_rate(self, ttm: FloatArrayLike) -> FloatArrayLike: + r"""Calculate the instantaneous forward rate for the CIR model.""" + arr = np.asarray(ttm, dtype=float) + ttma = np.maximum(arr, 0.0) + kappa = float(self.kappa) + theta = float(self.theta) + sigma = float(self.sigma) + rate = float(self.rate) + sigma2 = sigma * sigma + gamma = np.sqrt(kappa * kappa + 2.0 * sigma2) + gamma_kappa = gamma + kappa + egt = np.exp(gamma * ttma) + d = 2.0 * gamma + gamma_kappa * (egt - 1.0) + # Use -d/dt ln D(t) = r0 * dB/dt - (2*kappa*theta/sigma2) * d/dt ln(A) + egt_m1 = egt - 1.0 + db = (2.0 * gamma * egt * d - 2.0 * egt_m1 * gamma_kappa * gamma * egt) / ( + d * d + ) + # d/dt ln(A) = (2*kappa*theta/sigma2) * d/dt ln(numerator/d) + # numerator = 2*gamma*exp((gamma+kappa)*t/2) + # d/dt ln(num) = (gamma+kappa)/2 + # d/dt ln(d) = gamma_kappa * gamma * egt / d + d_ln_a = gamma_kappa / 2.0 - gamma_kappa * gamma * egt / d + kts = 2.0 * kappa * theta / sigma2 + fwd = rate * db + kts * d_ln_a + return maybe_float(fwd) + + def discount_factor(self, ttm: FloatArrayLike) -> FloatArrayLike: + r"""Calculate the discount factor using the CIR closed-form solution.""" + arr = np.asarray(ttm, dtype=float) + ttma = np.maximum(arr, 0.0) + kappa = float(self.kappa) + theta = float(self.theta) + sigma = float(self.sigma) + rate = float(self.rate) + sigma2 = sigma * sigma + gamma = np.sqrt(kappa * kappa + 2.0 * sigma2) + gamma_kappa = gamma + kappa + egt = np.exp(gamma * ttma) + d = 2.0 * gamma + gamma_kappa * (egt - 1.0) + b = 2.0 * (egt - 1.0) / d + log_a = (2.0 * kappa * theta / sigma2) * ( + np.log(2.0 * gamma) + 0.5 * gamma_kappa * ttma - np.log(d) + ) + df = np.exp(log_a - b * rate) + return maybe_float(df) + + def jacobian(self, ttm: FloatArrayLike) -> FloatArray | None: + """Analytical Jacobian of discount factors w.r.t. [rate, kappa, theta, sigma]. + + Returns shape (len(ttm), 4). + """ + arr = np.asarray(ttm, dtype=float) + ttma = np.maximum(arr, 0.0) + kappa = float(self.kappa) + theta = float(self.theta) + sigma = float(self.sigma) + rate = float(self.rate) + sigma2 = sigma * sigma + gamma = np.sqrt(kappa * kappa + 2.0 * sigma2) + gamma_kappa = gamma + kappa + egt = np.exp(gamma * ttma) + d = 2.0 * gamma + gamma_kappa * (egt - 1.0) + b = 2.0 * (egt - 1.0) / d + log_a = (2.0 * kappa * theta / sigma2) * ( + np.log(2.0 * gamma) + 0.5 * gamma_kappa * ttma - np.log(d) + ) + df = np.exp(log_a - b * rate) + # dD/d(rate) = -B * D + d_rate = -b * df + n = arr.shape[0] if arr.ndim > 0 else 1 + jac = np.column_stack([d_rate.reshape(n)]) + return jac.reshape(n, 1) if arr.ndim > 0 else jac + + @classmethod + def calibrate(cls, ttm: ArrayLike, rates: ArrayLike) -> Self: + """Fit the CIR curve to continuously compounded rates via least squares.""" + ttm_arr = np.asarray(ttm, dtype=float) + rates_arr = np.asarray(rates, dtype=float) + + def residuals(params: np.ndarray) -> np.ndarray: + curve = cls( + rate=params[0], kappa=params[1], theta=params[2], sigma=params[3] + ) + df = np.asarray(curve.discount_factor(ttm_arr), dtype=float) + fitted = -np.log(df) / ttm_arr + return fitted - rates_arr + + x0 = np.array([rates_arr[0], 1.0, rates_arr[-1], 0.1]) + result = least_squares( + residuals, + x0, + bounds=([0.0, 1e-4, 0.0, 1e-4], [1.0, 50.0, 1.0, 2.0]), + ) + r, k, th, s = result.x + return cls(rate=r, kappa=k, theta=th, sigma=s) diff --git a/quantflow/rates/nelson_siegel.py b/quantflow/rates/nelson_siegel.py index 58597919..5c089cf6 100644 --- a/quantflow/rates/nelson_siegel.py +++ b/quantflow/rates/nelson_siegel.py @@ -43,7 +43,7 @@ def calibrator(self) -> NelsonSiegelCalibration: this curve.""" return NelsonSiegelCalibration(yield_curve=self) - def instanteous_forward_rate(self, ttm: FloatArrayLike) -> FloatArrayLike: + def instantaneous_forward_rate(self, ttm: FloatArrayLike) -> FloatArrayLike: b1, b2, b3, lam = ( float(self.beta1), float(self.beta2), diff --git a/quantflow/rates/vasicek.py b/quantflow/rates/vasicek.py index 676406e2..0a02a918 100644 --- a/quantflow/rates/vasicek.py +++ b/quantflow/rates/vasicek.py @@ -33,7 +33,7 @@ def process(self) -> Vasicek: bdlp=WienerProcess(sigma=float(self.sigma)), ) - def instanteous_forward_rate(self, ttm: FloatArrayLike) -> FloatArrayLike: + def instantaneous_forward_rate(self, ttm: FloatArrayLike) -> FloatArrayLike: r"""Calculate the instantaneous forward rate.""" arr = np.asarray(ttm, dtype=float) ttma = np.maximum(arr, 0.0) diff --git a/quantflow/rates/yield_curve.py b/quantflow/rates/yield_curve.py index e0a83280..f6c500aa 100644 --- a/quantflow/rates/yield_curve.py +++ b/quantflow/rates/yield_curve.py @@ -37,7 +37,7 @@ class YieldCurve(BaseModel, ABC, extra="forbid"): ) @abstractmethod - def instanteous_forward_rate(self, ttm: FloatArrayLike) -> FloatArrayLike: + def instantaneous_forward_rate(self, ttm: FloatArrayLike) -> FloatArrayLike: r"""Calculate the instantaneous forward rate for a given time to maturity. The instantaneous forward rate is related to discount factor @@ -119,7 +119,7 @@ def continuously_compounded_rate( ttm_ = np.asarray(ttm, dtype=float) df = np.asarray(self.discount_factor(ttm_), dtype=float) result = np.where( - ttm_ <= 0, self.instanteous_forward_rate(0.0), -np.log(df) / ttm_ + ttm_ <= 0, self.instantaneous_forward_rate(0.0), -np.log(df) / ttm_ ) return maybe_float(result) @@ -161,7 +161,7 @@ class NoDiscount(YieldCurve): curve_type: Literal["no_discount"] = "no_discount" - def instanteous_forward_rate(self, ttm: FloatArrayLike) -> FloatArrayLike: + def instantaneous_forward_rate(self, ttm: FloatArrayLike) -> FloatArrayLike: arr = np.asarray(ttm, dtype=float) return np.zeros_like(arr) if arr.ndim > 0 else 0.0 diff --git a/quantflow/sp/base.py b/quantflow/sp/base.py index c46a1e32..cd255c35 100755 --- a/quantflow/sp/base.py +++ b/quantflow/sp/base.py @@ -23,7 +23,17 @@ class StochasticProcess(BaseModel, ABC, extra="forbid"): """ @abstractmethod - def sample_from_draws(self, draws: Paths, *args: Paths) -> Paths: + def sample_from_draws( + self, + draws: Annotated[ + Paths, + Doc("Pre-drawn random increments (typically standard normal)"), + ], + *args: Annotated[ + Paths, + Doc("Additional pre-drawn paths for multi-factor models"), + ], + ) -> Paths: """Sample [Paths][quantflow.ta.paths.Paths] from the process given a set of draws""" diff --git a/quantflow/sp/bns.py b/quantflow/sp/bns.py index 12854db5..43b837e8 100644 --- a/quantflow/sp/bns.py +++ b/quantflow/sp/bns.py @@ -109,25 +109,45 @@ def _characteristic_exponent( ) return diffusion - bdlp - def sample(self, n: int, time_horizon: float = 1, time_steps: int = 100) -> Paths: + def sample( + self, + n: Annotated[int, Doc("Number of paths")], + time_horizon: Annotated[float, Doc("Time horizon")] = 1, + time_steps: Annotated[ + int, Doc("Number of time steps to arrive at horizon") + ] = 100, + ) -> Paths: return self.sample_from_draws(Paths.normal_draws(n, time_horizon, time_steps)) - def sample_from_draws(self, path_dw: Paths, *args: Paths) -> Paths: + def sample_from_draws( + self, + draws: Annotated[ + Paths, + Doc("Pre-drawn standard normal increments for the Brownian motion"), + ], + *args: Annotated[ + Paths, + Doc( + "Optional pre-drawn subordinator paths; " + "new draws are generated if omitted" + ), + ], + ) -> Paths: path_dz = ( args[0] if args else self.variance_process.bdlp.sample( - path_dw.samples, - self.variance_process.kappa * path_dw.t, - path_dw.time_steps, + draws.samples, + self.variance_process.kappa * draws.t, + draws.time_steps, ) ) v = self._variance_path(path_dz) # Price: x_t = integral sqrt(v) dW + rho * z_{kappa t} - diffusion = np.sqrt(v[:-1] * path_dw.dt) * path_dw.data[:-1] - paths = np.zeros_like(path_dw.data) + diffusion = np.sqrt(v[:-1] * draws.dt) * draws.data[:-1] + paths = np.zeros_like(draws.data) paths[1:] = np.cumsum(diffusion, axis=0) + self.rho * path_dz.data[1:] - return Paths(t=path_dw.t, data=paths) + return Paths(t=draws.t, data=paths) def _variance_path(self, path_dz: Paths) -> np.ndarray: """Simulate the OU variance path on the same grid as `path_dz`.""" @@ -209,15 +229,15 @@ def sample( def sample_from_draws( self, - path_dw: Annotated[Paths, Doc("Single Brownian motion driving both factors")], + draws: Annotated[Paths, Doc("Single Brownian motion driving both factors")], *args: Annotated[ Paths, Doc("Optional pre-drawn BDLP paths for bns1 and bns2 (in that order)"), ], ) -> Paths: - time_horizon = path_dw.t - time_steps = path_dw.time_steps - n = path_dw.samples + time_horizon = draws.t + time_steps = draws.time_steps + n = draws.samples path_dz1 = ( args[0] if len(args) > 0 @@ -236,8 +256,8 @@ def sample_from_draws( v2 = self.bns2._variance_path(path_dz2) w = self.weight sigma2 = w * v1 + (1.0 - w) * v2 - diffusion = np.sqrt(sigma2[:-1] * path_dw.dt) * path_dw.data[:-1] - paths = np.zeros_like(path_dw.data) + diffusion = np.sqrt(sigma2[:-1] * draws.dt) * draws.data[:-1] + paths = np.zeros_like(draws.data) paths[1:] = ( np.cumsum(diffusion, axis=0) + self.bns1.rho * path_dz1.data[1:] diff --git a/quantflow/sp/cir.py b/quantflow/sp/cir.py index fd51be04..698e6ffd 100755 --- a/quantflow/sp/cir.py +++ b/quantflow/sp/cir.py @@ -4,6 +4,7 @@ from pydantic import Field from scipy import special from scipy.optimize import Bounds +from typing_extensions import Annotated, Doc from quantflow.utils.types import FloatArrayLike, Vector @@ -57,19 +58,31 @@ def is_positive(self) -> bool: return self.feller_condition >= 0.0 def sample( - self, paths: int, time_horizon: float = 1, time_steps: int = 100 + self, + n: Annotated[int, Doc("Number of paths")], + time_horizon: Annotated[float, Doc("Time horizon")] = 1, + time_steps: Annotated[ + int, Doc("Number of time steps to arrive at horizon") + ] = 100, ) -> Paths: - draws = Paths.normal_draws(paths, time_horizon, time_steps) + draws = Paths.normal_draws(n, time_horizon, time_steps) return self.sample_from_draws(draws) - def sample_from_draws(self, paths: Paths, *args: Paths) -> Paths: + def sample_from_draws( + self, + draws: Annotated[ + Paths, + Doc("Pre-drawn standard normal increments"), + ], + *args: Paths, + ) -> Paths: match self.sample_algo: case SamplingAlgorithm.EULER: - return self.sample_euler(paths) + return self.sample_euler(draws) case SamplingAlgorithm.MILSTEIN: - return self.sample_euler(paths, 0.25) + return self.sample_euler(draws, 0.25) case SamplingAlgorithm.IMPLICIT: - return self.sample_implicit(paths) + return self.sample_implicit(draws) def sample_euler(self, draws: Paths, milstein_coef: float = 0.0) -> Paths: kappa = self.kappa diff --git a/quantflow/sp/heston.py b/quantflow/sp/heston.py index f1c83e17..550d694b 100644 --- a/quantflow/sp/heston.py +++ b/quantflow/sp/heston.py @@ -167,7 +167,7 @@ def sample( def sample_from_draws( self, - path1: Annotated[ + draws: Annotated[ Paths, Doc("Pre-drawn standard normal increments for the first Brownian motion"), ], @@ -182,14 +182,14 @@ def sample_from_draws( if args: path2 = args[0] else: - path2 = Paths.normal_draws(path1.samples, path1.t, path1.time_steps) - dz = path1.data + path2 = Paths.normal_draws(draws.samples, draws.t, draws.time_steps) + dz = draws.data dw = self.rho * dz + np.sqrt(1 - self.rho * self.rho) * path2.data - v = self.variance_process.sample_from_draws(path1) - dx = dw * np.sqrt(v.data * path1.dt) + v = self.variance_process.sample_from_draws(draws) + dx = dw * np.sqrt(v.data * draws.dt) paths = np.zeros(dx.shape) paths[1:] = np.cumsum(dx[:-1], axis=0) - return Paths(t=path1.t, data=paths) + return Paths(t=draws.t, data=paths) class HestonJ(Heston, Generic[D]): @@ -315,8 +315,21 @@ def characteristic_exponent( t, u ) + self.jumps.characteristic_exponent(t, u) - def sample_from_draws(self, path1: Paths, *args: Paths) -> Paths: - diffusion = super().sample_from_draws(path1, *args) + def sample_from_draws( + self, + draws: Annotated[ + Paths, + Doc("Pre-drawn standard normal increments for the first Brownian motion"), + ], + *args: Annotated[ + Paths, + Doc( + "Optional pre-drawn increments for the second Brownian motion; " + "new draws are generated if omitted" + ), + ], + ) -> Paths: + diffusion = super().sample_from_draws(draws, *args) jump_path = self.jumps.sample( diffusion.samples, diffusion.t, diffusion.time_steps ) @@ -377,7 +390,7 @@ def sample( def sample_from_draws( self, - path1: Annotated[Paths, Doc("First Brownian motion draws for heston1")], + draws: Annotated[Paths, Doc("First Brownian motion draws for heston1")], *args: Annotated[ Paths, Doc( @@ -386,9 +399,9 @@ def sample_from_draws( ), ], ) -> Paths: - paths1 = self.heston1.sample_from_draws(path1, args[0]) + paths1 = self.heston1.sample_from_draws(draws, args[0]) paths2 = self.heston2.sample_from_draws(args[1], args[2]) - return Paths(t=path1.t, data=paths1.data + paths2.data) + return Paths(t=draws.t, data=paths1.data + paths2.data) class DoubleHestonJ(DoubleHeston, Generic[D]): diff --git a/quantflow/sp/jump_diffusion.py b/quantflow/sp/jump_diffusion.py index 520f43e5..0186b8e3 100644 --- a/quantflow/sp/jump_diffusion.py +++ b/quantflow/sp/jump_diffusion.py @@ -36,16 +36,33 @@ def characteristic_exponent(self, t: FloatArrayLike, u: Vector) -> Vector: t, u ) + self.jumps.characteristic_exponent(t, u) - def sample(self, n: int, time_horizon: float = 1, time_steps: int = 100) -> Paths: + def sample( + self, + n: Annotated[int, Doc("Number of paths")], + time_horizon: Annotated[float, Doc("Time horizon")] = 1, + time_steps: Annotated[ + int, Doc("Number of time steps to arrive at horizon") + ] = 100, + ) -> Paths: dw1 = Paths.normal_draws(n, time_horizon, time_steps) return self.sample_from_draws(dw1) - def sample_from_draws(self, path_w: Paths, *args: Paths) -> Paths: + def sample_from_draws( + self, + draws: Annotated[ + Paths, + Doc("Pre-drawn standard normal increments for the diffusion component"), + ], + *args: Annotated[ + Paths, + Doc("Optional pre-drawn jump paths; " "new draws are generated if omitted"), + ], + ) -> Paths: if args: path_j = args[0] else: - path_j = self.jumps.sample(path_w.samples, path_w.t, path_w.time_steps) - path_w = self.diffusion.sample_from_draws(path_w) + path_j = self.jumps.sample(draws.samples, draws.t, draws.time_steps) + path_w = self.diffusion.sample_from_draws(draws) return Paths(t=path_w.t, data=path_w.data + path_j.data) def analytical_mean(self, t: FloatArrayLike) -> FloatArrayLike: diff --git a/quantflow/sp/ou.py b/quantflow/sp/ou.py index cff1ca23..4359ecef 100755 --- a/quantflow/sp/ou.py +++ b/quantflow/sp/ou.py @@ -6,6 +6,7 @@ import numpy as np from pydantic import Field from scipy.stats import gamma, norm +from typing_extensions import Annotated, Doc from ..ta.paths import Paths from ..utils.distributions import Exponential @@ -59,11 +60,25 @@ def characteristic_exponent(self, t: FloatArrayLike, u: Vector) -> Vector: var = self.analytical_variance(t) return u * (-1j * mu + 0.5 * var * u) - def sample(self, n: int, time_horizon: float = 1, time_steps: int = 100) -> Paths: + def sample( + self, + n: Annotated[int, Doc("Number of paths")], + time_horizon: Annotated[float, Doc("Time horizon")] = 1, + time_steps: Annotated[ + int, Doc("Number of time steps to arrive at horizon") + ] = 100, + ) -> Paths: paths = Paths.normal_draws(n, time_horizon, time_steps) return self.sample_from_draws(paths) - def sample_from_draws(self, draws: Paths, *args: Paths) -> Paths: + def sample_from_draws( + self, + draws: Annotated[ + Paths, + Doc("Pre-drawn standard normal increments for the Brownian motion"), + ], + *args: Paths, + ) -> Paths: kappa = self.kappa theta = self.theta dt = draws.dt @@ -261,7 +276,14 @@ def integrated_log_laplace(self, t: FloatArrayLike, u: Vector) -> Vector: ) return c0 + c1 * self.rate - def sample(self, n: int, time_horizon: float = 1, time_steps: int = 100) -> Paths: + def sample( + self, + n: Annotated[int, Doc("Number of paths")], + time_horizon: Annotated[float, Doc("Time horizon")] = 1, + time_steps: Annotated[ + int, Doc("Number of time steps to arrive at horizon") + ] = 100, + ) -> Paths: dt = time_horizon / time_steps jump_process = self.bdlp paths = np.zeros((time_steps + 1, n)) diff --git a/quantflow/sp/wiener.py b/quantflow/sp/wiener.py index ed016c87..15c39265 100644 --- a/quantflow/sp/wiener.py +++ b/quantflow/sp/wiener.py @@ -3,6 +3,7 @@ import numpy as np from pydantic import Field from scipy.stats import norm +from typing_extensions import Annotated, Doc from ..ta.paths import Paths from ..utils.types import FloatArrayLike, Vector @@ -20,11 +21,25 @@ def characteristic_exponent(self, t: Vector, u: Vector) -> Vector: su = self.sigma * u return 0.5 * su * su * t - def sample(self, n: int, time_horizon: float = 1, time_steps: int = 100) -> Paths: + def sample( + self, + n: Annotated[int, Doc("Number of paths")], + time_horizon: Annotated[float, Doc("Time horizon")] = 1, + time_steps: Annotated[ + int, Doc("Number of time steps to arrive at horizon") + ] = 100, + ) -> Paths: paths = Paths.normal_draws(n, time_horizon, time_steps) return self.sample_from_draws(paths) - def sample_from_draws(self, draws: Paths, *args: Paths) -> Paths: + def sample_from_draws( + self, + draws: Annotated[ + Paths, + Doc("Pre-drawn standard normal increments for the Brownian motion"), + ], + *args: Paths, + ) -> Paths: sdt = self.sigma * np.sqrt(draws.dt) paths = np.zeros(draws.data.shape) paths[1:] = np.cumsum(draws.data[:-1], axis=0) diff --git a/quantflow/ta/__init__.py b/quantflow/ta/__init__.py index 330b1fde..e69de29b 100644 --- a/quantflow/ta/__init__.py +++ b/quantflow/ta/__init__.py @@ -1,5 +0,0 @@ -from .ewma import EWMA -from .kalman import KalmanFilter -from .supersmoother import SuperSmoother - -__all__ = ["SuperSmoother", "KalmanFilter", "EWMA"] diff --git a/quantflow/ta/base.py b/quantflow/ta/base.py index a49bc568..46bacb83 100644 --- a/quantflow/ta/base.py +++ b/quantflow/ta/base.py @@ -1,14 +1,5 @@ from typing import TypeAlias import pandas as pd -import polars as pl -DataFrame: TypeAlias = pl.DataFrame | pd.DataFrame - - -def to_polars(df: DataFrame, *, copy: bool = False) -> pl.DataFrame: - if isinstance(df, pd.DataFrame): - return pl.DataFrame(df) - elif copy: - return df.clone() - return df +DataFrame: TypeAlias = pd.DataFrame diff --git a/quantflow/ta/ewma.py b/quantflow/ta/ewma.py index 62efb1f1..fc9d7238 100644 --- a/quantflow/ta/ewma.py +++ b/quantflow/ta/ewma.py @@ -17,23 +17,22 @@ class EWMA(BaseModel): This implementation uses the standard EWMA formula: - $$ \begin{equation} s_t = \alpha x_t + (1 - \alpha) s_{t-1} \end{equation} - $$ where $\alpha$ is the smoothing factor derived from the period parameter. - To match SuperSmoother characteristics, the formula is: + The period represents the half-life of the exponential decay, that is, + the number of steps after which the weight assigned to a past observation + drops to half. The relationship is: - $$ \begin{equation} - \alpha = \frac{2}{\text{period} + 1} + \alpha = 1 - \exp\left(-\frac{\ln 2}{p}\right) \end{equation} - $$ - This provides frequency response similar to a simple moving average and - approximates the smoothing characteristics of higher-order filters. + where $N$ is the [period][.period]. This definition makes the period directly + comparable to the period used in + [SuperSmoother][quantflow.ta.supersmoother.SuperSmoother]. ## Example @@ -75,23 +74,20 @@ def from_half_life(cls, half_life: float, tau: float | None = None) -> Self: The half-life represents the time for weight to decay to 0.5. - $$ - \alpha = 1 - \exp\left(-\frac{\ln(2)}{\text{half life}}\right) - $$ + \begin{equation} + \alpha = 1 - \exp\left(-\frac{\ln(2)}{\tt{half\_life}}\right) + \end{equation} """ - # Calculate equivalent period that produces the desired half-life behavior - # From: alpha = 1 - exp(-ln(2) / half_life) - # And: alpha = 2 / (period + 1) - # We solve: 2 / (period + 1) = 1 - exp(-ln(2) / half_life) - alpha = 1.0 - math.exp(-log2 / half_life) + return cls.from_alpha(1.0 - math.exp(-log2 / half_life), tau=tau) + + @classmethod + def from_alpha(cls, alpha: float, tau: float | None = None) -> Self: + """Create an EWMA directly from a specified alpha value.""" period = int(2.0 / alpha - 1) return cls(period=max(1, period), tau=tau) def model_post_init(self, __context: Any) -> None: - # Convert period to alpha using the standard formula: - # alpha = 2 / (period + 1) - # This matches the smoothing characteristics of an SMA with the same period - self._alpha = 2.0 / (self.period + 1) + self._alpha = 1.0 - math.exp(-log2 / self.period) def update(self, value: float) -> float: """Update the filter with a new value and return the smoothed result. diff --git a/quantflow/ta/ohlc.py b/quantflow/ta/ohlc.py index ab8c1274..7b5a1de3 100644 --- a/quantflow/ta/ohlc.py +++ b/quantflow/ta/ohlc.py @@ -1,22 +1,22 @@ from datetime import timedelta import numpy as np -import polars as pl +import pandas as pd from pydantic import BaseModel -from .base import DataFrame, to_polars +from .base import DataFrame class OHLC(BaseModel): - """Aggregates OHLC data over a given period and serie + """Aggregates OHLC data over a given period and series - Optionally calculates the range-based variance estimators for the serie. + Optionally calculates the range-based variance estimators for the series. Range-based estimator are called like that because they are calculated from the difference between the period high and low. """ - serie: str - """serie to aggregate""" + series: str + """series to aggregate""" period: str | timedelta """down-sampling period, e.g. 1h, 1d, 1w""" index_column: str = "index" @@ -30,35 +30,23 @@ class OHLC(BaseModel): percent_variance: bool = False """log-transform the variance columns""" - @property - def open_col(self) -> pl.Expr: - return self.var_column("open") - - @property - def high_col(self) -> pl.Expr: - return self.var_column("high") - - @property - def low_col(self) -> pl.Expr: - return self.var_column("low") - - @property - def close_col(self) -> pl.Expr: - return self.var_column("close") - - def __call__(self, df: DataFrame) -> pl.DataFrame: + def __call__(self, df: DataFrame) -> pd.DataFrame: """Returns a dataframe with OHLC data sampled over the given period""" - result = ( - to_polars(df, copy=True) - .group_by_dynamic(self.index_column, every=self.period) - .agg( - pl.col(self.serie).first().alias(f"{self.serie}_open"), - pl.col(self.serie).max().alias(f"{self.serie}_high"), - pl.col(self.serie).min().alias(f"{self.serie}_low"), - pl.col(self.serie).last().alias(f"{self.serie}_close"), - pl.col(self.serie).mean().alias(f"{self.serie}_mean"), - ) + data = ( + df.set_index(self.index_column) if self.index_column in df.columns else df ) + col = self._resolve_column(data) + resampled = data[col].resample(self.period) + s = self.series + result = pd.DataFrame( + { + f"{s}_open": resampled.first(), + f"{s}_high": resampled.max(), + f"{s}_low": resampled.min(), + f"{s}_close": resampled.last(), + f"{s}_mean": resampled.mean(), + } + ).reset_index() if self.parkinson_variance: result = self.parkinson(result) if self.garman_klass_variance: @@ -67,43 +55,65 @@ def __call__(self, df: DataFrame) -> pl.DataFrame: result = self.rogers_satchell(result) return result - def parkinson(self, df: DataFrame) -> pl.DataFrame: + def _resolve_column(self, df: pd.DataFrame) -> str | int: + """Resolve the series column name, handling int vs string column names.""" + if self.series in df.columns: + return self.series + try: + col: int = int(self.series) + if col in df.columns: + return col + except ValueError: + pass + raise KeyError(f"Column '{self.series}' not found in dataframe") + + def _col(self, suffix: str) -> str: + return f"{self.series}_{suffix}" + + def _get_col(self, df: pd.DataFrame, suffix: str) -> pd.Series: + col = df[self._col(suffix)] + return np.log(col) if self.percent_variance else col + + def parkinson(self, df: DataFrame) -> pd.DataFrame: """Adds parkinson variance column to the dataframe - This requires the serie high and low columns to be present + This requires the series high and low columns to be present """ - c = (self.high_col - self.low_col) ** 2 / np.sqrt(4 * np.log(2)) - return to_polars(df).with_columns(c.alias(f"{self.serie}_pk")) - - def garman_klass(self, df: DataFrame) -> pl.DataFrame: + high = self._get_col(df, "high") + low = self._get_col(df, "low") + pk = (high - low) ** 2 / np.sqrt(4 * np.log(2)) + df = df.copy() + df[self._col("pk")] = pk + return df + + def garman_klass(self, df: DataFrame) -> pd.DataFrame: """Adds Garman Klass variance estimator column to the dataframe - This requires the serie high and low columns to be present. + This requires the series high and low columns to be present. """ - open = self.open_col - hh = self.high_col - open - ll = self.low_col - open - cc = self.close_col - open - c = ( + o = self._get_col(df, "open") + hh = self._get_col(df, "high") - o + ll = self._get_col(df, "low") - o + cc = self._get_col(df, "close") - o + gk = ( 0.522 * (hh - ll) ** 2 - 0.019 * (cc * (hh + ll) + 2.0 * ll * hh) - 0.383 * cc**2 ) - return to_polars(df).with_columns(c.alias(f"{self.serie}_gk")) + df = df.copy() + df[self._col("gk")] = gk + return df - def rogers_satchell(self, df: DataFrame) -> pl.DataFrame: + def rogers_satchell(self, df: DataFrame) -> pd.DataFrame: """Adds Rogers Satchell variance estimator column to the dataframe - This requires the serie high and low columns to be present. + This requires the series high and low columns to be present. """ - open = self.open_col - hh = self.high_col - open - ll = self.low_col - open - cc = self.close_col - open - c = hh * (hh - cc) + ll * (ll - cc) - return to_polars(df).with_columns(c.alias(f"{self.serie}_rs")) - - def var_column(self, suffix: str) -> pl.Expr: - """Returns a polars expression for the OHLC column""" - col = pl.col(f"{self.serie}_{suffix}") - return col.log() if self.percent_variance else col + o = self._get_col(df, "open") + hh = self._get_col(df, "high") - o + ll = self._get_col(df, "low") - o + cc = self._get_col(df, "close") - o + rs = hh * (hh - cc) + ll * (ll - cc) + df = df.copy() + df[self._col("rs")] = rs + return df diff --git a/quantflow/utils/price.py b/quantflow/utils/price.py index d7d02b4d..e7155a4c 100644 --- a/quantflow/utils/price.py +++ b/quantflow/utils/price.py @@ -35,3 +35,12 @@ def is_valid(self) -> bool: """Check if the price is valid, which means the bid is less than or equal to the ask""" return self.bid <= self.ask + + +class PriceVolume(Price): + """Base class for price with volume and open interest""" + + open_interest: DecimalNumber = Field( + default=ZERO, description="Open interest of the security" + ) + volume: DecimalNumber = Field(default=ZERO, description="Volume of the security") diff --git a/quantflow_tests/test_nelson_siegel.py b/quantflow_tests/test_nelson_siegel.py index 014a32fa..bfdf1f5e 100644 --- a/quantflow_tests/test_nelson_siegel.py +++ b/quantflow_tests/test_nelson_siegel.py @@ -69,7 +69,7 @@ def test_instantaneous_forward_rate_at_zero() -> None: beta3=Decimal("0.01"), lambda_=Decimal("1"), ) - assert float(ns.instanteous_forward_rate(0)) == pytest.approx(0.06, rel=1e-6) + assert float(ns.instantaneous_forward_rate(0)) == pytest.approx(0.06, rel=1e-6) def test_instantaneous_forward_rate_large_ttm() -> None: @@ -79,7 +79,7 @@ def test_instantaneous_forward_rate_large_ttm() -> None: beta3=Decimal("0.01"), lambda_=Decimal("1"), ) - assert float(ns.instanteous_forward_rate(100)) == pytest.approx(0.04, abs=1e-5) + assert float(ns.instantaneous_forward_rate(100)) == pytest.approx(0.04, abs=1e-5) def test_consistency_forward_and_discount() -> None: @@ -94,7 +94,9 @@ def test_consistency_forward_and_discount() -> None: math.log(float(ns.discount_factor(ttm + h))) - math.log(float(ns.discount_factor(ttm - h))) ) / (2 * h) - assert numerical == pytest.approx(float(ns.instanteous_forward_rate(ttm)), rel=1e-4) + assert numerical == pytest.approx( + float(ns.instantaneous_forward_rate(ttm)), rel=1e-4 + ) # --------------------------------------------------------------------------- diff --git a/quantflow_tests/test_ohlc.py b/quantflow_tests/test_ohlc.py index dd9c7e6c..9f0db865 100644 --- a/quantflow_tests/test_ohlc.py +++ b/quantflow_tests/test_ohlc.py @@ -4,14 +4,14 @@ def test_ohlc() -> None: ohlc = OHLC( - serie="0", - period="10m", + series="0", + period="10min", parkinson_variance=True, garman_klass_variance=True, rogers_satchell_variance=True, ) - assert ohlc.serie == "0" - assert ohlc.period == "10m" + assert ohlc.series == "0" + assert ohlc.period == "10min" assert ohlc.index_column == "index" assert ohlc.parkinson_variance is True assert ohlc.garman_klass_variance is True diff --git a/quantflow_tests/test_strategies.py b/quantflow_tests/test_strategies.py index 80552162..800de1e7 100644 --- a/quantflow_tests/test_strategies.py +++ b/quantflow_tests/test_strategies.py @@ -29,8 +29,8 @@ def test_straddle_from_strike(pricer: OptionPricer) -> None: assert p.gamma > 0 -def test_strangle_from_moneyness(pricer: OptionPricer) -> None: - p = Strangle.from_moneyness(FORWARD, MATURITY).price(pricer, FORWARD, REF_DATE) +def test_strangle_from_strikes_otm(pricer: OptionPricer) -> None: + p = Strangle.from_strikes(95.0, 105.0, MATURITY).price(pricer, FORWARD, REF_DATE) assert p.price > 0 assert p.gamma > 0 assert ( @@ -46,8 +46,10 @@ def test_strangle_from_strikes(pricer: OptionPricer) -> None: assert p.gamma > 0 -def test_butterfly_from_moneyness(pricer: OptionPricer) -> None: - p = Butterfly.from_moneyness(FORWARD, MATURITY).price(pricer, FORWARD, REF_DATE) +def test_butterfly_from_strikes_atm(pricer: OptionPricer) -> None: + p = Butterfly.from_strikes(95.0, 100.0, 105.0, MATURITY, FORWARD).price( + pricer, FORWARD, REF_DATE + ) assert p.price > 0 assert p.gamma < 0 assert Butterfly.description != "" diff --git a/uv.lock b/uv.lock index 02607ef0..5f2e1dbf 100644 --- a/uv.lock +++ b/uv.lock @@ -760,7 +760,7 @@ name = "cuda-bindings" version = "12.9.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder" }, + { name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/1f/a5/e9d37c10f6c27c9c65d53c6cd6d9763e1df99c004780585fc2ad9041fbe3/cuda_bindings-12.9.6-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2662f59db67d9aeaf8959c593c91f600792c2970cf02cae2814387fc687b115a", size = 7090971, upload-time = "2026-03-11T14:47:29.526Z" }, @@ -795,37 +795,37 @@ wheels = [ [package.optional-dependencies] cublas = [ - { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux'" }, ] cudart = [ - { name = "nvidia-cuda-runtime-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "sys_platform == 'linux'" }, ] cufft = [ - { name = "nvidia-cufft-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cufft-cu12", marker = "sys_platform == 'linux'" }, ] cufile = [ { name = "nvidia-cufile-cu12", marker = "sys_platform == 'linux'" }, ] cupti = [ - { name = "nvidia-cuda-cupti-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "sys_platform == 'linux'" }, ] curand = [ - { name = "nvidia-curand-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-curand-cu12", marker = "sys_platform == 'linux'" }, ] cusolver = [ - { name = "nvidia-cusolver-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cusolver-cu12", marker = "sys_platform == 'linux'" }, ] cusparse = [ - { name = "nvidia-cusparse-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cusparse-cu12", marker = "sys_platform == 'linux'" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "sys_platform == 'linux'" }, ] nvtx = [ - { name = "nvidia-nvtx-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-nvtx-cu12", marker = "sys_platform == 'linux'" }, ] [[package]] @@ -1711,7 +1711,7 @@ wheels = [ [[package]] name = "marimo" -version = "0.23.7" +version = "0.23.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -1734,9 +1734,9 @@ dependencies = [ { name = "uvicorn" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/a2/0a342245ebaf20a11217743783fcf7ddf7a9b8791fce19f306bdd4855661/marimo-0.23.7.tar.gz", hash = "sha256:93b05340f5c1a4beccaf1ce16d8cd96e8a663f5748339388454cbe78737b0db6", size = 38505806, upload-time = "2026-05-21T21:54:08.789Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/fe/a1dec6111a660ee3bd26521c24788e7aef519a31c60cf37f767f29313454/marimo-0.23.8.tar.gz", hash = "sha256:8049df4ad263e7126e959d7d910b014e6181dffe49f540a89c3174e61a446a99", size = 38505767, upload-time = "2026-05-22T16:24:19.881Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/fc/f1379d5ce672c0a7c981ca55abab84baa56ccdae9d11651dbacafae002cc/marimo-0.23.7-py3-none-any.whl", hash = "sha256:897ff45bbc0b57edde1faeaaf32ed96d423dbcafeaca3d45abca887ba0110073", size = 38939884, upload-time = "2026-05-21T21:54:04.702Z" }, + { url = "https://files.pythonhosted.org/packages/2d/76/51b57a2e521b9110f25ec935f2774412e2f3d678b3fdea7841987244fb2c/marimo-0.23.8-py3-none-any.whl", hash = "sha256:99a4035d035fb320c8f2dcefc2213e0d64e9de13e989bc3f2a973b19dc40542a", size = 38938839, upload-time = "2026-05-22T16:24:16.102Z" }, ] [[package]] @@ -2406,7 +2406,7 @@ name = "nvidia-cudnn-cu12" version = "9.10.2.21" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cublas-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/fa/41/e79269ce215c857c935fd86bcfe91a451a584dfc27f1e068f568b9ad1ab7/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c9132cc3f8958447b4910a1720036d9eff5928cc3179b0a51fb6d167c6cc87d8", size = 705026878, upload-time = "2025-06-06T21:52:51.348Z" }, @@ -2418,7 +2418,7 @@ name = "nvidia-cufft-cu12" version = "11.3.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/1f/37/c50d2b2f2c07e146776389e3080f4faf70bcc4fa6e19d65bb54ca174ebc3/nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d16079550df460376455cba121db6564089176d9bac9e4f360493ca4741b22a6", size = 200164144, upload-time = "2024-11-20T17:40:58.288Z" }, @@ -2452,9 +2452,9 @@ name = "nvidia-cusolver-cu12" version = "11.7.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12" }, - { name = "nvidia-cusparse-cu12" }, - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-cublas-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cusparse-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/93/17/dbe1aa865e4fdc7b6d4d0dd308fdd5aaab60f939abfc0ea1954eac4fb113/nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0ce237ef60acde1efc457335a2ddadfd7610b892d94efee7b776c64bb1cac9e0", size = 157833628, upload-time = "2024-10-01T17:05:05.591Z" }, @@ -2468,7 +2468,7 @@ name = "nvidia-cusparse-cu12" version = "12.5.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/eb/eb/6681efd0aa7df96b4f8067b3ce7246833dd36830bb4cec8896182773db7d/nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d25b62fb18751758fe3c93a4a08eff08effedfe4edf1c6bb5afd0890fe88f887", size = 216451147, upload-time = "2024-11-20T17:44:18.055Z" }, @@ -2771,43 +2771,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] -[[package]] -name = "polars" -version = "1.40.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "polars-runtime-32" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/8c/bc9bc948058348ed43117cecc3007cd608f395915dae8a00974579a5dab1/polars-1.40.1.tar.gz", hash = "sha256:ab2694134b137596b5a59bfd7b4c54ebbc9b59f9403127f18e32d363777552e8", size = 733574, upload-time = "2026-04-22T19:15:55.507Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/91/74fc60d94488685a92ac9d49d7ec55f3e91fe9b77942a6235a5fa7f249c3/polars-1.40.1-py3-none-any.whl", hash = "sha256:c0f861219d1319cdea45c4ce4d30355a47176b8f98dcedf95ea8269f131b8abd", size = 828723, upload-time = "2026-04-22T19:14:25.452Z" }, -] - -[package.optional-dependencies] -pandas = [ - { name = "pandas" }, - { name = "pyarrow" }, -] -pyarrow = [ - { name = "pyarrow" }, -] - -[[package]] -name = "polars-runtime-32" -version = "1.40.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ba/26d40f039be9f552b5fd7365a621bdfc0f8e912ef77094ae4693491b0bae/polars_runtime_32-1.40.1.tar.gz", hash = "sha256:37f3065615d1bf90d03b5326222df4c5c1f8a5d33e50470aa588e3465e6eb814", size = 2935843, upload-time = "2026-04-22T19:15:57.26Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/46/22c8af5eed68ac2eeb556e0fa3ca8a7b798e984ceff4450888f3b5ac61fd/polars_runtime_32-1.40.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b748ef652270cc49e9e69f99a035e0eb4d5f856d42bcd6ac4d9d80a40142aa1e", size = 52098755, upload-time = "2026-04-22T19:14:28.555Z" }, - { url = "https://files.pythonhosted.org/packages/c6/3e/48599a38009ca60ff82a6f38c8a621ce3c0286aa7397c7d79e741bd9060e/polars_runtime_32-1.40.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:d249b3743e05986060cec0a7aaa542d020df6c6b876e556023a310efd581f9be", size = 46367542, upload-time = "2026-04-22T19:14:32.433Z" }, - { url = "https://files.pythonhosted.org/packages/43/e9/384bc069367a1a36ee31c13782c178dbd039b2b873b772d4a0fc23a2373d/polars_runtime_32-1.40.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5987b30e7aa1059d069498496e8dda35afd592b0ac3d46ed87e3ff8df1ad652c", size = 50252104, upload-time = "2026-04-22T19:14:35.945Z" }, - { url = "https://files.pythonhosted.org/packages/15/ef/7d57ceb0651af74194e97ed6583e148d352f03d696090221b8059cdfc90b/polars_runtime_32-1.40.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d7f42a8b3f16fc66002cc0f6516f7dd7653396886ae0ed362ab95c0b3408b59", size = 56250788, upload-time = "2026-04-22T19:14:39.743Z" }, - { url = "https://files.pythonhosted.org/packages/10/0f/e4b3ffc748827a14a474ec9c42e45c066050e440fec57e914091d9adda75/polars_runtime_32-1.40.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e5f7becc237a7ec9d9a10878dc8e54b73bbf4e2d94a2991c37d7a0b38590d8f9", size = 50432590, upload-time = "2026-04-22T19:14:43.388Z" }, - { url = "https://files.pythonhosted.org/packages/d9/0b/b8d95fbed869fa4caabe9c400e4210374913b376e925e96fdcfa9be6416b/polars_runtime_32-1.40.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:992d14cf191dde043d36fbdbc98a65e43fbc7e9a5024cecd45f838ac4988c1ee", size = 54155564, upload-time = "2026-04-22T19:14:47.239Z" }, - { url = "https://files.pythonhosted.org/packages/06/d9/d091d8fb5cbed5e9536adfed955c4c89987a4cc3b8e73ae4532402b91c74/polars_runtime_32-1.40.1-cp310-abi3-win_amd64.whl", hash = "sha256:f78bb2abd00101cbb23cc0cb068f7e36e081057a15d2ec2dde3dda280709f030", size = 51829755, upload-time = "2026-04-22T19:14:50.85Z" }, - { url = "https://files.pythonhosted.org/packages/65/ad/b33c3022a394f3eb55c3310597cec615412a8a33880055eee191d154a628/polars_runtime_32-1.40.1-cp310-abi3-win_arm64.whl", hash = "sha256:b5cbfaf6b085b420b4bfcbe24e8f665076d1cccfdb80c0484c02a023ce205537", size = 45822104, upload-time = "2026-04-22T19:14:54.192Z" }, -] - [[package]] name = "prometheus-client" version = "0.25.0" @@ -2979,56 +2942,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, ] -[[package]] -name = "pyarrow" -version = "24.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/13/13e1069b351bdc3881266e11147ffccf687505dbb0ea74036237f5d454a5/pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", size = 1180261, upload-time = "2026-04-21T10:51:25.837Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/c9/a47ab7ece0d86cbe6678418a0fbd1ac4bb493b9184a3891dfa0e7f287ae0/pyarrow-24.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b0e131f880cda8d04e076cee175a46fc0e8bc8b65c99c6c09dff6669335fde74", size = 35068898, upload-time = "2026-04-21T10:46:36.599Z" }, - { url = "https://files.pythonhosted.org/packages/d1/bc/8db86617a9a58008acf8913d6fed68ea2a46acb6de928db28d724c891a68/pyarrow-24.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:1b2fe7f9a5566401a0ef2571f197eb92358925c1f0c8dba305d6e43ea0871bb3", size = 36679915, upload-time = "2026-04-21T10:46:42.602Z" }, - { url = "https://files.pythonhosted.org/packages/eb/8e/fb178720400ef69db251eb4a9c3ccf4af269bc1feb5055529b8fc87170d1/pyarrow-24.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:0b3537c00fb8d384f15ac1e79b6eb6db04a16514c8c1d22e59a9b95c8ba42868", size = 45697931, upload-time = "2026-04-21T10:46:48.403Z" }, - { url = "https://files.pythonhosted.org/packages/f3/27/99c42abe8e21b44f4917f62631f3aa31404882a2c41d8a4cd5c110e13d52/pyarrow-24.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:14e31a3c9e35f1ab6356c6378f6f72830e6d2d5f1791df3774a7b097d18a6a1e", size = 48837449, upload-time = "2026-04-21T10:46:55.329Z" }, - { url = "https://files.pythonhosted.org/packages/36/b6/333749e2666e9032891125bf9c691146e92901bece62030ac1430e2e7c88/pyarrow-24.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7d9a514e73bc42711e6a35aaccf3587c520024fe0a25d830a1a8a27c15f4f57", size = 49395949, upload-time = "2026-04-21T10:47:01.869Z" }, - { url = "https://files.pythonhosted.org/packages/17/25/c5201706a2dd374e8ba6ee3fd7a8c89fb7ffc16eed5217a91fd2bd7f7626/pyarrow-24.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b196eb3f931862af3fa84c2a253514d859c08e0d8fe020e07be12e75a5a9780c", size = 51912986, upload-time = "2026-04-21T10:47:09.872Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d2/4d1bbba65320b21a49678d6fbdc6ff7c649251359fdcfc03568c4136231d/pyarrow-24.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:35405aecb474e683fb36af650618fd5340ee5471fc65a21b36076a18bbc6c981", size = 27255371, upload-time = "2026-04-21T10:47:15.943Z" }, - { url = "https://files.pythonhosted.org/packages/b4/a9/9686d9f07837f91f775e8932659192e02c74f9d8920524b480b85212cc68/pyarrow-24.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:6233c9ed9ab9d1db47de57d9753256d9dcffbf42db341576099f0fd9f6bf4810", size = 34981559, upload-time = "2026-04-21T10:47:22.17Z" }, - { url = "https://files.pythonhosted.org/packages/80/b6/0ddf0e9b6ead3474ab087ae598c76b031fc45532bf6a63f3a553440fb258/pyarrow-24.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:f7616236ec1bc2b15bfdec22a71ab38851c86f8f05ff64f379e1278cf20c634a", size = 36663654, upload-time = "2026-04-21T10:47:28.315Z" }, - { url = "https://files.pythonhosted.org/packages/7c/3b/926382efe8ce27ba729071d3566ade6dfb86bdf112f366000196b2f5780a/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66", size = 45679394, upload-time = "2026-04-21T10:47:34.821Z" }, - { url = "https://files.pythonhosted.org/packages/b3/7a/829f7d9dfd37c207206081d6dad474d81dde29952401f07f2ba507814818/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb", size = 48863122, upload-time = "2026-04-21T10:47:42.056Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e8/f88ce625fe8babaae64e8db2d417c7653adb3019b08aae85c5ed787dc816/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e", size = 49376032, upload-time = "2026-04-21T10:47:48.967Z" }, - { url = "https://files.pythonhosted.org/packages/36/7a/82c363caa145fff88fb475da50d3bf52bb024f61917be5424c3392eaf878/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6", size = 51929490, upload-time = "2026-04-21T10:47:55.981Z" }, - { url = "https://files.pythonhosted.org/packages/66/1c/e3e72c8014ad2743ca64a701652c733cc5cbcee15c0463a32a8c55518d9e/pyarrow-24.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:295f0a7f2e242dabd513737cf076007dc5b2d59237e3eca37b05c0c6446f3826", size = 27355660, upload-time = "2026-04-21T10:48:01.718Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d3/a1abf004482026ddc17f4503db227787fa3cfe41ec5091ff20e4fea55e57/pyarrow-24.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba", size = 34976759, upload-time = "2026-04-21T10:48:07.258Z" }, - { url = "https://files.pythonhosted.org/packages/4f/4a/34f0a36d28a2dd32225301b79daad44e243dc1a2bb77d43b60749be255c4/pyarrow-24.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68", size = 36658471, upload-time = "2026-04-21T10:48:13.347Z" }, - { url = "https://files.pythonhosted.org/packages/1f/78/543b94712ae8bb1a6023bcc1acf1a740fbff8286747c289cd9468fced2a5/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2", size = 45675981, upload-time = "2026-04-21T10:48:20.201Z" }, - { url = "https://files.pythonhosted.org/packages/84/9f/8fb7c222b100d314137fa40ec050de56cd8c6d957d1cfff685ce72f15b17/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0", size = 48859172, upload-time = "2026-04-21T10:48:27.541Z" }, - { url = "https://files.pythonhosted.org/packages/a7/d3/1ea72538e6c8b3b475ed78d1049a2c518e655761ea50fe1171fc855fcab7/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495", size = 49385733, upload-time = "2026-04-21T10:48:34.7Z" }, - { url = "https://files.pythonhosted.org/packages/c3/be/c3d8b06a1ba35f2260f8e1f771abbee7d5e345c0937aab90675706b1690a/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f", size = 51934335, upload-time = "2026-04-21T10:48:42.099Z" }, - { url = "https://files.pythonhosted.org/packages/9c/62/89e07a1e7329d2cde3e3c6994ba0839a24977a2beda8be6005ea3d860b99/pyarrow-24.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91", size = 27271748, upload-time = "2026-04-21T10:49:42.532Z" }, - { url = "https://files.pythonhosted.org/packages/17/1a/cff3a59f80b5b1658549d46611b67163f65e0664431c076ad728bf9d5af4/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275", size = 35238554, upload-time = "2026-04-21T10:48:48.526Z" }, - { url = "https://files.pythonhosted.org/packages/a8/99/cce0f42a327bfef2c420fb6078a3eb834826e5d6697bf3009fe11d2ad051/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b", size = 36782301, upload-time = "2026-04-21T10:48:55.181Z" }, - { url = "https://files.pythonhosted.org/packages/2a/66/8e560d5ff6793ca29aca213c53eec0dd482dd46cb93b2819e5aab52e4252/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42", size = 45721929, upload-time = "2026-04-21T10:49:03.676Z" }, - { url = "https://files.pythonhosted.org/packages/27/0c/a26e25505d030716e078d9f16eb74973cbf0b33b672884e9f9da1c83b871/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b", size = 48825365, upload-time = "2026-04-21T10:49:11.714Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/771f9ecb0c65e73fe9dccdd1717901b9594f08c4515d000c7c62df573811/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37", size = 49451819, upload-time = "2026-04-21T10:49:21.474Z" }, - { url = "https://files.pythonhosted.org/packages/48/da/61ae89a88732f5a785646f3ec6125dbb640fa98a540eb2b9889caa561403/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca", size = 51909252, upload-time = "2026-04-21T10:49:31.164Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1a/8dd5cafab7b66573fa91c03d06d213356ad4edd71813aa75e08ce2b3a844/pyarrow-24.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d", size = 27388127, upload-time = "2026-04-21T10:49:37.334Z" }, - { url = "https://files.pythonhosted.org/packages/ad/80/d022a34ff05d2cbedd8ccf841fc1f532ecfa9eb5ed1711b56d0e0ea71fc9/pyarrow-24.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:1cc9057f0319e26333b357e17f3c2c022f1a83739b48a88b25bfd5fa2dc18838", size = 35007997, upload-time = "2026-04-21T10:49:48.796Z" }, - { url = "https://files.pythonhosted.org/packages/1a/ff/f01485fda6f4e5d441afb8dd5e7681e4db18826c1e271852f5d3957d6a80/pyarrow-24.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e6f1278ee4785b6db21229374a1c9e54ec7c549de5d1efc9630b6207de7e170b", size = 36678720, upload-time = "2026-04-21T10:49:55.858Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c2/2d2d5fea814237923f71b36495211f20b43a1576f9a4d6da7e751a64ec6f/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:adbbedc55506cbdabb830890444fb856bfb0060c46c6f8026c6c2f2cf86ae795", size = 45741852, upload-time = "2026-04-21T10:50:04.624Z" }, - { url = "https://files.pythonhosted.org/packages/8e/3a/28ba9c1c1ebdbb5f1b94dfebb46f207e52e6a554b7fe4132540fde29a3a0/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ae8a1145af31d903fa9bb166824d7abe9b4681a000b0159c9fb99c11bc11ad26", size = 48889852, upload-time = "2026-04-21T10:50:12.293Z" }, - { url = "https://files.pythonhosted.org/packages/df/51/4a389acfd31dca009f8fb82d7f510bb4130f2b3a8e18cf00194d0687d8ac/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d7027eba1df3b2069e2e8d80f644fa0918b68c46432af3d088ddd390d063ecde", size = 49445207, upload-time = "2026-04-21T10:50:20.677Z" }, - { url = "https://files.pythonhosted.org/packages/19/4b/0bab2b23d2ae901b1b9a03c0efd4b2d070256f8ce3fc43f6e58c167b2081/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e56a1ffe9bf7b727432b89104cc0849c21582949dd7bdcb34f17b2001a351a76", size = 51954117, upload-time = "2026-04-21T10:50:29.14Z" }, - { url = "https://files.pythonhosted.org/packages/29/88/f4e9145da0417b3d2c12035a8492b35ff4a3dbc653e614fcfb51d9dedb38/pyarrow-24.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:38be1808cdd068605b787e6ca9119b27eb275a0234e50212c3492331680c3b1e", size = 28001155, upload-time = "2026-04-21T10:51:22.337Z" }, - { url = "https://files.pythonhosted.org/packages/79/4f/46a49a63f43526da895b1a45bbb51d5baf8e4d77159f8528fc3e5490007f/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:418e48ce50a45a6a6c73c454677203a9c75c966cb1e92ca3370959185f197a05", size = 35250387, upload-time = "2026-04-21T10:50:35.552Z" }, - { url = "https://files.pythonhosted.org/packages/a0/da/d5e0cd5ef00796922404806d5f00325cdadc3441ce2c13fe7115f2df9a64/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:2f16197705a230a78270cdd4ea8a1d57e86b2fdcbc34a1f6aebc72e65c986f9a", size = 36797102, upload-time = "2026-04-21T10:50:42.417Z" }, - { url = "https://files.pythonhosted.org/packages/34/c7/5904145b0a593a05236c882933d439b5720f0a145381179063722fbfc123/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:fb24ac194bfc5e86839d7dcd52092ee31e5fe6733fe11f5e3b06ef0812b20072", size = 45745118, upload-time = "2026-04-21T10:50:49.324Z" }, - { url = "https://files.pythonhosted.org/packages/13/d3/cca42fe166d1c6e4d5b80e530b7949104d10e17508a90ae202dac205ce2a/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:9700ebd9a51f5895ce75ff4ac4b3c47a7d4b42bc618be8e713e5d56bacf5f931", size = 48844765, upload-time = "2026-04-21T10:50:55.579Z" }, - { url = "https://files.pythonhosted.org/packages/b0/49/942c3b79878ba928324d1e17c274ed84581db8c0a749b24bcf4cbdf15bd3/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d8ddd2768da81d3ee08cfea9b597f4abb4e8e1dc8ae7e204b608d23a0d3ab699", size = 49471890, upload-time = "2026-04-21T10:51:02.439Z" }, - { url = "https://files.pythonhosted.org/packages/76/97/ff71431000a75d84135a1ace5ca4ba11726a231a8007bbb320a4c54075d5/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:61a3d7eaa97a14768b542f3d284dc6400dd2470d9f080708b13cd46b6ae18136", size = 51932250, upload-time = "2026-04-21T10:51:10.576Z" }, - { url = "https://files.pythonhosted.org/packages/51/be/6f79d55816d5c22557cf27533543d5d70dfe692adfbee4b99f2760674f38/pyarrow-24.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19", size = 28131282, upload-time = "2026-04-21T10:51:16.815Z" }, -] - [[package]] name = "pyasn1" version = "0.6.3" @@ -3085,7 +2998,7 @@ wheels = [ [[package]] name = "pydantic-ai-slim" -version = "1.101.0" +version = "1.102.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "genai-prices" }, @@ -3096,9 +3009,9 @@ dependencies = [ { name = "pydantic-graph" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/a3/1271c2df8bf5579cff69e2dc0af03a0f3990ce866ae9ff0baff524b77a19/pydantic_ai_slim-1.101.0.tar.gz", hash = "sha256:11b3f61a4748f0b76b00fb91f1acbd9eb0096dca39bf82b93d071dbf7c8a19c2", size = 737068, upload-time = "2026-05-22T05:01:25.902Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/3e/14980440e8f0532535e1fbe936fec5f8d8e7bc6cafa81f6f3c51b1884fe5/pydantic_ai_slim-1.102.0.tar.gz", hash = "sha256:0b8f2b70fa2b40efcbd09d341a346934fc4e46622ae281f858c6bfd3d0d3152b", size = 739988, upload-time = "2026-05-23T01:14:32.808Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/a3/1b3ac32ba0932cc3e2e3045d50044fe3b88bbb77d6a8a48303088217ef39/pydantic_ai_slim-1.101.0-py3-none-any.whl", hash = "sha256:919a39da29f0315ad093446e00ee3252d4c90e42fee360f24e2ac636d5ff089f", size = 916632, upload-time = "2026-05-22T05:01:18.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2e/089df86adaf904dd97a1b139d29fe728af0e41430d747f5b6315df3b0c1e/pydantic_ai_slim-1.102.0-py3-none-any.whl", hash = "sha256:f9fa9c3fb58a76f85522f78d1037d201b424de46d532263ed780b3730060449f", size = 919311, upload-time = "2026-05-23T01:14:23.464Z" }, ] [[package]] @@ -3205,7 +3118,7 @@ wheels = [ [[package]] name = "pydantic-graph" -version = "1.101.0" +version = "1.102.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -3213,9 +3126,9 @@ dependencies = [ { name = "pydantic" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8f/5d/c432cb178ff93a07dca7d60aab271b57c7fbfaf1756a59ad22bc109a62be/pydantic_graph-1.101.0.tar.gz", hash = "sha256:9969047e69828294ec69ffdd3747e5e747198c497df36ef791e0b58ba8f723ca", size = 62559, upload-time = "2026-05-22T05:01:29.128Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/37/4265a1a63eddf35a5aa621c9b2355525bdeae3eb59c3954b165fbfe31404/pydantic_graph-1.102.0.tar.gz", hash = "sha256:e285bd7115e4e92676eaf0a5e7e6faa64cda8c4819f67923a118c50666b909ab", size = 62584, upload-time = "2026-05-23T01:14:36.056Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/56/d1637b48dcb326eaa19d7e7b48d7d585a3691d044635b68078fdc60c561c/pydantic_graph-1.101.0-py3-none-any.whl", hash = "sha256:ad017f75d89d4e3c38383b7ec3532905869980f890a2689cacafa5b8e13b9e8a", size = 80100, upload-time = "2026-05-22T05:01:21.833Z" }, + { url = "https://files.pythonhosted.org/packages/a4/49/5597c52d50114440047dd4ce4f6505e32ee336f43267639907d1a17648ee/pydantic_graph-1.102.0-py3-none-any.whl", hash = "sha256:b1a28314adc4abca4db02cf095d064782ec5712e0847ce7a6b79a3c84bf1fc01", size = 80100, upload-time = "2026-05-23T01:14:27.583Z" }, ] [[package]] @@ -3546,7 +3459,7 @@ version = "0.9.0" source = { editable = "." } dependencies = [ { name = "ccy" }, - { name = "polars", extra = ["pandas", "pyarrow"] }, + { name = "pandas" }, { name = "pydantic" }, { name = "python-dotenv" }, { name = "scipy" }, @@ -3631,9 +3544,9 @@ requires-dist = [ { name = "mkdocstrings", extras = ["python"], marker = "extra == 'docs'", specifier = "==1.0.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.14.1" }, { name = "openai", marker = "extra == 'ai'", specifier = ">=2.16.0" }, + { name = "pandas", specifier = ">=2.0.0" }, { name = "plotly", marker = "extra == 'book'", specifier = ">=6.2.0" }, { name = "plotly", marker = "extra == 'docs'", specifier = ">=6.2.0" }, - { name = "polars", extras = ["pandas", "pyarrow"], specifier = ">=1.11.0" }, { name = "pydantic", specifier = ">=2.0.2" }, { name = "pydantic-ai-slim", marker = "extra == 'ai'", specifier = ">=1.51.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.0.0" },