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.inverse_vol import InverseVolatilityStrategy
|
||||||
from strategies.momentum import MomentumStrategy
|
from strategies.momentum import MomentumStrategy
|
||||||
from strategies.momentum_quality import MomentumQualityStrategy
|
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.recovery_momentum import RecoveryMomentumStrategy
|
||||||
from strategies.trend_following import TrendFollowingStrategy
|
from strategies.trend_following import TrendFollowingStrategy
|
||||||
from universe import UNIVERSES
|
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_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_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),
|
"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
|
# Persistent state
|
||||||
@@ -383,9 +440,8 @@ def cmd_morning(args):
|
|||||||
"""Morning: download open prices, generate today's trade orders."""
|
"""Morning: download open prices, generate today's trade orders."""
|
||||||
market = args.market
|
market = args.market
|
||||||
strategy_name = args.strategy
|
strategy_name = args.strategy
|
||||||
universe = UNIVERSES[market]
|
tickers, benchmark = strategy_universe(market, strategy_name)
|
||||||
tickers = universe["fetch"]()
|
data_market = strategy_data_market(market, strategy_name)
|
||||||
benchmark = universe["benchmark"]
|
|
||||||
all_tickers = sorted(set(tickers + [benchmark]))
|
all_tickers = sorted(set(tickers + [benchmark]))
|
||||||
|
|
||||||
# Load or init state
|
# Load or init state
|
||||||
@@ -395,8 +451,8 @@ def cmd_morning(args):
|
|||||||
print(f"--- Initialized new portfolio: ${args.capital:,.0f} cash ---")
|
print(f"--- Initialized new portfolio: ${args.capital:,.0f} cash ---")
|
||||||
|
|
||||||
# Download data (close + open)
|
# Download data (close + open)
|
||||||
close_data, open_data = data_manager.update(market, all_tickers, with_open=True)
|
close_data, open_data = data_manager.update(data_market, all_tickers, with_open=True)
|
||||||
tickers = [t for t in tickers if t in close_data.columns]
|
tickers = filter_tradable_tickers(close_data, tickers)
|
||||||
|
|
||||||
today = open_data.index[-1]
|
today = open_data.index[-1]
|
||||||
today_str = str(today.date())
|
today_str = str(today.date())
|
||||||
@@ -473,9 +529,8 @@ def cmd_evening(args):
|
|||||||
"""Evening: record execution at close prices, update portfolio."""
|
"""Evening: record execution at close prices, update portfolio."""
|
||||||
market = args.market
|
market = args.market
|
||||||
strategy_name = args.strategy
|
strategy_name = args.strategy
|
||||||
universe = UNIVERSES[market]
|
tickers, benchmark = strategy_universe(market, strategy_name)
|
||||||
tickers = universe["fetch"]()
|
data_market = strategy_data_market(market, strategy_name)
|
||||||
benchmark = universe["benchmark"]
|
|
||||||
all_tickers = sorted(set(tickers + [benchmark]))
|
all_tickers = sorted(set(tickers + [benchmark]))
|
||||||
|
|
||||||
state = load_state(market, strategy_name)
|
state = load_state(market, strategy_name)
|
||||||
@@ -495,8 +550,8 @@ def cmd_evening(args):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Get close prices
|
# Get close prices
|
||||||
close_data = data_manager.update(market, all_tickers)
|
close_data = data_manager.update(data_market, all_tickers)
|
||||||
tickers = [t for t in tickers if t in close_data.columns]
|
tickers = filter_tradable_tickers(close_data, tickers)
|
||||||
|
|
||||||
target_date = pd.Timestamp(trade_date)
|
target_date = pd.Timestamp(trade_date)
|
||||||
all_held = list(set(
|
all_held = list(set(
|
||||||
@@ -577,11 +632,10 @@ def cmd_status(args):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Get latest prices
|
# Get latest prices
|
||||||
universe = UNIVERSES[market]
|
tickers, benchmark = strategy_universe(market, strategy_name)
|
||||||
tickers = universe["fetch"]()
|
data_market = strategy_data_market(market, strategy_name)
|
||||||
benchmark = universe["benchmark"]
|
|
||||||
all_tickers = sorted(set(tickers + [benchmark]))
|
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]
|
last_date = close_data.index[-1]
|
||||||
all_held = list(state["holdings"].keys())
|
all_held = list(state["holdings"].keys())
|
||||||
@@ -883,14 +937,13 @@ def cmd_simulate(args):
|
|||||||
"""Simulate day-by-day over a date range."""
|
"""Simulate day-by-day over a date range."""
|
||||||
market = args.market
|
market = args.market
|
||||||
strategy_name = args.strategy
|
strategy_name = args.strategy
|
||||||
universe = UNIVERSES[market]
|
tickers, benchmark = strategy_universe(market, strategy_name)
|
||||||
tickers = universe["fetch"]()
|
data_market = strategy_data_market(market, strategy_name)
|
||||||
benchmark = universe["benchmark"]
|
|
||||||
all_tickers = sorted(set(tickers + [benchmark]))
|
all_tickers = sorted(set(tickers + [benchmark]))
|
||||||
|
|
||||||
# Load both open and close data
|
# Load both open and close data
|
||||||
close_data, open_data = data_manager.update(market, all_tickers, with_open=True)
|
close_data, open_data = data_manager.update(data_market, all_tickers, with_open=True)
|
||||||
tickers = [t for t in tickers if t in close_data.columns]
|
tickers = filter_tradable_tickers(close_data, tickers)
|
||||||
|
|
||||||
# Date range
|
# Date range
|
||||||
start = pd.Timestamp(args.start)
|
start = pd.Timestamp(args.start)
|
||||||
@@ -1259,9 +1312,8 @@ def cmd_auto(args):
|
|||||||
|
|
||||||
market = args.market
|
market = args.market
|
||||||
strategy_name = args.strategy
|
strategy_name = args.strategy
|
||||||
universe = UNIVERSES[market]
|
tickers, benchmark = strategy_universe(market, strategy_name)
|
||||||
tickers = universe["fetch"]()
|
data_market = strategy_data_market(market, strategy_name)
|
||||||
benchmark = universe["benchmark"]
|
|
||||||
all_tickers = sorted(set(tickers + [benchmark]))
|
all_tickers = sorted(set(tickers + [benchmark]))
|
||||||
|
|
||||||
# Load or init state
|
# Load or init state
|
||||||
@@ -1271,8 +1323,8 @@ def cmd_auto(args):
|
|||||||
print(f"[auto] Initialized new portfolio: ${args.capital:,.0f} cash")
|
print(f"[auto] Initialized new portfolio: ${args.capital:,.0f} cash")
|
||||||
|
|
||||||
# Download data (close + open)
|
# Download data (close + open)
|
||||||
close_data, open_data = data_manager.update(market, all_tickers, with_open=True)
|
close_data, open_data = data_manager.update(data_market, all_tickers, with_open=True)
|
||||||
tickers = [t for t in tickers if t in close_data.columns]
|
tickers = filter_tradable_tickers(close_data, tickers)
|
||||||
|
|
||||||
today = close_data.index[-1]
|
today = close_data.index[-1]
|
||||||
today_str = str(today.date())
|
today_str = str(today.date())
|
||||||
@@ -1398,7 +1450,7 @@ def main():
|
|||||||
help="Markets to monitor (default: ALL)")
|
help="Markets to monitor (default: ALL)")
|
||||||
p_monitor.add_argument("--strategy", nargs="+",
|
p_monitor.add_argument("--strategy", nargs="+",
|
||||||
choices=list(STRATEGY_REGISTRY.keys()),
|
choices=list(STRATEGY_REGISTRY.keys()),
|
||||||
default=list(STRATEGY_REGISTRY.keys()),
|
default=DEFAULT_MONITOR_STRATEGIES,
|
||||||
help="Strategies to run (default: ALL)")
|
help="Strategies to run (default: ALL)")
|
||||||
p_monitor.add_argument("--capital", type=float, default=10_000)
|
p_monitor.add_argument("--capital", type=float, default=10_000)
|
||||||
p_monitor.add_argument("--tx-cost", type=float, default=0.001,
|
p_monitor.add_argument("--tx-cost", type=float, default=0.001,
|
||||||
|
|||||||
Reference in New Issue
Block a user