233 lines
7.2 KiB
Python
233 lines
7.2 KiB
Python
|
|
"""
|
||
|
|
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),
|
||
|
|
)
|