feat: enhance trader with expanded capabilities

This commit is contained in:
2026-05-14 12:52:49 +08:00
parent 4f2eb50802
commit 0a2d646b26

102
trader.py
View File

@@ -44,6 +44,14 @@ from strategies.factor_combo import FactorComboStrategy
from strategies.inverse_vol import InverseVolatilityStrategy
from strategies.momentum import MomentumStrategy
from strategies.momentum_quality import MomentumQualityStrategy
from strategies.permanent import (
ETF_UNIVERSE,
GLOBAL_ETF_UNIVERSE,
HK_ETF_UNIVERSE,
TREND_RIDER_V4_UNIVERSE,
TrendRiderV3,
TrendRiderV4,
)
from strategies.recovery_momentum import RecoveryMomentumStrategy
from strategies.trend_following import TrendFollowingStrategy
from universe import UNIVERSES
@@ -107,8 +115,57 @@ STRATEGY_REGISTRY = {
"fc_up_cap_mom_gap_weekly": lambda **kw: FactorComboStrategy("up_cap+mom_gap", rebal_freq=5),
"fc_up_cap_mom_gap_biweekly": lambda **kw: FactorComboStrategy("up_cap+mom_gap", rebal_freq=10),
"fc_up_cap_mom_gap_monthly": lambda **kw: FactorComboStrategy("up_cap+mom_gap", rebal_freq=21),
# --- ETF tactical allocation strategies ---
"trend_rider_v3_us": lambda **kw: TrendRiderV3(),
"trend_rider_v3_global": lambda **kw: TrendRiderV3(
risk_on=("TQQQ", "UPRO", "YINN", "CHAU"),
risk_off=("GLD", "DBC"),
),
"trend_rider_v3_hk": lambda **kw: TrendRiderV3(
risk_on=("7200.HK", "7500.HK"),
risk_off=("GLD", "DBC"),
),
"trend_rider_v4": lambda **kw: TrendRiderV4(),
}
ETF_STRATEGY_UNIVERSES = {
"trend_rider_v3_us": sorted(set(ETF_UNIVERSE)),
"trend_rider_v3_global": sorted(set(GLOBAL_ETF_UNIVERSE)),
"trend_rider_v3_hk": sorted(set(HK_ETF_UNIVERSE)),
"trend_rider_v4": sorted(set(TREND_RIDER_V4_UNIVERSE)),
}
DEFAULT_MONITOR_STRATEGIES = [
name for name in STRATEGY_REGISTRY
if name not in ETF_STRATEGY_UNIVERSES
]
def strategy_universe(market: str, strategy_name: str) -> tuple[list[str], str]:
"""Return tradable tickers and benchmark for a strategy.
Stock strategies use the market's dynamic universe. TrendRider variants
trade fixed USD/HK ETF baskets and use SPY as the regime benchmark.
"""
base_name = strategy_name.removeprefix("sim_")
if base_name in ETF_STRATEGY_UNIVERSES:
return ETF_STRATEGY_UNIVERSES[base_name], "SPY"
universe = UNIVERSES[market]
tickers = universe["fetch"]()
return tickers, universe["benchmark"]
def strategy_data_market(market: str, strategy_name: str) -> str:
"""Return the cache namespace used for a strategy's price data."""
base_name = strategy_name.removeprefix("sim_")
return "etfs" if base_name in ETF_STRATEGY_UNIVERSES else market
def filter_tradable_tickers(price_data: pd.DataFrame, tickers: list[str]) -> list[str]:
"""Keep requested tickers that are present in a downloaded price panel."""
return [t for t in tickers if t in price_data.columns]
# ---------------------------------------------------------------------------
# Persistent state
@@ -383,9 +440,8 @@ def cmd_morning(args):
"""Morning: download open prices, generate today's trade orders."""
market = args.market
strategy_name = args.strategy
universe = UNIVERSES[market]
tickers = universe["fetch"]()
benchmark = universe["benchmark"]
tickers, benchmark = strategy_universe(market, strategy_name)
data_market = strategy_data_market(market, strategy_name)
all_tickers = sorted(set(tickers + [benchmark]))
# Load or init state
@@ -395,8 +451,8 @@ def cmd_morning(args):
print(f"--- Initialized new portfolio: ${args.capital:,.0f} cash ---")
# Download data (close + open)
close_data, open_data = data_manager.update(market, all_tickers, with_open=True)
tickers = [t for t in tickers if t in close_data.columns]
close_data, open_data = data_manager.update(data_market, all_tickers, with_open=True)
tickers = filter_tradable_tickers(close_data, tickers)
today = open_data.index[-1]
today_str = str(today.date())
@@ -473,9 +529,8 @@ def cmd_evening(args):
"""Evening: record execution at close prices, update portfolio."""
market = args.market
strategy_name = args.strategy
universe = UNIVERSES[market]
tickers = universe["fetch"]()
benchmark = universe["benchmark"]
tickers, benchmark = strategy_universe(market, strategy_name)
data_market = strategy_data_market(market, strategy_name)
all_tickers = sorted(set(tickers + [benchmark]))
state = load_state(market, strategy_name)
@@ -495,8 +550,8 @@ def cmd_evening(args):
return
# Get close prices
close_data = data_manager.update(market, all_tickers)
tickers = [t for t in tickers if t in close_data.columns]
close_data = data_manager.update(data_market, all_tickers)
tickers = filter_tradable_tickers(close_data, tickers)
target_date = pd.Timestamp(trade_date)
all_held = list(set(
@@ -577,11 +632,10 @@ def cmd_status(args):
return
# Get latest prices
universe = UNIVERSES[market]
tickers = universe["fetch"]()
benchmark = universe["benchmark"]
tickers, benchmark = strategy_universe(market, strategy_name)
data_market = strategy_data_market(market, strategy_name)
all_tickers = sorted(set(tickers + [benchmark]))
close_data = data_manager.update(market, all_tickers)
close_data = data_manager.update(data_market, all_tickers)
last_date = close_data.index[-1]
all_held = list(state["holdings"].keys())
@@ -883,14 +937,13 @@ def cmd_simulate(args):
"""Simulate day-by-day over a date range."""
market = args.market
strategy_name = args.strategy
universe = UNIVERSES[market]
tickers = universe["fetch"]()
benchmark = universe["benchmark"]
tickers, benchmark = strategy_universe(market, strategy_name)
data_market = strategy_data_market(market, strategy_name)
all_tickers = sorted(set(tickers + [benchmark]))
# Load both open and close data
close_data, open_data = data_manager.update(market, all_tickers, with_open=True)
tickers = [t for t in tickers if t in close_data.columns]
close_data, open_data = data_manager.update(data_market, all_tickers, with_open=True)
tickers = filter_tradable_tickers(close_data, tickers)
# Date range
start = pd.Timestamp(args.start)
@@ -1259,9 +1312,8 @@ def cmd_auto(args):
market = args.market
strategy_name = args.strategy
universe = UNIVERSES[market]
tickers = universe["fetch"]()
benchmark = universe["benchmark"]
tickers, benchmark = strategy_universe(market, strategy_name)
data_market = strategy_data_market(market, strategy_name)
all_tickers = sorted(set(tickers + [benchmark]))
# Load or init state
@@ -1271,8 +1323,8 @@ def cmd_auto(args):
print(f"[auto] Initialized new portfolio: ${args.capital:,.0f} cash")
# Download data (close + open)
close_data, open_data = data_manager.update(market, all_tickers, with_open=True)
tickers = [t for t in tickers if t in close_data.columns]
close_data, open_data = data_manager.update(data_market, all_tickers, with_open=True)
tickers = filter_tradable_tickers(close_data, tickers)
today = close_data.index[-1]
today_str = str(today.date())
@@ -1398,7 +1450,7 @@ def main():
help="Markets to monitor (default: ALL)")
p_monitor.add_argument("--strategy", nargs="+",
choices=list(STRATEGY_REGISTRY.keys()),
default=list(STRATEGY_REGISTRY.keys()),
default=DEFAULT_MONITOR_STRATEGIES,
help="Strategies to run (default: ALL)")
p_monitor.add_argument("--capital", type=float, default=10_000)
p_monitor.add_argument("--tx-cost", type=float, default=0.001,