@senpi/runtime 3.0.146 → 3.0.149
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/guide-commands.d.ts.map +1 -1
- package/dist/cli/guide-commands.js +55 -0
- package/dist/cli/guide-commands.js.map +1 -1
- package/dist/cli/quant-command.d.ts +128 -0
- package/dist/cli/quant-command.d.ts.map +1 -0
- package/dist/cli/quant-command.js +345 -0
- package/dist/cli/quant-command.js.map +1 -0
- package/dist/cli/senpi-commands.d.ts +11 -0
- package/dist/cli/senpi-commands.d.ts.map +1 -1
- package/dist/cli/senpi-commands.js +49 -4
- package/dist/cli/senpi-commands.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -1
- package/dist/quant/engine/quantdesk/__init__.py +19 -0
- package/dist/quant/engine/quantdesk/__main__.py +281 -0
- package/dist/quant/engine/quantdesk/_version.py +24 -0
- package/dist/quant/engine/quantdesk/benchmark.py +90 -0
- package/dist/quant/engine/quantdesk/book.py +197 -0
- package/dist/quant/engine/quantdesk/deep.py +197 -0
- package/dist/quant/engine/quantdesk/desk.py +878 -0
- package/dist/quant/engine/quantdesk/dsl.py +224 -0
- package/dist/quant/engine/quantdesk/followups.py +117 -0
- package/dist/quant/engine/quantdesk/hl_api.py +473 -0
- package/dist/quant/engine/quantdesk/market.py +290 -0
- package/dist/quant/engine/quantdesk/mcp_client.py +158 -0
- package/dist/quant/engine/quantdesk/metrics.py +409 -0
- package/dist/quant/engine/quantdesk/opportunities.py +88 -0
- package/dist/quant/engine/quantdesk/render.py +787 -0
- package/dist/quant/engine/quantdesk/roundtrips.py +124 -0
- package/dist/quant/engine/quantdesk/score.py +817 -0
- package/dist/quant/engine/quantdesk/senpi_history.py +160 -0
- package/dist/quant/engine/quantdesk/smart_money.py +354 -0
- package/dist/quant/engine/quantdesk/strategy_read.py +292 -0
- package/dist/quant/engine/quantdesk/taxonomy.py +55 -0
- package/dist/quant/engine/quantdesk/timing.py +164 -0
- package/dist/quant/engine/quantdesk/voice.py +42 -0
- package/dist/quant/engine-runner.d.ts +133 -0
- package/dist/quant/engine-runner.d.ts.map +1 -0
- package/dist/quant/engine-runner.js +306 -0
- package/dist/quant/engine-runner.js.map +1 -0
- package/dist/quant/job.d.ts +125 -0
- package/dist/quant/job.d.ts.map +1 -0
- package/dist/quant/job.js +184 -0
- package/dist/quant/job.js.map +1 -0
- package/dist/quant/register.d.ts +53 -0
- package/dist/quant/register.d.ts.map +1 -0
- package/dist/quant/register.js +253 -0
- package/dist/quant/register.js.map +1 -0
- package/dist/quant/registry.d.ts +105 -0
- package/dist/quant/registry.d.ts.map +1 -0
- package/dist/quant/registry.js +248 -0
- package/dist/quant/registry.js.map +1 -0
- package/dist/quant/types.d.ts +216 -0
- package/dist/quant/types.d.ts.map +1 -0
- package/dist/quant/types.js +121 -0
- package/dist/quant/types.js.map +1 -0
- package/dist/skills-manager/manifest.js +1 -1
- package/dist/skills-manager/skills-manifest.json +73 -16
- package/dist/widgets/index.d.ts +8 -1
- package/dist/widgets/index.d.ts.map +1 -1
- package/dist/widgets/index.js +1 -0
- package/dist/widgets/index.js.map +1 -1
- package/dist/widgets/registry.d.ts +8 -0
- package/dist/widgets/registry.d.ts.map +1 -1
- package/dist/widgets/registry.js +26 -1
- package/dist/widgets/registry.js.map +1 -1
- package/dist/widgets/tool.d.ts +14 -4
- package/dist/widgets/tool.d.ts.map +1 -1
- package/dist/widgets/tool.js +36 -23
- package/dist/widgets/tool.js.map +1 -1
- package/dist/widgets/turn-cap.d.ts +33 -0
- package/dist/widgets/turn-cap.d.ts.map +1 -0
- package/dist/widgets/turn-cap.js +119 -0
- package/dist/widgets/turn-cap.js.map +1 -0
- package/dist/widgets/types.d.ts +33 -5
- package/dist/widgets/types.d.ts.map +1 -1
- package/dist/widgets/types.js +4 -3
- package/dist/widgets/types.js.map +1 -1
- package/dist/widgets/widgets/fund-user-wallet.d.ts.map +1 -1
- package/dist/widgets/widgets/fund-user-wallet.js +1 -0
- package/dist/widgets/widgets/fund-user-wallet.js.map +1 -1
- package/dist/widgets/widgets/quant-desk-recommendations.d.ts +52 -0
- package/dist/widgets/widgets/quant-desk-recommendations.d.ts.map +1 -0
- package/dist/widgets/widgets/quant-desk-recommendations.js +278 -0
- package/dist/widgets/widgets/quant-desk-recommendations.js.map +1 -0
- package/openclaw.plugin.json +12 -2
- package/package.json +4 -3
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The desk as Markdown for chat. Sections can be rendered alone (`--section`) so a follow-up question
|
|
3
|
+
("are my positions protected?") answers from the cached analysis without a refetch."""
|
|
4
|
+
# Copyright 2026 Senpi (https://senpi.ai) — Apache-2.0
|
|
5
|
+
import datetime
|
|
6
|
+
|
|
7
|
+
import metrics
|
|
8
|
+
import dsl as dsl_mod
|
|
9
|
+
import score as score_mod
|
|
10
|
+
|
|
11
|
+
SECTIONS = ("overview", "strategy", "context", "protection", "performance", "leaks", "smart", "market", "edge", "scout", "next", "followups")
|
|
12
|
+
from _version import VERSION # noqa: E402 shown in the header line, so a stale install is visible at a glance
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def pct_cost(x):
|
|
16
|
+
"""Cost shares under 1% keep a decimal — "0%" is a claim, "0.4%" is a measurement."""
|
|
17
|
+
if x is None:
|
|
18
|
+
return "—"
|
|
19
|
+
if 0 < x < 0.01:
|
|
20
|
+
return f"{100 * x:.1f}%" if x >= 0.0005 else "<0.1%"
|
|
21
|
+
return pct(x)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def rank_line(rank):
|
|
25
|
+
"""Three windows, so one bad week (or one good one) cannot pass for the trader."""
|
|
26
|
+
tp = rank["top_pct"]
|
|
27
|
+
w = rank.get("windows") or {}
|
|
28
|
+
wk = w.get("week") or {}
|
|
29
|
+
s = (f"**#{rank['rank']:,} of {rank['of']:,}** on Hyperliquid's leaderboard this week" + (f" ({usd(wk['pnl'], signed=True)})" if wk.get("pnl") is not None else "")
|
|
30
|
+
+ " · " + (f"top {tp:.1f}%" if tp <= 50 else f"bottom {max(0.1, 100 - tp):.1f}%"))
|
|
31
|
+
extra = []
|
|
32
|
+
for key, label in (("month", "on the month"), ("allTime", "all-time")):
|
|
33
|
+
rk = (rank.get("ranks") or {}).get(key)
|
|
34
|
+
if rk and (w.get(key) or {}).get("pnl") is not None:
|
|
35
|
+
extra.append(f"**#{rk:,}** {label} ({usd(w[key]['pnl'], signed=True)})")
|
|
36
|
+
return s + (" · " + " · ".join(extra) if extra else "")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def coverage_note(tr, meta=None):
|
|
40
|
+
"""Which record the trade-level reads come from, in the desk's own words — never a data-source apology."""
|
|
41
|
+
cov = tr.get("coverage") or {}
|
|
42
|
+
src = ((meta or {}).get("sources") or {}).get("trades") or "public fills"
|
|
43
|
+
if src != "public fills":
|
|
44
|
+
return f"_Trade history: {src}._"
|
|
45
|
+
# `effective` is `overall` after the ledger reconciliation — a book whose rebuilt P&L misses the
|
|
46
|
+
# exchange's own delta by more than the open positions can explain has volume the jump heuristic
|
|
47
|
+
# never saw, and quoting the unreconciled figure would read as fuller coverage than we have.
|
|
48
|
+
seen = cov.get("effective") if cov.get("effective") is not None else cov.get("overall")
|
|
49
|
+
if seen is None or seen >= 0.9:
|
|
50
|
+
return None
|
|
51
|
+
return f"_Trade-level reads cover about {pct(seen)} of executed volume; ledger figures are complete._"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def hold_fallback(tr):
|
|
55
|
+
w, l = tr["hold_n"]["winners"], tr["hold_n"]["losers"]
|
|
56
|
+
few = "of either" if (w < metrics.MIN_HOLD_N and l < metrics.MIN_HOLD_N) else ("losers" if l < metrics.MIN_HOLD_N else "winners")
|
|
57
|
+
return f"**Hold time:** {w} winning and {l} losing round trips with a clean open and close — too few {few} for a hold-time read (the desk wants {metrics.MIN_HOLD_N} of each)."
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def copy_warnings(book, cohorts):
|
|
61
|
+
"""What a copier inherits: naked positions and cohorts on the other side — grammar follows the counts."""
|
|
62
|
+
warn = []
|
|
63
|
+
if book["naked"]:
|
|
64
|
+
n = len(book["naked"])
|
|
65
|
+
warn.append(f"{n} of {len(book['positions'])} open positions {'has' if n == 1 else 'have'} no stop")
|
|
66
|
+
against = [x for x in (cohorts or []) if x.get("agreement") is not None and x["agreement"] <= -0.5]
|
|
67
|
+
if against:
|
|
68
|
+
names = [{"proven": "proven cohort", "hot": "hot 30-day cohort"}.get(x["name"], x["name"]) for x in against]
|
|
69
|
+
warn.append("the " + " and the ".join(names) + (" sits" if len(names) == 1 else " sit") + " on the other side of this book")
|
|
70
|
+
return warn
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def usd(x, signed=False):
|
|
74
|
+
if x is None:
|
|
75
|
+
return "—"
|
|
76
|
+
s = f"${abs(x):,.0f}"
|
|
77
|
+
return ("-" + s) if x < 0 else (("+" + s) if signed and x > 0 else s)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def pct(x, d=0, signed=False):
|
|
81
|
+
if x is None:
|
|
82
|
+
return "—"
|
|
83
|
+
v = 100 * x
|
|
84
|
+
return (f"{v:+.{d}f}%" if signed else f"{v:.{d}f}%")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def hrs(x):
|
|
88
|
+
"""Scalpers hold for seconds. At 1 decimal place every hold on a 1,117-trade book rendered
|
|
89
|
+
"0.0h", including the median-hold column and the winners/losers line."""
|
|
90
|
+
if x is None:
|
|
91
|
+
return "—"
|
|
92
|
+
if x >= 48:
|
|
93
|
+
return f"{x / 24:.1f}d"
|
|
94
|
+
if x >= 1:
|
|
95
|
+
return f"{x:.1f}h"
|
|
96
|
+
return f"{x * 60:.0f}m" if x * 60 >= 1 else "<1m"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def num(x, unit):
|
|
100
|
+
if x is None:
|
|
101
|
+
return "—"
|
|
102
|
+
if x == float("inf"):
|
|
103
|
+
return "∞"
|
|
104
|
+
if unit == "x" and x > 100:
|
|
105
|
+
return ">100×"
|
|
106
|
+
return {"h": hrs(x), "%": pct(x), "x": f"{x:.1f}×"}[unit]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def short(addr):
|
|
110
|
+
return f"{addr[:6]}…{addr[-4:]}"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def header(r):
|
|
114
|
+
a, tr, act, rank = r["address"], r["track"], r["activity"], r.get("rank")
|
|
115
|
+
# A book is not an address. Titling a union of six strategy wallets with the first one's hex is
|
|
116
|
+
# the same mistake the desk made by running on one wallet in the first place: it tells the reader
|
|
117
|
+
# they are looking at a wallet when they are looking at everything they trade.
|
|
118
|
+
ws = r.get("wallets") or []
|
|
119
|
+
title = f"across {len(ws)} wallets" if len(ws) > 1 else f"`{short(a)}`"
|
|
120
|
+
lines = [f"# Your desk — {title}",
|
|
121
|
+
f"{r['days']} days · {act['fills']:,} fills · {act['coins']} coins · updated {datetime.datetime.fromtimestamp(r['now_ms'] / 1000, datetime.timezone.utc).strftime('%Y-%m-%d %H:%M UTC')} · **YOUR QUANT — LIVE · READ-ONLY** · v{VERSION}"]
|
|
122
|
+
if rank:
|
|
123
|
+
lines.append(rank_line(rank))
|
|
124
|
+
lines.append(f"**{r['archetype']}**")
|
|
125
|
+
lab = r.get("labels") or {}
|
|
126
|
+
if any(lab.get(k) for k in ("consistency", "risk", "activity")):
|
|
127
|
+
lines.append("senpi's read: " + " · ".join(str(lab[k]).upper() for k in ("consistency", "risk", "activity") if lab.get(k))
|
|
128
|
+
# senpi's trader-score consistency, NOT the desk's own Consistency dimension —
|
|
129
|
+
# unlabelled they collided on one page as "consistency score 33" beside
|
|
130
|
+
# "Consistency 100"
|
|
131
|
+
+ (f" · senpi trader-score consistency {lab['tcs']}" if lab.get("tcs") is not None else ""))
|
|
132
|
+
lines.append(f"> **{r['verdict']}**")
|
|
133
|
+
if r["flags"]:
|
|
134
|
+
lines.append(" ".join(f"`{f}`" for f in r["flags"]))
|
|
135
|
+
return "\n".join(lines)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def overview(r):
|
|
139
|
+
tr, d, book = r["track"], r["dimensions"], r["book"]
|
|
140
|
+
q = r.get("quant_score")
|
|
141
|
+
out = [f"## Quant score **{q}**/100" if q is not None else
|
|
142
|
+
"## Quant score — _not enough of this book is measurable to score it_",
|
|
143
|
+
"", "| Dimension | Score | What it means |", "|---|---:|---|"]
|
|
144
|
+
names = {"timing": "Timing / edge", "risk": "Risk management", "cost": "Cost efficiency", "sizing": "Sizing / conviction", "consistency": "Consistency", "market_fit": "Market fit"}
|
|
145
|
+
for k in ("timing", "risk", "cost", "sizing", "consistency", "market_fit"):
|
|
146
|
+
# An unmeasured dimension shows a dash, not a number. A number here is a claim.
|
|
147
|
+
sc = d[k]["score"]
|
|
148
|
+
out.append(f"| {names[k]} | {'—' if sc is None else sc} | {d[k]['line']} |")
|
|
149
|
+
if any(d[k]["score"] is None for k in names):
|
|
150
|
+
out += ["", "_A dimension marked — could not be measured this window; the score is the weighted "
|
|
151
|
+
"average of the ones that could._"]
|
|
152
|
+
eq = r["equity"]
|
|
153
|
+
ledger = tr.get("ledger_net")
|
|
154
|
+
out += ["", f"## Track record ({r['days']} days)", "", "| Net P&L (ledger, incl. unrealized) | Return on avg equity | Realized (trades + funding − fees) | Win rate | Max drawdown | Profit factor | Trades | Active days |", "|---:|---:|---:|---:|---:|---:|---:|---:|",
|
|
155
|
+
f"| {usd(ledger, signed=True)} | {pct(eq.get('return_on_avg_equity'), 1, signed=True)} | {usd(tr['net'], signed=True)} | {pct(tr['win_rate'])} | {pct(-r['drawdown']['dd_pct'], 0, signed=True) if r['drawdown'].get('dd_pct') else '—'} | {num(tr['profit_factor'], 'x')} | {tr['trades']} | {r['activity']['active_days']} |"]
|
|
156
|
+
note = coverage_note(tr, r.get("meta"))
|
|
157
|
+
if note:
|
|
158
|
+
# The blank line is load-bearing. A line placed straight after a table row is parsed as
|
|
159
|
+
# ANOTHER ROW, so this caption rendered as a row with its text in column 1 and seven empty
|
|
160
|
+
# cells trailing it — which is what a reader sees as "the table has an empty row".
|
|
161
|
+
out += ["", note]
|
|
162
|
+
cr = tr.get("cost_ratio"); wb = (r.get("benchmark") or {}).get("cost_ratio")
|
|
163
|
+
n_tr = tr.get("trades") or 0
|
|
164
|
+
out += ["", "## Where your P&L went", ""]
|
|
165
|
+
if ledger is not None:
|
|
166
|
+
out.append(f"Ledger over {r['days']} days: **{usd(ledger, signed=True)}** — every fill, funding payment and the open book included.")
|
|
167
|
+
out.append(f"Closed trades ({n_tr}): gross **{usd(tr['gross_realized'], signed=True)}** → fees **{usd(-tr['fees'], signed=True)}** → funding **{usd(tr['funding'], signed=True)}** → net **{usd(tr['net'], signed=True)}**.")
|
|
168
|
+
if ledger is not None and n_tr < 5 and (ledger < 0) != ((tr.get("net") or 0) < 0):
|
|
169
|
+
out.append(f"The closed-trade record is thin ({n_tr} trade{'s' if n_tr != 1 else ''}); the ledger is the number to trust.")
|
|
170
|
+
if cr is not None:
|
|
171
|
+
if cr > 1:
|
|
172
|
+
out.append("Costs exceeded what the trades and funding made over the window.")
|
|
173
|
+
elif (tr.get("funding") or 0) > 0:
|
|
174
|
+
out.append(f"Fees took **{pct_cost(cr)}** of what you made — trade P&L plus **{usd(tr['funding'])}** of funding collected.")
|
|
175
|
+
else:
|
|
176
|
+
out.append(f"Fees + funding took **{pct_cost(cr)}** of your gross" + (f" — the whale median is {pct(wb)}." if wb is not None else "."))
|
|
177
|
+
if r["leaks"]:
|
|
178
|
+
# The heading counts what is actually below it. "Top 3" over two items is a small lie the
|
|
179
|
+
# reader checks in one glance, and it makes them wonder what else was rounded.
|
|
180
|
+
top = r["leaks"][:3]
|
|
181
|
+
out += ["", f"## Top {len(top)} thing{'s' if len(top) != 1 else ''} your agents found", ""]
|
|
182
|
+
for i, l in enumerate(top, 1):
|
|
183
|
+
head = f"**{l['agent']}**" if l.get("unpriced") else f"**{l['agent']} · ~{usd(l['usd'])} / {l['window']}**"
|
|
184
|
+
out.append(f"{i}. {head} — **{l['title']}.** {l['evidence']} _{l['counterfactual']}_ → {l['cta']}")
|
|
185
|
+
return "\n".join(out)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def protection(r):
|
|
189
|
+
b = r["book"]; sm = {x["coin"]: x for x in (r.get("smart") or {}).get("rows", [])}
|
|
190
|
+
n = len(b["positions"])
|
|
191
|
+
out = ["## Live positions — protection audit", "",
|
|
192
|
+
f"Account value **{usd(b['account_value'])}**" + (f" (perps equity {usd(b['account_value_perps'])})" if b.get("account_value_perps") and abs(b["account_value_perps"] - b["account_value"]) > 1 else "")
|
|
193
|
+
+ f" · margin used **{pct(b['margin_utilization'])}** · withdrawable **{usd(b['withdrawable'])}** · net uPnL **{usd(b['unrealized'], signed=True)}**",
|
|
194
|
+
f"{n} open position{'s' if n != 1 else ''} · {len(b['naked'])} with no stop · {len(b['partial'])} partly covered · {r['market']['stance'] if r.get('market') else ''}" + (f" · paying {usd(-b['funding_per_day'])}/day in funding" if b['funding_per_day'] < 0 else (f" · collecting {usd(b['funding_per_day'])}/day in funding" if b['funding_per_day'] > 0 else ""))]
|
|
195
|
+
if n:
|
|
196
|
+
# chat-shaped: nine short columns; the prose lives under the table, one line per position that needs a hand
|
|
197
|
+
out += ["", "| Coin | Side | Held | Notional | uPnL · ROE | Funding/day | To liq. | Stop | Status |", "|---|---|---:|---:|---:|---:|---:|---:|---|"]
|
|
198
|
+
todo = []
|
|
199
|
+
for p in b["positions"]:
|
|
200
|
+
status, note = _protection_note(p, sm.get(p["coin"]))
|
|
201
|
+
liq = "—" if p["liq_distance_pct"] is None else (">100%" if p["liq_distance_pct"] > 100 else pct(p["liq_distance_pct"] / 100, 1))
|
|
202
|
+
held = hrs((r["now_ms"] - p["opened_ms"]) / 3.6e6) if p.get("opened_ms") else "—"
|
|
203
|
+
out.append(f"| {p['coin']} | {p['side']} {p['leverage'] or '—'}x | {held} | {usd(p['notional'])} | {usd(p['unrealized'], signed=True)} · {pct(p['roe'], 0, signed=True)} | {usd(p['funding_per_day'], signed=True)} | {liq} | {pct(p['stop_covered_share'])} | {status} |")
|
|
204
|
+
if status != "PROTECTED":
|
|
205
|
+
todo.append(f"- **{p['coin']}** — {note}.")
|
|
206
|
+
# What senpi's runtime is doing to these positions, where it is doing anything. The stop in
|
|
207
|
+
# the table above is read off the exchange and is already correct; what it lacks is context,
|
|
208
|
+
# because a price with no context reads as a static stop when it is a floor that ratchets.
|
|
209
|
+
# State what is LOCKED; never present the ladder ahead as protection already in force. Only
|
|
210
|
+
# corroborated, armed tiers reach here — a row the venue does not confirm says nothing, so
|
|
211
|
+
# this block can never contradict the PROTECTED column beside it.
|
|
212
|
+
_dsl = [q for q in b["positions"] if q.get("dsl")]
|
|
213
|
+
if _dsl:
|
|
214
|
+
# not necessarily "your runtime": a row can come from a direct `ratchet_stop_add` on a
|
|
215
|
+
# raw position, with no runtime involved. (@0xsarvesh, #753.)
|
|
216
|
+
out += ["", "**Senpi's ratchet stop is managing these.**"]
|
|
217
|
+
for q in _dsl:
|
|
218
|
+
ln = dsl_mod.line(q)
|
|
219
|
+
if ln:
|
|
220
|
+
out.append(f"- **{q['coin']}** — {ln}")
|
|
221
|
+
out += ["", "_A tier that has not armed is a rule, not protection: the floor it would "
|
|
222
|
+
"set is not in force until its trigger is reached._"]
|
|
223
|
+
out += ["", "**Your quant would…**"] + (todo or ["- nothing here — every position carries a full stop."])
|
|
224
|
+
else:
|
|
225
|
+
out.append("\nNo open positions right now.")
|
|
226
|
+
return "\n".join(out)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _protection_note(p, smrow):
|
|
230
|
+
liq = p["liq_distance_pct"]; cov = p["stop_covered_share"]; against = smrow and smrow["read"].startswith("AGAINST")
|
|
231
|
+
if liq is not None and liq < 5 and cov < 0.9:
|
|
232
|
+
return "AT RISK", f"put a stop above the liquidation price now — at {p['leverage']}x a {liq:.1f}% move takes the whole margin"
|
|
233
|
+
if cov == 0:
|
|
234
|
+
return "UNPROTECTED", "attach a stop ladder: a hard floor plus a trailing lock as it runs" + (" — and you're against the whale cohort here" if against else "")
|
|
235
|
+
if cov < 0.9:
|
|
236
|
+
return "PARTLY COVERED", f"the other {pct(1 - cov)} rides naked — extend the stop to the full size"
|
|
237
|
+
if p["stop_distance_pct"] is not None and p["stop_distance_pct"] < 1.0:
|
|
238
|
+
return "PROTECTED", f"stop is {p['stop_distance_pct']:.1f}% from the mark — tight enough to be noise"
|
|
239
|
+
return "PROTECTED", "stop in place — looks good" + (" — but the whale cohort is on the other side" if against else "")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def by_wallet_table(r):
|
|
243
|
+
"""Which wallet carried the book. Only on a --book run, and only when more than one traded."""
|
|
244
|
+
rows = [w for w in (r.get("by_wallet") or []) if w["trades"] or w["open"]]
|
|
245
|
+
if len(rows) < 2:
|
|
246
|
+
return ""
|
|
247
|
+
out = ["", "**By wallet**", "",
|
|
248
|
+
"| Wallet | Trades | Win rate | Gross | Fees | Funding | Net | Open | Unrealized |",
|
|
249
|
+
"|---|---:|---:|---:|---:|---:|---:|---:|---:|"]
|
|
250
|
+
for w in rows:
|
|
251
|
+
wr = pct(w["wins"] / w["trades"]) if w["trades"] else "—"
|
|
252
|
+
out.append(f"| `{short(w['wallet'])}` | {w['trades']} | {wr} | {usd(w['realized'], signed=True)} "
|
|
253
|
+
f"| {usd(-w['fees'], signed=True)} | {usd(w.get('funding') or 0.0, signed=True)} "
|
|
254
|
+
f"| {usd(w['net'], signed=True)} | {w['open']} | {usd(w['unrealized'], signed=True)} |")
|
|
255
|
+
return "\n".join(out)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def performance(r):
|
|
259
|
+
tr = r["track"]
|
|
260
|
+
out = ["## Performance", "", "| Coin | Trades | Win rate | Realized | Fees | Funding | Share of volume | Median hold |", "|---|---:|---:|---:|---:|---:|---:|---:|"]
|
|
261
|
+
for k, v in list(tr["coins"].items())[:10]:
|
|
262
|
+
out.append(f"| {k} | {v['trades']} | {pct(v['win_rate'])} | {usd(v['realized'], signed=True)} | {usd(-v['fees'], signed=True)} | {usd(v['funding'], signed=True)} | {pct(v['volume_share'])} | {hrs(v['hold_median_h'])} |")
|
|
263
|
+
L, S = tr["long"], tr["short"]
|
|
264
|
+
out += ["", f"**Long / short:** longs {L['trades']} trades · win {pct(L['wins'] / L['trades']) if L['trades'] else '—'} · {usd(L['realized'], signed=True)}; shorts {S['trades']} trades · win {pct(S['wins'] / S['trades']) if S['trades'] else '—'} · {usd(S['realized'], signed=True)}",
|
|
265
|
+
(f"**Hold time (median):** winners {hrs(tr['hold_winners_h'])} · losers {hrs(tr['hold_losers_h'])}" + (f" — you hold losers {tr['hold_ratio']:.1f}× longer" if tr.get('hold_ratio') and tr['hold_ratio'] > 1.2 else (f" — you cut losers {1 / tr['hold_ratio']:.1f}× faster than you let winners run" if tr.get('hold_ratio') and 0 < tr['hold_ratio'] < 0.8 else ""))) if (tr.get('hold_winners_h') is not None and tr.get('hold_losers_h') is not None) else hold_fallback(tr),
|
|
266
|
+
f"**Execution:** {pct(tr['taker_share'])} taker · {score_mod._bp(tr['fee_rate_taker'])} taker / {score_mod._bp(tr['fee_rate_maker'])} maker · {tr['liquidations']} liquidation(s)"]
|
|
267
|
+
sb = tr.get("size_buckets") or {}
|
|
268
|
+
if sb.get("bands"):
|
|
269
|
+
out += ["", f"**Size vs outcome** (median position {usd(sb['median_notional'])} notional):", "", "| Size band | Winners | Losers | Realized |", "|---|---:|---:|---:|"]
|
|
270
|
+
out += [f"| {b['band']} | {b['winners']} | {b['losers']} | {usd(b['realized'], signed=True)} |" for b in sb["bands"]]
|
|
271
|
+
bw = by_wallet_table(r)
|
|
272
|
+
if bw:
|
|
273
|
+
out.append(bw)
|
|
274
|
+
return "\n".join(out)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def recoverable_line(r):
|
|
278
|
+
"""The one quotable number, with its shape. The leaks below are alternative fixes for the same
|
|
279
|
+
trades, so a reader who adds them up gets a figure larger than the money ever at stake.
|
|
280
|
+
|
|
281
|
+
The headline states the TOTAL and then attributes it. It used to read "<rule> would have kept
|
|
282
|
+
~$27,996" on a book where $18,898 of that was taker fees and the rule's own share was $9,098 —
|
|
283
|
+
crediting a trailing stop with money that came from not crossing the spread."""
|
|
284
|
+
rec = r.get("recoverable") or {}
|
|
285
|
+
total = rec.get("usd") or 0
|
|
286
|
+
# It used to also return [] when the leak list was empty, so a book whose only recoverable money
|
|
287
|
+
# was TAKER FEES — measured, not a counterfactual, and always real — got no number at all, while
|
|
288
|
+
# the sentence underneath said no leak clears the bar. The JSON still carried the figure, so the
|
|
289
|
+
# agent could quote a number the desk had just denied. (@danielmbirochi, #718, item 10.)
|
|
290
|
+
if total <= 0:
|
|
291
|
+
return []
|
|
292
|
+
fees, rule = rec.get("fees") or 0, rec.get("rule")
|
|
293
|
+
lever = total - fees
|
|
294
|
+
# "N% of what your losing trades gave up" was also gated on a NEGATIVE ledger, on the reasoning
|
|
295
|
+
# that the frame only suits a book that lost money. But a headline with no denominator is what
|
|
296
|
+
# made this number read as absurd in review: on a book that netted $36,481 after $140,698 of
|
|
297
|
+
# fees, "~$171,808" invites the reader to divide by the net and get 4.71x. The share is exactly
|
|
298
|
+
# the denominator that defuses it — 4% of what the losing trades gave up — and it is no less
|
|
299
|
+
# true on a profitable book. The `<= 1.0` guard below is what keeps the sentence meaningful.
|
|
300
|
+
# (B6, @0xsarvesh #718.)
|
|
301
|
+
share = rec.get("share_of_losses")
|
|
302
|
+
head = f"**Your quant would have kept ~{usd(total)} of this**"
|
|
303
|
+
# a denominator that means something: on a book with almost no losses the share is a division by
|
|
304
|
+
# noise (the fixture reads 20924%). The cap was 2.0, which still left "197% of what your losing
|
|
305
|
+
# trades gave up" printable — above 100% the frame stops meaning anything to a reader, however
|
|
306
|
+
# true the arithmetic is once fees come off the winners too. (@danielmbirochi, #718, round 2.)
|
|
307
|
+
# …and not when it rounds to nothing. On a book that lost $274,940 with $2,709 recoverable the
|
|
308
|
+
# share is 0.5%, and the clause printed "— 0% of what your losing trades gave up", which tells
|
|
309
|
+
# the reader nothing and reads as a broken number. The denominator exists to make the headline
|
|
310
|
+
# legible; below half a percent it does the opposite.
|
|
311
|
+
# Keyed on what actually renders, not on a threshold guessed against the formatter: 0.005 still
|
|
312
|
+
# prints "0%" under banker's rounding, so any constant here is one rounding rule away from wrong.
|
|
313
|
+
if share and 0 < share <= 1.0 and pct(share, 0) != "0%":
|
|
314
|
+
head += f" — {pct(share, 0)} of what your losing trades gave up"
|
|
315
|
+
# when one trade IS the number, say so in the headline. The disclosure below is the first thing
|
|
316
|
+
# a reader drops when they quote the figure, and on 0xb699…392e that figure was $1,072,010 of
|
|
317
|
+
# which 97% came from a single position on an 8-trade book.
|
|
318
|
+
top1 = ((rec.get("concentration") or {}).get("top1")) or 0
|
|
319
|
+
if top1 >= 0.8:
|
|
320
|
+
head += f" — though {pct(top1, 0)} of that is one trade, not a pattern"
|
|
321
|
+
out = [head + ".", ""]
|
|
322
|
+
|
|
323
|
+
fee_part = f"{usd(fees)} of it is taker fees you can stop paying on the same fills"
|
|
324
|
+
n = rec["n_trades"]
|
|
325
|
+
rule_part = (f"one rule — {rule} — applied to {'your one complete trade' if n == 1 else f'all {n} complete trades'} "
|
|
326
|
+
f"and charged on the ones it would have cost you")
|
|
327
|
+
if rule and fees > 0:
|
|
328
|
+
big, small = (fee_part, f"the other {usd(lever)} comes from {rule_part}") if fees >= lever \
|
|
329
|
+
else (f"{usd(lever)} of it comes from {rule_part}", f"the other {usd(fees)} is taker fees on the same fills")
|
|
330
|
+
out += [f"{big[0].upper() + big[1:]}, and {small}.", ""]
|
|
331
|
+
elif rule:
|
|
332
|
+
out += [f"All of it comes from {rule_part}.", ""]
|
|
333
|
+
else:
|
|
334
|
+
out += [f"{fee_part[0].upper() + fee_part[1:]}.", ""]
|
|
335
|
+
|
|
336
|
+
c = rec.get("concentration") or {}
|
|
337
|
+
if c.get("top1", 0) >= 0.4:
|
|
338
|
+
out += [f"**That total is not spread across your book — it is {'one trade' if c['top1'] >= 0.6 else 'a few trades'}.** "
|
|
339
|
+
f"The largest is {pct(c['top1'], 0)} of it on its own, and the top three are {pct(min(c['top3'], 1.0), 0)}. "
|
|
340
|
+
f"A handful of positions drove it; the rest of the book is not the problem.", ""]
|
|
341
|
+
elif c.get("n_positive"):
|
|
342
|
+
out += [f"No single trade dominates it — the largest is {pct(c['top1'], 0)}, spread over "
|
|
343
|
+
f"{c['n_positive']} of your trades. This one is a habit, not an accident.", ""]
|
|
344
|
+
|
|
345
|
+
if r.get("leaks"):
|
|
346
|
+
out += ["_The leaks below price each fix on its own. They land on the same trades — one oversized, "
|
|
347
|
+
"chased, held-too-long position shows up in several — so **they do not add up**. The number "
|
|
348
|
+
"above is the single best change, and it is the one to quote._", ""]
|
|
349
|
+
return out
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def leaks(r):
|
|
353
|
+
out = ["## Leaks — ranked by $ impact · counterfactual, not history", ""]
|
|
354
|
+
out += recoverable_line(r)
|
|
355
|
+
if not r["leaks"]:
|
|
356
|
+
_rec = r.get("recoverable") or {}
|
|
357
|
+
if (r["track"].get("trades") or 0) < 5:
|
|
358
|
+
out.append("Not enough closed trades to price a leak yet — the desk needs a handful of round trips before a counterfactual means anything.")
|
|
359
|
+
elif (_rec.get("usd") or 0) > 0:
|
|
360
|
+
# "No leak clears the bar" sat directly under a positive recoverable figure. Both were
|
|
361
|
+
# true of different things: no PROCESS counterfactual survived being charged, and the
|
|
362
|
+
# costs above are measured rather than counterfactual. Say which is which.
|
|
363
|
+
out.append("No *process* leak clears the bar on this window — every exit, sizing and entry rule the desk tests "
|
|
364
|
+
"came out flat or negative once it was charged on the trades it would have cost. The figure above is "
|
|
365
|
+
"not one of those: costs are measured, not modelled, which is why it stands on its own.")
|
|
366
|
+
else:
|
|
367
|
+
out.append("No leak clears the bar on this window: every counterfactual the desk tests came out flat or negative, which means the process is not where the money is going.")
|
|
368
|
+
for i, l in enumerate(r["leaks"], 1):
|
|
369
|
+
tag = f"_{l['agent']}_" + ("" if l.get("unpriced") else f" · **~{usd(l['usd'])} / {l['window']}**")
|
|
370
|
+
out += [f"**{i:02d} · {l['title']}** — {tag}", f"{l['evidence']} {l['counterfactual']}", f"→ {l['cta']}", ""]
|
|
371
|
+
tm = r.get("timing") or {}
|
|
372
|
+
if tm.get("n"):
|
|
373
|
+
neg = [k for k, g in (("time-cut on losers", tm.get("cut")), ("trailing lock on winners", tm.get("lock")))
|
|
374
|
+
if g and g.get("settings") and any(s["n"] for s in g["settings"].values()) and all(s["total"] <= 0 for s in g["settings"].values() if s["n"])]
|
|
375
|
+
if neg:
|
|
376
|
+
out.append(f"Tested and **rejected** for this book: a {' and a '.join(neg)} — each would have cost money on your biggest runs. Your edge is letting those run; don't fix what isn't leaking.")
|
|
377
|
+
return "\n".join(out)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def market(r):
|
|
381
|
+
m = r.get("market")
|
|
382
|
+
if not m:
|
|
383
|
+
return "## Market fit\n\nNo market read available."
|
|
384
|
+
b = r["book"]
|
|
385
|
+
out = ["## Market fit", "", f"**{m['headline']}**"]
|
|
386
|
+
sent = []
|
|
387
|
+
if m.get("median_funding_bp_8h") is not None:
|
|
388
|
+
sent.append(f"Funding is {m['median_funding_bp_8h']:+.0f} bp/8h across the coins you hold.")
|
|
389
|
+
if b["positions"]:
|
|
390
|
+
sent.append(f"You're {m['stance']}" + (f" — paying ~{usd(-m['funding_per_day'])}/day to hold." if m["funding_per_day"] < 0 else (f" — collecting ~{usd(m['funding_per_day'])}/day." if m["funding_per_day"] > 0 else ".")))
|
|
391
|
+
if m.get("btc"):
|
|
392
|
+
sent.append(f"BTC is {m['btc']['trend'].lower()} ({pct(m['btc']['change_7d'], 1, signed=True)} on the week).")
|
|
393
|
+
out.append(" ".join(sent))
|
|
394
|
+
if m["rows"]:
|
|
395
|
+
out += ["", "| Coin | Your side | Trend | Funding | Open interest | Fit |", "|---|---|---|---:|---:|---|"]
|
|
396
|
+
for x in m["rows"]:
|
|
397
|
+
fund = "—" if x["funding_bp_8h"] is None else "{:+.0f} bp/8h".format(x["funding_bp_8h"])
|
|
398
|
+
oi = "—" if not x["open_interest_usd"] else "${:,.0f}M".format(x["open_interest_usd"] / 1e6)
|
|
399
|
+
out.append("| {} | {} {}x | {} | {} | {} | **{}** |".format(x["coin"], x["side"].capitalize(), x["leverage"] or "", (x["trend"] or "—").capitalize(), fund, oi, x["fit"]))
|
|
400
|
+
return "\n".join(out)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def edge(r):
|
|
404
|
+
bs = r.get("setups") or {}
|
|
405
|
+
out = ["## Where your edge actually is", ""]
|
|
406
|
+
if bs.get("best"):
|
|
407
|
+
b = bs["best"][0]
|
|
408
|
+
out.append(f"**Your best setups:** {b['label']} — {b['wins']} of {b['n']} wins, profit factor {num(b['profit_factor'], 'x')}, {usd(b['realized'], signed=True)}." +
|
|
409
|
+
("".join(f" Also {x['label']}: {x['wins']}/{x['n']}, PF {num(x['profit_factor'], 'x')}." for x in bs["best"][1:3])))
|
|
410
|
+
else:
|
|
411
|
+
out.append("No setup clears a 1.5 profit factor on 3+ trades in this window — the sample is too small or the edge is not repeatable yet.")
|
|
412
|
+
if bs.get("worst"):
|
|
413
|
+
w = bs["worst"][0]
|
|
414
|
+
out.append(f"**Your worst:** {w['label']} — {w['wins']} of {w['n']} wins, {usd(w['realized'], signed=True)}.")
|
|
415
|
+
fam = r.get("families") or []
|
|
416
|
+
if fam:
|
|
417
|
+
out.append(f"\nThe way you win maps to the **{' / '.join(f.replace('_', ' ') for f in fam)}** families in the strategy catalog — the quick start if you want your quant to run this for you, under your name.")
|
|
418
|
+
out.append("\n_Process only — rules, risk and timing. Never a call to buy a coin._")
|
|
419
|
+
return "\n".join(out)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
CARD_FIELDS = ("id", "kind", "title", "detail", "usd", "window", "cta")
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _set_stop_loss_action(r, at_risk):
|
|
426
|
+
"""The protect card's `action`: one stop loss per at-risk position the engine priced one
|
|
427
|
+
for (`r["card_stop_losses"]`, written by desk.analyze from deep.card_stop_losses).
|
|
428
|
+
|
|
429
|
+
Matched on coin AND wallet: on a --book run two wallets can hold the same coin, and a stop loss priced
|
|
430
|
+
for one must never be offered on the other. An untagged position (a single-wallet run) is the
|
|
431
|
+
run's own address. None when no at-risk position has a stop loss — a card never carries an empty
|
|
432
|
+
action. Whether a card may carry an action at all is the runtime's call (ACTIONABLE_KINDS in
|
|
433
|
+
src/quant/types.ts); this only ever builds one for the protect card.
|
|
434
|
+
"""
|
|
435
|
+
stop_losses = r.get("card_stop_losses") or []
|
|
436
|
+
picked = []
|
|
437
|
+
for p in at_risk:
|
|
438
|
+
wallet = p.get("wallet") or r.get("address")
|
|
439
|
+
s = next((s for s in stop_losses if s["coin"] == p["coin"] and s["wallet"] == wallet), None)
|
|
440
|
+
if s is not None:
|
|
441
|
+
picked.append(s)
|
|
442
|
+
return dict(type="set_stop_loss", stop_losses=picked) if picked else None
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def next_step_items(r):
|
|
446
|
+
"""The desk's next steps as data — ONE producer for two readers.
|
|
447
|
+
|
|
448
|
+
`next_steps()` numbers these into Markdown; `cards()` projects them into the runtime's card
|
|
449
|
+
schema. Because both read this list, the prose a reader sees and the card the client renders
|
|
450
|
+
cannot disagree. `text` is the exact Markdown after the bold lead; `usd`/`window` are the leak
|
|
451
|
+
record's own values, never re-derived. An analyst run (whose == "other") has no items: its
|
|
452
|
+
"what to take" list is about someone else's book and is not a recommendation to the reader.
|
|
453
|
+
"""
|
|
454
|
+
if r.get("whose") == "other":
|
|
455
|
+
return []
|
|
456
|
+
b = r["book"]
|
|
457
|
+
items = []
|
|
458
|
+
at_risk = [p for p in b["positions"] if (p["liq_distance_pct"] is not None and p["liq_distance_pct"] < 5 and p["stop_covered_share"] < 0.9) or p["stop_covered_share"] == 0]
|
|
459
|
+
# "a hard floor now, a trailing lock as it runs … a signature on positions you already hold"
|
|
460
|
+
# promised something that does not exist for this reader. The integrated two-phase DSL is a
|
|
461
|
+
# RUNTIME feature, and `ratchet_stop_add` is keyed to a senpi strategy wallet — so on a book
|
|
462
|
+
# that sits on the reader's own wallet senpi cannot attach a ratchet, and the prose never
|
|
463
|
+
# promises one. What the reader CAN do is sign ONE fixed stop loss per position themselves: the card
|
|
464
|
+
# (not this text) carries it as a `set_stop_loss` action at the ladder's hard stop, and only when the
|
|
465
|
+
# engine priced one for a wallet the reader signs for (desk.analyze gates `card_stop_losses`). No
|
|
466
|
+
# trailing, no ladder; the Markdown names the naked positions and never mentions the button.
|
|
467
|
+
if at_risk:
|
|
468
|
+
coins = ", ".join(p["coin"] for p in at_risk)
|
|
469
|
+
protect = dict(id="protect-1", kind="protect", lead="Protect first.",
|
|
470
|
+
text=f"{coins}: every one of these is naked. Let me know if you want my help.",
|
|
471
|
+
title=f"Protect first: {coins}", detail=f"{coins}: every one of these is naked.",
|
|
472
|
+
usd=None, window=None, cta="Let me know if you want my help.")
|
|
473
|
+
action = _set_stop_loss_action(r, at_risk)
|
|
474
|
+
if action:
|
|
475
|
+
protect["action"] = action
|
|
476
|
+
items.append(protect)
|
|
477
|
+
priced = [l for l in r["leaks"] if not l.get("unpriced")]
|
|
478
|
+
if priced:
|
|
479
|
+
l = priced[0]
|
|
480
|
+
items.append(dict(id="leak-1", kind="leak", lead="Fix the biggest leak.",
|
|
481
|
+
text=f"{l['title']} — ~{usd(l['usd'])}/{l['window']}. {l['cta']}",
|
|
482
|
+
title=f"Fix the biggest leak: {l['title']}",
|
|
483
|
+
detail=" ".join(x for x in (l.get("evidence"), l.get("counterfactual")) if x),
|
|
484
|
+
usd=l["usd"], window=l["window"], cta=l["cta"]))
|
|
485
|
+
fam = r.get("families") or []
|
|
486
|
+
setup = fam[0].replace("_", " ") if fam else "your pattern"
|
|
487
|
+
hire = (f"Reply *hire my quant* and I'll run this desk on your book continuously — risk guard, smart money, "
|
|
488
|
+
f"market regime, leak finder — and turn your best setup ({setup}) into a strategy you approve, "
|
|
489
|
+
f"deployed as **your** strategy.")
|
|
490
|
+
items.append(dict(id="hire", kind="hire", lead="Keep the agents on.", text=hire, title="Keep the agents on",
|
|
491
|
+
detail=hire, usd=None, window=None, cta="Reply hire my quant"))
|
|
492
|
+
# The reader who does not want their own history mechanised still has somewhere to go. It is also
|
|
493
|
+
# the cheapest next step on the page: a sentence from them, no wallet, no deposit.
|
|
494
|
+
build = ("Tell me your thesis — what you think is about to happen and why — and I'll write the strategy "
|
|
495
|
+
"for it: the rules, the risk, the sizing, yours to approve before anything runs.")
|
|
496
|
+
items.append(dict(id="build", kind="build", lead="Or build something new.", text=build,
|
|
497
|
+
title="Or build something new", detail=build, usd=None, window=None, cta="Tell me your thesis"))
|
|
498
|
+
return items
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def next_steps(r):
|
|
502
|
+
if r.get("whose") == "other":
|
|
503
|
+
return next_steps_other(r)
|
|
504
|
+
out = ["## What your quant would do next", ""]
|
|
505
|
+
for i, it in enumerate(next_step_items(r), 1):
|
|
506
|
+
out.append(f"{i}. **{it['lead']}** {it['text']}")
|
|
507
|
+
return "\n".join(out)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def cards(r):
|
|
511
|
+
"""The next steps in the runtime's card schema: exactly CARD_FIELDS, plus `action` on a card the
|
|
512
|
+
engine made executable (today only the protect card's `set_stop_loss`). The runtime re-checks every
|
|
513
|
+
action against its allowlist before a client sees it."""
|
|
514
|
+
return [{**{k: it[k] for k in CARD_FIELDS}, **({"action": it["action"]} if it.get("action") else {})}
|
|
515
|
+
for it in next_step_items(r)]
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def strategy(r):
|
|
519
|
+
st = r.get("strategy") or {}
|
|
520
|
+
out = ["## What you've been doing", ""]
|
|
521
|
+
if not st.get("statements"):
|
|
522
|
+
out.append("Not enough trades in the window to read a strategy.")
|
|
523
|
+
return "\n".join(out)
|
|
524
|
+
out += [f"- {x[0].upper() + x[1:]}." for x in st["statements"]]
|
|
525
|
+
fp = st.get("fingerprint") or {}
|
|
526
|
+
rows = [x for x in fp.get("class_side") or [] if x["trades"] >= 2][:6]
|
|
527
|
+
if rows:
|
|
528
|
+
out += ["", "| Where the trades went | Trades | Wins | Realized | Profit factor |", "|---|---:|---:|---:|---:|"]
|
|
529
|
+
out += [f"| {x['label']} {x['side'].lower()}s ({', '.join(x['coins'][:3])}) | {x['trades']} | {x['wins']} | {usd(x['realized'], signed=True)} | {num(x['pf'], 'x')} |" for x in rows]
|
|
530
|
+
if st.get("critique"):
|
|
531
|
+
out += ["", "**The critique:**"] + [f"- {c}" for c in st["critique"]]
|
|
532
|
+
return "\n".join(out)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def context(r):
|
|
536
|
+
c = r.get("context") or {}; b = c.get("breadth") or {}
|
|
537
|
+
out = ["## The market you're trading in — right now", ""]
|
|
538
|
+
if b:
|
|
539
|
+
day = {"risk_on": "RISK-ON", "risk_off": "RISK-OFF", "mixed": "MIXED"}.get(b.get("day"), "UNKNOWN")
|
|
540
|
+
head = f"**{day}** — BTC {pct((b.get('btc_change_pct') or 0) / 100, 1, signed=True)} on the day, {pct(b.get('share_up'))} of perps up"
|
|
541
|
+
if b.get("memes_vs_majors") is not None:
|
|
542
|
+
head += f", memes {pct(b['memes_vs_majors'] / 100, 1, signed=True)} vs majors"
|
|
543
|
+
out.append(head + ".")
|
|
544
|
+
fr = c.get("funding_regime")
|
|
545
|
+
if fr and fr.get("regime"):
|
|
546
|
+
out.append(f"Funding regime (senpi): **{fr['regime']}**" + (f" — {fr['extreme_count']} extreme-funding assets" if fr.get("extreme_count") is not None else "") + ".")
|
|
547
|
+
groups = sorted((g for g in b.get("groups", {}).values()), key=lambda g: -g["oi_usd"])[:8]
|
|
548
|
+
if groups:
|
|
549
|
+
out += ["", "| Class | Avg 24h | Up / down | Median funding |", "|---|---:|---:|---:|"]
|
|
550
|
+
out += [f"| {g['label']} | {pct(g['avg_change_pct'] / 100, 1, signed=True)} | {g['up']} / {g['down']} | {g['median_funding_bp_8h']:+.1f} bp/8h |" for g in groups]
|
|
551
|
+
at = c.get("attention")
|
|
552
|
+
if at and at.get("markets"):
|
|
553
|
+
out += ["", "**Where the top traders' gains are right now** (senpi Hyperfeed, 4h): " + "; ".join(f"{m['coin']} {m['direction'].lower()}s {m['share_of_gains']:.0f}% ({m['traders']} traders)" for m in at["markets"][:5]) + "."]
|
|
554
|
+
if at.get("overlap"):
|
|
555
|
+
out.append("You: " + ", ".join(f"{o['coin']} **{o['read']}** the top traders" for o in at["overlap"]) + ".")
|
|
556
|
+
if at.get("momentum"):
|
|
557
|
+
out.append("Momentum events in the last 4h: " + ", ".join(f"{m['coin']} {m['direction'].lower()} ×{m['events']}" for m in at["momentum"][:5])
|
|
558
|
+
+ (f" — with you on {', '.join(at['with_momentum'])}" if at.get("with_momentum") else "") + (f"; against you on {', '.join(at['against_momentum'])}" if at.get("against_momentum") else "") + ".")
|
|
559
|
+
rp = c.get("regime_performance") or {}
|
|
560
|
+
cells = rp.get("cells") or {}
|
|
561
|
+
if cells:
|
|
562
|
+
hdr = ["", f"**How you trade the tape** — your record by the regime of the day you entered (last {r['days']} days: " + ", ".join(f"{v} {k.replace('_', '-')} days" for k, v in (rp.get('days') or {}).items()) + "):", "",
|
|
563
|
+
"| Entered on | Trades | Win rate | Realized | Profit factor |", "|---|---:|---:|---:|---:|"]
|
|
564
|
+
rows = []
|
|
565
|
+
for key in ("risk_on/ALL", "risk_off/ALL", "mixed/ALL", "risk_on/LONG", "risk_off/LONG", "risk_on/SHORT", "risk_off/SHORT"):
|
|
566
|
+
x = cells.get(key)
|
|
567
|
+
if x and x["trades"] >= 3:
|
|
568
|
+
rows.append(f"| {x['regime'].replace('_', '-')} days · {x['side'].lower() if x['side'] != 'ALL' else 'all'} | {x['trades']} | {pct(x['wins'] / x['trades'])} | {usd(x['realized'], signed=True)} | {num(x['pf'], 'x')} |")
|
|
569
|
+
if rows: # a header with no rows is a table that says nothing
|
|
570
|
+
out += hdr + rows
|
|
571
|
+
today = b.get("day")
|
|
572
|
+
best = max((x for x in cells.values() if x["side"] == "ALL" and x["trades"] >= 3), key=lambda x: (x["pf"] if x["pf"] not in (None, float("inf")) else 99), default=None)
|
|
573
|
+
if today and best:
|
|
574
|
+
same = best["regime"] == today
|
|
575
|
+
out.append(f"\nToday is **{today.replace('_', '-')}** — " + ("your best tape." if same else f"your best tape is {best['regime'].replace('_', '-')}; size accordingly."))
|
|
576
|
+
return "\n".join(out)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def smart_v2(r):
|
|
580
|
+
cohorts = r.get("cohorts") or []
|
|
581
|
+
out = ["## You vs smart money", ""]
|
|
582
|
+
if not cohorts:
|
|
583
|
+
out.append("No cohort view was available for this run.")
|
|
584
|
+
return "\n".join(out)
|
|
585
|
+
for cv in cohorts:
|
|
586
|
+
title = {"proven": "The proven cohort", "hot": "The hot 30-day cohort"}.get(cv["name"], cv["name"])
|
|
587
|
+
out += [f"**{title}** — _{cv['source']}_ · {cv['wallets']} wallets, {cv['coins']} coins held", ""]
|
|
588
|
+
if cv["rows"]:
|
|
589
|
+
out += ["| Coin | You | Cohort | Read |", "|---|---|---|---|"]
|
|
590
|
+
out += ["| {} | {} | {} | **{}** |".format(x["coin"], x["you"], x["cohort"], x["read"]) for x in cv["rows"]]
|
|
591
|
+
ag = cv.get("agreement")
|
|
592
|
+
if ag is not None:
|
|
593
|
+
out.append(f"\nBook-level agreement with this cohort: **{ag:+.2f}** (+1 = same side everywhere, −1 = opposite).")
|
|
594
|
+
if cv.get("tilt"):
|
|
595
|
+
out.append("The cohort's book by class: " + "; ".join(f"{t['label']} {'long' if t['bias'] > 0 else 'short'} {abs(t['bias']):.0%} net ({t['long']}L/{t['short']}S)" for t in cv["tilt"][:4]) + ".")
|
|
596
|
+
if cv.get("yours"):
|
|
597
|
+
out.append("Your book: " + "; ".join(f"{t['label']} {'long' if t['bias'] > 0 else 'short'} {abs(t['bias']):.0%} net, {t['weight']:.0%} of the book" for t in cv["yours"][:4]) + ".")
|
|
598
|
+
if cv.get("they_hold"):
|
|
599
|
+
out.append("The cohort holds, you don't: " + ", ".join(f"{h['coin']} ({h['side'].lower()}, {h['members']} wallets)" for h in cv["they_hold"][:6]) + ".")
|
|
600
|
+
if cv.get("you_alone"):
|
|
601
|
+
out.append("You hold, none of the cohort do: " + ", ".join(cv["you_alone"]) + ".")
|
|
602
|
+
if cv.get("entry_lag_h") is not None:
|
|
603
|
+
on = ', '.join(cv.get('lag_coins') or []) or 'the coins you share'
|
|
604
|
+
out.append(f"Entry timing: you entered in step with this cohort's median entry on {on}." if abs(cv['entry_lag_h']) < 1
|
|
605
|
+
else f"Entry timing: you are {abs(cv['entry_lag_h']):.0f}h {'behind' if cv['entry_lag_h'] > 0 else 'ahead of'} this cohort's median entry on {on}.")
|
|
606
|
+
out.append("")
|
|
607
|
+
bt = r.get("benchmark_table")
|
|
608
|
+
if bt and (r.get("benchmark") or {}).get("n", 0) >= 5:
|
|
609
|
+
out += [f"**You vs whale median** _(n={r['benchmark']['n']}, {r['benchmark'].get('computed_at', '')})_", "", "| Metric | You | Whale median |", "|---|---:|---:|"]
|
|
610
|
+
for row in bt:
|
|
611
|
+
you = num(row["you"], row["unit"]); wh = num(row["whale"], row["unit"]); mark = ""
|
|
612
|
+
if row["you"] is not None and row["whale"] is not None and row["you"] != float("inf"):
|
|
613
|
+
worse = (row["you"] > row["whale"]) if row["better"] == "lower" else (row["you"] < row["whale"])
|
|
614
|
+
mark = " 🔴" if worse else " 🟢"
|
|
615
|
+
out.append(f"| {row['metric']} | {you}{mark} | {wh} |")
|
|
616
|
+
return "\n".join(out)
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def scout(r):
|
|
620
|
+
opps = r.get("opportunities") or []
|
|
621
|
+
out = ["## Live matches — where today's market and your pattern agree", ""]
|
|
622
|
+
if not opps:
|
|
623
|
+
out.append("Nothing clears the bar right now: no coin has the cohorts, the tape and your own pattern on the same side. That is a read, not a gap.")
|
|
624
|
+
for o in opps:
|
|
625
|
+
held = f" — you already hold it {o['held'].lower()}" if o.get("held") else ""
|
|
626
|
+
out.append(f"- **{o['coin']} {o['side'].lower()}** ({o['cls']}, score {o['score']:.1f}){held}: " + "; ".join(o["why"]) + ".")
|
|
627
|
+
out.append("\n_Process only — rules, risk and timing. Never a call to buy a coin._")
|
|
628
|
+
return "\n".join(out)
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def followups_section(r):
|
|
632
|
+
fu = r.get("followups") or []
|
|
633
|
+
if not fu:
|
|
634
|
+
return ""
|
|
635
|
+
out = ["## Your quant is ready to go deeper", ""] + [f"{i}. {f['prompt']}" for i, f in enumerate(fu, 1)]
|
|
636
|
+
return "\n".join(out)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def render_deep(mode, d, r):
|
|
640
|
+
if mode == "protect":
|
|
641
|
+
out = ["## Stop ladder — every open position", "", f"Dollars at risk before: **{usd(d['total_risk_now'])}** → after: **{usd(d['total_risk_after'])}**", "",
|
|
642
|
+
"| Coin | Side | Mark | Hard stop | Distance | Daily range | Lock arms at | Covered today | Note |", "|---|---|---:|---:|---:|---:|---:|---:|---|"]
|
|
643
|
+
for x in d["rows"]:
|
|
644
|
+
atr = "—" if x["atr_pct"] is None else "{:.1f}%".format(x["atr_pct"])
|
|
645
|
+
out.append("| {} | {} | {:,.4g} | {:,.4g} | {:.1f}% | {} | {:,.4g} | {} | {} |".format(x["coin"], x["side"], x["mark"], x["hard_stop"], x["hard_stop_pct"], atr, x["lock_arms_at"], pct(x["covered_now"]), x["note"]))
|
|
646
|
+
# Same overclaim as the next-steps block: there is no signature to give for a book on the
|
|
647
|
+
# reader's own wallet. These levels are still the most actionable thing on the page — they are
|
|
648
|
+
# a worksheet, so say that plainly.
|
|
649
|
+
out += ["", "The hard stop sits one and a half days of normal range from the mark — the average "
|
|
650
|
+
"24-hour high-to-low of the last two weeks — or closer when liquidation is nearer "
|
|
651
|
+
"than that, because the stop has to trigger first. The lock trails at half the peak "
|
|
652
|
+
"gain once the trade is two ranges in the money.",
|
|
653
|
+
"", "**These are yours to place.** The *Hard stop* column is the number to set on each "
|
|
654
|
+
"position onchain on Hyperliquid; the *Lock arms at* column is where a trailing "
|
|
655
|
+
"stop should begin once the trade is in the money. Tell me if you want help with "
|
|
656
|
+
"any of them."]
|
|
657
|
+
return "\n".join(out)
|
|
658
|
+
if mode == "replay":
|
|
659
|
+
if not d or d.get("empty"):
|
|
660
|
+
return "No losing week in the window."
|
|
661
|
+
out = ["## Your worst week", "", f"Week of {datetime.datetime.fromtimestamp(d['start'] / 1000, datetime.timezone.utc).strftime('%Y-%m-%d')}: **{usd(d['realized'], signed=True)}** over {d['trades']} trades ({d['losers']} losers).", "",
|
|
662
|
+
"| Coin | Side | Hold | Realized |", "|---|---|---:|---:|"]
|
|
663
|
+
out += [f"| {e['coin']} | {e['direction']} | {hrs(e['hold_h'])} | {usd(e['realized'], signed=True)} |" for e in d["biggest"]]
|
|
664
|
+
for label, g in (("A time-cut on losers", d.get("cut")), ("A trailing lock on winners", d.get("lock"))):
|
|
665
|
+
if g and g.get("settings"):
|
|
666
|
+
out.append(f"\n**{label}** on exactly these trades: " + ", ".join(f"{k}: {usd(v['total'], signed=True)}" for k, v in g["settings"].items() if v["n"]) + (f" → robust {usd(g['robust'], signed=True)}" if g.get("robust") else " → not robust; would not have helped"))
|
|
667
|
+
if d.get("green_first") is not None:
|
|
668
|
+
out.append(f"\n{pct(d['green_first'])} of that week's losers were green first.")
|
|
669
|
+
return "\n".join(out)
|
|
670
|
+
if mode == "funding":
|
|
671
|
+
out = ["## Funding — next 30 days at today's rates", "", "| Coin | Side | Notional | Rate | Per day | 30 days | Paid since open |", "|---|---|---:|---:|---:|---:|---:|"]
|
|
672
|
+
out += [f"| {x['coin']} | {x['side']} | {usd(x['notional'])} | {x['rate_bp_8h']:+.1f} bp/8h | {usd(x['per_day'], signed=True)} | {usd(x['thirty_days'], signed=True)} | {usd(x['since_open'], signed=True)} |" for x in d["rows"]]
|
|
673
|
+
out.append(f"\nTotal: **{usd(d['thirty_days'], signed=True)}** over 30 days" + (f" — {pct(d['share_of_equity'])} of your equity" if d.get("share_of_equity") is not None else "") + (f". The payers: {', '.join(d['payers'])}." if d.get("payers") else "."))
|
|
674
|
+
return "\n".join(out)
|
|
675
|
+
if mode == "compare":
|
|
676
|
+
rc, pr = d.get("recent"), d.get("prior")
|
|
677
|
+
if not rc or not pr:
|
|
678
|
+
return "Not enough trades in both windows to compare."
|
|
679
|
+
rows = [("Trades", rc["trades"], pr["trades"], None), ("Win rate", rc["win_rate"], pr["win_rate"], "%"), ("Profit factor", rc["pf"], pr["pf"], "x"), ("Realized", rc["realized"], pr["realized"], "$"),
|
|
680
|
+
("Fees", rc["fees"], pr["fees"], "$"), ("Avg size", rc["avg_size"], pr["avg_size"], "$"), ("Median hold — winners", rc["hold_w"], pr["hold_w"], "h"), ("Median hold — losers", rc["hold_l"], pr["hold_l"], "h"), ("Taker share", rc["taker"], pr["taker"], "%")]
|
|
681
|
+
out = ["## Last 30 days vs the 60 before", "", "| Metric | Last 30d | Prior 60d |", "|---|---:|---:|"]
|
|
682
|
+
for m, a, b, u in rows:
|
|
683
|
+
f = (lambda v: "—" if v is None else (f"{v:,.0f}" if u is None else (usd(v) if u == "$" else num(v, u))))
|
|
684
|
+
out.append(f"| {m} | {f(a)} | {f(b)} |")
|
|
685
|
+
return "\n".join(out)
|
|
686
|
+
if mode == "rules":
|
|
687
|
+
out = ["## Your strategy as a rule set", ""]
|
|
688
|
+
out += [f"- **Entry setups:** {'; '.join(d['entries']) if d['entries'] else 'no setup clears the bar yet'}", f"- **Entry timing:** {d['entry_rule']}", f"- **Holding:** {d['hold_rule']}", f"- **Sizing:** {d['size_rule']}", f"- **Risk:** {d['risk_rule']}", f"- **Catalog families:** {', '.join(f.replace('_', ' ') for f in d['families'])}", "", d["handoff"] + "."]
|
|
689
|
+
return "\n".join(out)
|
|
690
|
+
if mode == "regime":
|
|
691
|
+
rr = dict(r); rr["context"] = dict(r.get("context") or {}, breadth=dict((r.get("context") or {}).get("breadth") or {}, day=d.get("today")))
|
|
692
|
+
return context(rr)
|
|
693
|
+
if mode == "watch":
|
|
694
|
+
out = ["## What your agents would watch", ""] + [f"- {x}" for x in d["items"]] + ["", "Say *hire my quant* to keep them on your book."]
|
|
695
|
+
return "\n".join(out)
|
|
696
|
+
if mode == "smart":
|
|
697
|
+
return smart_v2(dict(r, cohorts=d.get("cohorts")))
|
|
698
|
+
if mode == "scout":
|
|
699
|
+
return scout(dict(r, opportunities=d.get("opportunities")))
|
|
700
|
+
if mode == "strategy":
|
|
701
|
+
return strategy(dict(r, strategy=d))
|
|
702
|
+
return ""
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def next_steps_other(r):
|
|
706
|
+
bs = (r.get("setups") or {}).get("best") or []
|
|
707
|
+
fam = r.get("families") or []
|
|
708
|
+
b = r["book"]
|
|
709
|
+
out = ["## What to take from this trader", ""]
|
|
710
|
+
if bs:
|
|
711
|
+
x = bs[0]
|
|
712
|
+
out.append(f"1. **The playbook.** {x['label']} — {x['wins']} of {x['n']} wins, profit factor {num(x['profit_factor'], 'x')}. Say *write their playbook as rules* and your quant turns it into a rule set that runs under **your** name — the {' / '.join(f.replace('_', ' ') for f in fam) if fam else 'closest'} templates are the quick start.")
|
|
713
|
+
else:
|
|
714
|
+
out.append("1. **The playbook.** No setup clears the bar on this window — what works here is not yet repeatable enough to copy.")
|
|
715
|
+
warn = copy_warnings(b, r.get("cohorts"))
|
|
716
|
+
out.append("2. **Before copying anything.** " + ("; ".join(warn) + ". Mirroring inherits all of that." if warn else "The book is protected and the cohorts are with it — the process is copyable; the timing is not.") + " Say *is this trader worth copying* for the copyability read.")
|
|
717
|
+
out.append("3. **Learn the pattern, not the position.** The regime table says which tape they win in; the size-vs-outcome table says how they size. Those transfer. Their entries do not — by the time you see them, the move is theirs.")
|
|
718
|
+
return "\n".join(out)
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
COMPARE_ROWS = (("Weekly rank", lambda r: f"#{r['rank']['rank']:,}" if r.get("rank") else "—"), ("Archetype", lambda r: r["archetype"]),
|
|
722
|
+
("Quant score", lambda r: str(r["quant_score"]) if r.get("quant_score") is not None else "—"), ("Net P&L (ledger)", lambda r: usd(r["track"].get("ledger_net"), signed=True)),
|
|
723
|
+
("Return on avg equity", lambda r: pct(r["equity"].get("return_on_avg_equity"), 1, signed=True)), ("Max drawdown", lambda r: pct(-r["drawdown"]["dd_pct"], 0, signed=True) if r["drawdown"].get("dd_pct") is not None else "—"),
|
|
724
|
+
("Trades / win rate", lambda r: f"{r['track']['trades']} / {pct(r['track'].get('win_rate'))}"), ("Profit factor", lambda r: num(r["track"].get("profit_factor"), "x")),
|
|
725
|
+
("Taker share", lambda r: pct(r["track"].get("taker_share"))), ("Costs ÷ gross income", lambda r: pct(r["track"].get("cost_ratio"))),
|
|
726
|
+
("Open positions · unprotected", lambda r: f"{len(r['book']['positions'])} · {len(r['book']['naked'])}"), ("Margin used", lambda r: pct(r["book"].get("margin_utilization"))),
|
|
727
|
+
("vs proven cohort", lambda r: _agree(r, "proven")), ("vs hot 30-day cohort", lambda r: _agree(r, "hot")),
|
|
728
|
+
("Biggest leak", lambda r: f"{r['leaks'][0]['title']} (~{usd(r['leaks'][0]['usd'])})" if r["leaks"] else "—"),
|
|
729
|
+
("Best setup", lambda r: (lambda b: f"{b['label']} ({b['wins']}/{b['n']}, PF {num(b['profit_factor'], 'x')})")(r["setups"]["best"][0]) if (r.get("setups") or {}).get("best") else "—"))
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def _agree(r, name):
|
|
733
|
+
c = next((c for c in r.get("cohorts") or [] if c["name"] == name), None)
|
|
734
|
+
if not c or c.get("agreement") is None:
|
|
735
|
+
return "—"
|
|
736
|
+
a = c["agreement"]
|
|
737
|
+
return ("with" if a > 0.3 else ("against" if a < -0.3 else "split")) + f" ({a:+.2f})"
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def render_compare(rs):
|
|
741
|
+
heads = [f"`{short(r['address'])}`" for r in rs]
|
|
742
|
+
out = ["# Side by side — " + " · ".join(heads), "", "| | " + " | ".join(heads) + " |", "|---|" + "---|" * len(rs)]
|
|
743
|
+
for label, fn in COMPARE_ROWS:
|
|
744
|
+
cells = []
|
|
745
|
+
for r in rs:
|
|
746
|
+
try:
|
|
747
|
+
cells.append(fn(r))
|
|
748
|
+
except Exception: # noqa: BLE001
|
|
749
|
+
cells.append("—")
|
|
750
|
+
out.append(f"| {label} | " + " | ".join(cells) + " |")
|
|
751
|
+
out += ["", "**Verdicts**"] + [f"- `{short(r['address'])}`: {r['verdict']}" for r in rs]
|
|
752
|
+
# what separates them: the widest gaps on the things that transfer
|
|
753
|
+
def val(r, k):
|
|
754
|
+
return r["track"].get(k)
|
|
755
|
+
seps = []
|
|
756
|
+
for k, label, better_low in (("profit_factor", "profit factor", False), ("taker_share", "taker share", True), ("cost_ratio", "cost share of income", True), ("win_rate", "win rate", False)):
|
|
757
|
+
vals = [(val(r, k), short(r["address"])) for r in rs if val(r, k) not in (None, float("inf"))]
|
|
758
|
+
if len(vals) >= 2:
|
|
759
|
+
lo, hi = min(vals), max(vals)
|
|
760
|
+
if hi[0] and lo[0] is not None and (hi[0] - lo[0]) > (0.15 if k in ("taker_share", "cost_ratio", "win_rate") else 0.8):
|
|
761
|
+
best = lo if better_low else hi
|
|
762
|
+
seps.append(f"{label}: `{best[1]}` leads ({num(best[0], 'x') if k == 'profit_factor' else pct(best[0])} vs {num((hi if better_low else lo)[0], 'x') if k == 'profit_factor' else pct((hi if better_low else lo)[0])})")
|
|
763
|
+
naked = [(len(r["book"]["naked"]), short(r["address"])) for r in rs]
|
|
764
|
+
if max(n for n, _ in naked) != min(n for n, _ in naked):
|
|
765
|
+
seps.append("protection: " + ", ".join(f"`{a}` {n} unprotected" for n, a in naked))
|
|
766
|
+
if seps:
|
|
767
|
+
out += ["", "**What separates them**"] + [f"- {s}" for s in seps]
|
|
768
|
+
import voice
|
|
769
|
+
return voice.third_person("\n".join(out))
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
RENDERERS = {"overview": overview, "strategy": strategy, "context": context, "protection": protection, "performance": performance, "leaks": leaks, "smart": smart_v2,
|
|
773
|
+
"market": market, "edge": edge, "scout": scout, "next": next_steps, "followups": followups_section}
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def render(r, sections=None):
|
|
777
|
+
parts = [header(r), ""]
|
|
778
|
+
for s in (sections or SECTIONS):
|
|
779
|
+
parts += [RENDERERS[s](r), ""]
|
|
780
|
+
meta = r.get("meta") or {}
|
|
781
|
+
if meta.get("warnings"):
|
|
782
|
+
parts.append("_Notes: " + " · ".join(meta["warnings"]) + "_\n")
|
|
783
|
+
md = "\n".join(parts)
|
|
784
|
+
if r.get("whose") == "other":
|
|
785
|
+
import voice
|
|
786
|
+
md = voice.third_person(md, short(r["address"]))
|
|
787
|
+
return md
|