Files

233 lines
7.2 KiB
Python
Raw Permalink Normal View History

2026-03-23 23:34:28 +05:30
"""
ferro_ta.crypto — Crypto and 24/7 market helpers.
=================================================
Helpers designed for continuous (24/7) markets such as cryptocurrency or FX.
Functions
---------
funding_pnl(position_size, funding_rate)
Compute the cumulative PnL from periodic funding rate payments.
continuous_bar_labels(n_bars, period_bars)
Assign integer period labels to bars without calendar-based sessions.
session_boundaries(timestamps_ns)
Return bar indices at the start of each UTC-day session boundary.
resample_continuous(ohlcv, period_bars)
Resample a continuous OHLCV series by grouping every *period_bars* input
bars into one output bar (no session filtering).
Rust backend
------------
ferro_ta._ferro_ta.funding_cumulative_pnl
ferro_ta._ferro_ta.continuous_bar_labels
ferro_ta._ferro_ta.mark_session_boundaries
"""
from __future__ import annotations
from typing import Union
import numpy as np
from numpy.typing import ArrayLike, NDArray
from ferro_ta._ferro_ta import (
continuous_bar_labels as _rust_continuous_bar_labels,
)
from ferro_ta._ferro_ta import (
funding_cumulative_pnl as _rust_funding_cumulative_pnl,
)
from ferro_ta._ferro_ta import (
mark_session_boundaries as _rust_mark_session_boundaries,
)
from ferro_ta._ferro_ta import (
ohlcv_agg as _rust_ohlcv_agg,
)
from ferro_ta._utils import _to_f64
__all__ = [
"funding_pnl",
"continuous_bar_labels",
"session_boundaries",
"resample_continuous",
]
# type alias
OHLCVTuple = tuple[
NDArray[np.float64],
NDArray[np.float64],
NDArray[np.float64],
NDArray[np.float64],
NDArray[np.float64],
]
def funding_pnl(
position_size: ArrayLike,
funding_rate: ArrayLike,
) -> NDArray[np.float64]:
"""Compute cumulative PnL from periodic funding rate payments.
Crypto perpetual contracts charge a periodic funding rate to position
holders. A long position pays when the funding rate is positive; a short
position receives.
PnL at period *i* = ``-position_size[i] * funding_rate[i]``
Returned array is the cumulative sum of those per-period PnLs.
Parameters
----------
position_size : array-like — signed position size per funding period.
Positive = long, negative = short.
funding_rate : array-like — periodic funding rate in decimal notation
(e.g. 0.0001 = 0.01%). Must have the same length as *position_size*.
Returns
-------
numpy.ndarray of float64 — cumulative funding PnL.
Examples
--------
>>> import numpy as np
>>> from ferro_ta.analysis.crypto import funding_pnl
>>> pos = np.ones(5) # long 1 contract
>>> rate = np.array([0.0001, 0.0002, -0.0001, 0.0001, 0.0001])
>>> pnl = funding_pnl(pos, rate)
>>> pnl.round(6)
array([-0.0001, -0.0003, 0. , -0.0001, -0.0002])
"""
return np.asarray(
_rust_funding_cumulative_pnl(_to_f64(position_size), _to_f64(funding_rate)),
dtype=np.float64,
)
def continuous_bar_labels(
n_bars: int,
period_bars: int,
) -> NDArray[np.int64]:
"""Assign sequential integer labels to bars in equal-size buckets.
Useful for grouping continuous data (no session gaps) into periods without
relying on calendar logic. Bars 0…(period_bars-1) get label 0,
bars period_bars…(2·period_bars-1) get label 1, etc.
Parameters
----------
n_bars : int — total number of bars
period_bars : int — number of bars per period (e.g. 24 for hourly → daily)
Returns
-------
numpy.ndarray of int64 — period label per bar.
Examples
--------
>>> from ferro_ta.analysis.crypto import continuous_bar_labels
>>> continuous_bar_labels(10, 3)
array([0, 0, 0, 1, 1, 1, 2, 2, 2, 3])
"""
return np.asarray(
_rust_continuous_bar_labels(int(n_bars), int(period_bars)),
dtype=np.int64,
)
def session_boundaries(
timestamps_ns: ArrayLike,
) -> NDArray[np.int64]:
"""Return bar indices at the start of each UTC-day boundary.
Intended for 24/7 data where no exchange session gaps exist. Useful for
building daily OHLCV bars from intraday continuous data.
Parameters
----------
timestamps_ns : array-like of int64 — UTC timestamps in nanoseconds
(e.g. ``pandas.DatetimeIndex.astype('int64')``).
Returns
-------
numpy.ndarray of int64 — indices of the first bar in each UTC day
(always includes index 0).
Examples
--------
>>> import numpy as np
>>> from ferro_ta.analysis.crypto import session_boundaries
>>> # Two UTC days of hourly bars: day 0 = bars 0-23, day 1 = bars 24-47
>>> base_ns = np.int64(1_700_000_000_000_000_000) # some UTC timestamp
>>> ns_per_hour = np.int64(3_600_000_000_000)
>>> ts = base_ns + np.arange(48, dtype=np.int64) * ns_per_hour
>>> bounds = session_boundaries(ts)
"""
ts = np.asarray(timestamps_ns, dtype=np.int64)
return np.asarray(
_rust_mark_session_boundaries(ts),
dtype=np.int64,
)
def resample_continuous(
ohlcv: Union[
tuple[ArrayLike, ArrayLike, ArrayLike, ArrayLike, ArrayLike],
object, # pandas.DataFrame
],
period_bars: int,
) -> OHLCVTuple:
"""Resample a continuous OHLCV series by grouping *period_bars* input bars.
Unlike time-based resampling, this function requires no calendar or
session information. Every *period_bars* consecutive input bars are
aggregated into one output bar. Ideal for 24/7 crypto data.
Parameters
----------
ohlcv : tuple ``(open, high, low, close, volume)`` of array-like,
**or** a ``pandas.DataFrame`` with columns ``open/high/low/close/volume``
(case-insensitive).
period_bars : int — number of input bars per output bar (must be >= 1).
Returns
-------
tuple ``(open, high, low, close, volume)`` of numpy.ndarray — resampled bars.
Notes
-----
The last output bar may aggregate fewer than *period_bars* input bars if
``len(close) % period_bars != 0``.
"""
try:
import pandas as pd
if isinstance(ohlcv, pd.DataFrame):
cols = {c.lower(): c for c in ohlcv.columns} # type: ignore[union-attr]
o = _to_f64(ohlcv[cols["open"]].values) # type: ignore[index]
h = _to_f64(ohlcv[cols["high"]].values) # type: ignore[index]
lo = _to_f64(ohlcv[cols["low"]].values) # type: ignore[index]
c = _to_f64(ohlcv[cols["close"]].values) # type: ignore[index]
v = _to_f64(ohlcv[cols["volume"]].values) # type: ignore[index]
else:
o, h, lo, c, v = [_to_f64(x) for x in ohlcv] # type: ignore[union-attr]
except ImportError:
o, h, lo, c, v = [_to_f64(x) for x in ohlcv] # type: ignore[union-attr]
n = len(c)
if period_bars < 1:
raise ValueError("period_bars must be >= 1")
# Build bar-group labels
labels = np.asarray(
_rust_continuous_bar_labels(n, int(period_bars)),
dtype=np.int64,
)
ro, rh, rl, rc, rv = _rust_ohlcv_agg(o, h, lo, c, v, labels)
return (
np.asarray(ro, dtype=np.float64),
np.asarray(rh, dtype=np.float64),
np.asarray(rl, dtype=np.float64),
np.asarray(rc, dtype=np.float64),
np.asarray(rv, dtype=np.float64),
)