mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-30 14:02:06 +00:00
* refactor openbb-technical for v5 * forward refs * linting * no python 3.15 in test matrix * reame * ty * move all params to body --------- Co-authored-by: deeleeramone <>
179 lines
6.0 KiB
Python
179 lines
6.0 KiB
Python
"""Moving-average crossover signal."""
|
|
|
|
from datetime import (
|
|
date as dateType,
|
|
datetime,
|
|
)
|
|
from typing import Literal
|
|
|
|
from openbb_core.app.model.example import APIEx, PythonEx
|
|
from openbb_core.app.model.obbject import OBBject
|
|
from openbb_core.app.router import Router
|
|
from openbb_core.app.utils import basemodel_to_df
|
|
from openbb_core.provider.abstract.data import Data
|
|
from openbb_core.provider.abstract.query_params import QueryParams
|
|
from pydantic import Field, PositiveInt, field_validator
|
|
|
|
from openbb_technical.helpers import validate_data
|
|
|
|
# Bare 'date' alias for function signatures so the static-package builder
|
|
# writes 'date' (which it imports from datetime) rather than 'dateType' which it does not.
|
|
date = dateType
|
|
|
|
|
|
router = Router(prefix="", description="Signal-style technical indicators.")
|
|
|
|
|
|
MaMode = Literal["sma", "ema", "wma", "hma", "zlma"]
|
|
|
|
|
|
class CrossoversQueryParams(QueryParams):
|
|
"""Query parameters for the moving-average crossover endpoint.
|
|
|
|
Parameters
|
|
----------
|
|
data : list[Data]
|
|
Input price series containing ``target``.
|
|
target : str, optional
|
|
Column on which to compute the moving averages, by default ``"close"``.
|
|
index : str, optional
|
|
Index column name in ``data``, by default ``"date"``.
|
|
fast_length : PositiveInt, optional
|
|
Lookback for the fast moving average, by default 20.
|
|
slow_length : PositiveInt, optional
|
|
Lookback for the slow moving average. Must be strictly greater than
|
|
``fast_length``, by default 50.
|
|
mamode : {"sma", "ema", "wma", "hma", "zlma"}, optional
|
|
Moving-average flavour, by default ``"sma"``.
|
|
"""
|
|
|
|
__category__ = "signal"
|
|
__output_columns__ = (
|
|
"date",
|
|
"direction",
|
|
"fast_value",
|
|
"slow_value",
|
|
"price",
|
|
"distance",
|
|
)
|
|
|
|
data: list[Data] = Field(description="Input price series.")
|
|
target: str = Field(
|
|
default="close", description="Column on which to compute moving averages."
|
|
)
|
|
index: str = Field(default="date", description="Index column name in ``data``.")
|
|
fast_length: PositiveInt = Field(
|
|
default=20, description="Lookback for the fast moving average."
|
|
)
|
|
slow_length: PositiveInt = Field(
|
|
default=50, description="Lookback for the slow moving average."
|
|
)
|
|
mamode: MaMode = Field(
|
|
default="sma",
|
|
description="Moving-average flavour. ``sma`` is the default; ``ema``, ``wma``, ``hma`` and ``zlma`` are also supported.",
|
|
)
|
|
|
|
@field_validator("slow_length")
|
|
@classmethod
|
|
def slow_must_exceed_fast(cls, v: int, info) -> int:
|
|
"""Enforce ``slow_length > fast_length``."""
|
|
fast = info.data.get("fast_length")
|
|
if fast is not None and v <= fast:
|
|
raise ValueError("slow_length must be greater than fast_length.")
|
|
return v
|
|
|
|
|
|
class CrossoverEvent(Data):
|
|
"""One row per moving-average crossover bar (sparse output).
|
|
|
|
Parameters
|
|
----------
|
|
date : date | str
|
|
Date of the crossover bar.
|
|
direction : {"bullish", "bearish"}
|
|
``"bullish"`` when the fast average crosses above the slow average;
|
|
``"bearish"`` when the fast average crosses below the slow average.
|
|
fast_value : float
|
|
Fast moving-average value at the crossover bar.
|
|
slow_value : float
|
|
Slow moving-average value at the crossover bar.
|
|
price : float
|
|
Target-column price at the crossover bar.
|
|
distance : float
|
|
Signed gap ``fast_value - slow_value``. Positive for bullish crossovers
|
|
and negative for bearish crossovers.
|
|
"""
|
|
|
|
date: datetime | dateType | str = Field(description="Date of the crossover bar.")
|
|
direction: Literal["bullish", "bearish"] = Field(
|
|
description="``bullish`` when fast crosses above slow, ``bearish`` when fast crosses below.",
|
|
)
|
|
fast_value: float = Field(
|
|
description="Fast moving-average value at the crossover bar."
|
|
)
|
|
slow_value: float = Field(
|
|
description="Slow moving-average value at the crossover bar."
|
|
)
|
|
price: float = Field(description="Target-column price at the crossover bar.")
|
|
distance: float = Field(
|
|
description="``fast_value - slow_value``. Positive for bullish, negative for bearish crossovers.",
|
|
)
|
|
|
|
|
|
@router.command(
|
|
methods=["POST"],
|
|
examples=[
|
|
PythonEx(
|
|
description="20/50 SMA crossover events on daily SPY.",
|
|
code=[
|
|
"data = obb.equity.price.historical(symbol='SPY', start_date='2022-01-01', provider='yfinance').results",
|
|
"events = obb.technical.crossovers(data=data, fast_length=20, slow_length=50)",
|
|
],
|
|
),
|
|
APIEx(parameters={"data": APIEx.mock_data("timeseries")}),
|
|
],
|
|
)
|
|
def crossovers(params: CrossoversQueryParams) -> OBBject[list[CrossoverEvent]]:
|
|
"""Detect bullish and bearish moving-average crossover events."""
|
|
import numpy as np
|
|
import pandas_ta as ta # noqa: F401
|
|
|
|
validate_data(params.data, [params.slow_length])
|
|
df = basemodel_to_df(params.data, index=params.index)
|
|
series = df[params.target]
|
|
fast = getattr(series.to_frame(name=params.target).ta, params.mamode)(
|
|
length=params.fast_length
|
|
)
|
|
slow = getattr(series.to_frame(name=params.target).ta, params.mamode)(
|
|
length=params.slow_length
|
|
)
|
|
diff = (fast - slow).dropna()
|
|
if diff.empty:
|
|
return OBBject(results=[])
|
|
sign = np.sign(diff)
|
|
prev = sign.shift(1)
|
|
flips = (sign != prev) & (sign != 0) & (prev != 0) & (sign * prev < 0)
|
|
events: list[CrossoverEvent] = []
|
|
for ts in diff.index[flips.fillna(False)]:
|
|
d = float(diff.loc[ts])
|
|
events.append(
|
|
CrossoverEvent(
|
|
date=ts,
|
|
direction="bullish" if d > 0 else "bearish",
|
|
fast_value=float(fast.loc[ts]),
|
|
slow_value=float(slow.loc[ts]),
|
|
price=float(series.loc[ts]),
|
|
distance=d,
|
|
)
|
|
)
|
|
return OBBject(results=events)
|
|
|
|
|
|
__all__ = [
|
|
"CrossoverEvent",
|
|
"CrossoversQueryParams",
|
|
"MaMode",
|
|
"crossovers",
|
|
"router",
|
|
]
|