HARSHARAVURI/stoker-mft
0
1"""2Phase 6 — Weekly Evaluation & Feedback Loop3Analyses closed losing trades and generates prompt improvement suggestions.4 5Usage:6 python feedback_loop.py # analyse last 7 days7 python feedback_loop.py --days 14 # analyse last 14 days8 python feedback_loop.py --save # save suggestions to prompts/refinements.md9"""10import sys11import math12import argparse13import json14from datetime import datetime, timedelta15from zoneinfo import ZoneInfo16from dotenv import load_dotenv17 18_IST = ZoneInfo("Asia/Kolkata")19load_dotenv()20 21if sys.platform == "win32":22 sys.stdout.reconfigure(encoding="utf-8")23 24from database.ledger import init_db, get_all_trades, get_portfolio_value25from tools.llm_factory import get_llm_instance26from langchain_core.messages import SystemMessage, HumanMessage27 28DIVIDER = "─" * 6029 30ANALYSIS_PROMPT = """You are a quantitative trading desk review committee.31You are reviewing a set of paper trades that hit their stop-loss this week.32Each entry includes the agent's reasoning at the time of the trade.33 34Your job is to:351. Identify recurring logical flaws or blind spots in the reasoning362. Identify which data signals were ignored or misweighted373. Suggest SPECIFIC changes to the agent system prompts to prevent these errors384. Rate the severity of each flaw: HIGH / MEDIUM / LOW39 40Be direct and specific. Reference the actual rationale text."""41 42 43def compute_stats(trades: list[dict], days: int) -> dict:44 cutoff = (datetime.now(_IST) - timedelta(days=days)).isoformat()45 recent = [t for t in trades if t["timestamp"] >= cutoff]46 47 total = len(recent)48 wins = [t for t in recent if t["status"] == "CLOSED_TP"]49 losses = [t for t in recent if t["status"] == "CLOSED_SL"]50 open_pos = [t for t in recent if t["status"] == "OPEN"]51 52 closed = wins + losses53 win_rate = len(wins) / len(closed) * 100 if closed else 054 pnl_series = [float(t["realized_pnl"] or 0) for t in closed]55 total_pnl = sum(pnl_series)56 avg_win = sum(t["realized_pnl"] or 0 for t in wins) / len(wins) if wins else 057 avg_loss = sum(t["realized_pnl"] or 0 for t in losses) / len(losses) if losses else 058 r_multiple = abs(avg_win / avg_loss) if avg_loss != 0 else 059 60 # Sharpe ratio (annualised, assuming each trade = 1 day unit)61 sharpe = 0.062 if len(pnl_series) >= 2:63 mean_pnl = total_pnl / len(pnl_series)64 variance = sum((p - mean_pnl) ** 2 for p in pnl_series) / len(pnl_series)65 std_pnl = math.sqrt(variance)66 sharpe = round((mean_pnl / std_pnl) * math.sqrt(252), 2) if std_pnl > 0 else 0.067 68 # Confidence calibration: avg predicted confidence vs actual win rate per confidence bucket69 conf_buckets: dict[str, dict] = {}70 for t in closed:71 conf = float(t.get("agent_confidence") or 0)72 bucket = f"{int(conf * 10) * 10}-{int(conf * 10) * 10 + 10}%"73 if bucket not in conf_buckets:74 conf_buckets[bucket] = {"trades": 0, "wins": 0, "avg_conf": []}75 conf_buckets[bucket]["trades"] += 176 conf_buckets[bucket]["avg_conf"].append(conf)77 if t["status"] == "CLOSED_TP":78 conf_buckets[bucket]["wins"] += 179 calibration = {80 k: {81 "trades": v["trades"],82 "predicted_conf": round(sum(v["avg_conf"]) / len(v["avg_conf"]) * 100, 1),83 "actual_win_rate": round(v["wins"] / v["trades"] * 100, 1),84 }85 for k, v in conf_buckets.items()86 }87 88 return {89 "period_days": days,90 "total_trades": total,91 "wins": len(wins),92 "losses": len(losses),93 "open": len(open_pos),94 "win_rate": round(win_rate, 1),95 "total_pnl": round(total_pnl, 2),96 "avg_win": round(avg_win, 2),97 "avg_loss": round(avg_loss, 2),98 "r_multiple": round(r_multiple, 2),99 "sharpe": sharpe,100 "confidence_calibration": calibration,101 "losing_trades": losses,102 }103 104 105def analyse_losing_trades(losses: list[dict]) -> str:106 if not losses:107 return "No losing trades to analyse."108 109 trade_summaries = []110 for t in losses:111 trade_summaries.append({112 "asset": t["asset"],113 "direction": t["direction"],114 "entry": t["simulated_entry"],115 "stop_loss": t["stop_loss"],116 "close_price": t["close_price"],117 "pnl": t["realized_pnl"],118 "market_theme": t["market_theme"],119 "rationale": (t["rationale_log"] or "")[:600],120 })121 122 llm = get_llm_instance(temperature=0.2)123 messages = [124 SystemMessage(content=ANALYSIS_PROMPT),125 HumanMessage(content=f"Losing trades to review:\n\n{json.dumps(trade_summaries, indent=2)}"),126 ]127 response = llm.invoke(messages)128 return response.content.strip()129 130 131def print_stats(stats: dict):132 print(f"\n{'=' * 60}")133 print(f" PERFORMANCE REVIEW — Last {stats['period_days']} days")134 print(f"{'=' * 60}")135 print(f" Total Trades : {stats['total_trades']} ({stats['open']} still open)")136 print(f" Wins / Losses: {stats['wins']} / {stats['losses']}")137 print(f" Win Rate : {stats['win_rate']}%")138 print(f" Total P&L : ₹{stats['total_pnl']:,.2f}")139 print(f" Avg Win : ₹{stats['avg_win']:,.2f}")140 print(f" Avg Loss : ₹{stats['avg_loss']:,.2f}")141 print(f" R-Multiple : {stats['r_multiple']:.2f}x")142 print(f" Sharpe Ratio : {stats['sharpe']:.2f}")143 144 portfolio = get_portfolio_value()145 print(f" Portfolio : ₹{portfolio:,.2f}")146 147 # Confidence calibration table148 if stats["confidence_calibration"]:149 print(f"\n Confidence Calibration:")150 print(f" {'Bucket':<12} {'Trades':>6} {'Predicted':>10} {'Actual WR':>10}")151 print(f" {'-'*42}")152 for bucket, data in sorted(stats["confidence_calibration"].items()):153 print(f" {bucket:<12} {data['trades']:>6} {data['predicted_conf']:>9.1f}% {data['actual_win_rate']:>9.1f}%")154 155 # Live trading gate156 total_closed = stats["wins"] + stats["losses"]157 print(f"\n Live Trading Gate:")158 print(f" {'✅' if total_closed >= 100 else '⬜'} 100 closed trades ({total_closed}/100)")159 print(f" {'✅' if stats['win_rate'] >= 52 else '⬜'} Win rate ≥ 52% ({stats['win_rate']}%)")160 print(f" {'✅' if stats['r_multiple'] >= 1.0 else '⬜'} R-Multiple ≥ 1.0 ({stats['r_multiple']}x)")161 print(f" {'✅' if stats['sharpe'] >= 1.0 else '⬜'} Sharpe ≥ 1.0 ({stats['sharpe']:.2f})")162 163 164def run(days: int = 7, save: bool = False):165 init_db()166 trades = get_all_trades()167 168 if not trades:169 print("No trades in ledger yet. Run some cycles first.")170 return171 172 stats = compute_stats(trades, days)173 print_stats(stats)174 175 losses = stats["losing_trades"]176 if not losses:177 print(f"\n No stop-loss hits in the last {days} days. Nothing to refine.")178 return179 180 print(f"\n{DIVIDER}")181 print(f" LLM ANALYSIS OF {len(losses)} LOSING TRADE(S)")182 print(DIVIDER)183 analysis = analyse_losing_trades(losses)184 print(f"\n{analysis}")185 186 if save:187 from pathlib import Path188 out_path = Path("prompts/refinements.md")189 out_path.parent.mkdir(exist_ok=True)190 with open(out_path, "a", encoding="utf-8") as f:191 f.write(f"\n\n## Review — {datetime.now().date()} (last {days} days)\n\n")192 f.write(f"**Stats:** {stats['wins']}W / {stats['losses']}L | "193 f"Win rate: {stats['win_rate']}% | P&L: ₹{stats['total_pnl']:,.2f}\n\n")194 f.write(analysis)195 print(f"\n Saved to {out_path}")196 197 print()198 199 200if __name__ == "__main__":201 parser = argparse.ArgumentParser(description="Weekly feedback loop analysis")202 parser.add_argument("--days", type=int, default=7, help="Look-back window in days")203 parser.add_argument("--save", action="store_true", help="Save suggestions to prompts/refinements.md")204 args = parser.parse_args()205 run(args.days, args.save)206 