Baseline evaluation logics added
This commit is contained in:
parent
b512027574
commit
c29ca882af
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<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),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
Loading…
Reference in New Issue