Trade Psychology Coach
import pandas as pd, numpy as np
from datetime import datetime, timedelta
class TradePsychologyCoach:
@staticmethod
def tilt_detector(trades: pd.DataFrame) -> dict:
"""Detect revenge trading and tilt from trade patterns."""
if trades.empty or len(trades) < 5: return {"tilt_level": "INSUFFICIENT DATA"}
recent = trades.tail(10)
# Signs of tilt
signals = []
consecutive_losses = 0
for _, t in recent.iterrows():
if t.get("pnl_pips", 0) < 0: consecutive_losses += 1
else: consecutive_losses = 0
if consecutive_losses >= 3: signals.append(f"3+ consecutive losses ({consecutive_losses})")
# Increasing position sizes after losses
if len(recent) >= 4:
last_lots = recent["lot_size"].tail(3).values
if all(last_lots[i] > last_lots[i-1] for i in range(1, len(last_lots))):
signals.append("DANGER: Increasing size after losses (revenge pattern)")
# Rapid-fire trading
if "entry_time" in recent.columns and len(recent) >= 3:
times = pd.to_datetime(recent["entry_time"])
gaps = times.diff().dt.total_seconds() / 60
if gaps.tail(3).mean() < 15:
signals.append("Rapid-fire entries (<15 min apart) — possible overtrading")
# Wider stops (desperation)
if "stop_loss" in recent.columns and "entry_price" in recent.columns:
sl_distances = abs(recent["entry_price"] - recent["stop_loss"])
if sl_distances.tail(3).mean() > sl_distances.mean() * 1.5:
signals.append("Stop losses widening — possible hope trading")
tilt_level = "HIGH" if len(signals) >= 3 else "MODERATE" if len(signals) >= 2 else "ELEVATED" if signals else "CALM"
return {
"tilt_level": tilt_level,
"signals": signals,
"consecutive_losses": consecutive_losses,
"action": {
"HIGH": "STOP TRADING IMMEDIATELY. Walk away for minimum 4 hours. Review journal.",
"MODERATE": "Take a 30-minute break. Reduce position size by 50%.",
"ELEVATED": "Be extra cautious. Only take A+ setups.",
"CALM": "Emotional state appears normal. Continue with plan.",
}[tilt_level],
}
@staticmethod
def trade-psychology-coach_bias_check(trade_context: dict) -> list[dict]:
"""Flag common trade-psychology-coach biases that may be affecting decision-making."""
biases = []
if trade_context.get("adding_to_loser"):
biases.append({"bias": "Sunk Cost Fallacy", "fix": "Your average price is irrelevant. Would you enter this trade fresh at current price?"})
if trade_context.get("moved_stop_loss_further"):
biases.append({"bias": "Loss Aversion", "fix": "A stop loss is a contract with yourself. Moving it breaks trust with your system."})
if trade_context.get("closed_winner_early"):
biases.append({"bias": "Disposition Effect", "fix": "You cut winners short and let losers run. Reverse this: trail winners, cut losers fast."})
if trade_context.get("entered_because_others_said"):
biases.append({"bias": "Herding", "fix": "Other people's analysis is noise unless you independently verified it."})
if trade_context.get("big_win_then_bigger_risk"):
biases.append({"bias": "House Money Effect", "fix": "Profits are real money, not 'house money'. Maintain standard risk."})
if trade_context.get("missed_trade_then_chased"):
biases.append({"bias": "FOMO", "fix": "There will always be another setup. Chasing fills at bad prices is -EV."})
return biases
@staticmethod
def discipline_score(journal: pd.DataFrame) -> dict:
"""Score trading discipline from journal data."""
if journal.empty: return {"score": 0, "error": "No journal data"}
scores = []
if "followed_plan" in journal.columns:
scores.append(journal["followed_plan"].mean() * 100)
if "setup_type" in journal.columns:
has_setup = (journal["setup_type"] != "").mean() * 100
scores.append(has_setup)
if "lot_size" in journal.columns and "pnl_pips" in journal.columns:
losers = journal[journal["pnl_pips"] < 0]
if len(losers) > 0:
size_after_loss = journal["lot_size"].shift(-1).loc[losers.index]
increased = (size_after_loss > losers["lot_size"]).mean()
scores.append((1 - increased) * 100)
avg = np.mean(scores) if scores else 0
return {
"discipline_score": round(avg, 1),
"grade": "A" if avg >= 85 else "B" if avg >= 70 else "C" if avg >= 55 else "D" if avg >= 40 else "F",
"components": scores,
}
@staticmethod
def pre_trade_checklist() -> dict:
return {
"questions": [
"Am I following my trading plan?",
"Is this setup in my playbook (not a random idea)?",
"Am I trading because the setup is there, or because I WANT to trade?",
"Would I take this trade if I'd just had 3 losses in a row?",
"Is my position size within my rules?",
"Have I defined my exit BEFORE entering?",
"Am I emotionally neutral right now?"],
"rule": "If ANY answer is NO or uncertain — do NOT take the trade.",
}
1---2name: trade-psychology-coach3description: Trading psychology monitoring — tilt detection, emotional state tracking, trade-psychology-coach bias alerts, discipline scoring, and behavioral pattern recognition. Use for "trading psychology", "am I tilted", "revenge trading", "emotional trading", "discipline check", "trading mindset", "fear of missing out", "FOMO", "trading emotions", "overtrading detection", "psychology check", or any trading psychology question. Works with trade-journal-performance and drawdown-playbook.4---56# Trade Psychology Coach78```python9import pandas as pd, numpy as np10from datetime import datetime, timedelta1112class TradePsychologyCoach:1314 @staticmethod15 def tilt_detector(trades: pd.DataFrame) -> dict:16 """Detect revenge trading and tilt from trade patterns."""17 if trades.empty or len(trades) < 5: return {"tilt_level": "INSUFFICIENT DATA"}18 recent = trades.tail(10)19 # Signs of tilt20 signals = []21 consecutive_losses = 022 for _, t in recent.iterrows():23 if t.get("pnl_pips", 0) < 0: consecutive_losses += 124 else: consecutive_losses = 025 if consecutive_losses >= 3: signals.append(f"3+ consecutive losses ({consecutive_losses})")26 # Increasing position sizes after losses27 if len(recent) >= 4:28 last_lots = recent["lot_size"].tail(3).values29 if all(last_lots[i] > last_lots[i-1] for i in range(1, len(last_lots))):30 signals.append("DANGER: Increasing size after losses (revenge pattern)")31 # Rapid-fire trading32 if "entry_time" in recent.columns and len(recent) >= 3:33 times = pd.to_datetime(recent["entry_time"])34 gaps = times.diff().dt.total_seconds() / 6035 if gaps.tail(3).mean() < 15:36 signals.append("Rapid-fire entries (<15 min apart) — possible overtrading")37 # Wider stops (desperation)38 if "stop_loss" in recent.columns and "entry_price" in recent.columns:39 sl_distances = abs(recent["entry_price"] - recent["stop_loss"])40 if sl_distances.tail(3).mean() > sl_distances.mean() * 1.5:41 signals.append("Stop losses widening — possible hope trading")4243 tilt_level = "HIGH" if len(signals) >= 3 else "MODERATE" if len(signals) >= 2 else "ELEVATED" if signals else "CALM"44 return {45 "tilt_level": tilt_level,46 "signals": signals,47 "consecutive_losses": consecutive_losses,48 "action": {49 "HIGH": "STOP TRADING IMMEDIATELY. Walk away for minimum 4 hours. Review journal.",50 "MODERATE": "Take a 30-minute break. Reduce position size by 50%.",51 "ELEVATED": "Be extra cautious. Only take A+ setups.",52 "CALM": "Emotional state appears normal. Continue with plan.",53 }[tilt_level],54 }5556 @staticmethod57 def trade-psychology-coach_bias_check(trade_context: dict) -> list[dict]:58 """Flag common trade-psychology-coach biases that may be affecting decision-making."""59 biases = []60 if trade_context.get("adding_to_loser"):61 biases.append({"bias": "Sunk Cost Fallacy", "fix": "Your average price is irrelevant. Would you enter this trade fresh at current price?"})62 if trade_context.get("moved_stop_loss_further"):63 biases.append({"bias": "Loss Aversion", "fix": "A stop loss is a contract with yourself. Moving it breaks trust with your system."})64 if trade_context.get("closed_winner_early"):65 biases.append({"bias": "Disposition Effect", "fix": "You cut winners short and let losers run. Reverse this: trail winners, cut losers fast."})66 if trade_context.get("entered_because_others_said"):67 biases.append({"bias": "Herding", "fix": "Other people's analysis is noise unless you independently verified it."})68 if trade_context.get("big_win_then_bigger_risk"):69 biases.append({"bias": "House Money Effect", "fix": "Profits are real money, not 'house money'. Maintain standard risk."})70 if trade_context.get("missed_trade_then_chased"):71 biases.append({"bias": "FOMO", "fix": "There will always be another setup. Chasing fills at bad prices is -EV."})72 return biases7374 @staticmethod75 def discipline_score(journal: pd.DataFrame) -> dict:76 """Score trading discipline from journal data."""77 if journal.empty: return {"score": 0, "error": "No journal data"}78 scores = []79 if "followed_plan" in journal.columns:80 scores.append(journal["followed_plan"].mean() * 100)81 if "setup_type" in journal.columns:82 has_setup = (journal["setup_type"] != "").mean() * 10083 scores.append(has_setup)84 if "lot_size" in journal.columns and "pnl_pips" in journal.columns:85 losers = journal[journal["pnl_pips"] < 0]86 if len(losers) > 0:87 size_after_loss = journal["lot_size"].shift(-1).loc[losers.index]88 increased = (size_after_loss > losers["lot_size"]).mean()89 scores.append((1 - increased) * 100)90 avg = np.mean(scores) if scores else 091 return {92 "discipline_score": round(avg, 1),93 "grade": "A" if avg >= 85 else "B" if avg >= 70 else "C" if avg >= 55 else "D" if avg >= 40 else "F",94 "components": scores,95 }9697 @staticmethod98 def pre_trade_checklist() -> dict:99 return {100 "questions": [101 "Am I following my trading plan?",102 "Is this setup in my playbook (not a random idea)?",103 "Am I trading because the setup is there, or because I WANT to trade?",104 "Would I take this trade if I'd just had 3 losses in a row?",105 "Is my position size within my rules?",106 "Have I defined my exit BEFORE entering?",107 "Am I emotionally neutral right now?"],108 "rule": "If ANY answer is NO or uncertain — do NOT take the trade.",109 }110```