trade-journal-analytics — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited trade-journal-analytics (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Skill: Trade Journal Analytics | Domain: trading | Category: infrastructure | Level: beginner Tags:trading,infrastructure,journal,analytics,performance,review
A full-featured trade journalling and performance analysis system. Handles every step from logging individual trades to generating publication-quality HTML/Markdown reports. Built on pandas for fast aggregation and uses no external report dependencies.
xtrading/skills/trade_journal.py
TradeRecord structurefrom datetime import datetime
from xtrading.skills.trade_journal import TradeRecord
trade = TradeRecord(
trade_id="T001",
symbol="EURUSD",
direction="long",
entry_price=1.1000,
exit_price=1.1050,
quantity=1.0, # lots
entry_time=datetime(2025, 3, 10, 9, 0),
exit_time=datetime(2025, 3, 10, 11, 30),
stop_loss=1.0970,
take_profit=1.1060,
gross_pnl=500.0,
commission=3.5,
swap=0.0,
risk_amount=300.0,
r_multiple=1.65, # net_pnl / risk_amount
setup_tag="OB_pullback",
session_tag="London",
timeframe="H1",
market_condition="trending",
notes="Clean OB retest, strong momentum",
)
# Auto-computed fields:
# trade.net_pnl → 496.5 (gross - commission - swap)
# trade.outcome → "win" (net_pnl > 0)
# trade.duration_minutes → 150
# trade.day_of_week → "Mon"
# trade.hour_of_day → 9| Field | Type | Description | |||
|---|---|---|---|---|---|
trade_id | str | Unique identifier (required) | |||
symbol | str | Instrument (e.g. "EURUSD") | |||
direction | `"long"\ | "short"` | Trade direction | ||
entry_price | float | Entry fill price | |||
exit_price | float | Exit fill price | |||
quantity | float | Lots / shares / contracts | |||
gross_pnl | float | P&L before costs | |||
commission | float | Total broker commission | |||
swap | float | Overnight swap/rollover cost | |||
net_pnl | float | Auto-computed if left 0.0 | |||
risk_amount | float | Dollar risk per trade | |||
r_multiple | float | net_pnl / risk_amount | |||
mae | float | Max Adverse Excursion (price) | |||
mfe | float | Max Favorable Excursion (price) | |||
setup_tag | str | Setup label (e.g. "FVG_fill") | |||
session_tag | str | Session (e.g. "London") | |||
outcome | `"win"\ | "loss"\ | "breakeven"\ | "open"` | Auto-detected from net_pnl |
from xtrading.skills.trade_journal import TradeJournal
# Create journal (optional JSON persistence)
journal = TradeJournal(
journal_path="my_journal.json",
account_name="Prop Account",
initial_balance=50_000.0,
)
# Add trades
journal.add_trade(trade) # returns trade_id
# Update a field
journal.update_trade("T001", notes="Updated note", r_multiple=1.70)
# Retrieve
t = journal.get_trade("T001")
# Delete
journal.delete_trade("T001") # returns True if found
# Querying with filters
trades = journal.get_trades(
symbol="EURUSD",
setup_tag="OB_pullback",
session_tag="London",
from_date=date(2025, 1, 1),
to_date=date(2025, 3, 31),
direction="long",
outcome="win",
min_r=1.0, # only trades with R ≥ 1.0
)
# Convert to DataFrame for custom analysis
df = journal.to_dataframe()
# Export to CSV
journal.export_csv("journal_export.csv")
# Persistence is automatic — every add/update/delete saves to JSON
len(journal) # → 42 trades
repr(journal) # → "TradeJournal(account='Prop Account', trades=42)"The JSON file structure allows portable sharing between machines:
{
"account": "Prop Account",
"initial_balance": 50000.0,
"trades": [
{
"trade_id": "T001",
"entry_time": "2025-03-10T09:00:00",
"exit_time": "2025-03-10T11:30:00",
...
}
]
}from xtrading.skills.trade_journal import PerformanceAnalytics
analytics = PerformanceAnalytics(journal.get_trades())
# Individual metrics
wr = analytics.win_rate() # 0.623 (62.3%)
exp = analytics.expectancy() # +0.42R per trade
pf = analytics.profit_factor() # 2.15
sharpe = analytics.sharpe_ratio() # 1.34 (annualised)
sortino = analytics.sortino_ratio() # 1.89 (downside only)
sqn = analytics.sqn() # 2.87 (> 2 = good)
dur = analytics.avg_trade_duration() # 142.5 minutes
# Full report (all metrics in one call)
report = analytics.full_report()
# {
# "n_trades": 50, "n_wins": 31, "n_losses": 18,
# "win_rate": 0.62, "win_rate_pct": 62.0,
# "expectancy_r": 0.42,
# "profit_factor": 2.15,
# "avg_win_r": 1.85, "avg_loss_r": 1.12,
# "best_trade_r": 4.20, "worst_trade_r": -2.10,
# "total_net_pnl": 8420.0,
# "sharpe_ratio": 1.34, "sortino_ratio": 1.89,
# "sqn": 2.87,
# "max_drawdown_pct": 8.2,
# "calmar_ratio": 5.1,
# "avg_duration_min": 142.5,
# "total_commission": 175.0,
# }| Metric | Poor | Acceptable | Good | Excellent |
|---|---|---|---|---|
| Win Rate | < 35% | 35–45% | 45–65% | > 65% |
| Expectancy | < 0 | 0–0.2R | 0.2–0.5R | > 0.5R |
| Profit Factor | < 1.0 | 1.0–1.5 | 1.5–2.0 | > 2.0 |
| SQN | < 1.6 | 1.6–2.0 | 2.0–3.0 | > 3.0 |
| Sharpe | < 0.5 | 0.5–1.0 | 1.0–2.0 | > 2.0 |
| Max Drawdown | > 25% | 15–25% | 8–15% | < 8% |
Expectancy = WR × Avg_Win_R − (1−WR) × Avg_Loss_R
Profit Factor = Gross_Profit / Gross_Loss
SQN = (Expectancy / StdDev_R) × √(n_trades)
Sharpe = (Daily_PnL_mean − rf/252) / Daily_PnL_std × √252
Sortino = (Daily_PnL_mean − rf/252) / Downside_Std × √252
Calmar = Annualised_Return / Max_Drawdown_pctfrom xtrading.skills.trade_journal import StreakAnalyser
streaks = StreakAnalyser(journal.get_trades())
# Current active streak
current = streaks.current_streak()
# {"streak": 4, "type": "win", "message": "4-trade WIN streak"}
# Maximum streaks in history
maxima = streaks.max_streaks()
# {
# "max_win_streak": 8,
# "max_loss_streak": 5,
# "current": {"streak": 4, "type": "win", "message": "..."}
# }
# Distribution of streak lengths
dist = streaks.streak_distribution()
# {
# "win_streaks": {1: 12, 2: 8, 3: 4, 4: 2, 8: 1},
# "loss_streaks": {1: 10, 2: 5, 3: 2, 5: 1},
# "avg_win_streak": 1.9,
# "avg_loss_streak": 1.7,
# }
# Revenge trading detection (performance after losing trades)
after_loss = streaks.after_loss_performance()
# {
# "n_after_loss": 18,
# "avg_r_after_loss": -0.15,
# "revenge_trading_risk": True, # True when avg < -0.3R
# "trades_after_loss": [-1.2, 0.8, -0.9, ...]
# }Revenge Trading Alert: revenge_trading_risk=True indicates the trader is taking on poor quality trades after losses (avg R < −0.3 in the next trade).
from xtrading.skills.trade_journal import DrawdownTracker
dd = DrawdownTracker(journal.get_trades(), initial_balance=50_000.0)
# Build equity curve (pd.Series indexed by datetime)
equity = dd.equity_curve()
# 2025-01-02 50000.0
# 2025-01-03 50450.0
# ...
# Full drawdown statistics
result = dd.calculate()
# {
# "max_drawdown": 4200.0, # in dollars
# "max_drawdown_pct": 8.2, # % from peak
# "max_drawdown_duration_days": 12, # days to recovery
# "current_drawdown_pct": 1.5, # current DD from peak
# "peak_equity": 58400.0,
# "current_equity": 57524.0,
# "total_return_pct": 15.05,
# }
# Monthly P&L breakdown
monthly = dd.monthly_pnl()
# net_pnl n_trades avg_pnl
# 2025-01 1842.00 18 102.33
# 2025-02 2315.00 22 105.23
# 2025-03 763.00 10 76.30from xtrading.skills.trade_journal import TagAnalytics
tag = TagAnalytics(journal.get_trades())
# Performance by any field
by_setup = tag.by_tag("setup_tag")
# setup_tag n_trades win_rate expectancy profit_factor avg_r total_pnl
# 0 OB_pullback 18 0.722 0.845 3.12 1.25 4215.0
# 1 FVG_fill 12 0.583 0.320 1.85 0.72 2100.0
# 2 BOS_entry 8 0.500 0.125 1.42 0.55 800.0
by_session = tag.by_tag("session_tag")
by_symbol = tag.by_tag("symbol")
by_dow = tag.by_tag("day_of_week")
by_tf = tag.by_tag("timeframe")
by_market = tag.by_tag("market_condition")
# Top setups with at least N trades
top_setups = tag.best_setups(min_trades=5)
# Session performance as list of dicts
sessions = tag.session_breakdown()
# [{"session_tag": "London", "n_trades": 22, "win_rate": 0.68, ...},
# {"session_tag": "NewYork", "n_trades": 18, "win_rate": 0.61, ...}]
# Heatmap: average R by hour and day of week
heatmap = tag.time_of_day_heatmap()
# Mon Tue Wed Thu Fri
# hour
# 7 0.82 1.20 0.45 0.91 0.33
# 8 1.42 0.98 1.15 0.72 0.88
# 9 1.85 1.32 1.60 1.45 1.10
# 10 0.65 0.72 0.80 0.55 0.42| Tag Field | Purpose |
|---|---|
setup_tag | ICT model, pattern type |
session_tag | London / NewYork / Asian |
symbol | Per-instrument performance |
timeframe | H1 / H4 / D1 |
direction | Long vs Short bias |
day_of_week | Best trading days |
market_condition | Trending vs Ranging |
from xtrading.skills.trade_journal import JournalReporter
reporter = JournalReporter(journal)
# Console text summary
print(reporter.text_summary())
# ============================================================
# TRADING PERFORMANCE REPORT — Prop Account
# ============================================================
# Trades : 50 total (31W / 18L)
# Win Rate : 62.0%
# Expectancy : +0.42R per trade
# Profit Fac : 2.15
# Total P&L : $8,420.00
# Avg Win : +1.85R | Avg Loss: 1.12R
# Best Trade : +4.20R | Worst: -2.10R
# SQN : 2.87
# Sharpe : 1.34
# Max DD : 8.2%
# Current DD : 1.5%
# Max W Str : 8 | Max L Str: 5
# Current : 4-trade WIN streak
# ============================================================
# Markdown report (for Obsidian, Notion, GitHub)
md = reporter.markdown_report()
# Includes: Summary stats table + Performance by Setup table
# Self-contained HTML report (dark theme, no dependencies)
html = reporter.html_report()
with open("report.html", "w") as f:
f.write(html)
# Report on a subset of trades
london_trades = journal.get_trades(session_tag="London")
print(reporter.text_summary(london_trades))from datetime import datetime, date
from xtrading.skills.trade_journal import (
TradeRecord, TradeJournal, PerformanceAnalytics,
StreakAnalyser, DrawdownTracker, TagAnalytics, JournalReporter,
)
# 1. Create / load journal
journal = TradeJournal("journal.json", "My Prop Account", 100_000)
# 2. Log trades
for trade_data in my_trade_source:
journal.add_trade(TradeRecord(**trade_data))
# 3. Analyse all trades
analytics = PerformanceAnalytics(journal.get_trades())
report = analytics.full_report()
# 4. Check for patterns
streaks = StreakAnalyser(journal.get_trades())
if streaks.after_loss_performance().get("revenge_trading_risk"):
print("⚠️ Revenge trading detected — consider a break rule")
# 5. Drawdown check
dd = DrawdownTracker(journal.get_trades(), journal.initial_balance).calculate()
if dd["current_drawdown_pct"] > 10:
print(f"⛔ Max daily loss approaching: {dd['current_drawdown_pct']:.1f}% DD")
# 6. Find your best setups
tag = TagAnalytics(journal.get_trades())
print(tag.best_setups(min_trades=5).to_string())
# 7. Generate reports
reporter = JournalReporter(journal)
print(reporter.text_summary())
html = reporter.html_report()risk_amount for meaningful r_multiple valuesnet_pnl; explicitly set "open" for live trades"London", "NewYork", "Asian", "Overlap"journal_path is setget_trades() filters then pass subset to any analyser class~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.