Files
my-python-backteat/ferro-ta-main/python/ferro_ta/data/aggregation.py
T
2026-07-09 05:08:16 +08:00

239 lines
8.0 KiB
Python

"""
ferro_ta.aggregation — Tick and trade aggregation pipeline.
Aggregates raw tick or trade data (streams of (timestamp, price, size)) into
OHLCV bars using three bar types:
- **time bars** — fixed time intervals (e.g. every 1 minute)
- **volume bars** — fixed volume threshold per bar
- **tick bars** — fixed number of ticks per bar
The compute-intensive bar accumulation is implemented in Rust; this module
provides the Python-facing API with DataFrame support.
Functions
---------
aggregate_ticks(ticks, rule)
Aggregate a tick stream to OHLCV bars. The *rule* string specifies the
bar type and parameter:
- ``'time:<seconds>'`` — e.g. ``'time:60'`` for 1-minute bars
- ``'volume:<threshold>'`` — e.g. ``'volume:1000'`` for 1000-unit volume bars
- ``'tick:<n>'`` — e.g. ``'tick:100'`` for 100-tick bars
Rust backend
------------
All accumulation logic delegates to::
ferro_ta._ferro_ta.aggregate_tick_bars
ferro_ta._ferro_ta.aggregate_volume_bars_ticks
ferro_ta._ferro_ta.aggregate_time_bars
"""
from __future__ import annotations
from typing import Any, Optional
import numpy as np
from numpy.typing import NDArray
from ferro_ta._ferro_ta import aggregate_tick_bars as _rust_tick_bars
from ferro_ta._ferro_ta import aggregate_time_bars as _rust_time_bars
from ferro_ta._ferro_ta import aggregate_volume_bars_ticks as _rust_volume_bars_ticks
from ferro_ta._utils import _to_f64
from ferro_ta.core.exceptions import FerroTAValueError
__all__ = [
"aggregate_ticks",
"TickAggregator",
]
# ---------------------------------------------------------------------------
# _parse_rule
# ---------------------------------------------------------------------------
def _parse_rule(rule: str) -> tuple[str, float]:
"""Parse a rule string into (bar_type, parameter).
Supported formats::
'time:60' → ('time', 60.0)
'volume:1000' → ('volume', 1000.0)
'tick:100' → ('tick', 100.0)
"""
parts = rule.split(":", 1)
if len(parts) != 2:
raise FerroTAValueError(
f"Invalid rule format: {rule!r}. "
"Expected 'time:<seconds>', 'volume:<threshold>', or 'tick:<n>'."
)
bar_type = parts[0].lower().strip()
if bar_type not in ("time", "volume", "tick"):
raise FerroTAValueError(
f"Unknown bar type {bar_type!r}. Supported types: 'time', 'volume', 'tick'."
)
try:
param = float(parts[1].strip())
except ValueError as exc:
raise FerroTAValueError(
f"Cannot parse parameter {parts[1]!r} as a number in rule {rule!r}."
) from exc
if param <= 0:
raise FerroTAValueError(
f"Rule parameter must be > 0, got {param} in rule {rule!r}."
)
return bar_type, param
# ---------------------------------------------------------------------------
# aggregate_ticks
# ---------------------------------------------------------------------------
def aggregate_ticks(
ticks: Any,
rule: str = "tick:100",
*,
timestamp_col: str = "timestamp",
price_col: str = "price",
size_col: str = "size",
) -> Any:
"""Aggregate tick/trade data into OHLCV bars.
Parameters
----------
ticks : pandas.DataFrame, list of (timestamp, price, size), or dict of arrays
Tick data. Accepted formats:
1. **pandas DataFrame** with columns ``timestamp``, ``price``, ``size``
(column names configurable via *_col* parameters). The timestamp
column must contain numeric Unix timestamps (seconds) for time bars.
2. **list of tuples** ``[(ts, price, size), …]``.
3. **dict** ``{'timestamp': array, 'price': array, 'size': array}``.
rule : str
Bar specification:
- ``'tick:<n>'`` — every N ticks become one bar (default ``'tick:100'``)
- ``'volume:<threshold>'`` — every N units of volume become one bar
- ``'time:<seconds>'`` — every N seconds become one bar
timestamp_col, price_col, size_col : str
Column names when *ticks* is a DataFrame.
Returns
-------
pandas.DataFrame or tuple of numpy arrays
If a DataFrame was passed in (or pandas is available), returns a
DataFrame with columns ``open``, ``high``, ``low``, ``close``,
``volume``, and (for time bars) ``timestamp``.
Otherwise returns a tuple ``(open, high, low, close, volume)``.
Examples
--------
>>> import numpy as np
>>> from ferro_ta.data.aggregation import aggregate_ticks
>>> rng = np.random.default_rng(42)
>>> n = 500
>>> price = 100 + np.cumsum(rng.normal(0, 0.1, n))
>>> size = rng.uniform(10, 100, n)
>>> bars = aggregate_ticks({"price": price, "size": size}, rule="tick:50")
>>> len(bars["open"]) == n // 50 + (1 if n % 50 != 0 else 0)
True
"""
bar_type, param = _parse_rule(rule)
# --- Normalise input ---
ts_arr: Optional[NDArray[np.float64]] = None
if isinstance(ticks, list):
# list of (ts, price, size) tuples
arr = np.ascontiguousarray(ticks, dtype=np.float64)
ts_arr = np.ascontiguousarray(arr[:, 0])
price_arr = np.ascontiguousarray(arr[:, 1])
size_arr = np.ascontiguousarray(arr[:, 2])
elif isinstance(ticks, dict):
price_arr = _to_f64(ticks[price_col])
size_arr = _to_f64(ticks[size_col])
if timestamp_col in ticks:
ts_arr = _to_f64(ticks[timestamp_col])
else:
# pandas DataFrame
try:
import pandas as pd
except ImportError as exc:
raise ImportError("pandas is required for DataFrame input") from exc
price_arr = _to_f64(ticks[price_col].values)
size_arr = _to_f64(ticks[size_col].values)
if timestamp_col in ticks.columns:
ts_arr = _to_f64(ticks[timestamp_col].values)
# --- Aggregate ---
if bar_type == "tick":
ro, rh, rl, rc, rv = _rust_tick_bars(price_arr, size_arr, int(param))
extra: Optional[NDArray] = None
elif bar_type == "volume":
ro, rh, rl, rc, rv = _rust_volume_bars_ticks(price_arr, size_arr, param)
extra = None
else: # time
if ts_arr is None:
raise FerroTAValueError(
"Time bars require a timestamp column in the tick data."
)
period_secs = int(param)
labels = (ts_arr // period_secs).astype(np.int64)
ro, rh, rl, rc, rv, lbl = _rust_time_bars(price_arr, size_arr, labels)
extra = lbl
# --- Return ---
try:
import pandas as pd
df: dict[str, Any] = {
"open": ro,
"high": rh,
"low": rl,
"close": rc,
"volume": rv,
}
if extra is not None:
df["timestamp"] = (extra * int(param)).astype(np.int64)
return pd.DataFrame(df)
except ImportError:
return {"open": ro, "high": rh, "low": rl, "close": rc, "volume": rv}
# ---------------------------------------------------------------------------
# TickAggregator — class-based API
# ---------------------------------------------------------------------------
class TickAggregator:
"""Class-based API for tick aggregation.
Parameters
----------
rule : str
Bar specification (see :func:`aggregate_ticks`).
Examples
--------
>>> from ferro_ta.data.aggregation import TickAggregator
>>> agg = TickAggregator(rule="tick:50")
>>> import numpy as np
>>> rng = np.random.default_rng(0)
>>> ticks = {"price": rng.uniform(99, 101, 200), "size": rng.uniform(1, 10, 200)}
>>> bars = agg.aggregate(ticks)
>>> len(bars["open"]) >= 4
True
"""
def __init__(self, rule: str = "tick:100") -> None:
self.rule = rule
# Validate rule at construction time
_parse_rule(rule)
def aggregate(self, ticks: Any, **kwargs: Any) -> Any:
"""Aggregate *ticks* into bars. See :func:`aggregate_ticks`."""
return aggregate_ticks(ticks, rule=self.rule, **kwargs)
def __repr__(self) -> str:
return f"TickAggregator(rule={self.rule!r})"