feat: enhance trader with expanded capabilities
This commit is contained in:
102
trader.py
102
trader.py
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user