53 lines
1.9 KiB
Python
53 lines
1.9 KiB
Python
"""Signal-driven multi-stock strategy."""
|
|
import backtrader as bt
|
|
import pandas as pd
|
|
|
|
|
|
class AlphaStrategy(bt.Strategy):
|
|
"""Trade feeds based on precomputed ``signal`` line.
|
|
|
|
Supports two builder modes:
|
|
- ThresholdBuilder: per-stock threshold (passed ``(signal_value, in_position)``)
|
|
- RankEqualWeightBuilder: cross-sectional ranking (passed ``{symbol: signal}`` dict)
|
|
"""
|
|
|
|
def __init__(self, builder):
|
|
self.builder = builder
|
|
|
|
def next(self):
|
|
# Collect all signals
|
|
signals: dict[str, float] = {}
|
|
for data in self.datas:
|
|
sig = data.signal[0]
|
|
if not pd.isna(sig):
|
|
signals[data._name] = float(sig)
|
|
|
|
if not signals:
|
|
return
|
|
|
|
# Detect builder type: if RankEqualWeightBuilder, use cross-sectional mode
|
|
from portfolio.builder import RankEqualWeightBuilder
|
|
if isinstance(self.builder, RankEqualWeightBuilder):
|
|
actions = self.builder.build(signals)
|
|
for data in self.datas:
|
|
name = data._name
|
|
if name not in actions:
|
|
continue
|
|
action = actions[name]
|
|
if action.action == "buy":
|
|
self.order_target_percent(data=data, target=action.size_pct)
|
|
elif action.action == "sell":
|
|
self.close(data=data)
|
|
else:
|
|
# Legacy per-stock ThresholdBuilder
|
|
for data in self.datas:
|
|
name = data._name
|
|
if name not in signals:
|
|
continue
|
|
in_position = bool(self.getposition(data).size)
|
|
action = self.builder.build(signals[name], in_position)
|
|
if action.action == "buy":
|
|
self.order_target_percent(data=data, target=action.size_pct)
|
|
elif action.action == "sell":
|
|
self.close(data=data)
|