Baseline evaluation logics added

This commit is contained in:
Quanliang Liu 2025-10-31 16:34:29 -05:00
parent b512027574
commit c29ca882af
3 changed files with 96 additions and 106 deletions

View File

@ -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

View File

@ -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-onlyMACD>signal → SELL(退出)MACD<signal → BUY(做多)."""
def generate_signals(self, data: pd.DataFrame) -> 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),
}
}

View File

@ -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()