// Chart Growth Hub — Student Trading Journal (premium).
//
// Table: journal_entries (+ lot_size, stop_loss, take_profit, strategy_tag,
// session_tag, emotion, followed_plan, signal_id). RLS: a student reads/writes
// only their own rows; staff have a SELECT-only policy so a coach can review
// but can never edit or delete a student's record of their own trades.
//
// The "Private note" lives in a separate table (journal_private) with no staff
// policy at all. RLS cannot be scoped to one column, so the only way to keep a
// note out of a staff `select *` is to keep it out of the row entirely.
//
// Logging is open to every student. VIP gates the pro analytics block and the
// branded PDF export — that gating is presentational only, enforced here.
//
// Exposes window.JournalPanel({client, session, isVIP, onNeedVip, member,
// prefill, onPrefillUsed}).
(function(){
const { useState, useEffect, useMemo, useRef } = React;
/* ── constants ───────────────────────────────────────────────────────────── */
const STRATS = [
{v:'A', label:'Strategy A — EMA Trend'},
{v:'B', label:'Strategy B — ADX Momentum'},
{v:'C', label:'Strategy C — StochRSI Reversal'},
{v:'D', label:'Strategy D — SMI Supply & Demand'},
{v:'manual', label:'Manual / my own setup'},
];
const STRAT_SHORT = {A:'A',B:'B',C:'C',D:'D',manual:'Manual'};
// Session windows in PH time (UTC+8) — the same three the landing page and the
// Session Timings tab publish. Used to auto-suggest a session tag.
const SESSIONS = [
{v:'asian', label:'Asian (7:00 AM PH)', from:7*60, to:11*60},
{v:'london', label:'London (4:00 PM PH)', from:16*60, to:20*60},
{v:'ny', label:'New York (9:30 PM PH)', from:21*60+30,to:25*60+30},
{v:'other', label:'Outside a main session',from:-1, to:-1},
];
const SESSION_SHORT = {asian:'Asian',london:'London',ny:'New York',other:'Other'};
const EMOTIONS = [
{v:'calm', label:'Calm & patient'},
{v:'confident', label:'Confident'},
{v:'fomo', label:'FOMO — chased it'},
{v:'fear', label:'Fearful / hesitant'},
{v:'greed', label:'Greedy — oversized'},
{v:'revenge', label:'Revenge trading'},
{v:'tired', label:'Tired / unfocused'},
];
const EMOTION_SHORT = {calm:'Calm',confident:'Confident',fomo:'FOMO',fear:'Fear',greed:'Greed',revenge:'Revenge',tired:'Tired'};
// Which moods count as a red flag in the discipline read-out.
const BAD_MOOD = {fomo:1, greed:1, revenge:1, tired:1};
const RANGES = [
{v:'all', label:'All time', days:0},
{v:'7', label:'Last 7 days', days:7},
{v:'30', label:'Last 30 days', days:30},
{v:'90', label:'Last 90 days', days:90},
];
/* ── helpers ─────────────────────────────────────────────────────────────── */
const num = v => (v===null || v===undefined || v==='') ? null : Number(v);
const fmtDate = d => d ? new Date(d+'T00:00:00').toLocaleDateString(undefined,{month:'short',day:'numeric',year:'numeric'}) : '—';
const money = n => (n===null||n===undefined||n==='') ? '—'
: (Number(n)>=0?'+':'−') + '$' + Math.abs(Number(n)).toFixed(2);
const money0 = n => (Number(n)>=0?'+':'−') + '$' + Math.abs(Number(n)).toFixed(2);
// Signed R multiple from price levels. Needs entry, exit and a stop, because R
// is defined against the risk actually taken, not against the P&L.
function rMultiple(r){
const e = num(r.entry_price), x = num(r.exit_price), sl = num(r.stop_loss);
if(e===null || x===null || sl===null) return null;
const risk = Math.abs(e - sl);
if(!(risk > 0)) return null; // guards 0 and NaN
const dir = r.direction === 'short' ? -1 : 1;
return ((x - e) * dir) / risk;
}
// Today in PH time, as YYYY-MM-DD. Same trick used across the site: shift the
// epoch by +8h and then read the UTC fields, so the answer never depends on the
// device timezone. PH has no DST.
function phToday(){
const d = new Date(Date.now() + 8*3600000);
return d.toISOString().slice(0,10);
}
function phMinutesNow(){
const d = new Date(Date.now() + 8*3600000);
return d.getUTCHours()*60 + d.getUTCMinutes();
}
function guessSession(){
const m = phMinutesNow();
for(const s of SESSIONS){
if(s.from < 0) continue;
// NY spills past midnight, so compare against the wrapped minute too.
if((m >= s.from && m < s.to) || (m + 1440 >= s.from && m + 1440 < s.to)) return s.v;
}
return 'other';
}
function daysAgoISO(n){
const d = new Date(Date.now() + 8*3600000 - n*86400000);
return d.toISOString().slice(0,10);
}
function blankForm(){
return { trade_date: phToday(), pair:'XAU/USD', direction:'long', lot_size:'',
entry_price:'', exit_price:'', stop_loss:'', take_profit:'', pnl:'',
strategy_tag:'', session_tag: guessSession(), emotion:'',
followed_plan:null, notes:'', private_note:'', signal_id:null };
}
/* ── one-time stylesheet ─────────────────────────────────────────────────── */
// Injected once rather than inlined per element, because the responsive
// table/card swap and the print layout both need real media queries, which
// inline styles cannot express.
(function injectCSS(){
if(document.getElementById('cg-journal-css')) return;
const s = document.createElement('style');
s.id = 'cg-journal-css';
s.textContent = `
.jr-actions{display:flex;gap:9px;align-items:center;flex-wrap:wrap}
.jr-btn-sm{cursor:pointer;padding:8px 13px;border-radius:9px;border:1px solid var(--border);
background:var(--surface);color:var(--ink-dim);font-size:.78rem;font-weight:600;
display:inline-flex;align-items:center;gap:7px;transition:border-color .18s,color .18s}
.jr-btn-sm:hover{border-color:var(--border-gold);color:var(--gold-2)}
.jr-btn-sm svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.8;
stroke-linecap:round;stroke-linejoin:round}
.jr-menu{position:absolute;right:0;top:calc(100% + 6px);z-index:40;min-width:230px;
background:var(--navy-950);border:1px solid var(--border-gold);border-radius:11px;
box-shadow:0 18px 44px rgba(0,0,0,.55);overflow:hidden}
.jr-menu button{display:block;width:100%;text-align:left;padding:11px 14px;background:none;
border:0;color:var(--ink-dim);font-size:.8rem;cursor:pointer;font-family:inherit}
.jr-menu button:hover:not(:disabled){background:rgba(212,175,55,.09);color:var(--gold-2)}
.jr-menu button:disabled{opacity:.45;cursor:not-allowed}
.jr-menu .jr-menu-sub{display:block;font-size:.7rem;color:var(--ink-faint);margin-top:2px}
.jr-filters{display:flex;gap:9px;flex-wrap:wrap;align-items:center;margin-bottom:18px}
.jr-filters select{background:var(--surface);border:1px solid var(--border);color:var(--ink-dim);
border-radius:9px;padding:8px 11px;font-size:.78rem;font-family:inherit;cursor:pointer}
.jr-filters select:focus{outline:none;border-color:var(--border-gold)}
.jr-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:18px}
.jr-stat{background:linear-gradient(165deg,var(--surface-2),var(--surface));
border:1px solid var(--border);border-radius:var(--radius);padding:15px 17px}
.jr-stat .v{font-family:var(--font-mono);font-size:1.34rem;font-weight:600;color:var(--gold-2);line-height:1.15}
.jr-stat .l{font-size:.75rem;color:var(--ink-dim);margin-top:3px}
@media(max-width:820px){ .jr-stats{grid-template-columns:repeat(2,1fr)} }
.jr-pro{border:1px solid var(--border-gold);border-radius:var(--radius);
background:linear-gradient(165deg,rgba(212,175,55,.05),transparent 60%),var(--navy-950);
padding:20px 22px;margin-bottom:20px}
.jr-metrics{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-top:18px}
.jr-metric{padding:11px 13px;border:1px solid var(--border);border-radius:10px;background:var(--surface)}
.jr-metric .v{font-family:var(--font-mono);font-size:.98rem;font-weight:600}
.jr-metric .l{font-size:.68rem;color:var(--ink-faint);margin-top:2px;text-transform:uppercase;letter-spacing:.04em}
@media(max-width:820px){ .jr-metrics{grid-template-columns:repeat(2,1fr)} }
.jr-split{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:18px}
@media(max-width:820px){ .jr-split{grid-template-columns:1fr} }
.jr-bd{border:1px solid var(--border);border-radius:10px;overflow:hidden}
.jr-bd h4{margin:0;padding:10px 13px;font-size:.76rem;text-transform:uppercase;letter-spacing:.06em;
color:var(--ink-faint);border-bottom:1px solid var(--border);background:var(--surface)}
.jr-bd table{width:100%;border-collapse:collapse}
.jr-bd td{padding:8px 13px;font-size:.79rem;border-bottom:1px solid var(--border)}
.jr-bd tr:last-child td{border-bottom:0}
.jr-bd td.n{font-family:var(--font-mono);text-align:right;white-space:nowrap}
.jr-table{width:100%;border-collapse:separate;border-spacing:0;
border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}
.jr-table th{font-size:.68rem;text-transform:uppercase;letter-spacing:.06em;color:var(--ink-faint);
text-align:left;padding:10px 12px;background:var(--surface);border-bottom:1px solid var(--border);
cursor:pointer;white-space:nowrap;user-select:none}
.jr-table th:hover{color:var(--gold-2)}
.jr-table td{padding:11px 12px;font-size:.82rem;border-bottom:1px solid var(--border);vertical-align:top}
.jr-table tr:last-child td{border-bottom:0}
.jr-table tbody tr:hover{background:rgba(212,175,55,.035)}
.jr-table td.n{font-family:var(--font-mono);white-space:nowrap}
.jr-pill{font-family:var(--font-mono);font-size:.64rem;text-transform:uppercase;letter-spacing:.03em;
padding:2px 7px;border-radius:999px;border:1px solid var(--border);white-space:nowrap;display:inline-block}
.jr-cards{display:grid;gap:10px}
.jr-card{padding:14px 16px;border:1px solid var(--border);border-radius:var(--radius-sm);
background:linear-gradient(165deg,var(--surface-2),var(--surface))}
/* Desktop shows the dense table; phones get the card list. */
.jr-desktop{display:block}
.jr-mobile{display:none}
@media(max-width:760px){ .jr-desktop{display:none} .jr-mobile{display:block} }
/* view switch */
.jr-seg{display:inline-flex;border:1px solid var(--border);border-radius:9px;overflow:hidden}
.jr-seg button{background:none;border:0;padding:8px 14px;font-size:.78rem;font-weight:600;
color:var(--ink-faint);cursor:pointer;font-family:inherit;display:inline-flex;align-items:center;gap:6px}
.jr-seg button+button{border-left:1px solid var(--border)}
.jr-seg button.on{background:rgba(212,175,55,.12);color:var(--gold-2)}
.jr-seg svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.8;
stroke-linecap:round;stroke-linejoin:round}
/* month calendar */
.jr-cal{border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;margin-bottom:16px}
.jr-cal-head{display:flex;align-items:center;gap:8px;padding:12px 14px;background:var(--surface);
border-bottom:1px solid var(--border);flex-wrap:wrap}
.jr-cal-title{font-weight:700;font-size:.95rem;min-width:150px}
.jr-cal-sum{margin-left:auto;font-family:var(--font-mono);font-size:.82rem;font-weight:600}
.jr-cal-dow{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));background:var(--surface)}
.jr-cal-dow>div{padding:7px 6px;text-align:center;font-size:.64rem;text-transform:uppercase;
letter-spacing:.07em;color:var(--ink-faint)}
.jr-cal-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:5px;padding:8px;background:var(--navy-950)}
.jr-cal-pad{min-height:74px}
.jr-cal-day{position:relative;min-width:0;min-height:74px;border:1px solid var(--border);border-radius:9px;
padding:6px 7px;display:flex;flex-direction:column;align-items:flex-start;gap:2px;
cursor:pointer;font-family:inherit;text-align:left;transition:border-color .15s,transform .12s}
.jr-cal-day:hover{border-color:var(--border-gold);transform:translateY(-1px)}
.jr-cal-day.sel{box-shadow:0 0 0 1px var(--gold-2) inset}
.jr-cal-day:focus-visible{outline:2px solid var(--gold-2);outline-offset:1px}
.jr-cal-num{font-family:var(--font-mono);font-size:.72rem;color:var(--ink-faint)}
.jr-cal-pnl{font-family:var(--font-mono);font-size:.78rem;font-weight:700;margin-top:auto;
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
.jr-cal-pnl-s{display:none;font-family:var(--font-mono);font-size:.7rem;font-weight:700;
margin-top:auto;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
.jr-cal-n{font-size:.62rem;color:var(--ink-faint);white-space:nowrap}
.jr-cal-flag{position:absolute;top:6px;right:6px;width:6px;height:6px;border-radius:50%;
background:var(--red);box-shadow:0 0 0 2px var(--navy-950)}
@media(max-width:640px){
.jr-cal-grid{gap:3px;padding:5px}
.jr-cal-day,.jr-cal-pad{min-height:58px}
.jr-cal-day{padding:4px 5px;border-radius:7px}
.jr-cal-pnl{display:none}
.jr-cal-pnl-s{display:block}
.jr-cal-n{display:none}
.jr-cal-sum{width:100%;margin-left:0;order:9}
}
.jr-day{border:1px solid var(--border-gold);border-radius:var(--radius);padding:16px 18px;margin-bottom:16px;
background:linear-gradient(165deg,rgba(212,175,55,.05),transparent 60%),var(--navy-950)}
.jr-day-head{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:12px}
.jr-lock{display:flex;gap:16px;align-items:center;justify-content:space-between;flex-wrap:wrap;
padding:20px 22px;border:1px dashed var(--border-gold);border-radius:var(--radius);
background:var(--surface);margin-bottom:20px}
`;
document.head.appendChild(s);
})();
/* ── analytics ───────────────────────────────────────────────────────────── */
// One pass over the filtered rows. Everything downstream (cards, curve,
// breakdowns, PDF) reads from this so the numbers can never disagree.
function analyse(rows){
const withPnl = rows.filter(r => num(r.pnl) !== null);
const wins = withPnl.filter(r => Number(r.pnl) > 0);
const losses= withPnl.filter(r => Number(r.pnl) < 0);
const gross = wins.reduce((s,r)=>s+Number(r.pnl),0);
const bad = Math.abs(losses.reduce((s,r)=>s+Number(r.pnl),0));
const total = withPnl.reduce((s,r)=>s+Number(r.pnl),0);
const rs = rows.map(rMultiple).filter(v => v !== null);
const avgR = rs.length ? rs.reduce((a,b)=>a+b,0)/rs.length : null;
const planned = rows.filter(r => r.followed_plan !== null && r.followed_plan !== undefined);
const kept = planned.filter(r => r.followed_plan === true).length;
// Chronological order for the streak and the equity curve; the list itself is
// newest-first, so this must sort its own copy rather than reuse it.
const chrono = withPnl.slice().sort((a,b)=>
a.trade_date === b.trade_date
? String(a.created_at||'').localeCompare(String(b.created_at||''))
: String(a.trade_date).localeCompare(String(b.trade_date)));
let streak = 0, streakWin = null;
for(let i = chrono.length - 1; i >= 0; i--){
const w = Number(chrono[i].pnl) > 0;
if(streakWin === null){ streakWin = w; streak = 1; }
else if(w === streakWin) streak++;
else break;
}
let cum = 0;
const curve = chrono.map(r => { cum += Number(r.pnl); return { d:r.trade_date, v:cum }; });
return {
count: rows.length,
scored: withPnl.length,
winRate: withPnl.length ? Math.round(wins.length/withPnl.length*100) : null,
total,
// No losses at all means the ratio is undefined, not infinite — show a dash
// rather than an "∞" that looks like a bug.
profitFactor: bad > 0 ? gross/bad : null,
avgWin: wins.length ? gross/wins.length : null,
avgLoss: losses.length ? bad/losses.length : null,
best: withPnl.length ? Math.max(...withPnl.map(r=>Number(r.pnl))) : null,
worst: withPnl.length ? Math.min(...withPnl.map(r=>Number(r.pnl))) : null,
avgR,
rCount: rs.length,
discipline: planned.length ? Math.round(kept/planned.length*100) : null,
disciplineOf: planned.length,
streak, streakWin,
curve,
};
}
// win-rate / P&L split by an arbitrary key, used for both breakdown tables.
function bucket(rows, key, labels){
const out = {};
rows.forEach(r => {
const k = r[key] || '—';
if(!out[k]) out[k] = { k, n:0, wins:0, scored:0, pnl:0 };
const b = out[k]; b.n++;
const p = num(r.pnl);
if(p !== null){ b.scored++; b.pnl += p; if(p > 0) b.wins++; }
});
return Object.values(out)
.map(b => ({...b, label: labels[b.k] || b.k, wr: b.scored ? Math.round(b.wins/b.scored*100) : null}))
.sort((a,b) => b.pnl - a.pnl);
}
/* ── equity curve ────────────────────────────────────────────────────────── */
function EquityCurve({curve, height}){
const H = height || 168, W = 720, PAD = 6;
if(!curve.length) return
Log a trade with a P&L to see your equity curve.
;
const vals = curve.map(p=>p.v);
let lo = Math.min(0, ...vals), hi = Math.max(0, ...vals);
if(hi === lo){ hi = lo + 1; } // flat line still needs a range
const span = hi - lo;
const x = i => PAD + (curve.length === 1 ? (W-2*PAD)/2 : i*(W-2*PAD)/(curve.length-1));
const y = v => PAD + (hi - v)*(H-2*PAD)/span;
const pts = curve.map((p,i)=> x(i)+','+y(p.v)).join(' ');
const up = vals[vals.length-1] >= 0;
const col = up ? 'var(--green)' : 'var(--red)';
const zero = y(0);
return ;
}
/* ── month calendar ──────────────────────────────────────────────────────── */
// trade_date is a plain 'YYYY-MM-DD' string, never a timestamp. Every date
// calculation below is done on numbers or on UTC, never with a local-time
// Date — `new Date('2026-08-01')` is midnight UTC, which is the previous day
// for anyone west of Greenwich and would shift the whole grid by one cell.
const MONTHS = ['January','February','March','April','May','June',
'July','August','September','October','November','December'];
const DOW = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
// Phone calendar cells are ~44px wide. Drop the currency symbol and the cents,
// and fold anything past 999 into a k suffix, so the number still fits.
function shortMoney(n){
const v = Number(n), a = Math.abs(v), sign = v < 0 ? '\u2212' : '+';
if(a >= 10000) return sign + Math.round(a/1000) + 'k';
if(a >= 1000) return sign + (a/1000).toFixed(1).replace(/\.0$/,'') + 'k';
return sign + Math.round(a);
}
const pad2 = n => (n < 10 ? '0' : '') + n;
function ymd(y, m, d){ return y + '-' + pad2(m+1) + '-' + pad2(d); } // m is 0-based
function daysInMonth(y, m){ return new Date(Date.UTC(y, m+1, 0)).getUTCDate(); }
function firstDow(y, m){ return new Date(Date.UTC(y, m, 1)).getUTCDay(); } // 0 = Sunday
function addMonth(ym, delta){
let y = ym.y, m = ym.m + delta;
y += Math.floor(m/12); m = ((m % 12) + 12) % 12;
return { y, m };
}
function phMonth(){
const d = new Date(Date.now() + 8*3600000);
return { y: d.getUTCFullYear(), m: d.getUTCMonth() };
}
function monthOf(iso){ return { y: Number(iso.slice(0,4)), m: Number(iso.slice(5,7)) - 1 }; }
function inMonth(iso, ym){
return Number(iso.slice(0,4)) === ym.y && Number(iso.slice(5,7)) - 1 === ym.m;
}
function MonthCalendar({rows, ym, onPrev, onNext, onToday, selected, onSelect}){
// Roll the month's trades up per day once, rather than filtering the full
// list inside every one of the ~35 cells.
const byDay = useMemo(()=>{
const m = {};
rows.forEach(r => {
const k = r.trade_date;
if(!m[k]) m[k] = { n:0, pnl:0, scored:0, offPlan:0 };
const b = m[k]; b.n++;
const p = num(r.pnl);
if(p !== null){ b.scored++; b.pnl += p; }
if(r.followed_plan === false) b.offPlan++;
});
return m;
},[rows]);
const dim = daysInMonth(ym.y, ym.m);
const lead = firstDow(ym.y, ym.m);
const today = phToday();
// Colour intensity is relative to the biggest day of this month, so a quiet
// month is still readable instead of washing out to a single flat tone.
const peak = Math.max(1, ...Object.keys(byDay)
.filter(k => inMonth(k, ym))
.map(k => Math.abs(byDay[k].pnl)));
const cells = [];
for(let i = 0; i < lead; i++) cells.push();
for(let d = 1; d <= dim; d++){
const key = ymd(ym.y, ym.m, d);
const b = byDay[key];
const isToday = key === today;
const isSel = key === selected;
let bg = 'transparent', bd = 'var(--border)';
if(b && b.scored){
const t = Math.min(1, Math.abs(b.pnl)/peak);
const a = (0.10 + 0.30*t).toFixed(3);
bg = b.pnl > 0 ? 'rgba(74,222,128,'+a+')' : b.pnl < 0 ? 'rgba(248,113,113,'+a+')' : 'transparent';
bd = b.pnl > 0 ? 'rgba(74,222,128,.4)' : b.pnl < 0 ? 'rgba(248,113,113,.4)' : 'var(--border)';
}
cells.push(
);
}
const monthRows = rows.filter(r => inMonth(r.trade_date, ym));
const monthPnl = monthRows.reduce((s,r)=> s + (num(r.pnl)||0), 0);
return
'
+ '';
}
function printReport(html){
const old = document.getElementById('cg-journal-print');
if(old) old.remove();
const f = document.createElement('iframe');
f.id = 'cg-journal-print';
f.setAttribute('aria-hidden','true');
// Off-screen rather than display:none — a display:none iframe does not lay
// out in some browsers and prints blank.
f.style.cssText = 'position:fixed;left:-10000px;top:0;width:1024px;height:768px;border:0';
document.body.appendChild(f);
const d = f.contentDocument;
d.open(); d.write(html); d.close();
const go = () => {
try { f.contentWindow.focus(); f.contentWindow.print(); }
catch(e){ /* printing blocked — the iframe is harmless, just leave it */ }
};
// Give the logo a beat to load so it is not missing from the first page.
if(d.readyState === 'complete') setTimeout(go, 350);
else f.onload = () => setTimeout(go, 350);
}
/* ── small pieces ────────────────────────────────────────────────────────── */
function Stat({label, value, tone}){
return
{value}
{label}
;
}
function Metric({label, value, tone}){
return
{value}
{label}
;
}
function Breakdown({title, rows, empty}){
return
;
}
/* ── trade list ──────────────────────────────────────────────────────────── */
// Shared by the list view and the calendar's day panel, so a trade renders
// identically wherever it is reached from. `showSort` is off inside the day
// panel — a single day has nothing worth re-sorting.
function TradeList({rows, sort, onSort, onEdit, onDel, showSort}){
const toggleSort = k => { if(showSort && onSort) onSort(k); };
return
{/* desktop table */}
Log every trade and let the numbers show you what actually works.
Your coach and CGH HQ can review your entries to mentor you — the
private note on each trade stays yours alone.
{menu &&
}
{/* filters */}
{view==='list' && }
{((view==='list'&&range!=='all')||fStrat||fSess||fRes) &&
}
{scoped.length} of {rows.length} {rows.length===1?'entry':'entries'}
R is calculated on the {stats.rCount} of {stats.count} {stats.count===1?'trade':'trades'} that have an entry, exit and stop loss.
}
{/* discipline */}
Discipline score
{stats.discipline===null
? 'Answer “Did I follow my plan?” when you log a trade to start tracking this.'
: 'You followed your plan on '+stats.discipline+'% of your last '+stats.disciplineOf+' answered trades.'}
Your equity curve, average R, profit factor, a win-rate breakdown by strategy and session,
your discipline score, and the branded PDF statement. Keep logging — your entries are
saved and every metric fills in the moment you upgrade.