PnlChart
Profit and loss per day as green and red bars, with the running total and its drawdowns shaded, and the key figures above.
Demo
- Total
- +$27,941
- Winning days
- 66% (42 of 64)
- Best day
- +$2,042
- Worst day
- -$1,900
- Max drawdown
- -$4,636
| Date | Day | Total | Drawdown |
|---|---|---|---|
| Jul 1, 2026 | -$1,900 | -$1,900 | -$1,900 |
| Jul 2, 2026 | +$940 | -$960 | -$960 |
| Jul 3, 2026 | +$1,391 | $431 | $0 |
| Jul 6, 2026 | +$1,950 | $2,381 | $0 |
| Jul 7, 2026 | +$1,804 | $4,185 | $0 |
| Jul 8, 2026 | +$1,486 | $5,671 | $0 |
| Jul 9, 2026 | +$546 | $6,217 | $0 |
| Jul 10, 2026 | +$1,401 | $7,618 | $0 |
| Jul 13, 2026 | +$1,423 | $9,041 | $0 |
| Jul 14, 2026 | -$1,296 | $7,745 | -$1,296 |
| Jul 15, 2026 | +$2,042 | $9,787 | $0 |
| Jul 16, 2026 | +$1,110 | $10,897 | $0 |
| Jul 17, 2026 | +$1,310 | $12,207 | $0 |
| Jul 20, 2026 | -$102 | $12,105 | -$102 |
| Jul 21, 2026 | +$880 | $12,985 | $0 |
| Jul 22, 2026 | +$1,644 | $14,629 | $0 |
| Jul 23, 2026 | +$1,000 | $15,629 | $0 |
| Jul 24, 2026 | -$1,500 | $14,129 | -$1,500 |
| Jul 27, 2026 | +$2,038 | $16,167 | $0 |
| Jul 28, 2026 | +$1,576 | $17,743 | $0 |
| Jul 29, 2026 | -$191 | $17,552 | -$191 |
| Jul 30, 2026 | +$1,812 | $19,364 | $0 |
| Jul 31, 2026 | +$727 | $20,091 | $0 |
| Aug 3, 2026 | -$1,517 | $18,574 | -$1,517 |
| Aug 4, 2026 | -$1,899 | $16,675 | -$3,416 |
| Aug 5, 2026 | +$1,500 | $18,175 | -$1,916 |
| Aug 6, 2026 | -$1,118 | $17,057 | -$3,034 |
| Aug 7, 2026 | +$104 | $17,161 | -$2,930 |
| Aug 10, 2026 | -$268 | $16,893 | -$3,198 |
| Aug 11, 2026 | -$1,438 | $15,455 | -$4,636 |
| Aug 12, 2026 | +$1,437 | $16,892 | -$3,199 |
| Aug 13, 2026 | +$1,734 | $18,626 | -$1,465 |
| Aug 14, 2026 | -$252 | $18,374 | -$1,717 |
| Aug 17, 2026 | +$568 | $18,942 | -$1,149 |
| Aug 18, 2026 | +$376 | $19,318 | -$773 |
| Aug 19, 2026 | -$832 | $18,486 | -$1,605 |
| Aug 20, 2026 | -$1,003 | $17,483 | -$2,608 |
| Aug 21, 2026 | +$1,433 | $18,916 | -$1,175 |
| Aug 24, 2026 | +$1,634 | $20,550 | $0 |
| Aug 25, 2026 | +$1,098 | $21,648 | $0 |
| Aug 26, 2026 | -$1,054 | $20,594 | -$1,054 |
| Aug 27, 2026 | +$1,193 | $21,787 | $0 |
| Aug 28, 2026 | -$322 | $21,465 | -$322 |
| Aug 31, 2026 | +$1,275 | $22,740 | $0 |
| Sep 1, 2026 | -$21 | $22,719 | -$21 |
| Sep 2, 2026 | +$1,880 | $24,599 | $0 |
| Sep 3, 2026 | -$1,055 | $23,544 | -$1,055 |
| Sep 4, 2026 | +$105 | $23,649 | -$950 |
| Sep 7, 2026 | +$281 | $23,930 | -$669 |
| Sep 8, 2026 | +$793 | $24,723 | $0 |
| Sep 9, 2026 | +$1,958 | $26,681 | $0 |
| Sep 10, 2026 | -$1,058 | $25,623 | -$1,058 |
| Sep 11, 2026 | -$1,536 | $24,087 | -$2,594 |
| Sep 14, 2026 | +$395 | $24,482 | -$2,199 |
| Sep 15, 2026 | +$782 | $25,264 | -$1,417 |
| Sep 16, 2026 | -$1,242 | $24,022 | -$2,659 |
| Sep 17, 2026 | -$601 | $23,421 | -$3,260 |
| Sep 18, 2026 | +$778 | $24,199 | -$2,482 |
| Sep 21, 2026 | +$1,379 | $25,578 | -$1,103 |
| Sep 22, 2026 | +$1,249 | $26,827 | $0 |
| Sep 23, 2026 | +$1,142 | $27,969 | $0 |
| Sep 24, 2026 | +$335 | $28,304 | $0 |
| Sep 25, 2026 | +$947 | $29,251 | $0 |
| Sep 28, 2026 | -$1,310 | $27,941 | -$1,310 |
| Date | Day | Total | Drawdown |
|---|---|---|---|
| Oct 12, 2026 | +$120.50 | $120.50 | $0.00 |
| Oct 13, 2026 | -$40.25 | $80.25 | -$40.25 |
| Oct 14, 2026 | -$95.00 | -$14.75 | -$135.25 |
| Oct 15, 2026 | +$60.00 | $45.25 | -$75.25 |
| Oct 16, 2026 | +$210.75 | $256.00 | $0.00 |
| Oct 19, 2026 | $0.00 | $256.00 | $0.00 |
| Oct 20, 2026 | -$130.00 | $126.00 | -$130.00 |
| Oct 21, 2026 | +$85.50 | $211.50 | -$44.50 |
| Oct 22, 2026 | +$40.00 | $251.50 | -$4.50 |
| Oct 23, 2026 | +$155.00 | $406.50 | $0.00 |
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/pnl_chart.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/pnl_chart.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/pnl_chart.qwe
// Trading days of results from a seeded random walk (Park–Miller), the same on every render.fn pnl_chart_demo_days(seed: int, count: int, from: str, size: num, edge: num) -> list { let s = seed let day = from let out = [] while len(out) < count { if date.weekday(day) <= 5 { s = s * 16807 % 2147483647 out = out | push({ date: day, pnl: round((s / 2147483647 - 0.5 + edge) * size) }) } day = date.add_days(day, 1) } out}let pnl_chart_demo_q3 = pnl_chart_demo_days(13, 64, "2026-07-01", 4000, 0.025)view PnlChartDemo() = <div.demo-stack> <div id="pnl_chart-demo-q3"> <PnlChart label="Strategy P&L, Q3 2026" show_label={true} days={pnl_chart_demo_q3} prefix="$" decimals={0}/> </div> <div id="pnl_chart-demo-small" style="max-width: 520px"> <PnlChart label="Two weeks, no figures" show_label={true} stats={false} height={140} bars_height={60} prefix="$" days={[{ date: "2026-10-12", pnl: 120.5 }, { date: "2026-10-13", pnl: -40.25 }, { date: "2026-10-14", pnl: -95 }, { date: "2026-10-15", pnl: 60 }, { date: "2026-10-16", pnl: 210.75 }, { date: "2026-10-19", pnl: 0 }, { date: "2026-10-20", pnl: -130 }, { date: "2026-10-21", pnl: 85.5 }, { date: "2026-10-22", pnl: 40 }, { date: "2026-10-23", pnl: 155 }]}/> </div> </div>Source
ui/pnl_chart.qwe
// PnlChart: profit and loss day by day: the running total as a line, with the// drawdown (how far it has fallen from its best so far) shaded under that best, and// each day's result as a bar, green for a gain and red for a loss, in a pane below.// Above them: the total, the share of winning days, the best and worst days and the// largest drawdown. CSS only: hovering a day shows its result, the total and the// drawdown then, and screen readers get the numbers as a table.//// <PnlChart label="Strategy P&L, Q3" days={[{ date: "2026-07-01", pnl: 1240 }, { date: "2026-07-02", pnl: -380 }]} prefix="$"/>// <PnlChart label="Paper account" days={days} prefix="$" decimals={0} height={180} stats={false}/>//// days: a list of { date, pnl }, oldest first: date a date or a date-time (or any// label), pnl the day's profit (below 0 a loss). height: the total's pane in pixels// (220); bars_height: the daily bars' pane (90). stats: the figures above the chart// (true). prefix and suffix: around every amount ("$"); decimals: digits after the// point (2). label: what the chart shows; it names it for screen readers and captions// their table. show_label also writes it above the chart.import ui/theme.qwe use ui_clslet pnl_chart_css = css`.ui-pnl { --ui-pnl-line: light-dark(#2a78d6, #3987e5); display: grid; gap: 10px; min-width: 0; margin: 0; font: 12px/1.3 var(--ui-font); color: var(--ui-fg); }.ui-pnl-title { margin: 0; font-size: 14px; font-weight: 600; }.ui-pnl-up { --k: var(--ui-success); } .ui-pnl-down { --k: var(--ui-danger); }.ui-pnl-stats { display: flex; flex-wrap: wrap; gap: 8px 28px; margin: 0; }.ui-pnl-stats div { display: grid; gap: 2px; }.ui-pnl-stats dt { color: var(--ui-muted); font-size: 12px; }.ui-pnl-stats dd { margin: 0; font-size: 16px; font-weight: 650; font-variant-numeric: tabular-nums; }.ui-pnl-stats .ui-pnl-up dd, .ui-pnl-stats .ui-pnl-down dd { color: var(--k); }.ui-pnl-frame { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 8px; padding-top: 8px; }.ui-pnl-plot { position: relative; }.ui-pnl-pane, .ui-pnl-cp, .ui-pnl-yp { position: relative; height: var(--h); }.ui-pnl-bpane, .ui-pnl-cb, .ui-pnl-yb { position: relative; height: var(--bh); margin-top: 26px; }.ui-pnl-grid { position: absolute; left: 0; right: 0; height: 1px; background: color-mix(in srgb, var(--ui-border) 70%, transparent); }.ui-pnl-zero { background: var(--ui-strong); }.ui-pnl-svg { position: absolute; z-index: 1; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }.ui-pnl-total { fill: none; stroke: var(--ui-pnl-line); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }.ui-pnl-peak { fill: none; stroke: var(--ui-danger); stroke-width: 1; stroke-dasharray: 3 3; opacity: .6; }.ui-pnl-dd { fill: var(--ui-danger); opacity: .14; }.ui-pnl-cols { position: absolute; inset: 0; display: flex; }.ui-pnl-col { position: relative; flex: 1; min-width: 0; }.ui-pnl-col::before { content: ""; position: absolute; top: 0; bottom: 0; left: 50%; width: 1px; background: var(--ui-strong); opacity: 0; }.ui-pnl-col:hover::before { opacity: .9; }.ui-pnl-bar { position: absolute; left: 15%; right: 15%; min-height: 1px; border-radius: 1px; background: var(--k); opacity: .85; }.ui-pnl-col:hover .ui-pnl-bar { opacity: 1; }.ui-pnl-dot { position: absolute; z-index: 2; left: 50%; display: none; width: 7px; height: 7px; margin: -3.5px 0 0 -3.5px; border-radius: 50%; background: var(--ui-pnl-line); box-shadow: 0 0 0 2px var(--ui-bg); }.ui-pnl-col:hover .ui-pnl-dot { display: block; }.ui-pnl-tip { position: absolute; z-index: 20; top: 0; left: calc(50% + 10px); display: none; min-width: 150px; padding: 7px 10px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); box-shadow: var(--ui-shadow); color: var(--ui-fg); font-size: 12px; pointer-events: none; }.ui-pnl-flip .ui-pnl-tip { left: auto; right: calc(50% + 10px); }.ui-pnl-col:hover .ui-pnl-tip { display: block; }.ui-pnl-tip strong { display: block; margin-bottom: 3px; font-weight: 600; white-space: nowrap; }.ui-pnl-tip p { display: flex; align-items: center; gap: 6px; margin: 2px 0 0; color: var(--ui-muted); white-space: nowrap; }.ui-pnl-tip b { margin-left: auto; padding-left: 12px; color: var(--ui-fg); font-weight: 600; font-variant-numeric: tabular-nums; }.ui-pnl-tip .ui-pnl-up b, .ui-pnl-tip .ui-pnl-down b { color: var(--k); }.ui-pnl-y span { position: absolute; left: 0; transform: translateY(-50%); color: var(--ui-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-pnl-x { position: relative; height: 16px; margin-top: 6px; }.ui-pnl-x span { position: absolute; top: 0; transform: translateX(-50%); color: var(--ui-muted); white-space: nowrap; }.ui-pnl-x .ui-pnl-first { transform: none; }.ui-pnl-x .ui-pnl-end { transform: translateX(-100%); }`// n (a whole number, 0 or more) with at least `width` digits.fn pnl_chart_pad(n: int, width: int) -> str { let s = str(n) len(s) < width ? repeat("0", width - len(s)) + s : s}// A whole number with commas: 1234567 is "1,234,567".fn pnl_chart_group(n: int) -> str = n < 1000 ? str(n) : pnl_chart_group(floor(n / 1000)) + "," + pnl_chart_pad(n % 1000, 3)// A value as it reads, with commas and `places` digits after the point (up to two as// needed when that is null): 48210.5 is "48,210.50" with 2. Worked out in whole// numbers, so the server and the browser write the same text.fn pnl_chart_num(v: num, places: int? = null) -> str { let digits = places == null ? 2 : (places < 0 ? 0 : (places > 6 ? 6 : places)) let scale = [1, 10, 100, 1000, 10000, 100000, 1000000][digits] let units = round(abs(v) * scale) let frac = units % scale let tail = digits == 0 || (places == null && frac == 0) ? "" : (places == null && frac % 10 == 0 ? `.${floor(frac / 10)}` : `.${pnl_chart_pad(frac, digits)}`) `${v < 0 && units > 0 ? "-" : ""}${pnl_chart_group(floor(units / scale))}${tail}`}// A large number, short: 1500 is "1.5k", 2000000 is "2M", 0.25 is "0.25".fn pnl_chart_short(v: num) -> str { let a = abs(v) let unit = a >= 1000000000 ? 1000000000 : (a >= 1000000 ? 1000000 : (a >= 1000 ? 1000 : 1)) let tenths = round(a * 10 / unit) let mark = unit == 1000000000 ? "B" : (unit == 1000000 ? "M" : "k") unit == 1 ? pnl_chart_num(v) : `${v < 0 ? "-" : ""}${pnl_chart_group(floor(tenths / 10))}${tenths % 10 != 0 ? `.${tenths % 10}` : ""}${mark}`}// A change in percent, signed, to two decimals: "+1.25%", "-0.40%".fn pnl_chart_signed(p: num) -> str = `${p > 0 ? "+" : (p < 0 && round(abs(p) * 100) > 0 ? "-" : "")}${pnl_chart_num(abs(p), 2)}%`// A coordinate out of 100 (or a length), to two decimals.fn pnl_chart_pct(n: num) -> str { let r = round(n * 100) / 100 r == floor(r) ? str(floor(r)) : str(r)}// The axis from lo to hi, widened to round ticks: about `steps` steps of 1, 2, 2.5 or 5 x 10^n.fn pnl_chart_scale(lo: num, hi: num, steps: int = 5) -> map { let span = hi - lo == 0 ? (lo == 0 ? 1 : abs(lo) / 10) : hi - lo let raw = span / steps let mag = 1 while mag * 10 <= raw { mag = mag * 10 } while mag > raw { mag = mag / 10 } let r = raw / mag let step = (r <= 1 ? 1 : (r <= 2 ? 2 : (r <= 2.5 ? 2.5 : (r <= 5 ? 5 : 10)))) * mag let a = floor(lo / step + 0.000001) let b0 = ceil(hi / step - 0.000001) let b = b0 == a ? a + 1 : b0 { lo: a * step, hi: b * step, ticks: for i in range(b - a + 1) => (a + i) * step }}// A candle's time as it reads: "Mar 2, 2026" for a date, "Mar 2, 14:30" for a date-time.fn pnl_chart_when(t) -> str = type_of(t) != "str" || !date.valid(t) ? str(t) : (len(t) > 10 ? date.format(t, "MMM D, HH:mm") : date.format(t, "MMM D, YYYY"))// Where the time axis is labelled: about `limit` of { i, text }, at the starts of years,// months or days when the times cross them ("2026", "Mar", "Mar 2"), else every few// candles ("14:30", "Mar 2").fn pnl_chart_ticks(times: list, limit: int) -> list { let n = len(times) let thin = (idx: list) => { let step = ceil(len(idx) / limit) range(len(idx)) | filter((k) => k % step == 0) | map((k) => idx[k]) } let spaced = (fmt_at) => { let step = ceil(n / limit) range(n) | filter((i) => i % step == floor(step / 2)) | map((i) => { i, text: fmt_at(i) }) } if n == 0 { return [] } if !all(times, (t) => type_of(t) == "str" && date.valid(t)) { return spaced((i) => str(times[i])) } let days = times | map((t) => slice(t, 0, 10)) let changes = (keys: list) => range(1, n) | filter((i) => keys[i] != keys[i - 1]) let day_starts = changes(days) let month_starts = changes(days | map((d) => slice(d, 0, 7))) let year_starts = changes(days | map((d) => slice(d, 0, 4))) let picked = if any(times, (t) => len(t) > 10) { len(day_starts) >= 3 ? thin(day_starts) | map((i) => { i, text: date.format(days[i], "MMM D") }) : spaced((i) => index_of(day_starts, i) != null ? date.format(days[i], "MMM D") : date.format(times[i], "HH:mm")) } else if len(year_starts) >= 3 && len(month_starts) > limit * 2 { thin(year_starts) | map((i) => { i, text: slice(days[i], 0, 4) }) } else if len(month_starts) >= 2 { thin(month_starts) | map((i) => { i, text: index_of(year_starts, i) != null ? slice(days[i], 0, 4) : date.format(days[i], "MMM") }) } else { spaced((i) => date.format(days[i], "MMM D")) } // Starts that fall close together (a short month) keep the first. let gap = n / (limit * 1.5) reduce(picked, [], (kept, t) => len(kept) == 0 || t.i - kept[-1].i >= gap ? kept + [t] : kept)}view PnlChart(days: list, height: int = 220, bars_height: int = 90, stats: bool = true, prefix: str = "", suffix: str = "", decimals: int = 2, label: str = "Profit and loss", show_label: bool = false, class: str? = null) { style global pnl_chart_css let n = len(days) let pnl = days | map((d) => d.pnl ?? 0) let total = vec.cumsum([0] + pnl) | drop(1) let peak = drop(reduce(total, [0], (acc, v) => acc + [v > acc[-1] ? v : acc[-1]]), 1) let dd = for i in range(n) => total[i] - peak[i] let sc = pnl_chart_scale(min([0] + total), max([0] + total), 4) let bsc = pnl_chart_scale(min([0] + pnl), max([0] + pnl), 2) let y = (v) => (sc.hi - v) * 100 / (sc.hi - sc.lo) let by = (v) => (bsc.hi - v) * 100 / (bsc.hi - bsc.lo) let x = (i) => (i + 0.5) * 100 / n let pct = (v) => pnl_chart_pct(v) let money = (v) => `${v < 0 ? "-" : (v > 0 ? "+" : "")}${prefix}${pnl_chart_num(abs(v), decimals)}${suffix}` let plain = (v) => `${v < 0 ? "-" : ""}${prefix}${pnl_chart_num(abs(v), decimals)}${suffix}` let tick = (t) => `${t < 0 ? "-" : ""}${prefix}${pnl_chart_short(abs(t))}` let side = (v) => v >= 0 ? "ui-pnl-up" : "ui-pnl-down" let pts = (vals: list, idx: list) => join(idx | map((i) => `${pct(x(i))},${pct(y(vals[i]))}`), " L") let line = n == 0 ? "" : "M" + pts(total, range(n)) let shade = n == 0 ? "" : `M${pts(peak, range(n))} L${pts(total, reverse(range(n)))} Z` let decided = pnl | filter((v) => v != 0) let wins = len(pnl | filter((v) => v > 0)) let worst_dd = min([0] + dd) let when = (d) => pnl_chart_when(d.date) let figures = [ { name: "Total", value: n == 0 ? 0 : total[-1], signed: true }, { name: "Winning days", text: len(decided) == 0 ? "—" : `${round(wins * 100 / len(decided))}% (${wins} of ${len(decided)})` }, { name: "Best day", value: max([0] + pnl), signed: true }, { name: "Worst day", value: min([0] + pnl), signed: true }, { name: "Max drawdown", value: worst_dd, signed: true }, ] let axis_width = max([4] + ((sc.ticks + bsc.ticks) | map((t) => len(tick(t))))) <figure class={ui_cls(["ui-pnl", class])} aria-label={label} style={`--h: ${height}px; --bh: ${bars_height}px`}> {show_label && <figcaption.ui-pnl-title>{label}</figcaption>} {stats && <dl.ui-pnl-stats> {for f in figures => <div class={f.value != null && f.value != 0 ? side(f.value) : null}> <dt>{f.name}</dt><dd>{f.value != null ? money(f.value) : f.text}</dd> </div>} </dl>} <div.ui-pnl-frame aria-hidden="true"> <div.ui-pnl-plot> <div.ui-pnl-pane> {for t in sc.ticks => <span class={ui_cls(["ui-pnl-grid", t == 0 && "ui-pnl-zero"])} style={`top: ${pct(y(t))}%`}></span>} <svg.ui-pnl-svg viewBox="0 0 100 100" preserveAspectRatio="none"> <path.ui-pnl-dd d={shade}/> {worst_dd < 0 && <path.ui-pnl-peak d={"M" + pts(peak, range(n))} vector-effect="non-scaling-stroke"/>} <path.ui-pnl-total d={line} vector-effect="non-scaling-stroke"/> </svg> </div> <div.ui-pnl-bpane> {for t in bsc.ticks => <span class={ui_cls(["ui-pnl-grid", t == 0 && "ui-pnl-zero"])} style={`top: ${pct(by(t))}%`}></span>} </div> <div.ui-pnl-cols> {for [i, d] in enumerate(days) => <div class={ui_cls(["ui-pnl-col", side(pnl[i]), n > 1 && i >= n / 2 && "ui-pnl-flip"])}> <div.ui-pnl-cp><span.ui-pnl-dot style={`top: ${pct(y(total[i]))}%`}></span></div> <div.ui-pnl-cb> <span.ui-pnl-bar style={`top: ${pct(by(pnl[i] > 0 ? pnl[i] : 0))}%; height: ${pct(abs(by(pnl[i]) - by(0)))}%`}></span> </div> <span.ui-pnl-tip> <strong>{when(d)}</strong> <p class={side(pnl[i])}>Day<b>{money(pnl[i])}</b></p> <p>Total<b>{money(total[i])}</b></p> <p class={dd[i] < 0 ? "ui-pnl-down" : null}>Drawdown<b>{dd[i] < 0 ? money(dd[i]) : "—"}</b></p> </span> </div>} </div> </div> <div.ui-pnl-y style={`width: ${axis_width}ch`}> <div.ui-pnl-yp>{for t in sc.ticks => <span style={`top: ${pct(y(t))}%`}>{tick(t)}</span>}</div> <div.ui-pnl-yb>{for t in bsc.ticks => <span style={`top: ${pct(by(t))}%`}>{tick(t)}</span>}</div> </div> <div.ui-pnl-x> {for t in pnl_chart_ticks(days | map((d) => d.date), 7) => <span class={ui_cls([x(t.i) < 4 && "ui-pnl-first", x(t.i) > 96 && "ui-pnl-end"])} style={`left: ${pct(x(t.i))}%`}>{t.text}</span>} </div> <span></span> </div> <table.ui-sr> <caption>{label}</caption> <thead><tr><th scope="col">Date</th><th scope="col">Day</th><th scope="col">Total</th><th scope="col">Drawdown</th></tr></thead> <tbody> {for [i, d] in enumerate(days) => <tr> <th scope="row">{when(d)}</th><td>{money(pnl[i])}</td><td>{plain(total[i])}</td><td>{plain(dd[i])}</td> </tr>} </tbody> </table> </figure>}Related
MarketHeatmapA treemap of stocks sized by market value and coloured by their change, grouped by sector, with each one's details on hover.6 views · 7 downloadsStockChartA full trading chart: candles, line or area, volume, range buttons, a crosshair with the OHLC readout, wheel zoom and drag pan, indicators and RSI and MACD panes.6 views · 6 downloadsAreaChartFilled areas for one or more series, overlapping or stacked into a total, with a hover crosshair listing every value.6 views · 8 downloadsBollingerChartPrices with Bollinger bands of a period and a number of deviations, closes outside them marked, as a line or candles.7 views · 5 downloadsBulletChartMeasures as thin bars against a target mark over grey bands from poor to good, a row each, with the band named on hover.5 views · 7 downloadsCandlestickChartOHLC candles, green up and red down, with a volume pane, a date axis, moving averages and the details of each candle on hover.5 views · 7 downloads
Thread
Thread 0 posts
On PnlChart: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.