fixed long-short evaluation
This commit is contained in:
parent
e3952edf91
commit
55e0287ace
|
|
@ -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()
|
for i, (date, price) in enumerate(close.items()):
|
||||||
portval = self.initial_capital * cumret
|
action = signals.iloc[i]
|
||||||
|
|
||||||
# Build portfolio DataFrame with same structure as baseline strategies
|
# 先计算上一个交易日的组合价值
|
||||||
portfolio = pd.DataFrame(index=df.index)
|
portfolio_value = cash + shares * price
|
||||||
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
|
|
||||||
|
|
||||||
|
# === 若方向改变,先平仓 ===
|
||||||
|
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}")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue