fixed long-short evaluation

This commit is contained in:
Zichen Liu 2025-11-10 11:37:25 -06:00
parent e3952edf91
commit 55e0287ace
2 changed files with 78 additions and 43 deletions

View File

@ -30,72 +30,81 @@ class TradingAgentsBacktester:
def backtest(self, ticker: str, start_date: str, end_date: str, data: pd.DataFrame) -> pd.DataFrame: def backtest(self, ticker: str, start_date: str, end_date: str, data: pd.DataFrame) -> pd.DataFrame:
""" """
Backtest TradingAgents using the same return calculation logic as rule-based strategies. Backtest TradingAgents with realistic single-asset account simulation.
Supports long, short, and flat positions with 1× leverage on shorts.
Process:
1. Collect signals (actions: 1=BUY, 0=HOLD, -1=SELL) for all dates
2. Convert actions to positions (0=flat, 1=long) using same logic as baselines
3. Calculate returns as: strategy_return = position.shift(1) * market_return
""" """
# Restrict to window
df = data.loc[start_date:end_date].copy() df = data.loc[start_date:end_date].copy()
decisions = []
decisions: List[Dict] = []
signals = pd.Series(0, index=df.index, dtype=float) signals = pd.Series(0, index=df.index, dtype=float)
print(f"\nRunning TradingAgents backtest on {ticker} from {start_date} to {end_date}") print(f"\nRunning TradingAgents backtest on {ticker} from {start_date} to {end_date}")
print(f"Total trading days: {len(df)}") print(f"Total trading days: {len(df)}")
print("-" * 80) print("-" * 80)
# Step 1: Collect all signals/decisions # === Step 1: Collect signals ===
for i, (date, row) in enumerate(df.iterrows()): for i, (date, row) in enumerate(df.iterrows()):
date_str = date.strftime("%Y-%m-%d") date_str = date.strftime("%Y-%m-%d")
price = float(row["Close"]) price = float(row["Close"])
# Get decision from TradingAgents graph
try: try:
print(f"\n[{i+1}/{len(df)}] {date_str} ... ", end="") print(f"\n[{i+1}/{len(df)}] {date_str} ... ", end="")
final_state, decision = self.graph.propagate(ticker, date_str) final_state, decision = self.graph.propagate(ticker, date_str)
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
decisions.append({"date": date_str, "decision": "ERROR", "signal": 0, "price": price, "error": str(e)}) decisions.append({"date": date_str, "decision": "ERROR", "signal": 0, "price": price, "error": str(e)})
signals.loc[date] = signal signals.loc[date] = signal
# Step 2: Convert actions to positions (same logic as baseline strategies) # === Step 2: Run realistic cash+shares backtest ===
position = self._actions_to_position(signals)
# Step 3: Calculate returns using standardized logic
close = pd.to_numeric(df["Close"], errors="coerce") close = pd.to_numeric(df["Close"], errors="coerce")
market_ret = close.pct_change().fillna(0.0) cash = self.initial_capital
exposure = position.shift(1).fillna(0.0) # Yesterday's position determines today's exposure shares = 0.0
strat_ret = (exposure * market_ret).astype(float) prev_value = cash
records = []
cumret = (1.0 + strat_ret).cumprod()
portval = self.initial_capital * cumret
# Build portfolio DataFrame with same structure as baseline strategies
portfolio = pd.DataFrame(index=df.index)
portfolio["action"] = signals # 1=BUY, 0=HOLD, -1=SELL
portfolio["position"] = position # 1=long, 0=flat
portfolio["close"] = close
if "Volume" in df.columns:
vol = df["Volume"]
if isinstance(vol, pd.DataFrame) and vol.shape[1] == 1:
vol = vol.iloc[:, 0]
if isinstance(vol, pd.Series):
portfolio["Volume"] = vol
portfolio["market_return"] = market_ret
portfolio["strategy_return"] = strat_ret
portfolio["cumulative_return"] = cumret
portfolio["portfolio_value"] = portval
portfolio["trade_delta"] = portfolio["position"].diff().fillna(0.0) # +1=buy, -1=sell
for i, (date, price) in enumerate(close.items()):
action = signals.iloc[i]
# 先计算上一个交易日的组合价值
portfolio_value = cash + shares * price
# === 若方向改变,先平仓 ===
if (shares > 0 and action <= 0) or (shares < 0 and action >= 0):
cash += shares * price # 卖出现有股票或回补空头
shares = 0.0
# === 建仓逻辑 ===
if action == 1 and shares == 0:
# 做多
shares = cash / price
cash = 0.0
elif action == -1 and shares == 0:
# 做空1倍杠杆
shares = -cash / price
cash = 2 * cash # 保证金 + 卖出所得
# === 更新组合价值 ===
new_value = cash + shares * price
daily_return = (new_value / prev_value) - 1 if prev_value != 0 else 0.0
prev_value = new_value
records.append({
"date": date.strftime("%Y-%m-%d"),
"action": action,
"shares": shares,
"close_price": price,
"cash": cash,
"portfolio_value": new_value,
"strategy_return": daily_return,
})
# === Step 3: 转为 DataFrame 并计算累计收益 ===
portfolio = pd.DataFrame(records).set_index("date")
portfolio["cumulative_return"] = (1 + portfolio["strategy_return"]).cumprod()
portfolio["ticker"] = ticker
self.latest_portfolio = portfolio
self._save_decisions_log(ticker, decisions, start_date, end_date) self._save_decisions_log(ticker, decisions, start_date, end_date)
return portfolio return portfolio
@ -135,13 +144,37 @@ class TradingAgentsBacktester:
return 0 return 0
def _save_decisions_log(self, ticker: str, decisions: List[Dict], start_date: str, end_date: str): def _save_decisions_log(self, ticker: str, decisions: List[Dict], start_date: str, end_date: str):
# Use output_dir if provided, otherwise use default """
Save detailed TradingAgents decisions and portfolio state to JSON.
Adds shares, cash, and cumulative return (cr) from the latest backtest results.
"""
if self.output_dir: if self.output_dir:
out = Path(self.output_dir) / ticker / "TradingAgents" out = Path(self.output_dir) / ticker / "TradingAgents"
else: else:
out = Path(f"eval_results/{ticker}/TradingAgents") out = Path(f"eval_results/{ticker}/TradingAgents")
out.mkdir(parents=True, exist_ok=True) out.mkdir(parents=True, exist_ok=True)
fp = out / f"decisions_{start_date}_to_{end_date}.json" fp = out / f"decisions_{start_date}_to_{end_date}.json"
# Try to include computed portfolio metrics if available
try:
# Attempt to load the latest portfolio CSV/DF from memory
if hasattr(self, "latest_portfolio"):
port = self.latest_portfolio
port = port.reset_index()
port_dict = {d["date"]: d for d in port.to_dict(orient="records")}
# Merge portfolio stats into each decision record
for d in decisions:
date = d["date"]
if date in port_dict:
d.update({
"shares": port_dict[date].get("shares"),
"cash": port_dict[date].get("cash"),
"portfolio_value": port_dict[date].get("portfolio_value"),
"cumulative_return": port_dict[date].get("cumulative_return"),
})
except Exception as e:
print(f"Warning: could not merge portfolio stats into log ({e})")
with open(fp, "w") as f: with open(fp, "w") as f:
json.dump({ json.dump({
"strategy": "TradingAgents", "strategy": "TradingAgents",
@ -151,6 +184,7 @@ class TradingAgentsBacktester:
"total_days": len(decisions), "total_days": len(decisions),
"decisions": decisions "decisions": decisions
}, f, indent=2) }, f, indent=2)
print(f" ✓ Saved TradingAgents detailed decisions to: {fp}") print(f" ✓ Saved TradingAgents detailed decisions to: {fp}")

View File

@ -151,7 +151,8 @@ def run_evaluation(
print(f" Debate Rounds: {cfg['max_debate_rounds']}") print(f" Debate Rounds: {cfg['max_debate_rounds']}")
graph = TradingAgentsGraph( graph = TradingAgentsGraph(
selected_analysts=["market", "social", "news", "fundamentals"], selected_analysts=["news"],
# selected_analysts=["market", "social", "news", "fundamentals"],
debug=False, debug=False,
config=cfg config=cfg
) )