
Trade Journal
- 239 installs
- 257 repo stars
- Updated June 24, 2026
- agiprolabs/claude-trading-skills
trade-journal is a Claude Code skill that logs trades with a structured schema and computes performance and behavioral analytics to attribute returns to strategies.
About
A Claude Code skill for structured trade journaling. It logs every trade with an 18-field schema, reviews performance at daily, weekly, and monthly cadences, and computes analytics like win rate by strategy and P&L by time of day. Developers use it to attribute returns to specific setups and to catch behavioral patterns that erode trading edge.
- Logs every trade with an 18-field record schema in JSON and CSV
- Detects behavioral patterns like revenge trading and FOMO entries
- Attributes returns to specific strategies via consistent tags
Trade Journal by the numbers
- 239 all-time installs (skills.sh)
- Ranked #378 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
trade-journal capabilities & compatibility
Free; runs locally in Python with no external API keys
- Capabilities
- trade journal · trade accounting · performance attribution · behavioral detection · win rate analysis
- Use cases
- data analysis · trading · project management
- Pricing
- Free
What trade-journal says it does
Structured trade journaling for systematic improvement.
Most traders fail not from bad strategies but from bad behavior.
See `references/record_format.md` for the complete 18-field schema.
npx skills add https://github.com/agiprolabs/claude-trading-skills --skill trade-journalAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 239 |
|---|---|
| repo stars | ★ 257 |
| Last updated | June 24, 2026 |
| Repository | agiprolabs/claude-trading-skills ↗ |
What it does
Log trades with rationale and outcome, then review performance and attribute returns to strategies for systematic improvement.
Who is it for?
Systematically improving trading by attributing P&L to setups and detecting behavioral leaks.
Skip if: Executing trades or providing accounting-grade financial statements.
When should I use this skill?
You want to log trades with rationale and review win rate and behavioral patterns.
What you get
A structured trade log plus win-rate, profit-factor, and behavioral analytics.
- A structured trade log (JSON/CSV) with per-strategy and time-of-day performance analytics
By the numbers
- 18-field trade record schema
- Daily, weekly, and monthly review cadences
Files
Trade Journal
Structured trade journaling for systematic improvement. Log every trade with context, review performance at multiple cadences, detect behavioral patterns that destroy edge, and attribute returns to specific strategies.
Why Journaling Matters
Most traders fail not from bad strategies but from bad behavior. A trade journal transforms subjective "feel" into objective data:
- Strategy Attribution: Know which setups actually make money vs. which feel profitable
- Behavioral Detection: Catch revenge trading, FOMO entries, and premature exits before they compound
- Pattern Recognition: Discover that your Monday morning trades lose money, or that you cut SOL winners too early
- Accountability: Written rationale before entry forces deliberate decision-making
- Improvement Tracking: Measure whether changes to your process actually improve results
Without a journal, you optimize on noise. With one, you optimize on signal.
Trade Record Structure
Every trade record captures context at entry and outcome at exit. See references/record_format.md for the complete 18-field schema.
Minimum Required Fields
trade = {
"id": "T-20250310-001",
"token": "SOL",
"direction": "long",
"entry_date": "2025-03-10T14:30:00Z",
"entry_price": 142.50,
"size_sol": 5.0,
"strategy": "momentum-breakout",
"rationale": "Breaking above 4h resistance at 141.80 with volume confirmation",
"exit_date": "2025-03-10T16:45:00Z",
"exit_price": 146.20,
"pnl_sol": 0.648,
"outcome": "win",
"lessons": "Held through initial pullback to 143.0, rewarded for patience"
}Strategy Tagging
Use consistent tags to enable performance attribution:
| Category | Tags |
|---|---|
| Momentum | momentum-breakout, trend-continuation, pullback-entry |
| Mean Reversion | range-fade, oversold-bounce, deviation-snap |
| Event-Driven | listing-play, catalyst-trade, news-reaction |
| On-Chain | whale-follow, wallet-copy, flow-signal |
| DeFi | lp-entry, yield-farm, arb-capture |
Rationale Templates
Write rationale before entering. Templates by setup type:
Momentum: "[Token] breaking [level] on [timeframe] with [confirmation]. Target [price], stop [price]."
Mean Reversion: "[Token] at [X] std devs from [mean] on [timeframe]. Expecting reversion to [target]."
On-Chain: "[Signal type] detected — [wallet/flow description]. Historical hit rate [X]%."Storage Format
The journal uses JSON for structured querying and CSV for spreadsheet compatibility.
JSON Format (Primary)
{
"journal_version": "1.0",
"trader_id": "anon",
"trades": [
{
"id": "T-20250310-001",
"token": "SOL",
"direction": "long",
"entry_date": "2025-03-10T14:30:00Z",
"entry_price": 142.50,
"size_sol": 5.0,
"size_usd": 712.50,
"strategy": "momentum-breakout",
"setup_quality": 8,
"rationale": "Breaking above 4h resistance with volume",
"exit_date": "2025-03-10T16:45:00Z",
"exit_price": 146.20,
"pnl_sol": 0.648,
"pnl_pct": 2.60,
"outcome": "win",
"hold_time_minutes": 135,
"emotional_state": "calm",
"lessons": "Patience through pullback paid off",
"tags": ["high-conviction", "clean-setup"]
}
]
}CSV Format (Export)
id,token,direction,entry_date,entry_price,size_sol,strategy,exit_date,exit_price,pnl_sol,pnl_pct,outcome,lessons
T-20250310-001,SOL,long,2025-03-10T14:30:00Z,142.50,5.0,momentum-breakout,2025-03-10T16:45:00Z,146.20,0.648,2.60,win,"Patience paid off"Analytics from Journal Data
Win Rate by Strategy
from collections import Counter
def win_rate_by_strategy(trades: list[dict]) -> dict[str, float]:
"""Compute win rate grouped by strategy tag."""
strategy_outcomes: dict[str, list[str]] = {}
for t in trades:
strat = t["strategy"]
strategy_outcomes.setdefault(strat, []).append(t["outcome"])
return {
strat: outcomes.count("win") / len(outcomes)
for strat, outcomes in strategy_outcomes.items()
if len(outcomes) >= 5 # minimum sample size
}Performance by Time of Day
from datetime import datetime
def pnl_by_hour(trades: list[dict]) -> dict[int, float]:
"""Aggregate P&L by entry hour (UTC)."""
hourly: dict[int, float] = {}
for t in trades:
hour = datetime.fromisoformat(t["entry_date"].rstrip("Z")).hour
hourly[hour] = hourly.get(hour, 0.0) + t.get("pnl_sol", 0.0)
return dict(sorted(hourly.items()))Profit Factor by Token Type
def profit_factor(trades: list[dict], group_key: str = "token") -> dict[str, float]:
"""Compute profit factor (gross wins / gross losses) by grouping key."""
groups: dict[str, dict[str, float]] = {}
for t in trades:
key = t.get(group_key, "unknown")
groups.setdefault(key, {"wins": 0.0, "losses": 0.0})
pnl = t.get("pnl_sol", 0.0)
if pnl > 0:
groups[key]["wins"] += pnl
else:
groups[key]["losses"] += abs(pnl)
return {
k: v["wins"] / v["losses"] if v["losses"] > 0 else float("inf")
for k, v in groups.items()
}Behavioral Pattern Detection
The journal enables detection of destructive trading patterns. See references/review_framework.md for the full framework.
Revenge Trading
Rapid re-entry after a loss, often with larger size:
def detect_revenge_trades(trades: list[dict], max_gap_minutes: int = 15) -> list[dict]:
"""Find trades entered within max_gap_minutes of a losing exit."""
sorted_trades = sorted(trades, key=lambda t: t["entry_date"])
revenge = []
for i in range(1, len(sorted_trades)):
prev, curr = sorted_trades[i - 1], sorted_trades[i]
if prev["outcome"] == "loss":
prev_exit = datetime.fromisoformat(prev["exit_date"].rstrip("Z"))
curr_entry = datetime.fromisoformat(curr["entry_date"].rstrip("Z"))
gap = (curr_entry - prev_exit).total_seconds() / 60
if gap <= max_gap_minutes:
revenge.append(curr)
return revengeFOMO Detection
Entering after large moves without proper setup:
- Entry rationale is vague or missing
- Setup quality self-rated below 5/10
- Entry during a move that already exceeded 1 ATR
Cutting Winners / Riding Losers
def winner_loser_hold_times(trades: list[dict]) -> dict[str, float]:
"""Compare average hold time for wins vs losses."""
win_times = [t["hold_time_minutes"] for t in trades if t["outcome"] == "win"]
loss_times = [t["hold_time_minutes"] for t in trades if t["outcome"] == "loss"]
return {
"avg_win_hold_min": sum(win_times) / len(win_times) if win_times else 0,
"avg_loss_hold_min": sum(loss_times) / len(loss_times) if loss_times else 0,
}
# RED FLAG: if avg_loss_hold > avg_win_hold, you're cutting winners and riding losersTilt Detection
Size escalation after losses suggests emotional trading:
def detect_tilt(trades: list[dict], threshold: float = 1.5) -> list[dict]:
"""Flag trades where size increased >threshold after a loss."""
tilt_trades = []
for i in range(1, len(trades)):
prev, curr = trades[i - 1], trades[i]
if prev["outcome"] == "loss" and curr["size_sol"] > prev["size_sol"] * threshold:
tilt_trades.append(curr)
return tilt_tradesReview Cadence
Daily Review (5 minutes)
- How many trades today? P&L?
- Did I follow my rules on every trade?
- Any emotional decisions?
- One thing I did well, one thing to improve
Weekly Review (30 minutes)
- Win rate and profit factor by strategy
- Behavioral pattern check (revenge trades, tilt, FOMO)
- Best and worst trade of the week — what made them different?
- Strategy performance vs. expectations
- Adjust position sizing if needed
Monthly Review (2 hours)
- Full strategy attribution analysis
- Equity curve review — drawdown periods and recovery
- Compare actual vs. planned risk per trade
- Performance by token type, time of day, day of week
- Are any strategies consistently losing? Consider dropping them
- Review and update strategy parameters
See references/review_framework.md for detailed review checklists and questions.
Partial Exits and Scaled Entries
Real trading involves scaling in and out. The journal handles this with child records:
# Parent trade with two scale-out exits
parent = {
"id": "T-20250310-001",
"token": "BONK",
"direction": "long",
"entry_date": "2025-03-10T14:30:00Z",
"entry_price": 0.000023,
"size_sol": 10.0,
"strategy": "momentum-breakout",
"exits": [
{"date": "2025-03-10T15:00:00Z", "price": 0.000025, "size_pct": 50, "reason": "first-target"},
{"date": "2025-03-10T16:30:00Z", "price": 0.000028, "size_pct": 50, "reason": "trailing-stop"},
]
}See references/record_format.md for full documentation of partial exit handling.
Files
References
references/record_format.md— Complete 18-field trade record schema, field descriptions, tagging taxonomy, CSV/JSON examples, partial exit handlingreferences/review_framework.md— Daily/weekly/monthly review checklists, behavioral red flags, performance decay detection
Scripts
scripts/trade_logger.py— CLI trade logger: add, list, update, compute stats, filter, demo mode (stdlib only)scripts/journal_analyzer.py— Journal analysis: strategy performance, behavioral patterns, time-based analysis, demo mode (stdlib only)
Dependencies
Both scripts use Python standard library only (json, datetime, argparse, collections). No external packages required.
# No installation needed — stdlib only
python scripts/trade_logger.py --demo
python scripts/journal_analyzer.py --demoDisclaimer
This skill provides tools for trade record-keeping and performance analysis. It does not provide financial advice, trading recommendations, or guarantee any trading outcomes. All analysis is informational and for personal review purposes only.
Trade Record Format — Complete Schema
Overview
Every trade is a structured record capturing context at entry, outcome at exit, and reflective notes. The schema supports single-fill trades, scaled entries, and partial exits.
Complete Field Schema (18 Fields)
Required Fields
| # | Field | Type | Description |
|---|---|---|---|
| 1 | id | string | Unique trade ID. Format: T-YYYYMMDD-NNN |
| 2 | token | string | Token symbol (e.g., SOL, BONK, JUP) |
| 3 | direction | string | long or short |
| 4 | entry_date | string | ISO 8601 UTC timestamp of entry |
| 5 | entry_price | float | Entry price in USD or SOL |
| 6 | size_sol | float | Position size in SOL |
| 7 | strategy | string | Strategy tag from taxonomy |
| 8 | rationale | string | Written reason for the trade, recorded before entry |
| 9 | outcome | string | win, loss, or breakeven |
Recommended Fields
| # | Field | Type | Description |
|---|---|---|---|
| 10 | exit_date | string | ISO 8601 UTC timestamp of final exit |
| 11 | exit_price | float | Exit price (or VWAP for scaled exits) |
| 12 | pnl_sol | float | Realized P&L in SOL |
| 13 | pnl_pct | float | Realized P&L as percentage |
| 14 | hold_time_minutes | int | Total hold time in minutes |
Optional Fields
| # | Field | Type | Description |
|---|---|---|---|
| 15 | size_usd | float | Position size in USD at entry |
| 16 | setup_quality | int | Self-rated 1-10 quality of the setup |
| 17 | emotional_state | string | Emotional state at entry: calm, anxious, excited, frustrated, fomo, revenge |
| 18 | lessons | string | Post-trade reflection on what was learned |
| — | tags | list[str] | Freeform tags for filtering: high-conviction, scalp, swing, etc. |
| — | stop_price | float | Planned stop-loss price |
| — | target_price | float | Planned take-profit price |
| — | risk_reward | float | Planned risk:reward ratio |
| — | fees_sol | float | Transaction fees paid |
| — | slippage_bps | float | Observed slippage in basis points |
| — | exits | list | Partial exit records (see below) |
| — | entries | list | Scaled entry records (see below) |
| — | screenshot_path | string | Path to chart screenshot at entry |
Strategy Tagging Taxonomy
Use a two-level hierarchy: category-subcategory.
Momentum
| Tag | Description |
|---|---|
momentum-breakout | Price breaks key level with volume confirmation |
trend-continuation | Entry on pullback within established trend |
pullback-entry | Buy the dip in an uptrend |
volume-spike | Unusual volume triggers entry |
new-high | Token making new highs, riding momentum |
Mean Reversion
| Tag | Description |
|---|---|
range-fade | Fade the edges of an established range |
oversold-bounce | RSI/BB-based oversold entry |
deviation-snap | Entry when price deviates N std devs from VWAP |
gap-fill | Trading a gap fill |
Event-Driven
| Tag | Description |
|---|---|
listing-play | New CEX/DEX listing momentum |
catalyst-trade | Known upcoming catalyst (airdrop, launch) |
news-reaction | Trading the reaction to news/announcement |
On-Chain
| Tag | Description |
|---|---|
whale-follow | Following large wallet activity |
wallet-copy | Copying a tracked profitable wallet |
flow-signal | DEX flow imbalance signal |
holder-shift | Significant change in holder distribution |
DeFi
| Tag | Description |
|---|---|
lp-entry | Providing liquidity to a pool |
yield-farm | Yield farming position |
arb-capture | Arbitrage opportunity |
Discretionary
| Tag | Description |
|---|---|
gut-feel | No formal setup — mark these for review |
revenge | Re-entry after loss — always flag |
fomo | Fear of missing out entry — always flag |
Rationale Templates
Record rationale before placing the trade to enforce deliberation.
Momentum Template
[TOKEN] breaking [LEVEL] on [TIMEFRAME] with [CONFIRMATION].
Volume is [X]x average. Target [PRICE] ([X]% up), stop [PRICE] ([X]% down).
R:R = [X]:1. Sizing [X] SOL ([X]% of portfolio).Mean Reversion Template
[TOKEN] at [X] standard deviations from [MEAN_TYPE] on [TIMEFRAME].
Historical reversion rate at this level: [X]%. Target reversion to [PRICE].
Stop beyond [PRICE] ([X] additional std devs). R:R = [X]:1.On-Chain Template
[SIGNAL_TYPE] detected: [DESCRIPTION].
Wallet [ADDRESS_PREFIX] has [X]% historical accuracy on similar trades.
Entry at [PRICE], target [PRICE], stop [PRICE].Event-Driven Template
Catalyst: [EVENT] expected [DATE/TIME].
Historical precedent: [SIMILAR_EVENTS] resulted in [X]% avg move.
Entry at [PRICE], exit plan: [DESCRIPTION].Partial Exits
When scaling out of a position, use the exits array:
{
"id": "T-20250310-001",
"token": "SOL",
"entry_price": 142.50,
"size_sol": 10.0,
"exits": [
{
"date": "2025-03-10T15:00:00Z",
"price": 145.00,
"size_pct": 33,
"size_sol": 3.3,
"reason": "first-target",
"pnl_sol": 0.058
},
{
"date": "2025-03-10T16:00:00Z",
"price": 148.00,
"size_pct": 33,
"size_sol": 3.3,
"reason": "second-target",
"pnl_sol": 0.127
},
{
"date": "2025-03-10T17:30:00Z",
"price": 144.00,
"size_pct": 34,
"size_sol": 3.4,
"reason": "trailing-stop",
"pnl_sol": 0.036
}
],
"pnl_sol": 0.221,
"exit_price": 145.67
}The parent exit_price is the VWAP of all exits. The parent pnl_sol is the sum.
Scaled Entries
When scaling into a position, use the entries array:
{
"id": "T-20250310-002",
"token": "JUP",
"entries": [
{"date": "2025-03-10T10:00:00Z", "price": 0.85, "size_sol": 3.0, "reason": "initial"},
{"date": "2025-03-10T11:00:00Z", "price": 0.82, "size_sol": 3.0, "reason": "add-on-dip"},
{"date": "2025-03-10T12:30:00Z", "price": 0.80, "size_sol": 4.0, "reason": "final-add"}
],
"entry_price": 0.821,
"size_sol": 10.0
}The parent entry_price is the VWAP of all entries.
JSON Storage Format
{
"journal_version": "1.0",
"trader_id": "anon",
"created": "2025-01-01T00:00:00Z",
"updated": "2025-03-10T14:30:00Z",
"trades": []
}CSV Export Format
CSV uses flat columns. Partial exits are collapsed to summary values.
id,token,direction,entry_date,entry_price,size_sol,size_usd,strategy,setup_quality,rationale,exit_date,exit_price,pnl_sol,pnl_pct,outcome,hold_time_minutes,emotional_state,lessons
T-20250310-001,SOL,long,2025-03-10T14:30:00Z,142.50,5.0,712.50,momentum-breakout,8,"4h resistance break with volume",2025-03-10T16:45:00Z,146.20,0.648,2.60,win,135,calm,"Patience through pullback"Field Validation Rules
| Field | Validation |
|---|---|
id | Must match T-YYYYMMDD-NNN pattern |
direction | Must be long or short |
entry_price | Must be > 0 |
size_sol | Must be > 0 |
outcome | Must be win, loss, or breakeven |
setup_quality | Must be 1-10 |
emotional_state | Must be from allowed list |
pnl_pct | Computed: (exit_price - entry_price) / entry_price * 100 for longs |
hold_time_minutes | Computed from entry/exit dates |
Review Framework — Daily, Weekly, Monthly
Overview
Consistent review cadence is what transforms raw trade data into actionable improvement. Each level of review serves a different purpose: daily catches immediate behavioral issues, weekly reveals strategy-level patterns, and monthly drives structural changes.
Daily Review (5 Minutes)
Perform at end of trading day or after last trade closes.
Checklist
- [ ] How many trades today? What was net P&L?
- [ ] Did every trade have a written rationale before entry?
- [ ] Did I follow my position sizing rules on every trade?
- [ ] Did I honor my stop-losses without moving them?
- [ ] Were any trades emotional (FOMO, revenge, boredom)?
- [ ] One thing I did well today
- [ ] One thing to improve tomorrow
Quick Stats to Compute
daily_stats = {
"trade_count": len(today_trades),
"wins": sum(1 for t in today_trades if t["outcome"] == "win"),
"losses": sum(1 for t in today_trades if t["outcome"] == "loss"),
"net_pnl_sol": sum(t.get("pnl_sol", 0) for t in today_trades),
"largest_win": max((t.get("pnl_sol", 0) for t in today_trades), default=0),
"largest_loss": min((t.get("pnl_sol", 0) for t in today_trades), default=0),
}Red Flags — Immediate Action Required
| Flag | Signal | Action |
|---|---|---|
| 3+ losses in a row | Possible tilt | Stop trading for the day |
| Trade without rationale | Discipline breakdown | Write rationale retroactively, add to improvement list |
| Size > max allowed | Risk violation | Review position sizing rules |
| Trade entered < 10 min after loss | Revenge trading | Mark trade, review in weekly |
Weekly Review (30 Minutes)
Perform every Sunday or on your designated review day.
Checklist
- [ ] Total trades this week, win rate, net P&L
- [ ] Win rate and P&L by strategy — which strategies performed?
- [ ] Any revenge trades detected? (re-entry within 15 min of loss)
- [ ] Any tilt trades? (size increase > 1.5x after loss)
- [ ] Best trade of the week — what made it good?
- [ ] Worst trade of the week — what went wrong?
- [ ] Compare planned R:R vs actual R:R
- [ ] Are my self-rated setup quality scores predictive? (Do high-quality setups actually win more?)
- [ ] Any new patterns noticed?
Questions to Ask
1. Strategy performance: Which strategies have positive expectancy over the last 20+ trades? Any strategies I should pause? 2. Behavioral check: Am I trading more after losses? Am I cutting winners short? 3. Risk adherence: Did I exceed my max daily loss on any day? Did I stick to position limits? 4. Edge validation: Is my overall profit factor above 1.5? If not, what is dragging it down? 5. Improvement tracking: Did the thing I said I would improve last week actually improve?
Key Metrics to Compute
weekly_metrics = {
"total_trades": len(week_trades),
"win_rate": wins / total if total > 0 else 0,
"profit_factor": gross_wins / gross_losses if gross_losses > 0 else float("inf"),
"avg_win_sol": avg(t["pnl_sol"] for t in wins),
"avg_loss_sol": avg(abs(t["pnl_sol"]) for t in losses),
"expectancy_sol": (win_rate * avg_win) - ((1 - win_rate) * avg_loss),
"max_consecutive_losses": max_streak(week_trades, "loss"),
"revenge_trade_count": len(detect_revenge_trades(week_trades)),
"avg_setup_quality_winners": avg(t["setup_quality"] for t in wins),
"avg_setup_quality_losers": avg(t["setup_quality"] for t in losses),
}Monthly Review (2 Hours)
Comprehensive analysis on the first weekend of each month.
Checklist
- [ ] Full P&L breakdown by strategy, token, and time period
- [ ] Equity curve — any significant drawdowns?
- [ ] Strategy attribution — which strategies earned their keep?
- [ ] Behavioral pattern summary — trends in emotional trading?
- [ ] Compare this month to previous months — improving or declining?
- [ ] Review and update strategy parameters if needed
- [ ] Set specific improvement goals for next month
Deep Analysis Questions
1. Strategy lifecycle: Are any strategies decaying? Compare last 30 trades vs prior 30 trades per strategy. 2. Time analysis: What hours/days are most profitable? Should I restrict trading to certain windows? 3. Token analysis: Am I better at trading certain token types? Large caps vs small caps? 4. Hold time analysis: Am I holding winners long enough? Am I cutting losses quickly enough? 5. Sizing analysis: Am I sizing correctly? Larger on high-conviction, smaller on speculative? 6. Fee analysis: What percentage of gross P&L goes to fees and slippage?
Performance Decay Detection
Track rolling metrics to detect when a strategy or your overall trading is deteriorating.
def detect_decay(trades: list[dict], window: int = 20) -> dict:
"""Compare recent window to prior window."""
if len(trades) < window * 2:
return {"status": "insufficient_data"}
recent = trades[-window:]
prior = trades[-window * 2:-window]
recent_wr = sum(1 for t in recent if t["outcome"] == "win") / window
prior_wr = sum(1 for t in prior if t["outcome"] == "win") / window
recent_pf = compute_profit_factor(recent)
prior_pf = compute_profit_factor(prior)
return {
"win_rate_change": recent_wr - prior_wr,
"profit_factor_change": recent_pf - prior_pf,
"decaying": recent_wr < prior_wr * 0.8 or recent_pf < prior_pf * 0.7,
"recent_win_rate": recent_wr,
"prior_win_rate": prior_wr,
}Decay thresholds:
- Win rate drops > 20% relative: investigate
- Profit factor drops > 30% relative: pause the strategy
- 3 consecutive losing weeks: full strategy review required
Behavioral Red Flags
Revenge Trading
Definition: Entering a new trade within 15 minutes of a losing exit, often with equal or larger size.
Detection: Compare entry_date of trade N to exit_date of trade N-1 when N-1 was a loss.
Severity levels:
- Mild: 1-2 revenge trades per week — note and monitor
- Moderate: 3-5 per week — implement mandatory 30-minute cooldown after losses
- Severe: Daily occurrence — stop live trading, review process
FOMO (Fear of Missing Out)
Indicators:
- Setup quality self-rated < 5
- Rationale mentions "already moved" or "catching up"
- Entry after token already moved > 1 ATR from prior level
- No clear stop-loss defined
Detection: Correlate low setup quality scores with outcomes.
Cutting Winners
Definition: Average hold time for winning trades is significantly shorter than for losing trades.
Threshold: If avg_win_hold / avg_loss_hold < 0.7, you are likely cutting winners.
Fix: Use trailing stops instead of fixed targets. Review exits on winners that continued moving favorably.
Riding Losers
Definition: Holding losing trades far beyond the planned stop.
Detection: Compare actual exit price on losses to the recorded stop_price.
Threshold: If > 30% of losses exceed the planned stop by > 50%, there is a stop-honoring problem.
Tilt / Emotional Sizing
Definition: Increasing position size after losses, driven by desire to "make it back."
Detection: Size of trade N vs trade N-1 when N-1 was a loss. Increase > 1.5x is a red flag.
Impact analysis: Compute P&L of tilt trades separately — they almost always have negative expectancy.
Overtrading
Definition: Taking trades that do not meet setup criteria, often from boredom.
Detection:
- Trade count per day significantly above your baseline
- Setup quality scores trending downward through the day
- Trades without complete rationale
Threshold: More than 2x your average daily trade count without proportional increase in opportunities.
Improvement Goal Setting
After monthly review, set 1-3 specific, measurable goals:
Good goals:
- "Reduce revenge trades from 4/week to 1/week"
- "Increase average setup quality of entries from 6.2 to 7.0"
- "Hold winning momentum trades for at least 2x the current average hold time"
- "Stop trading the
gut-feelstrategy until I can define entry criteria"
Bad goals:
- "Make more money" (not specific)
- "Be more disciplined" (not measurable)
- "Never have a losing trade" (not realistic)
Track goals in the journal metadata and review progress weekly.
#!/usr/bin/env python3
"""Trade journal analyzer — behavioral pattern detection and performance analysis.
Reads a trade journal (JSON) and produces a comprehensive analysis report
including strategy performance, time-based patterns, behavioral red flags,
and actionable recommendations.
Usage:
python scripts/journal_analyzer.py --demo
python scripts/journal_analyzer.py --journal trade_journal.json
python scripts/journal_analyzer.py --journal trade_journal.json --strategy momentum-breakout
Dependencies:
None — uses Python standard library only (json, datetime, collections)
Environment Variables:
TRADE_JOURNAL_PATH: Path to journal JSON file (default: trade_journal.json)
"""
import argparse
import json
import math
import os
import random
import sys
from collections import Counter, defaultdict
from datetime import datetime, timedelta, timezone
from typing import Optional
# ── Configuration ───────────────────────────────────────────────────
JOURNAL_PATH = os.getenv("TRADE_JOURNAL_PATH", "trade_journal.json")
REVENGE_MAX_GAP_MINUTES = 15
TILT_SIZE_THRESHOLD = 1.5
MIN_SAMPLE_SIZE = 5
DECAY_WINDOW = 20
# ── Data Loading ────────────────────────────────────────────────────
def load_journal(path: str) -> dict:
"""Load journal from JSON file.
Args:
path: Path to the journal JSON file.
Returns:
Parsed journal dictionary.
Raises:
FileNotFoundError: If journal file does not exist.
json.JSONDecodeError: If file contains invalid JSON.
"""
if not os.path.exists(path):
raise FileNotFoundError(f"Journal not found: {path}")
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def get_closed_trades(journal: dict) -> list[dict]:
"""Extract closed trades (those with an outcome) sorted by entry date.
Args:
journal: Journal dictionary.
Returns:
List of closed trade records sorted chronologically.
"""
closed = [t for t in journal.get("trades", []) if t.get("outcome") is not None]
return sorted(closed, key=lambda t: t.get("entry_date", ""))
# ── Performance Analysis ───────────────────────────────────────────
def performance_by_strategy(trades: list[dict]) -> dict[str, dict]:
"""Compute performance metrics grouped by strategy.
Args:
trades: List of closed trade records.
Returns:
Dictionary mapping strategy name to performance metrics.
"""
by_strat: dict[str, list[dict]] = defaultdict(list)
for t in trades:
by_strat[t.get("strategy", "unknown")].append(t)
results: dict[str, dict] = {}
for strat, strat_trades in sorted(by_strat.items()):
total = len(strat_trades)
wins = sum(1 for t in strat_trades if t["outcome"] == "win")
pnl_values = [t.get("pnl_sol", 0) for t in strat_trades if t.get("pnl_sol") is not None]
gross_wins = sum(v for v in pnl_values if v > 0)
gross_losses = sum(abs(v) for v in pnl_values if v < 0)
pf = gross_wins / gross_losses if gross_losses > 0 else float("inf")
results[strat] = {
"trades": total,
"win_rate": wins / total if total > 0 else 0,
"total_pnl_sol": sum(pnl_values),
"profit_factor": pf,
"avg_pnl_sol": sum(pnl_values) / total if total > 0 else 0,
}
return results
def performance_by_day_of_week(trades: list[dict]) -> dict[str, dict]:
"""Compute performance metrics grouped by day of week.
Args:
trades: List of closed trade records.
Returns:
Dictionary mapping day name to performance metrics.
"""
days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
by_day: dict[str, list[dict]] = defaultdict(list)
for t in trades:
entry = t.get("entry_date", "")
if not entry:
continue
try:
dt = datetime.fromisoformat(entry.replace("Z", "+00:00"))
day_name = days[dt.weekday()]
by_day[day_name].append(t)
except (ValueError, IndexError):
continue
results: dict[str, dict] = {}
for day in days:
day_trades = by_day.get(day, [])
if not day_trades:
continue
total = len(day_trades)
wins = sum(1 for t in day_trades if t["outcome"] == "win")
pnl = sum(t.get("pnl_sol", 0) for t in day_trades if t.get("pnl_sol") is not None)
results[day] = {
"trades": total,
"win_rate": wins / total if total > 0 else 0,
"total_pnl_sol": pnl,
}
return results
def performance_by_hold_time(trades: list[dict]) -> dict[str, dict]:
"""Compute performance grouped by hold time buckets.
Args:
trades: List of closed trade records.
Returns:
Dictionary mapping hold time bucket to performance metrics.
"""
buckets: dict[str, list[dict]] = defaultdict(list)
for t in trades:
hold = t.get("hold_time_minutes")
if hold is None:
continue
if hold < 30:
bucket = "< 30 min"
elif hold < 60:
bucket = "30-60 min"
elif hold < 180:
bucket = "1-3 hours"
elif hold < 480:
bucket = "3-8 hours"
else:
bucket = "8+ hours"
buckets[bucket].append(t)
results: dict[str, dict] = {}
for bucket in ["< 30 min", "30-60 min", "1-3 hours", "3-8 hours", "8+ hours"]:
bt = buckets.get(bucket, [])
if not bt:
continue
total = len(bt)
wins = sum(1 for t in bt if t["outcome"] == "win")
pnl = sum(t.get("pnl_sol", 0) for t in bt if t.get("pnl_sol") is not None)
results[bucket] = {
"trades": total,
"win_rate": wins / total if total > 0 else 0,
"total_pnl_sol": pnl,
}
return results
# ── Behavioral Pattern Detection ───────────────────────────────────
def detect_revenge_trades(trades: list[dict], max_gap_minutes: int = REVENGE_MAX_GAP_MINUTES) -> list[dict]:
"""Detect revenge trades — entries shortly after a loss.
Args:
trades: Chronologically sorted list of closed trade records.
max_gap_minutes: Maximum minutes between loss exit and next entry to flag.
Returns:
List of trades flagged as potential revenge trades.
"""
revenge: list[dict] = []
for i in range(1, len(trades)):
prev, curr = trades[i - 1], trades[i]
if prev.get("outcome") != "loss":
continue
prev_exit = prev.get("exit_date", "")
curr_entry = curr.get("entry_date", "")
if not prev_exit or not curr_entry:
continue
try:
prev_dt = datetime.fromisoformat(prev_exit.replace("Z", "+00:00"))
curr_dt = datetime.fromisoformat(curr_entry.replace("Z", "+00:00"))
gap = (curr_dt - prev_dt).total_seconds() / 60
if gap <= max_gap_minutes:
revenge.append({
"trade": curr,
"gap_minutes": round(gap, 1),
"preceding_loss_id": prev["id"],
})
except ValueError:
continue
return revenge
def detect_tilt_trades(trades: list[dict], threshold: float = TILT_SIZE_THRESHOLD) -> list[dict]:
"""Detect tilt — size escalation after losses.
Args:
trades: Chronologically sorted list of trade records.
threshold: Size increase multiplier that triggers a flag.
Returns:
List of trades flagged as potential tilt.
"""
tilt: list[dict] = []
for i in range(1, len(trades)):
prev, curr = trades[i - 1], trades[i]
if prev.get("outcome") != "loss":
continue
prev_size = prev.get("size_sol", 0)
curr_size = curr.get("size_sol", 0)
if prev_size > 0 and curr_size > prev_size * threshold:
tilt.append({
"trade": curr,
"size_increase": round(curr_size / prev_size, 2),
"preceding_loss_id": prev["id"],
})
return tilt
def detect_loss_streaks(trades: list[dict]) -> list[dict]:
"""Detect consecutive loss streaks.
Args:
trades: Chronologically sorted list of trade records.
Returns:
List of streak records with start, end, and length.
"""
streaks: list[dict] = []
current_start: Optional[int] = None
current_length = 0
for i, t in enumerate(trades):
if t.get("outcome") == "loss":
if current_start is None:
current_start = i
current_length += 1
else:
if current_length >= 3:
streaks.append({
"start_trade": trades[current_start]["id"],
"end_trade": trades[i - 1]["id"],
"length": current_length,
"total_loss_sol": sum(
abs(trades[j].get("pnl_sol", 0))
for j in range(current_start, i)
),
})
current_start = None
current_length = 0
# Handle streak at end
if current_length >= 3 and current_start is not None:
streaks.append({
"start_trade": trades[current_start]["id"],
"end_trade": trades[-1]["id"],
"length": current_length,
"total_loss_sol": sum(
abs(trades[j].get("pnl_sol", 0))
for j in range(current_start, len(trades))
),
})
return streaks
def winner_loser_hold_comparison(trades: list[dict]) -> dict[str, float]:
"""Compare average hold times for winners vs losers.
Args:
trades: List of closed trade records.
Returns:
Dictionary with average hold times and a cutting_winners flag.
"""
win_holds = [
t["hold_time_minutes"] for t in trades
if t.get("outcome") == "win" and t.get("hold_time_minutes") is not None
]
loss_holds = [
t["hold_time_minutes"] for t in trades
if t.get("outcome") == "loss" and t.get("hold_time_minutes") is not None
]
avg_win = sum(win_holds) / len(win_holds) if win_holds else 0
avg_loss = sum(loss_holds) / len(loss_holds) if loss_holds else 0
cutting_winners = avg_loss > 0 and avg_win > 0 and (avg_win / avg_loss < 0.7)
return {
"avg_win_hold_min": round(avg_win, 1),
"avg_loss_hold_min": round(avg_loss, 1),
"ratio": round(avg_win / avg_loss, 2) if avg_loss > 0 else 0,
"cutting_winners": cutting_winners,
}
def setup_quality_analysis(trades: list[dict]) -> dict:
"""Analyze whether setup quality scores predict outcomes.
Args:
trades: List of closed trade records with setup_quality.
Returns:
Comparison of quality scores for wins vs losses.
"""
with_quality = [t for t in trades if t.get("setup_quality") is not None]
if len(with_quality) < MIN_SAMPLE_SIZE:
return {"status": "insufficient_data"}
win_quality = [t["setup_quality"] for t in with_quality if t["outcome"] == "win"]
loss_quality = [t["setup_quality"] for t in with_quality if t["outcome"] == "loss"]
avg_win_q = sum(win_quality) / len(win_quality) if win_quality else 0
avg_loss_q = sum(loss_quality) / len(loss_quality) if loss_quality else 0
# High quality (>= 7) vs low quality (< 7) win rates
high_q = [t for t in with_quality if t["setup_quality"] >= 7]
low_q = [t for t in with_quality if t["setup_quality"] < 7]
high_wr = sum(1 for t in high_q if t["outcome"] == "win") / len(high_q) if high_q else 0
low_wr = sum(1 for t in low_q if t["outcome"] == "win") / len(low_q) if low_q else 0
return {
"avg_quality_winners": round(avg_win_q, 1),
"avg_quality_losers": round(avg_loss_q, 1),
"high_quality_win_rate": round(high_wr, 3),
"low_quality_win_rate": round(low_wr, 3),
"quality_is_predictive": high_wr > low_wr + 0.1,
}
# ── Report Generation ──────────────────────────────────────────────
def generate_recommendations(
trades: list[dict],
revenge: list[dict],
tilt: list[dict],
hold_comp: dict,
quality: dict,
strat_perf: dict[str, dict],
) -> list[str]:
"""Generate actionable recommendations from analysis.
Args:
trades: All closed trades.
revenge: Detected revenge trades.
tilt: Detected tilt trades.
hold_comp: Hold time comparison results.
quality: Setup quality analysis results.
strat_perf: Strategy performance results.
Returns:
List of recommendation strings.
"""
recs: list[str] = []
if revenge:
recs.append(
f"BEHAVIORAL: {len(revenge)} revenge trade(s) detected. "
"Implement a mandatory 30-minute cooldown after any loss."
)
if tilt:
recs.append(
f"BEHAVIORAL: {len(tilt)} tilt trade(s) detected (size escalation after loss). "
"Lock position sizing to prevent emotional increases."
)
if hold_comp.get("cutting_winners"):
recs.append(
f"EXECUTION: Cutting winners — avg win hold ({hold_comp['avg_win_hold_min']} min) "
f"is much shorter than avg loss hold ({hold_comp['avg_loss_hold_min']} min). "
"Consider trailing stops to let winners run."
)
if quality.get("quality_is_predictive"):
recs.append(
f"EDGE: Setup quality IS predictive — high quality win rate "
f"({quality['high_quality_win_rate']:.0%}) vs low quality "
f"({quality['low_quality_win_rate']:.0%}). "
"Only take setups rated 7+."
)
# Flag losing strategies
for strat, perf in strat_perf.items():
if perf["trades"] >= MIN_SAMPLE_SIZE and perf["profit_factor"] < 1.0:
recs.append(
f"STRATEGY: '{strat}' has negative expectancy "
f"(PF={perf['profit_factor']:.2f}, {perf['trades']} trades). "
"Consider pausing this strategy."
)
# Flag emotional strategies
for strat in ["gut-feel", "fomo", "revenge"]:
if strat in strat_perf:
recs.append(
f"STRATEGY: '{strat}' trades should be eliminated. "
f"P&L: {strat_perf[strat]['total_pnl_sol']:+.4f} SOL "
f"over {strat_perf[strat]['trades']} trades."
)
if not recs:
recs.append("No critical issues detected. Continue logging and review next week.")
return recs
def print_report(
trades: list[dict],
strat_perf: dict[str, dict],
day_perf: dict[str, dict],
hold_perf: dict[str, dict],
revenge: list[dict],
tilt: list[dict],
streaks: list[dict],
hold_comp: dict,
quality: dict,
recs: list[str],
) -> None:
"""Print the full analysis report.
Args:
trades: All closed trades.
strat_perf: Strategy performance data.
day_perf: Day-of-week performance data.
hold_perf: Hold-time bucket performance data.
revenge: Revenge trade detections.
tilt: Tilt trade detections.
streaks: Loss streak records.
hold_comp: Hold time comparison.
quality: Setup quality analysis.
recs: Recommendations list.
"""
total = len(trades)
wins = sum(1 for t in trades if t["outcome"] == "win")
pnl = sum(t.get("pnl_sol", 0) for t in trades if t.get("pnl_sol") is not None)
print("\n" + "=" * 60)
print(" TRADE JOURNAL ANALYSIS REPORT")
print("=" * 60)
# Overview
print(f"\n Trades Analyzed: {total}")
print(f" Win Rate: {wins / total:.1%}" if total > 0 else " Win Rate: N/A")
print(f" Net P&L: {pnl:+.4f} SOL")
# Strategy performance
print(f"\n{'─' * 60}")
print(" PERFORMANCE BY STRATEGY")
print(f"{'─' * 60}")
print(f" {'Strategy':<25} {'Trades':>7} {'WR':>7} {'P&L SOL':>10} {'PF':>7}")
print(f" {'-' * 56}")
for strat, perf in sorted(strat_perf.items(), key=lambda x: x[1]["total_pnl_sol"], reverse=True):
pf_str = f"{perf['profit_factor']:.2f}" if perf['profit_factor'] < 1000 else "inf"
print(
f" {strat:<25} {perf['trades']:>7} "
f"{perf['win_rate']:>6.0%} {perf['total_pnl_sol']:>+10.4f} {pf_str:>7}"
)
# Day of week
print(f"\n{'─' * 60}")
print(" PERFORMANCE BY DAY OF WEEK")
print(f"{'─' * 60}")
print(f" {'Day':<15} {'Trades':>7} {'WR':>7} {'P&L SOL':>10}")
print(f" {'-' * 39}")
for day, perf in day_perf.items():
print(f" {day:<15} {perf['trades']:>7} {perf['win_rate']:>6.0%} {perf['total_pnl_sol']:>+10.4f}")
# Hold time buckets
print(f"\n{'─' * 60}")
print(" PERFORMANCE BY HOLD TIME")
print(f"{'─' * 60}")
print(f" {'Bucket':<15} {'Trades':>7} {'WR':>7} {'P&L SOL':>10}")
print(f" {'-' * 39}")
for bucket, perf in hold_perf.items():
print(f" {bucket:<15} {perf['trades']:>7} {perf['win_rate']:>6.0%} {perf['total_pnl_sol']:>+10.4f}")
# Behavioral patterns
print(f"\n{'─' * 60}")
print(" BEHAVIORAL PATTERNS")
print(f"{'─' * 60}")
print(f"\n Revenge Trades: {len(revenge)}")
for r in revenge:
t = r["trade"]
print(f" - {t['id']}: entered {r['gap_minutes']} min after loss {r['preceding_loss_id']}, "
f"outcome: {t['outcome']}")
print(f"\n Tilt Trades (size escalation after loss): {len(tilt)}")
for ti in tilt:
t = ti["trade"]
print(f" - {t['id']}: size {ti['size_increase']}x previous, outcome: {t['outcome']}")
print(f"\n Loss Streaks (3+): {len(streaks)}")
for s in streaks:
print(f" - {s['start_trade']} to {s['end_trade']}: {s['length']} consecutive losses, "
f"total: {s['total_loss_sol']:.4f} SOL lost")
# Hold time comparison
print(f"\n Winner vs Loser Hold Times:")
print(f" Avg win hold: {hold_comp['avg_win_hold_min']:.0f} min")
print(f" Avg loss hold: {hold_comp['avg_loss_hold_min']:.0f} min")
print(f" Ratio: {hold_comp['ratio']:.2f}")
if hold_comp["cutting_winners"]:
print(" WARNING: You appear to be cutting winners short.")
# Setup quality
if quality.get("status") != "insufficient_data":
print(f"\n Setup Quality Analysis:")
print(f" Avg quality (winners): {quality['avg_quality_winners']}")
print(f" Avg quality (losers): {quality['avg_quality_losers']}")
print(f" High quality WR: {quality['high_quality_win_rate']:.0%}")
print(f" Low quality WR: {quality['low_quality_win_rate']:.0%}")
pred = "YES" if quality.get("quality_is_predictive") else "NO"
print(f" Quality is predictive: {pred}")
# Recommendations
print(f"\n{'─' * 60}")
print(" RECOMMENDATIONS")
print(f"{'─' * 60}")
for i, rec in enumerate(recs, 1):
print(f" {i}. {rec}")
print(f"\n{'=' * 60}")
print(" This report is informational only — not financial advice.")
print(f"{'=' * 60}\n")
# ── Demo Mode ──────────────────────────────────────────────────────
def generate_demo_trades(n: int = 50) -> dict:
"""Generate synthetic trade journal data for demo purposes.
Creates realistic-looking trades across multiple strategies,
tokens, and time periods with intentional behavioral patterns.
Args:
n: Number of trades to generate.
Returns:
Journal dictionary with synthetic trades.
"""
random.seed(42) # Reproducible output
strategies = [
("momentum-breakout", 0.60),
("pullback-entry", 0.55),
("oversold-bounce", 0.50),
("whale-follow", 0.55),
("trend-continuation", 0.58),
("volume-spike", 0.45),
("gut-feel", 0.30),
("fomo", 0.25),
]
tokens = ["SOL", "BONK", "JUP", "WIF", "RAY", "ORCA", "PYTH"]
emotions = ["calm", "calm", "calm", "excited", "anxious", "frustrated", "fomo", "revenge"]
journal: dict = {
"journal_version": "1.0",
"trader_id": "demo",
"created": "2025-02-01T00:00:00+00:00",
"updated": "2025-03-10T00:00:00+00:00",
"trades": [],
}
base_date = datetime(2025, 2, 1, 9, 0, 0, tzinfo=timezone.utc)
prev_outcome: Optional[str] = None
prev_exit_dt: Optional[datetime] = None
for i in range(n):
# Pick strategy — bias toward gut-feel/fomo after losses
if prev_outcome == "loss" and random.random() < 0.25:
strat_name = random.choice(["gut-feel", "fomo", "revenge"])
strat_wr = dict(strategies).get(strat_name, 0.3)
else:
strat_name, strat_wr = random.choice(strategies)
token = random.choice(tokens)
direction = "long"
# Entry timing — sometimes revenge-quick after loss
if prev_outcome == "loss" and prev_exit_dt and random.random() < 0.2:
entry_dt = prev_exit_dt + timedelta(minutes=random.randint(3, 12))
else:
entry_dt = base_date + timedelta(
days=i // 3,
hours=random.randint(0, 8),
minutes=random.randint(0, 59),
)
# Price and size
if token == "SOL":
entry_price = round(random.uniform(130, 160), 2)
elif token == "BONK":
entry_price = round(random.uniform(0.000015, 0.000030), 8)
elif token == "JUP":
entry_price = round(random.uniform(0.70, 1.10), 4)
elif token == "WIF":
entry_price = round(random.uniform(1.50, 3.00), 4)
else:
entry_price = round(random.uniform(2.0, 8.0), 4)
base_size = round(random.uniform(2, 10), 1)
# Tilt: increase size after loss
if prev_outcome == "loss" and random.random() < 0.2:
base_size = round(base_size * random.uniform(1.6, 2.5), 1)
# Outcome based on strategy win rate
is_win = random.random() < strat_wr
outcome = "win" if is_win else "loss"
# P&L
if is_win:
pnl_pct = round(random.uniform(0.5, 8.0), 2)
else:
pnl_pct = round(-random.uniform(0.3, 5.0), 2)
if direction == "long":
exit_price = round(entry_price * (1 + pnl_pct / 100), 8)
else:
exit_price = round(entry_price * (1 - pnl_pct / 100), 8)
pnl_sol = round(base_size * pnl_pct / 100, 6)
hold_minutes = random.randint(10, 480)
# Winners held shorter if cutting (intentional pattern)
if is_win and random.random() < 0.4:
hold_minutes = random.randint(10, 60)
exit_dt = entry_dt + timedelta(minutes=hold_minutes)
quality = random.randint(1, 10)
if strat_name in ("gut-feel", "fomo"):
quality = random.randint(1, 4)
elif strat_name in ("momentum-breakout", "pullback-entry"):
quality = random.randint(5, 10)
emotion = "calm"
if strat_name == "fomo":
emotion = "fomo"
elif strat_name == "revenge":
emotion = "revenge"
elif prev_outcome == "loss":
emotion = random.choice(["calm", "frustrated", "anxious"])
else:
emotion = random.choice(["calm", "calm", "excited"])
trade: dict = {
"id": f"T-{entry_dt.strftime('%Y%m%d')}-{(i % 10) + 1:03d}",
"token": token,
"direction": direction,
"entry_date": entry_dt.isoformat(),
"entry_price": entry_price,
"size_sol": base_size,
"strategy": strat_name,
"rationale": f"Demo trade {i + 1} — {strat_name} setup on {token}",
"exit_date": exit_dt.isoformat(),
"exit_price": exit_price,
"pnl_sol": pnl_sol,
"pnl_pct": pnl_pct,
"outcome": outcome,
"hold_time_minutes": hold_minutes,
"setup_quality": quality,
"emotional_state": emotion,
"lessons": None,
}
journal["trades"].append(trade)
prev_outcome = outcome
prev_exit_dt = exit_dt
return journal
def run_demo() -> None:
"""Run full analysis on 50 synthetic demo trades."""
print("=" * 60)
print(" Journal Analyzer — Demo Mode (50 synthetic trades)")
print("=" * 60)
journal = generate_demo_trades(50)
trades = get_closed_trades(journal)
run_analysis(trades)
# ── Analysis Runner ─────────────────────────────────────────────────
def run_analysis(trades: list[dict], strategy_filter: Optional[str] = None) -> None:
"""Run full analysis pipeline and print report.
Args:
trades: List of closed trade records.
strategy_filter: Optional strategy to filter by.
"""
if strategy_filter:
trades = [t for t in trades if t.get("strategy") == strategy_filter]
if not trades:
print("No trades to analyze.")
return
strat_perf = performance_by_strategy(trades)
day_perf = performance_by_day_of_week(trades)
hold_perf = performance_by_hold_time(trades)
revenge = detect_revenge_trades(trades)
tilt = detect_tilt_trades(trades)
streaks = detect_loss_streaks(trades)
hold_comp = winner_loser_hold_comparison(trades)
quality = setup_quality_analysis(trades)
recs = generate_recommendations(trades, revenge, tilt, hold_comp, quality, strat_perf)
print_report(
trades, strat_perf, day_perf, hold_perf,
revenge, tilt, streaks, hold_comp, quality, recs,
)
# ── CLI ────────────────────────────────────────────────────────────
def main() -> None:
"""Entry point for the journal analyzer CLI."""
parser = argparse.ArgumentParser(
description="Trade Journal Analyzer — behavioral patterns and performance analysis",
)
parser.add_argument("--demo", action="store_true", help="Run demo with synthetic trades")
parser.add_argument("--journal", default=JOURNAL_PATH, help="Path to journal file")
parser.add_argument("--strategy", help="Filter analysis to a single strategy")
args = parser.parse_args()
if args.demo:
run_demo()
return
try:
journal = load_journal(args.journal)
except FileNotFoundError as e:
print(f"Error: {e}")
print("Use --demo to run with synthetic data, or specify --journal path.")
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in journal file — {e}")
sys.exit(1)
trades = get_closed_trades(journal)
run_analysis(trades, strategy_filter=args.strategy)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Trade journal logger — add, list, update, and analyze trades.
A command-line trade journal that stores records in JSON format.
Supports adding trades, listing with filters, updating fields,
computing running statistics, and a demo mode with example data.
Usage:
python scripts/trade_logger.py --demo
python scripts/trade_logger.py add --token SOL --direction long --entry-price 142.5 --size 5.0 --strategy momentum-breakout --rationale "Breaking 4h resistance"
python scripts/trade_logger.py list --strategy momentum-breakout --outcome win
python scripts/trade_logger.py update T-20250310-001 --exit-price 146.2 --outcome win --lessons "Patience paid off"
python scripts/trade_logger.py stats
python scripts/trade_logger.py stats --strategy momentum-breakout
Dependencies:
None — uses Python standard library only (json, datetime, argparse)
Environment Variables:
TRADE_JOURNAL_PATH: Path to journal JSON file (default: trade_journal.json)
"""
import argparse
import json
import os
import sys
from datetime import datetime, timezone
from typing import Optional
# ── Configuration ───────────────────────────────────────────────────
JOURNAL_PATH = os.getenv("TRADE_JOURNAL_PATH", "trade_journal.json")
VALID_DIRECTIONS = {"long", "short"}
VALID_OUTCOMES = {"win", "loss", "breakeven"}
VALID_EMOTIONS = {"calm", "anxious", "excited", "frustrated", "fomo", "revenge"}
JOURNAL_TEMPLATE: dict = {
"journal_version": "1.0",
"trader_id": "anon",
"created": "",
"updated": "",
"trades": [],
}
# ── Journal I/O ─────────────────────────────────────────────────────
def load_journal(path: str) -> dict:
"""Load journal from JSON file, creating a new one if it doesn't exist.
Args:
path: Path to the journal JSON file.
Returns:
Parsed journal dictionary.
"""
if os.path.exists(path):
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
now = datetime.now(timezone.utc).isoformat()
journal = {**JOURNAL_TEMPLATE, "created": now, "updated": now}
return journal
def save_journal(journal: dict, path: str) -> None:
"""Save journal to JSON file.
Args:
journal: Journal dictionary to save.
path: Output file path.
"""
journal["updated"] = datetime.now(timezone.utc).isoformat()
with open(path, "w", encoding="utf-8") as f:
json.dump(journal, f, indent=2, ensure_ascii=False)
print(f"Journal saved to {path}")
# ── Trade ID Generation ────────────────────────────────────────────
def generate_trade_id(journal: dict) -> str:
"""Generate the next sequential trade ID for today.
Args:
journal: Current journal dictionary.
Returns:
Trade ID in format T-YYYYMMDD-NNN.
"""
today = datetime.now(timezone.utc).strftime("%Y%m%d")
prefix = f"T-{today}-"
existing = [
t["id"] for t in journal["trades"]
if t["id"].startswith(prefix)
]
next_num = len(existing) + 1
return f"{prefix}{next_num:03d}"
# ── Trade Operations ───────────────────────────────────────────────
def add_trade(
journal: dict,
token: str,
direction: str,
entry_price: float,
size_sol: float,
strategy: str,
rationale: str,
setup_quality: Optional[int] = None,
emotional_state: Optional[str] = None,
stop_price: Optional[float] = None,
target_price: Optional[float] = None,
tags: Optional[list[str]] = None,
) -> dict:
"""Add a new trade to the journal.
Args:
journal: Journal dictionary.
token: Token symbol (e.g., SOL, BONK).
direction: Trade direction — long or short.
entry_price: Entry price.
size_sol: Position size in SOL.
strategy: Strategy tag from taxonomy.
rationale: Written reason for the trade.
setup_quality: Self-rated 1-10 quality score.
emotional_state: Emotional state at entry.
stop_price: Planned stop-loss price.
target_price: Planned take-profit price.
tags: Freeform tags for filtering.
Returns:
The created trade record.
Raises:
ValueError: If direction or emotional_state is invalid.
"""
if direction not in VALID_DIRECTIONS:
raise ValueError(f"direction must be one of {VALID_DIRECTIONS}")
if emotional_state and emotional_state not in VALID_EMOTIONS:
raise ValueError(f"emotional_state must be one of {VALID_EMOTIONS}")
if setup_quality is not None and not (1 <= setup_quality <= 10):
raise ValueError("setup_quality must be between 1 and 10")
if entry_price <= 0:
raise ValueError("entry_price must be positive")
if size_sol <= 0:
raise ValueError("size_sol must be positive")
trade: dict = {
"id": generate_trade_id(journal),
"token": token.upper(),
"direction": direction,
"entry_date": datetime.now(timezone.utc).isoformat(),
"entry_price": entry_price,
"size_sol": size_sol,
"strategy": strategy,
"rationale": rationale,
"exit_date": None,
"exit_price": None,
"pnl_sol": None,
"pnl_pct": None,
"outcome": None,
"hold_time_minutes": None,
"lessons": None,
}
if setup_quality is not None:
trade["setup_quality"] = setup_quality
if emotional_state:
trade["emotional_state"] = emotional_state
if stop_price is not None:
trade["stop_price"] = stop_price
if target_price is not None:
trade["target_price"] = target_price
if tags:
trade["tags"] = tags
journal["trades"].append(trade)
print(f"Added trade {trade['id']}: {direction} {size_sol} SOL of {token} at {entry_price}")
return trade
def update_trade(
journal: dict,
trade_id: str,
exit_price: Optional[float] = None,
outcome: Optional[str] = None,
lessons: Optional[str] = None,
emotional_state: Optional[str] = None,
) -> Optional[dict]:
"""Update an existing trade with exit information.
Args:
journal: Journal dictionary.
trade_id: ID of the trade to update.
exit_price: Exit price.
outcome: Trade outcome — win, loss, or breakeven.
lessons: Post-trade reflection.
emotional_state: Emotional state (can be updated post-trade).
Returns:
Updated trade record, or None if not found.
"""
trade = next((t for t in journal["trades"] if t["id"] == trade_id), None)
if trade is None:
print(f"Trade {trade_id} not found")
return None
if exit_price is not None:
trade["exit_price"] = exit_price
trade["exit_date"] = datetime.now(timezone.utc).isoformat()
# Compute P&L
if trade["direction"] == "long":
pnl_pct = (exit_price - trade["entry_price"]) / trade["entry_price"] * 100
else:
pnl_pct = (trade["entry_price"] - exit_price) / trade["entry_price"] * 100
trade["pnl_pct"] = round(pnl_pct, 4)
trade["pnl_sol"] = round(trade["size_sol"] * pnl_pct / 100, 6)
# Compute hold time
entry_dt = datetime.fromisoformat(trade["entry_date"].replace("Z", "+00:00"))
exit_dt = datetime.fromisoformat(trade["exit_date"].replace("Z", "+00:00"))
trade["hold_time_minutes"] = int((exit_dt - entry_dt).total_seconds() / 60)
if outcome is not None:
if outcome not in VALID_OUTCOMES:
print(f"Invalid outcome '{outcome}'. Must be one of {VALID_OUTCOMES}")
return trade
trade["outcome"] = outcome
if lessons is not None:
trade["lessons"] = lessons
if emotional_state is not None:
if emotional_state not in VALID_EMOTIONS:
print(f"Invalid emotional_state. Must be one of {VALID_EMOTIONS}")
return trade
trade["emotional_state"] = emotional_state
print(f"Updated trade {trade_id}")
return trade
# ── Filtering ──────────────────────────────────────────────────────
def filter_trades(
trades: list[dict],
strategy: Optional[str] = None,
outcome: Optional[str] = None,
token: Optional[str] = None,
date_from: Optional[str] = None,
date_to: Optional[str] = None,
) -> list[dict]:
"""Filter trades by various criteria.
Args:
trades: List of trade records.
strategy: Filter by strategy tag.
outcome: Filter by outcome.
token: Filter by token symbol.
date_from: ISO date string for start of range.
date_to: ISO date string for end of range.
Returns:
Filtered list of trades.
"""
result = trades
if strategy:
result = [t for t in result if t.get("strategy") == strategy]
if outcome:
result = [t for t in result if t.get("outcome") == outcome]
if token:
result = [t for t in result if t.get("token", "").upper() == token.upper()]
if date_from:
result = [t for t in result if t.get("entry_date", "") >= date_from]
if date_to:
result = [t for t in result if t.get("entry_date", "") <= date_to]
return result
# ── Statistics ─────────────────────────────────────────────────────
def compute_stats(trades: list[dict]) -> dict:
"""Compute trading statistics from a list of completed trades.
Args:
trades: List of trade records (only closed trades are analyzed).
Returns:
Dictionary of computed statistics.
"""
closed = [t for t in trades if t.get("outcome") is not None]
if not closed:
return {"status": "no_closed_trades"}
wins = [t for t in closed if t["outcome"] == "win"]
losses = [t for t in closed if t["outcome"] == "loss"]
breakevens = [t for t in closed if t["outcome"] == "breakeven"]
total = len(closed)
win_count = len(wins)
loss_count = len(losses)
# P&L stats
pnl_values = [t.get("pnl_sol", 0) for t in closed if t.get("pnl_sol") is not None]
total_pnl = sum(pnl_values)
gross_wins = sum(t.get("pnl_sol", 0) for t in wins if t.get("pnl_sol") is not None)
gross_losses = sum(abs(t.get("pnl_sol", 0)) for t in losses if t.get("pnl_sol") is not None)
profit_factor = gross_wins / gross_losses if gross_losses > 0 else float("inf")
avg_win = gross_wins / win_count if win_count > 0 else 0
avg_loss = gross_losses / loss_count if loss_count > 0 else 0
# Win rate
win_rate = win_count / total if total > 0 else 0
# Expectancy
expectancy = (win_rate * avg_win) - ((1 - win_rate) * avg_loss)
# Hold time
hold_times = [t["hold_time_minutes"] for t in closed if t.get("hold_time_minutes") is not None]
avg_hold = sum(hold_times) / len(hold_times) if hold_times else 0
# Consecutive losses
max_consec_losses = 0
current_streak = 0
for t in closed:
if t["outcome"] == "loss":
current_streak += 1
max_consec_losses = max(max_consec_losses, current_streak)
else:
current_streak = 0
return {
"total_trades": total,
"wins": win_count,
"losses": loss_count,
"breakevens": len(breakevens),
"win_rate": round(win_rate, 4),
"total_pnl_sol": round(total_pnl, 6),
"gross_wins_sol": round(gross_wins, 6),
"gross_losses_sol": round(gross_losses, 6),
"profit_factor": round(profit_factor, 4),
"avg_win_sol": round(avg_win, 6),
"avg_loss_sol": round(avg_loss, 6),
"expectancy_sol": round(expectancy, 6),
"avg_hold_minutes": round(avg_hold, 1),
"max_consecutive_losses": max_consec_losses,
}
def print_stats(stats: dict, label: str = "Overall") -> None:
"""Print statistics in a formatted table.
Args:
stats: Statistics dictionary from compute_stats.
label: Label for the statistics section.
"""
if stats.get("status") == "no_closed_trades":
print(f"\n{label}: No closed trades to analyze.")
return
print(f"\n{'=' * 50}")
print(f" {label} Statistics")
print(f"{'=' * 50}")
print(f" Total Trades: {stats['total_trades']}")
print(f" Wins / Losses / BE: {stats['wins']} / {stats['losses']} / {stats['breakevens']}")
print(f" Win Rate: {stats['win_rate']:.1%}")
print(f" Total P&L: {stats['total_pnl_sol']:+.4f} SOL")
print(f" Profit Factor: {stats['profit_factor']:.2f}")
print(f" Avg Win: {stats['avg_win_sol']:+.4f} SOL")
print(f" Avg Loss: {stats['avg_loss_sol']:.4f} SOL")
print(f" Expectancy: {stats['expectancy_sol']:+.4f} SOL")
print(f" Avg Hold Time: {stats['avg_hold_minutes']:.0f} min")
print(f" Max Consec Losses: {stats['max_consecutive_losses']}")
print(f"{'=' * 50}")
# ── Display ────────────────────────────────────────────────────────
def print_trades(trades: list[dict]) -> None:
"""Print a formatted list of trades.
Args:
trades: List of trade records to display.
"""
if not trades:
print("No trades found.")
return
print(f"\n{'ID':<20} {'Token':<8} {'Dir':<6} {'Entry':>10} {'Exit':>10} {'P&L SOL':>10} {'Outcome':<10} {'Strategy'}")
print("-" * 100)
for t in trades:
exit_p = f"{t['exit_price']:.4f}" if t.get("exit_price") else "—"
pnl = f"{t['pnl_sol']:+.4f}" if t.get("pnl_sol") is not None else "—"
outcome = t.get("outcome", "open")
print(
f"{t['id']:<20} {t['token']:<8} {t['direction']:<6} "
f"{t['entry_price']:>10.4f} {exit_p:>10} {pnl:>10} "
f"{outcome:<10} {t['strategy']}"
)
print(f"\nTotal: {len(trades)} trades")
# ── Demo Mode ──────────────────────────────────────────────────────
def run_demo() -> None:
"""Run demo mode: create 10 example trades and show statistics.
Generates synthetic trades across multiple strategies and tokens
to demonstrate the journal's capabilities.
"""
print("=" * 50)
print(" Trade Journal — Demo Mode")
print("=" * 50)
journal: dict = {
"journal_version": "1.0",
"trader_id": "demo",
"created": "2025-03-01T00:00:00+00:00",
"updated": "2025-03-10T00:00:00+00:00",
"trades": [],
}
demo_trades: list[dict] = [
{"token": "SOL", "dir": "long", "entry": 142.5, "exit": 146.2, "size": 5.0,
"strat": "momentum-breakout", "outcome": "win", "hold": 135,
"rationale": "4h resistance break with volume", "quality": 8, "emotion": "calm"},
{"token": "BONK", "dir": "long", "entry": 0.000023, "exit": 0.000021, "size": 10.0,
"strat": "volume-spike", "outcome": "loss", "hold": 45,
"rationale": "Volume spike 3x avg on 5m", "quality": 6, "emotion": "excited"},
{"token": "JUP", "dir": "long", "entry": 0.85, "exit": 0.92, "size": 8.0,
"strat": "pullback-entry", "outcome": "win", "hold": 240,
"rationale": "Pullback to 20 EMA in uptrend", "quality": 9, "emotion": "calm"},
{"token": "SOL", "dir": "long", "entry": 145.0, "exit": 143.5, "size": 7.0,
"strat": "momentum-breakout", "outcome": "loss", "hold": 60,
"rationale": "Breaking daily high with volume", "quality": 7, "emotion": "calm"},
{"token": "WIF", "dir": "long", "entry": 2.10, "exit": 2.35, "size": 4.0,
"strat": "whale-follow", "outcome": "win", "hold": 180,
"rationale": "Large wallet accumulated 500K tokens", "quality": 7, "emotion": "calm"},
{"token": "BONK", "dir": "long", "entry": 0.000020, "exit": 0.000019, "size": 15.0,
"strat": "gut-feel", "outcome": "loss", "hold": 20,
"rationale": "Felt like it would bounce", "quality": 3, "emotion": "revenge"},
{"token": "SOL", "dir": "long", "entry": 140.0, "exit": 144.0, "size": 6.0,
"strat": "oversold-bounce", "outcome": "win", "hold": 300,
"rationale": "RSI 25 on 4h, at strong support", "quality": 8, "emotion": "calm"},
{"token": "RAY", "dir": "long", "entry": 4.50, "exit": 4.80, "size": 5.0,
"strat": "pullback-entry", "outcome": "win", "hold": 120,
"rationale": "Pullback to VWAP in uptrend", "quality": 8, "emotion": "calm"},
{"token": "JUP", "dir": "long", "entry": 0.90, "exit": 0.87, "size": 12.0,
"strat": "fomo", "outcome": "loss", "hold": 30,
"rationale": "Already up 15% but entering anyway", "quality": 2, "emotion": "fomo"},
{"token": "SOL", "dir": "long", "entry": 143.0, "exit": 147.5, "size": 5.0,
"strat": "trend-continuation", "outcome": "win", "hold": 360,
"rationale": "Higher low confirmed, trend intact", "quality": 9, "emotion": "calm"},
]
base_date = datetime(2025, 3, 1, 10, 0, 0, tzinfo=timezone.utc)
for i, dt in enumerate(demo_trades):
entry_dt = base_date.replace(day=1 + i, hour=10 + (i % 6))
exit_dt = entry_dt.replace(
hour=entry_dt.hour + dt["hold"] // 60,
minute=dt["hold"] % 60,
)
if dt["dir"] == "long":
pnl_pct = (dt["exit"] - dt["entry"]) / dt["entry"] * 100
else:
pnl_pct = (dt["entry"] - dt["exit"]) / dt["entry"] * 100
trade: dict = {
"id": f"T-2025030{1 + i}-001",
"token": dt["token"],
"direction": dt["dir"],
"entry_date": entry_dt.isoformat(),
"entry_price": dt["entry"],
"size_sol": dt["size"],
"strategy": dt["strat"],
"rationale": dt["rationale"],
"exit_date": exit_dt.isoformat(),
"exit_price": dt["exit"],
"pnl_sol": round(dt["size"] * pnl_pct / 100, 6),
"pnl_pct": round(pnl_pct, 4),
"outcome": dt["outcome"],
"hold_time_minutes": dt["hold"],
"setup_quality": dt["quality"],
"emotional_state": dt["emotion"],
"lessons": None,
}
journal["trades"].append(trade)
print(f"\nGenerated {len(journal['trades'])} demo trades.\n")
# Show all trades
print_trades(journal["trades"])
# Overall stats
stats = compute_stats(journal["trades"])
print_stats(stats, "Overall")
# Stats by strategy
strategies = {t["strategy"] for t in journal["trades"]}
for strat in sorted(strategies):
strat_trades = [t for t in journal["trades"] if t["strategy"] == strat]
strat_stats = compute_stats(strat_trades)
print_stats(strat_stats, f"Strategy: {strat}")
# Flag behavioral issues
print(f"\n{'=' * 50}")
print(" Behavioral Flags")
print(f"{'=' * 50}")
revenge_trades = [
t for t in journal["trades"]
if t.get("emotional_state") == "revenge"
]
fomo_trades = [
t for t in journal["trades"]
if t.get("emotional_state") == "fomo"
]
low_quality = [
t for t in journal["trades"]
if t.get("setup_quality") is not None and t["setup_quality"] <= 4
]
print(f" Revenge trades: {len(revenge_trades)}")
print(f" FOMO trades: {len(fomo_trades)}")
print(f" Low quality (<= 4): {len(low_quality)}")
if revenge_trades or fomo_trades:
emotional_pnl = sum(
t.get("pnl_sol", 0) for t in revenge_trades + fomo_trades
if t.get("pnl_sol") is not None
)
print(f" Emotional trade P&L: {emotional_pnl:+.4f} SOL")
print(f"{'=' * 50}")
print("\nDemo complete. No files were written.")
# ── CLI ────────────────────────────────────────────────────────────
def build_parser() -> argparse.ArgumentParser:
"""Build the argument parser for the trade logger CLI.
Returns:
Configured ArgumentParser.
"""
parser = argparse.ArgumentParser(
description="Trade Journal Logger — log, list, update, and analyze trades",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("--demo", action="store_true", help="Run demo with example trades")
parser.add_argument("--journal", default=JOURNAL_PATH, help="Path to journal file")
sub = parser.add_subparsers(dest="command")
# Add trade
add_p = sub.add_parser("add", help="Add a new trade")
add_p.add_argument("--token", required=True, help="Token symbol")
add_p.add_argument("--direction", required=True, choices=["long", "short"])
add_p.add_argument("--entry-price", required=True, type=float)
add_p.add_argument("--size", required=True, type=float, help="Size in SOL")
add_p.add_argument("--strategy", required=True, help="Strategy tag")
add_p.add_argument("--rationale", required=True, help="Trade rationale")
add_p.add_argument("--setup-quality", type=int, help="Setup quality 1-10")
add_p.add_argument("--emotion", help="Emotional state")
add_p.add_argument("--stop", type=float, help="Stop-loss price")
add_p.add_argument("--target", type=float, help="Take-profit price")
add_p.add_argument("--tags", nargs="+", help="Freeform tags")
# List trades
list_p = sub.add_parser("list", help="List trades with optional filters")
list_p.add_argument("--strategy", help="Filter by strategy")
list_p.add_argument("--outcome", choices=["win", "loss", "breakeven"])
list_p.add_argument("--token", help="Filter by token")
list_p.add_argument("--from", dest="date_from", help="Start date (ISO)")
list_p.add_argument("--to", dest="date_to", help="End date (ISO)")
# Update trade
upd_p = sub.add_parser("update", help="Update a trade")
upd_p.add_argument("trade_id", help="Trade ID to update")
upd_p.add_argument("--exit-price", type=float)
upd_p.add_argument("--outcome", choices=["win", "loss", "breakeven"])
upd_p.add_argument("--lessons", help="Post-trade lessons")
upd_p.add_argument("--emotion", help="Emotional state")
# Stats
stats_p = sub.add_parser("stats", help="Compute trading statistics")
stats_p.add_argument("--strategy", help="Filter by strategy")
stats_p.add_argument("--token", help="Filter by token")
return parser
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Entry point for the trade logger CLI."""
parser = build_parser()
args = parser.parse_args()
if args.demo:
run_demo()
return
if not args.command:
parser.print_help()
sys.exit(0)
journal = load_journal(args.journal)
if args.command == "add":
add_trade(
journal,
token=args.token,
direction=args.direction,
entry_price=args.entry_price,
size_sol=args.size,
strategy=args.strategy,
rationale=args.rationale,
setup_quality=args.setup_quality,
emotional_state=args.emotion,
stop_price=args.stop,
target_price=args.target,
tags=args.tags,
)
save_journal(journal, args.journal)
elif args.command == "list":
filtered = filter_trades(
journal["trades"],
strategy=args.strategy,
outcome=args.outcome,
token=args.token,
date_from=args.date_from,
date_to=args.date_to,
)
print_trades(filtered)
elif args.command == "update":
update_trade(
journal,
trade_id=args.trade_id,
exit_price=args.exit_price,
outcome=args.outcome,
lessons=args.lessons,
emotional_state=args.emotion,
)
save_journal(journal, args.journal)
elif args.command == "stats":
trades = journal["trades"]
if args.strategy:
trades = [t for t in trades if t.get("strategy") == args.strategy]
if args.token:
trades = [t for t in trades if t.get("token", "").upper() == args.token.upper()]
stats = compute_stats(trades)
label = "Overall"
if args.strategy:
label = f"Strategy: {args.strategy}"
if args.token:
label = f"Token: {args.token}"
print_stats(stats, label)
if __name__ == "__main__":
main()
Related skills
FAQ
What does a trade record capture?
Context at entry and outcome at exit across an 18-field schema, including strategy tag, rationale, P&L, and lessons.
How does it help improve trading?
It attributes returns to specific strategies and detects behaviors like revenge trading, FOMO entries, and premature exits.