Baseline evaluation logics added
This commit is contained in:
parent
b512027574
commit
c29ca882af
|
|
@ -50,6 +50,7 @@ class TradingAgentsBacktester:
|
||||||
print(f"Decision: {decision}")
|
print(f"Decision: {decision}")
|
||||||
signal = self._parse_decision(decision)
|
signal = self._parse_decision(decision)
|
||||||
decisions.append({"date": date_str, "decision": decision, "signal": signal, "price": price})
|
decisions.append({"date": date_str, "decision": decision, "signal": signal, "price": price})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error: {e}")
|
print(f"Error: {e}")
|
||||||
signal = 0
|
signal = 0
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,10 @@
|
||||||
"""
|
|
||||||
Baseline trading strategies for comparison.
|
|
||||||
Implements: Buy&Hold, MACD, KDJ+RSI, ZMR, SMA
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
class BaseStrategy(ABC):
|
class BaseStrategy(ABC):
|
||||||
"""Base class for trading strategies."""
|
"""Base class for trading strategies (long-only, action-based)."""
|
||||||
|
|
||||||
def __init__(self, initial_capital=100000):
|
def __init__(self, initial_capital=100000):
|
||||||
self.initial_capital = float(initial_capital)
|
self.initial_capital = float(initial_capital)
|
||||||
|
|
@ -26,7 +21,13 @@ class BaseStrategy(ABC):
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def generate_signals(self, data: pd.DataFrame) -> pd.Series:
|
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
|
pass
|
||||||
|
|
||||||
def _prep_ohlcv(self, data: pd.DataFrame) -> pd.DataFrame:
|
def _prep_ohlcv(self, data: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
|
@ -36,25 +37,44 @@ class BaseStrategy(ABC):
|
||||||
raise ValueError(f"Data missing column '{col}'")
|
raise ValueError(f"Data missing column '{col}'")
|
||||||
return data.copy()
|
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:
|
def backtest(self, data: pd.DataFrame) -> pd.DataFrame:
|
||||||
df = self._prep_ohlcv(data)
|
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)
|
# 1) get actions (1, 0, -1)
|
||||||
position = signals.replace(0, np.nan).ffill().fillna(0)
|
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)
|
close = self._close_series(df)
|
||||||
market_ret = close.pct_change().fillna(0.0)
|
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)
|
strat_ret = (exposure * market_ret).astype(float)
|
||||||
|
|
||||||
cumret = (1.0 + strat_ret).cumprod()
|
cumret = (1.0 + strat_ret).cumprod()
|
||||||
portval = self.initial_capital * cumret
|
portval = self.initial_capital * cumret
|
||||||
|
|
||||||
portfolio = pd.DataFrame(index=df.index)
|
portfolio = pd.DataFrame(index=df.index)
|
||||||
portfolio["signal"] = signals
|
portfolio["action"] = actions # 1 buy / 0 hold / -1 sell
|
||||||
portfolio["position"] = position
|
portfolio["position"] = position # 1 long / 0 flat
|
||||||
portfolio["close"] = close
|
portfolio["close"] = close
|
||||||
if "Volume" in df.columns:
|
if "Volume" in df.columns:
|
||||||
vol = df["Volume"]
|
vol = df["Volume"]
|
||||||
|
|
@ -66,113 +86,82 @@ class BaseStrategy(ABC):
|
||||||
portfolio["strategy_return"] = strat_ret
|
portfolio["strategy_return"] = strat_ret
|
||||||
portfolio["cumulative_return"] = cumret
|
portfolio["cumulative_return"] = cumret
|
||||||
portfolio["portfolio_value"] = portval
|
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
|
return portfolio
|
||||||
|
|
||||||
|
|
||||||
class BuyAndHoldStrategy(BaseStrategy):
|
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:
|
def generate_signals(self, data: pd.DataFrame) -> pd.Series:
|
||||||
s = pd.Series(1.0, index=data.index)
|
a = pd.Series(0.0, index=data.index)
|
||||||
return s
|
if len(a) > 0:
|
||||||
|
a.iloc[0] = 1.0 # buy once at start
|
||||||
|
return a
|
||||||
|
|
||||||
|
|
||||||
class MACDStrategy(BaseStrategy):
|
class MACDStrategy(BaseStrategy):
|
||||||
"""
|
"""MACD(12,26,9) Contrarian, long-only:MACD>signal → SELL(退出),MACD<signal → BUY(做多)."""
|
||||||
MACD Strategy.
|
|
||||||
Long when MACD > signal, Short when MACD < signal.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def generate_signals(self, data: pd.DataFrame) -> pd.Series:
|
def generate_signals(self, data):
|
||||||
df = data.copy()
|
df = data.copy()
|
||||||
if "macd" not in df.columns or "macds" not in df.columns:
|
ema_fast = df["Close"].ewm(span=12, adjust=False).mean()
|
||||||
df = self._calculate_macd(df)
|
ema_slow = df["Close"].ewm(span=26, adjust=False).mean()
|
||||||
macd_diff = (df["macd"] - df["macds"]).fillna(0.0)
|
macd = ema_fast - ema_slow
|
||||||
sig = pd.Series(0.0, index=df.index)
|
signal = macd.ewm(span=9, adjust=False).mean()
|
||||||
sig[macd_diff > 0] = 1.0
|
diff = macd - signal
|
||||||
sig[macd_diff < 0] = -1.0
|
|
||||||
return sig
|
|
||||||
|
|
||||||
def _calculate_macd(self, data: pd.DataFrame, fast=12, slow=26, signal=9):
|
a = pd.Series(0.0, index=df.index)
|
||||||
exp1 = data["Close"].ewm(span=fast, adjust=False).mean()
|
a[diff > 0] = -1.0 # 卖出/退出(之前是做空)
|
||||||
exp2 = data["Close"].ewm(span=slow, adjust=False).mean()
|
a[diff < 0] = 1.0 # 买入/做多
|
||||||
macd = exp1 - exp2
|
return a
|
||||||
macds = macd.ewm(span=signal, adjust=False).mean()
|
|
||||||
data["macd"] = macd
|
|
||||||
data["macds"] = macds
|
|
||||||
data["macdh"] = macd - macds
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class KDJRSIStrategy(BaseStrategy):
|
class KDJRSIStrategy(BaseStrategy):
|
||||||
"""
|
"""KDJ + RSI 逆势逻辑(长多-only):超买 → 卖出;超卖 → 买入"""
|
||||||
KDJ & RSI Strategy (classic oversold/overbought gating).
|
|
||||||
Long when RSI<30 & K<20; Short when RSI>70 & K>80.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def generate_signals(self, data: pd.DataFrame) -> pd.Series:
|
def generate_signals(self, data):
|
||||||
df = data.copy()
|
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)
|
# === RSI ===
|
||||||
sig[(df["rsi"] < 30) & (df["kdj_k"] < 20)] = 1.0
|
delta = df["Close"].diff()
|
||||||
sig[(df["rsi"] > 70) & (df["kdj_k"] > 80)] = -1.0
|
up, down = delta.clip(lower=0), -delta.clip(upper=0)
|
||||||
return sig
|
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):
|
# === KDJ ===
|
||||||
# Wilder's smoothing approximation via EMA improves stability
|
low = df["Low"].rolling(9).min()
|
||||||
delta = data["Close"].diff()
|
high = df["High"].rolling(9).max()
|
||||||
up = delta.clip(lower=0)
|
denom = (high - low).replace(0, np.nan)
|
||||||
down = -delta.clip(upper=0)
|
rsv = 100 * (df["Close"] - low) / denom
|
||||||
roll_up = up.ewm(alpha=1/period, adjust=False).mean()
|
k = rsv.ewm(com=2, adjust=False).mean()
|
||||||
roll_down = down.ewm(alpha=1/period, adjust=False).mean()
|
df["kdj_k"] = k
|
||||||
rs = roll_up / roll_down.replace(0, np.nan)
|
|
||||||
data["rsi"] = 100 - (100 / (1 + rs))
|
|
||||||
return data
|
|
||||||
|
|
||||||
def _calculate_kdj(self, data: pd.DataFrame, period=9):
|
# === Actions ===
|
||||||
low_min = data["Low"].rolling(window=period, min_periods=period).min()
|
a = pd.Series(0.0, index=df.index)
|
||||||
high_max = data["High"].rolling(window=period, min_periods=period).max()
|
# 收紧阈值:RSI>75,K>85 → 卖出;RSI<25,K<15 → 买入
|
||||||
den = (high_max - low_min).replace(0, np.nan)
|
a[(df["rsi"] > 75) & (df["kdj_k"] > 85)] = -1.0
|
||||||
rsv = 100 * (data["Close"] - low_min) / den
|
a[(df["rsi"] < 25) & (df["kdj_k"] < 15)] = 1.0
|
||||||
k = rsv.ewm(com=2, adjust=False, min_periods=1).mean()
|
return a
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class ZMRStrategy(BaseStrategy):
|
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):
|
def generate_signals(self, data):
|
||||||
super().__init__(initial_capital)
|
|
||||||
self.lookback = int(lookback)
|
|
||||||
self.threshold = float(threshold)
|
|
||||||
|
|
||||||
def generate_signals(self, data: pd.DataFrame) -> pd.Series:
|
|
||||||
close = self._close_series(data)
|
close = self._close_series(data)
|
||||||
rm = close.rolling(window=self.lookback, min_periods=self.lookback).mean()
|
mean = close.rolling(50).mean()
|
||||||
rs = close.rolling(window=self.lookback, min_periods=self.lookback).std()
|
std = close.rolling(50).std()
|
||||||
z = (close - rm) / rs.replace(0, pd.NA)
|
z = (close - mean) / std.replace(0, np.nan)
|
||||||
sig = pd.Series(0.0, index=data.index)
|
|
||||||
sig[z < -self.threshold] = 1.0
|
a = pd.Series(0.0, index=data.index)
|
||||||
sig[z > self.threshold] = -1.0
|
a[z > 1.3] = -1.0 # 高估 → 卖出/退出
|
||||||
return sig
|
a[z < -1.3] = 1.0 # 低估 → 买入/做多
|
||||||
|
return a
|
||||||
|
|
||||||
|
|
||||||
class SMAStrategy(BaseStrategy):
|
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)
|
super().__init__(initial_capital)
|
||||||
self.short_window = int(short_window)
|
self.short_window = int(short_window)
|
||||||
self.long_window = int(long_window)
|
self.long_window = int(long_window)
|
||||||
|
|
@ -181,18 +170,18 @@ class SMAStrategy(BaseStrategy):
|
||||||
close = self._close_series(data)
|
close = self._close_series(data)
|
||||||
short = close.rolling(window=self.short_window, min_periods=self.short_window).mean()
|
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()
|
long_ = close.rolling(window=self.long_window, min_periods=self.long_window).mean()
|
||||||
sig = pd.Series(0.0, index=data.index)
|
a = pd.Series(0.0, index=data.index)
|
||||||
sig[short > long_] = 1.0
|
a[short > long_] = 1.0
|
||||||
sig[short < long_] = -1.0
|
a[short < long_] = -1.0
|
||||||
return sig
|
return a
|
||||||
|
|
||||||
|
|
||||||
def get_all_baseline_strategies(initial_capital=100000):
|
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 {
|
return {
|
||||||
"BuyAndHold": BuyAndHoldStrategy(initial_capital),
|
"BuyAndHold": BuyAndHoldStrategy(initial_capital),
|
||||||
"MACD": MACDStrategy(initial_capital),
|
"MACD": MACDStrategy(initial_capital),
|
||||||
"KDJ&RSI": KDJRSIStrategy(initial_capital),
|
"KDJ&RSI": KDJRSIStrategy(initial_capital),
|
||||||
"ZMR": ZMRStrategy(initial_capital),
|
"ZMR": ZMRStrategy(initial_capital),
|
||||||
"SMA": SMAStrategy(initial_capital),
|
"SMA": SMAStrategy(initial_capital),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,8 +83,8 @@ def run_evaluation(
|
||||||
try:
|
try:
|
||||||
cfg = (config or DEFAULT_CONFIG).copy()
|
cfg = (config or DEFAULT_CONFIG).copy()
|
||||||
# Fast eval defaults (you can override from CLI)
|
# Fast eval defaults (you can override from CLI)
|
||||||
cfg["deep_think_llm"] = cfg.get("deep_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-4o-mini")
|
cfg["quick_think_llm"] = cfg.get("quick_think_llm", "gpt-5-nano")
|
||||||
cfg["max_debate_rounds"] = cfg.get("max_debate_rounds", 1)
|
cfg["max_debate_rounds"] = cfg.get("max_debate_rounds", 1)
|
||||||
cfg["max_risk_discuss_rounds"] = cfg.get("max_risk_discuss_rounds", 1)
|
cfg["max_risk_discuss_rounds"] = cfg.get("max_risk_discuss_rounds", 1)
|
||||||
# Deterministic-ish decoding for reproducibility
|
# Deterministic-ish decoding for reproducibility
|
||||||
|
|
@ -154,7 +154,7 @@ def run_evaluation(
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="Run TradingAgents evaluation with baseline comparisons")
|
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("--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("--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)")
|
parser.add_argument("--capital", type=float, default=100000, help="Initial capital (default: 100000)")
|
||||||
|
|
@ -169,8 +169,8 @@ def main():
|
||||||
if is_debugging():
|
if is_debugging():
|
||||||
config = DEFAULT_CONFIG.copy()
|
config = DEFAULT_CONFIG.copy()
|
||||||
config.update({
|
config.update({
|
||||||
"deep_think_llm": "gpt-4o-mini",
|
"deep_think_llm": "gpt-5-nano",
|
||||||
"quick_think_llm": "gpt-4o-mini",
|
"quick_think_llm": "gpt-5-nano",
|
||||||
"max_debate_rounds": 1,
|
"max_debate_rounds": 1,
|
||||||
"max_risk_discuss_rounds": 1,
|
"max_risk_discuss_rounds": 1,
|
||||||
"llm_params": {"temperature": 0, "top_p": 1.0, "seed": 42},
|
"llm_params": {"temperature": 0, "top_p": 1.0, "seed": 42},
|
||||||
|
|
@ -178,9 +178,9 @@ def main():
|
||||||
run_evaluation(
|
run_evaluation(
|
||||||
ticker="AAPL",
|
ticker="AAPL",
|
||||||
start_date="2024-01-01",
|
start_date="2024-01-01",
|
||||||
end_date="2024-03-30",
|
end_date="2024-01-04",
|
||||||
initial_capital=1000,
|
initial_capital=1000,
|
||||||
include_tradingagents=False,
|
include_tradingagents=True,
|
||||||
output_dir="./evaluation/results",
|
output_dir="./evaluation/results",
|
||||||
config=config
|
config=config
|
||||||
)
|
)
|
||||||
|
|
@ -206,4 +206,4 @@ def main():
|
||||||
)
|
)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
Loading…
Reference in New Issue