diff --git a/evaluation/backtest.py b/evaluation/backtest.py index 47a1eea9..5a6f12c9 100644 --- a/evaluation/backtest.py +++ b/evaluation/backtest.py @@ -50,6 +50,7 @@ class TradingAgentsBacktester: print(f"Decision: {decision}") signal = self._parse_decision(decision) decisions.append({"date": date_str, "decision": decision, "signal": signal, "price": price}) + except Exception as e: print(f"Error: {e}") signal = 0 diff --git a/evaluation/baseline_strategies.py b/evaluation/baseline_strategies.py index fd0d5ca7..30fed908 100644 --- a/evaluation/baseline_strategies.py +++ b/evaluation/baseline_strategies.py @@ -1,15 +1,10 @@ -""" -Baseline trading strategies for comparison. -Implements: Buy&Hold, MACD, KDJ+RSI, ZMR, SMA -""" - import pandas as pd import numpy as np from abc import ABC, abstractmethod class BaseStrategy(ABC): - """Base class for trading strategies.""" + """Base class for trading strategies (long-only, action-based).""" def __init__(self, initial_capital=100000): self.initial_capital = float(initial_capital) @@ -26,7 +21,13 @@ class BaseStrategy(ABC): @abstractmethod def generate_signals(self, data: pd.DataFrame) -> pd.Series: - """Generate *target* position by date (1 long, -1 short, 0 flat).""" + """ + Generate *actions* by date: + 1 = BUY (open / go long, or stay long) + 0 = HOLD (no change) + -1 = SELL (exit to flat) + Shorting is NOT allowed. + """ pass def _prep_ohlcv(self, data: pd.DataFrame) -> pd.DataFrame: @@ -36,25 +37,44 @@ class BaseStrategy(ABC): raise ValueError(f"Data missing column '{col}'") return data.copy() + @staticmethod + def _actions_to_position(actions: pd.Series) -> pd.Series: + """Convert action series to a long-only position series in {0,1}.""" + a = actions.astype(float).fillna(0.0).clip(-1, 1).values + pos = np.zeros_like(a, dtype=float) + for i in range(len(a)): + if i == 0: + pos[i] = 1.0 if a[i] > 0 else 0.0 + else: + if a[i] > 0: # buy → long + pos[i] = 1.0 + elif a[i] < 0: # sell → flat + pos[i] = 0.0 + else: # hold → keep previous + pos[i] = pos[i-1] + return pd.Series(pos, index=actions.index, name="position") + def backtest(self, data: pd.DataFrame) -> pd.DataFrame: df = self._prep_ohlcv(data) - signals = self.generate_signals(df).astype(float) - signals = signals.clip(lower=-1, upper=1).reindex(df.index).fillna(0) - # ONE place for hold semantics (Option A: 0 = no new signal) - position = signals.replace(0, np.nan).ffill().fillna(0) + # 1) get actions (1, 0, -1) + actions = self.generate_signals(df).reindex(df.index).fillna(0).clip(-1, 1).astype(float) + # 2) map actions → long-only position {0,1} + position = self._actions_to_position(actions) + + # 3) compute returns (note: sell today → flat tomorrow → 0 return tomorrow) close = self._close_series(df) market_ret = close.pct_change().fillna(0.0) - exposure = position.shift(1).fillna(0.0) + exposure = position.shift(1).fillna(0.0) # use yesterday's position strat_ret = (exposure * market_ret).astype(float) cumret = (1.0 + strat_ret).cumprod() portval = self.initial_capital * cumret portfolio = pd.DataFrame(index=df.index) - portfolio["signal"] = signals - portfolio["position"] = position + portfolio["action"] = actions # 1 buy / 0 hold / -1 sell + portfolio["position"] = position # 1 long / 0 flat portfolio["close"] = close if "Volume" in df.columns: vol = df["Volume"] @@ -66,113 +86,82 @@ class BaseStrategy(ABC): portfolio["strategy_return"] = strat_ret portfolio["cumulative_return"] = cumret portfolio["portfolio_value"] = portval - portfolio["trade"] = portfolio["position"].diff().fillna(0.0) + portfolio["trade_delta"] = portfolio["position"].diff().fillna(0.0) # +1 buy, -1 sell return portfolio class BuyAndHoldStrategy(BaseStrategy): - """Buy at start and hold long the whole period (no short).""" + """Buy on day 1 and hold long (no shorting).""" def generate_signals(self, data: pd.DataFrame) -> pd.Series: - s = pd.Series(1.0, index=data.index) - return s + a = pd.Series(0.0, index=data.index) + if len(a) > 0: + a.iloc[0] = 1.0 # buy once at start + return a class MACDStrategy(BaseStrategy): - """ - MACD Strategy. - Long when MACD > signal, Short when MACD < signal. - """ + """MACD(12,26,9) Contrarian, long-only:MACD>signal → SELL(退出),MACD pd.Series: + def generate_signals(self, data): df = data.copy() - if "macd" not in df.columns or "macds" not in df.columns: - df = self._calculate_macd(df) - macd_diff = (df["macd"] - df["macds"]).fillna(0.0) - sig = pd.Series(0.0, index=df.index) - sig[macd_diff > 0] = 1.0 - sig[macd_diff < 0] = -1.0 - return sig + ema_fast = df["Close"].ewm(span=12, adjust=False).mean() + ema_slow = df["Close"].ewm(span=26, adjust=False).mean() + macd = ema_fast - ema_slow + signal = macd.ewm(span=9, adjust=False).mean() + diff = macd - signal - def _calculate_macd(self, data: pd.DataFrame, fast=12, slow=26, signal=9): - exp1 = data["Close"].ewm(span=fast, adjust=False).mean() - exp2 = data["Close"].ewm(span=slow, adjust=False).mean() - macd = exp1 - exp2 - macds = macd.ewm(span=signal, adjust=False).mean() - data["macd"] = macd - data["macds"] = macds - data["macdh"] = macd - macds - return data + a = pd.Series(0.0, index=df.index) + a[diff > 0] = -1.0 # 卖出/退出(之前是做空) + a[diff < 0] = 1.0 # 买入/做多 + return a class KDJRSIStrategy(BaseStrategy): - """ - KDJ & RSI Strategy (classic oversold/overbought gating). - Long when RSI<30 & K<20; Short when RSI>70 & K>80. - """ + """KDJ + RSI 逆势逻辑(长多-only):超买 → 卖出;超卖 → 买入""" - def generate_signals(self, data: pd.DataFrame) -> pd.Series: + def generate_signals(self, data): df = data.copy() - if "rsi" not in df.columns: - df = self._calculate_rsi(df) - if "kdj_k" not in df.columns: - df = self._calculate_kdj(df) - sig = pd.Series(0.0, index=df.index) - sig[(df["rsi"] < 30) & (df["kdj_k"] < 20)] = 1.0 - sig[(df["rsi"] > 70) & (df["kdj_k"] > 80)] = -1.0 - return sig + # === RSI === + delta = df["Close"].diff() + up, down = delta.clip(lower=0), -delta.clip(upper=0) + rs = up.ewm(span=14, adjust=False).mean() / down.ewm(span=14, adjust=False).mean().replace(0, np.nan) + df["rsi"] = 100 - 100 / (1 + rs) - def _calculate_rsi(self, data: pd.DataFrame, period=14): - # Wilder's smoothing approximation via EMA improves stability - delta = data["Close"].diff() - up = delta.clip(lower=0) - down = -delta.clip(upper=0) - roll_up = up.ewm(alpha=1/period, adjust=False).mean() - roll_down = down.ewm(alpha=1/period, adjust=False).mean() - rs = roll_up / roll_down.replace(0, np.nan) - data["rsi"] = 100 - (100 / (1 + rs)) - return data + # === KDJ === + low = df["Low"].rolling(9).min() + high = df["High"].rolling(9).max() + denom = (high - low).replace(0, np.nan) + rsv = 100 * (df["Close"] - low) / denom + k = rsv.ewm(com=2, adjust=False).mean() + df["kdj_k"] = k - def _calculate_kdj(self, data: pd.DataFrame, period=9): - low_min = data["Low"].rolling(window=period, min_periods=period).min() - high_max = data["High"].rolling(window=period, min_periods=period).max() - den = (high_max - low_min).replace(0, np.nan) - rsv = 100 * (data["Close"] - low_min) / den - k = rsv.ewm(com=2, adjust=False, min_periods=1).mean() - d = k.ewm(com=2, adjust=False, min_periods=1).mean() - j = 3 * k - 2 * d - data["kdj_k"], data["kdj_d"], data["kdj_j"] = k, d, j - return data + # === Actions === + a = pd.Series(0.0, index=df.index) + # 收紧阈值:RSI>75,K>85 → 卖出;RSI<25,K<15 → 买入 + a[(df["rsi"] > 75) & (df["kdj_k"] > 85)] = -1.0 + a[(df["rsi"] < 25) & (df["kdj_k"] < 15)] = 1.0 + return a class ZMRStrategy(BaseStrategy): - """ - Zero-mean reversion on z-score of Close vs rolling mean. - """ - def __init__(self, initial_capital=100000, lookback=20, threshold=1.0): - super().__init__(initial_capital) - self.lookback = int(lookback) - self.threshold = float(threshold) - - def generate_signals(self, data: pd.DataFrame) -> pd.Series: + def generate_signals(self, data): close = self._close_series(data) - rm = close.rolling(window=self.lookback, min_periods=self.lookback).mean() - rs = close.rolling(window=self.lookback, min_periods=self.lookback).std() - z = (close - rm) / rs.replace(0, pd.NA) - sig = pd.Series(0.0, index=data.index) - sig[z < -self.threshold] = 1.0 - sig[z > self.threshold] = -1.0 - return sig + mean = close.rolling(50).mean() + std = close.rolling(50).std() + z = (close - mean) / std.replace(0, np.nan) + + a = pd.Series(0.0, index=data.index) + a[z > 1.3] = -1.0 # 高估 → 卖出/退出 + a[z < -1.3] = 1.0 # 低估 → 买入/做多 + return a class SMAStrategy(BaseStrategy): - """ - SMA crossover (50/200 by default). - """ - def __init__(self, initial_capital=100000, short_window=50, long_window=200): + def __init__(self, initial_capital=100000, short_window=5, long_window=20): super().__init__(initial_capital) self.short_window = int(short_window) self.long_window = int(long_window) @@ -181,18 +170,18 @@ class SMAStrategy(BaseStrategy): close = self._close_series(data) short = close.rolling(window=self.short_window, min_periods=self.short_window).mean() long_ = close.rolling(window=self.long_window, min_periods=self.long_window).mean() - sig = pd.Series(0.0, index=data.index) - sig[short > long_] = 1.0 - sig[short < long_] = -1.0 - return sig + a = pd.Series(0.0, index=data.index) + a[short > long_] = 1.0 + a[short < long_] = -1.0 + return a def get_all_baseline_strategies(initial_capital=100000): - """Get all baseline strategies for comparison.""" + """Get all baseline strategies for comparison (long-only, action-based).""" return { "BuyAndHold": BuyAndHoldStrategy(initial_capital), "MACD": MACDStrategy(initial_capital), "KDJ&RSI": KDJRSIStrategy(initial_capital), "ZMR": ZMRStrategy(initial_capital), "SMA": SMAStrategy(initial_capital), - } \ No newline at end of file + } diff --git a/evaluation/run_evaluation.py b/evaluation/run_evaluation.py index 20c09abb..2d43192d 100644 --- a/evaluation/run_evaluation.py +++ b/evaluation/run_evaluation.py @@ -83,8 +83,8 @@ def run_evaluation( try: cfg = (config or DEFAULT_CONFIG).copy() # Fast eval defaults (you can override from CLI) - cfg["deep_think_llm"] = cfg.get("deep_think_llm", "gpt-4o-mini") - cfg["quick_think_llm"] = cfg.get("quick_think_llm", "gpt-4o-mini") + cfg["deep_think_llm"] = cfg.get("deep_think_llm", "gpt-5-nano") + cfg["quick_think_llm"] = cfg.get("quick_think_llm", "gpt-5-nano") cfg["max_debate_rounds"] = cfg.get("max_debate_rounds", 1) cfg["max_risk_discuss_rounds"] = cfg.get("max_risk_discuss_rounds", 1) # Deterministic-ish decoding for reproducibility @@ -154,7 +154,7 @@ def run_evaluation( def main(): parser = argparse.ArgumentParser(description="Run TradingAgents evaluation with baseline comparisons") - parser.add_argument("ticker", type=str, help="Stock ticker symbol (e.g., AAPL)") + parser.add_argument("--ticker", type=str, help="Stock ticker symbol (e.g., AAPL)") parser.add_argument("--start-date", type=str, required=True, help="Start date (YYYY-MM-DD)") parser.add_argument("--end-date", type=str, required=True, help="End date (YYYY-MM-DD)") parser.add_argument("--capital", type=float, default=100000, help="Initial capital (default: 100000)") @@ -169,8 +169,8 @@ def main(): if is_debugging(): config = DEFAULT_CONFIG.copy() config.update({ - "deep_think_llm": "gpt-4o-mini", - "quick_think_llm": "gpt-4o-mini", + "deep_think_llm": "gpt-5-nano", + "quick_think_llm": "gpt-5-nano", "max_debate_rounds": 1, "max_risk_discuss_rounds": 1, "llm_params": {"temperature": 0, "top_p": 1.0, "seed": 42}, @@ -178,9 +178,9 @@ def main(): run_evaluation( ticker="AAPL", start_date="2024-01-01", - end_date="2024-03-30", + end_date="2024-01-04", initial_capital=1000, - include_tradingagents=False, + include_tradingagents=True, output_dir="./evaluation/results", config=config ) @@ -206,4 +206,4 @@ def main(): ) if __name__ == "__main__": - main() + main() \ No newline at end of file