mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-29 21:42:22 +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 <>
194 lines
6.1 KiB
Python
194 lines
6.1 KiB
Python
"""Channel-breakout signals."""
|
|
|
|
from datetime import (
|
|
date as dateType,
|
|
datetime,
|
|
)
|
|
from typing import Literal
|
|
|
|
from openbb_core.app.model.example import APIEx
|
|
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, PositiveFloat, PositiveInt
|
|
|
|
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="Channel-breakout signals.")
|
|
|
|
|
|
BreakoutMethod = Literal["donchian", "bollinger"]
|
|
|
|
|
|
class BreakoutsQueryParams(QueryParams):
|
|
"""Query parameters for the channel-breakouts endpoint.
|
|
|
|
Parameters
|
|
----------
|
|
data : list[Data]
|
|
Input OHLC price series.
|
|
index : str, optional
|
|
Index column name in ``data``, by default ``"date"``.
|
|
method : {"donchian", "bollinger"}, optional
|
|
Channel definition. ``donchian`` uses the rolling high/low envelope;
|
|
``bollinger`` uses the rolling mean plus or minus ``band_std``
|
|
standard deviations. Default is ``"donchian"``.
|
|
length : PositiveInt, optional
|
|
Lookback window for the channel, by default 20.
|
|
band_std : PositiveFloat, optional
|
|
Bollinger band multiplier. Ignored for ``donchian``. ``None``
|
|
resolves to 2.0 when ``method='bollinger'``.
|
|
"""
|
|
|
|
__category__ = "signal"
|
|
__output_columns__ = (
|
|
"date",
|
|
"direction",
|
|
"price",
|
|
"band",
|
|
"magnitude",
|
|
"bars_in_range",
|
|
)
|
|
|
|
data: list[Data] = Field(description="Input OHLC price series.")
|
|
index: str = Field(default="date", description="Index column name in ``data``.")
|
|
method: BreakoutMethod = Field(
|
|
default="donchian",
|
|
description=(
|
|
"Channel definition: Donchian (rolling high/low) or Bollinger "
|
|
"(mean +/- ``band_std`` * stdev)."
|
|
),
|
|
)
|
|
length: PositiveInt = Field(
|
|
default=20, description="Lookback window for the channel."
|
|
)
|
|
band_std: PositiveFloat | None = Field(
|
|
default=None,
|
|
description=(
|
|
"Bollinger band multiplier. Ignored for ``donchian``. ``None`` "
|
|
"resolves to 2.0 when ``method='bollinger'``."
|
|
),
|
|
)
|
|
|
|
|
|
class BreakoutEvent(Data):
|
|
"""One row per breakout bar.
|
|
|
|
Parameters
|
|
----------
|
|
date : date | str
|
|
Bar on which the breakout occurred.
|
|
direction : {"upside", "downside"}
|
|
Which band was crossed.
|
|
price : float
|
|
Close price on the breakout bar.
|
|
band : float
|
|
Previous bar's band level that was breached.
|
|
magnitude : float
|
|
Signed distance ``price - band``. Positive for upside breakouts;
|
|
negative for downside breakouts.
|
|
bars_in_range : int
|
|
Bars elapsed since the most recent breakout in either direction.
|
|
"""
|
|
|
|
date: datetime | dateType | str = Field(
|
|
description="Bar on which the breakout occurred."
|
|
)
|
|
direction: Literal["upside", "downside"] = Field(
|
|
description="Which band was crossed.",
|
|
)
|
|
price: float = Field(description="Close price on the breakout bar.")
|
|
band: float = Field(description="Previous bar's band level that was breached.")
|
|
magnitude: float = Field(
|
|
description="Signed distance: ``price - band``. Positive for upside, negative for downside.",
|
|
)
|
|
bars_in_range: int = Field(
|
|
description="Bars since the most recent breakout (in either direction).",
|
|
)
|
|
|
|
|
|
def _channel_bands(df, method: str, length: int, band_std: float | None):
|
|
"""Return ``(upper_band, lower_band)`` Series aligned to ``df.index``."""
|
|
import pandas_ta as ta # noqa: F401
|
|
|
|
if method == "donchian":
|
|
dc = df.ta.donchian(lower_length=length, upper_length=length)
|
|
upper = dc[f"DCU_{length}_{length}"]
|
|
lower = dc[f"DCL_{length}_{length}"]
|
|
return upper, lower
|
|
std = band_std if band_std is not None else 2.0
|
|
bb = df.ta.bbands(length=length, std=std)
|
|
bbu_col = next(c for c in bb.columns if c.startswith("BBU_"))
|
|
bbl_col = next(c for c in bb.columns if c.startswith("BBL_"))
|
|
return bb[bbu_col], bb[bbl_col]
|
|
|
|
|
|
@router.command(
|
|
methods=["POST"],
|
|
examples=[
|
|
APIEx(
|
|
parameters={
|
|
"data": APIEx.mock_data("timeseries"),
|
|
"method": "donchian",
|
|
"length": 20,
|
|
},
|
|
),
|
|
],
|
|
)
|
|
def breakouts(params: BreakoutsQueryParams) -> OBBject[list[BreakoutEvent]]:
|
|
"""Detect channel breakouts where price closes beyond a rolling envelope."""
|
|
validate_data(params.data, [params.length])
|
|
df = basemodel_to_df(params.data, index=params.index)
|
|
|
|
upper, lower = _channel_bands(df, params.method, params.length, params.band_std)
|
|
prev_upper = upper.shift(1)
|
|
prev_lower = lower.shift(1)
|
|
close = df["close"].astype(float)
|
|
|
|
up_break = close > prev_upper
|
|
dn_break = close < prev_lower
|
|
|
|
events: list[dict] = []
|
|
last_event_pos: int | None = None
|
|
dates = df.index.tolist()
|
|
|
|
for i, dt in enumerate(dates):
|
|
is_up = bool(up_break.iloc[i])
|
|
is_dn = bool(dn_break.iloc[i])
|
|
if not is_up and not is_dn:
|
|
continue
|
|
direction = "upside" if is_up else "downside"
|
|
band = float(prev_upper.iloc[i] if is_up else prev_lower.iloc[i])
|
|
price = float(close.iloc[i])
|
|
magnitude = price - band
|
|
bars_in_range = i if last_event_pos is None else i - last_event_pos
|
|
events.append(
|
|
{
|
|
"date": dt,
|
|
"direction": direction,
|
|
"price": price,
|
|
"band": band,
|
|
"magnitude": magnitude,
|
|
"bars_in_range": int(bars_in_range),
|
|
}
|
|
)
|
|
last_event_pos = i
|
|
|
|
return OBBject(results=[BreakoutEvent(**row) for row in events])
|
|
|
|
|
|
__all__ = [
|
|
"BreakoutEvent",
|
|
"BreakoutMethod",
|
|
"BreakoutsQueryParams",
|
|
"breakouts",
|
|
"router",
|
|
]
|