Simplify public disclosure reader UI
Browse files- README.md +4 -5
- app.py +539 -131
- space_bundle_manifest.json +5 -5
README.md
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji: 📊
|
| 4 |
colorFrom: green
|
| 5 |
colorTo: yellow
|
| 6 |
sdk: gradio
|
|
@@ -10,8 +9,8 @@ pinned: false
|
|
| 10 |
license: cc-by-4.0
|
| 11 |
---
|
| 12 |
|
| 13 |
-
#
|
| 14 |
|
| 15 |
-
|
| 16 |
|
| 17 |
-
This Space does not claim intent, illegality, causality, insider trading, realized private returns, or
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Senate Disclosure Reader
|
|
|
|
| 3 |
colorFrom: green
|
| 4 |
colorTo: yellow
|
| 5 |
sdk: gradio
|
|
|
|
| 9 |
license: cc-by-4.0
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# Senate Disclosure Reader
|
| 13 |
|
| 14 |
+
Plain-English reader for official Senate financial disclosures: search filers, reported wealth ranges, transaction rows, filing dates, and source receipts.
|
| 15 |
|
| 16 |
+
This Space does not claim intent, illegality, causality, insider trading, realized private returns, or market-beating performance.
|
app.py
CHANGED
|
@@ -82,8 +82,8 @@ def window_label(value: str) -> str:
|
|
| 82 |
|
| 83 |
def claim_label(value: str) -> str:
|
| 84 |
labels = {
|
| 85 |
-
"blocked_market_data_policy": "Not
|
| 86 |
-
"blocked": "Not
|
| 87 |
}
|
| 88 |
return labels.get(str(value), str(value).replace("_", " "))
|
| 89 |
|
|
@@ -101,6 +101,7 @@ RETURN_HANDOFF = pd.read_csv(DATA / "reporter_handoff" / "return_test_summary_ha
|
|
| 101 |
TRADES = read_jsonl_gz("trades.jsonl.gz")
|
| 102 |
WEALTH = read_jsonl_gz("net_worth_intervals.jsonl.gz")
|
| 103 |
FILINGS = read_jsonl_gz("filings.jsonl.gz")
|
|
|
|
| 104 |
|
| 105 |
TRADES["transaction_year"] = pd.to_datetime(TRADES["transaction_date"], errors="coerce").dt.year
|
| 106 |
TRADES["filing_lag_days_num"] = pd.to_numeric(TRADES.get("filing_lag_days"), errors="coerce")
|
|
@@ -111,26 +112,143 @@ for col in ["net_worth_min", "net_worth_midpoint", "net_worth_max", "uncertainty
|
|
| 111 |
WEALTH[col] = pd.to_numeric(WEALTH.get(col), errors="coerce")
|
| 112 |
|
| 113 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
def hero_html() -> str:
|
| 115 |
return """
|
| 116 |
-
<section class="hero-panel"
|
| 117 |
-
<div
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
filing delays, and cautious market-context checks. It is built to show what the records say, where the uncertainty is,
|
| 124 |
-
and what cannot be concluded from the disclosures alone.
|
| 125 |
-
</p>
|
| 126 |
-
</div>
|
| 127 |
-
<div style="background:#f8fafc;border:1px solid #d7dee8;border-radius:10px;padding:14px 16px;min-width:250px;flex:0 1 310px;">
|
| 128 |
-
<div style="color:#64748b;font-size:.84rem;font-weight:800;text-transform:uppercase;letter-spacing:.04em;">Current evidence status</div>
|
| 129 |
-
<div style="color:#0f172a;font-size:1.35rem;font-weight:850;line-height:1.2;margin-top:5px;">Disclosure explorer: ready</div>
|
| 130 |
-
<div style="color:#8a4b00;font-size:1rem;font-weight:800;margin-top:10px;">Market-beating proof: not established</div>
|
| 131 |
-
<p style="color:#475569;font-size:.95rem;line-height:1.45;margin:8px 0 0;">The market checks are exploratory because the public bundle does not use a licensed total-return source with delisting handling.</p>
|
| 132 |
-
</div>
|
| 133 |
-
</div>
|
| 134 |
</section>
|
| 135 |
"""
|
| 136 |
|
|
@@ -138,87 +256,87 @@ def hero_html() -> str:
|
|
| 138 |
def plain_english_cards_html() -> str:
|
| 139 |
cards = [
|
| 140 |
(
|
| 141 |
-
"
|
| 142 |
-
"
|
| 143 |
"#e8f7f3",
|
| 144 |
"#0f766e",
|
| 145 |
),
|
| 146 |
(
|
| 147 |
-
"
|
| 148 |
-
"
|
| 149 |
"#eef4ff",
|
| 150 |
"#3155b7",
|
| 151 |
),
|
| 152 |
(
|
| 153 |
-
"
|
| 154 |
-
"
|
| 155 |
"#fff7e8",
|
| 156 |
"#8a4b00",
|
| 157 |
),
|
| 158 |
]
|
| 159 |
cells = "".join(
|
| 160 |
f"""
|
| 161 |
-
<div style="background:{bg};border
|
| 162 |
-
<div style="color:{accent};
|
| 163 |
-
<div
|
| 164 |
</div>
|
| 165 |
"""
|
| 166 |
for title, body, bg, accent in cards
|
| 167 |
)
|
| 168 |
-
return f"<div class='plain-cards'
|
| 169 |
|
| 170 |
|
| 171 |
def metric_html() -> str:
|
| 172 |
counts = MANIFEST.get("counts", {})
|
| 173 |
dataset = counts.get("dataset", {})
|
| 174 |
-
returns = counts.get("return_tests", {})
|
| 175 |
items = [
|
| 176 |
-
(
|
| 177 |
-
(
|
| 178 |
-
(
|
| 179 |
-
("Wealth range estimates", fmt_int(dataset.get("net_worth_intervals", "")), "low/mid/high disclosure estimates"),
|
| 180 |
-
("Market-context event rows", fmt_int(returns.get("return_test_events", "")), "exploratory benchmark comparisons"),
|
| 181 |
-
("Public alpha claim", "Not established", "blocked by market-data standard"),
|
| 182 |
]
|
| 183 |
cells = "".join(
|
| 184 |
-
f""
|
| 185 |
-
|
| 186 |
-
<div class='metric-value' style='font-size:1.38rem;font-weight:850;color:#0f766e;line-height:1.15;overflow-wrap:anywhere;'>{value}</div>
|
| 187 |
-
<div class='metric-label' style='font-size:.92rem;color:#0f172a;font-weight:800;margin-top:5px;line-height:1.25;'>{label}</div>
|
| 188 |
-
<div style='font-size:.82rem;color:#475569;font-weight:600;margin-top:4px;line-height:1.25;'>{note}</div>
|
| 189 |
-
</div>
|
| 190 |
-
"""
|
| 191 |
-
for label, value, note in items
|
| 192 |
)
|
| 193 |
-
return f"<div class='
|
| 194 |
|
| 195 |
|
| 196 |
def boundary_html() -> str:
|
| 197 |
return """
|
| 198 |
-
<div class='boundary'
|
| 199 |
-
<strong
|
| 200 |
-
|
| 201 |
-
insider trading, realized private returns, or proven above-market performance.
|
| 202 |
</div>
|
| 203 |
"""
|
| 204 |
|
| 205 |
|
| 206 |
def overview_intro_html() -> str:
|
| 207 |
return """
|
| 208 |
-
<div class="
|
| 209 |
-
<
|
| 210 |
-
<
|
| 211 |
-
|
| 212 |
-
|
|
|
|
|
|
|
| 213 |
</div>
|
| 214 |
</div>
|
| 215 |
"""
|
| 216 |
|
| 217 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
def market_note_html() -> str:
|
| 219 |
return """
|
| 220 |
<div class="section-intro" style="background:#f8fafc;border:1px solid #cbd5e1;border-radius:10px;padding:15px 16px;margin:12px 0;">
|
| 221 |
-
<div style="color:#0f172a;font-size:1.08rem;font-weight:
|
| 222 |
<div style="color:#334155;font-size:1rem;line-height:1.55;">
|
| 223 |
Bars above zero mean the disclosed transaction set moved more than a simple benchmark in that window under one assumption.
|
| 224 |
Bars below zero mean it moved less. Because this public bundle uses no-cost adjusted-close data, every row remains exploratory.
|
|
@@ -235,20 +353,19 @@ def return_chart():
|
|
| 235 |
name=str(date_basis).capitalize(),
|
| 236 |
x=frame["window_label"],
|
| 237 |
y=frame["equal_weight_alpha_pct"],
|
| 238 |
-
customdata=frame[["claim_status_label", "n_events"
|
| 239 |
hovertemplate=(
|
| 240 |
"%{x}<br>"
|
| 241 |
"Benchmark difference: %{y:.3f}%<br>"
|
| 242 |
"Rows: %{customdata[1]}<br>"
|
| 243 |
-
"%{customdata[0]}<
|
| 244 |
-
"q-value: %{customdata[2]}<extra></extra>"
|
| 245 |
),
|
| 246 |
)
|
| 247 |
)
|
| 248 |
fig.add_hline(y=0, line_width=1, line_color="#555")
|
| 249 |
fig.update_layout(
|
| 250 |
-
title="
|
| 251 |
-
yaxis_title="
|
| 252 |
xaxis_title="Market window after date",
|
| 253 |
barmode="group",
|
| 254 |
margin=dict(l=64, r=20, t=70, b=62),
|
|
@@ -297,9 +414,9 @@ def top_ticker_chart():
|
|
| 297 |
counts.columns = ["ticker", "rows"]
|
| 298 |
fig = px.bar(counts, x="rows", y="ticker", orientation="h", color="rows", color_continuous_scale="Viridis")
|
| 299 |
fig.update_layout(
|
| 300 |
-
title="
|
| 301 |
-
xaxis_title="
|
| 302 |
-
yaxis_title="
|
| 303 |
margin=dict(l=80, r=20, t=60, b=50),
|
| 304 |
coloraxis_showscale=False,
|
| 305 |
)
|
|
@@ -314,7 +431,18 @@ def wealth_senators() -> list[str]:
|
|
| 314 |
def wealth_chart(senator_id: str):
|
| 315 |
frame = WEALTH[WEALTH["senator_id"] == senator_id].sort_values("report_year_num")
|
| 316 |
if frame.empty:
|
| 317 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
fig = go.Figure()
|
| 319 |
fig.add_trace(
|
| 320 |
go.Scatter(
|
|
@@ -349,7 +477,7 @@ def wealth_chart(senator_id: str):
|
|
| 349 |
)
|
| 350 |
)
|
| 351 |
fig.update_layout(
|
| 352 |
-
title=f"Reported wealth range over time: {senator_id}",
|
| 353 |
xaxis_title="Report year",
|
| 354 |
yaxis_title="Estimated range from disclosure brackets ($)",
|
| 355 |
margin=dict(l=70, r=20, t=60, b=50),
|
|
@@ -381,18 +509,84 @@ def wealth_table(senator_id: str) -> pd.DataFrame:
|
|
| 381 |
)
|
| 382 |
|
| 383 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
def trade_search(query: str, min_lag: int, max_rows: int) -> pd.DataFrame:
|
| 385 |
frame = TRADES.copy()
|
| 386 |
if query:
|
| 387 |
q = query.lower().strip()
|
|
|
|
| 388 |
frame = frame[
|
| 389 |
frame["asset_name"].fillna("").str.lower().str.contains(q, regex=False)
|
| 390 |
| frame["ticker_candidate"].fillna("").str.lower().str.contains(q, regex=False)
|
| 391 |
| frame["senator_id"].fillna("").str.lower().str.contains(q, regex=False)
|
|
|
|
| 392 |
]
|
| 393 |
frame = frame[frame["filing_lag_days_num"].fillna(-1) >= min_lag]
|
|
|
|
|
|
|
| 394 |
cols = [
|
| 395 |
-
"
|
| 396 |
"transaction_date",
|
| 397 |
"filing_date",
|
| 398 |
"filing_lag_days",
|
|
@@ -404,16 +598,16 @@ def trade_search(query: str, min_lag: int, max_rows: int) -> pd.DataFrame:
|
|
| 404 |
"amount_max",
|
| 405 |
"what_this_does_not_prove",
|
| 406 |
]
|
| 407 |
-
return frame[cols].
|
| 408 |
columns={
|
| 409 |
-
"
|
| 410 |
"transaction_date": "Reported transaction date",
|
| 411 |
"filing_date": "Public filing date",
|
| 412 |
"filing_lag_days": "Filing lag days",
|
| 413 |
"transaction_type": "Disclosure transaction type",
|
| 414 |
"owner": "Reported owner",
|
| 415 |
"asset_name": "Asset name from filing",
|
| 416 |
-
"ticker_candidate": "Ticker
|
| 417 |
"amount_min": "Amount low",
|
| 418 |
"amount_max": "Amount high",
|
| 419 |
"what_this_does_not_prove": "What this row does not prove",
|
|
@@ -421,6 +615,12 @@ def trade_search(query: str, min_lag: int, max_rows: int) -> pd.DataFrame:
|
|
| 421 |
)
|
| 422 |
|
| 423 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 424 |
def return_handoff_display() -> pd.DataFrame:
|
| 425 |
frame = RETURN_HANDOFF.copy()
|
| 426 |
if "date_basis" in frame:
|
|
@@ -439,7 +639,6 @@ def return_handoff_display() -> pd.DataFrame:
|
|
| 439 |
"midpoint_weight_alpha_pct",
|
| 440 |
"bootstrap_ci_low_pct",
|
| 441 |
"bootstrap_ci_high_pct",
|
| 442 |
-
"q_value",
|
| 443 |
"claim_status",
|
| 444 |
"plain_english_status",
|
| 445 |
]
|
|
@@ -448,15 +647,14 @@ def return_handoff_display() -> pd.DataFrame:
|
|
| 448 |
columns={
|
| 449 |
"date_basis": "Measured from",
|
| 450 |
"window": "Window",
|
| 451 |
-
"events": "
|
| 452 |
-
"senators": "
|
| 453 |
-
"tickers": "
|
| 454 |
"equal_weight_alpha_pct": "Equal-weight benchmark difference (%)",
|
| 455 |
"midpoint_weight_alpha_pct": "Midpoint-weight benchmark difference (%)",
|
| 456 |
"bootstrap_ci_low_pct": "Bootstrap low (%)",
|
| 457 |
"bootstrap_ci_high_pct": "Bootstrap high (%)",
|
| 458 |
-
"
|
| 459 |
-
"claim_status": "Claim status",
|
| 460 |
"plain_english_status": "Plain English status",
|
| 461 |
}
|
| 462 |
)
|
|
@@ -480,6 +678,23 @@ def file_inventory() -> pd.DataFrame:
|
|
| 480 |
return pd.DataFrame(rows)
|
| 481 |
|
| 482 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
CSS = """
|
| 484 |
:root {
|
| 485 |
--accent: #1b6b63;
|
|
@@ -489,6 +704,7 @@ CSS = """
|
|
| 489 |
--line: #cbd5e1;
|
| 490 |
--soft: #f8fafc;
|
| 491 |
--font: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
|
|
| 492 |
}
|
| 493 |
html,
|
| 494 |
body,
|
|
@@ -499,6 +715,7 @@ body,
|
|
| 499 |
.gradio-container .contain {
|
| 500 |
background: #f6f8fb !important;
|
| 501 |
color: var(--ink) !important;
|
|
|
|
| 502 |
}
|
| 503 |
.dark .gradio-container,
|
| 504 |
.dark .gradio-container .main,
|
|
@@ -506,6 +723,7 @@ body,
|
|
| 506 |
.dark .gradio-container .contain {
|
| 507 |
background: #f6f8fb !important;
|
| 508 |
color: var(--ink) !important;
|
|
|
|
| 509 |
}
|
| 510 |
.gradio-container,
|
| 511 |
.gradio-container * {
|
|
@@ -516,13 +734,177 @@ body,
|
|
| 516 |
letter-spacing: 0;
|
| 517 |
}
|
| 518 |
.gradio-container {
|
| 519 |
-
max-width:
|
| 520 |
margin: auto;
|
| 521 |
color: var(--ink);
|
| 522 |
font-size: 16px;
|
| 523 |
line-height: 1.48;
|
| 524 |
padding: 0 18px 28px;
|
| 525 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 526 |
.gradio-container h1,
|
| 527 |
.gradio-container .prose h1,
|
| 528 |
.gradio-container .markdown h1 {
|
|
@@ -545,7 +927,7 @@ body,
|
|
| 545 |
.gradio-container button[role="tab"] {
|
| 546 |
color: #0f172a !important;
|
| 547 |
font-size: 1rem !important;
|
| 548 |
-
font-weight:
|
| 549 |
}
|
| 550 |
.gradio-container button[role="tab"][aria-selected="true"] {
|
| 551 |
color: var(--accent) !important;
|
|
@@ -561,19 +943,37 @@ body,
|
|
| 561 |
font-size: 1rem !important;
|
| 562 |
background: #ffffff !important;
|
| 563 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 564 |
.gradio-container .block,
|
| 565 |
.gradio-container .form,
|
| 566 |
.gradio-container .panel,
|
| 567 |
.gradio-container .plot-container,
|
| 568 |
.gradio-container .table-wrap,
|
| 569 |
-
.gradio-container .dataframe
|
|
|
|
|
|
|
| 570 |
background: #ffffff !important;
|
| 571 |
color: #111827 !important;
|
| 572 |
}
|
| 573 |
.metrics { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 12px; margin: 14px 0 18px; }
|
| 574 |
.metric { border: 1px solid var(--line); border-radius: 8px; padding: 13px 14px; background: #ffffff; }
|
| 575 |
.metric-value { font-size: 1.45rem; font-weight: 800; color: var(--accent); line-height: 1.18; overflow-wrap: anywhere; }
|
| 576 |
-
.metric-label { font-size: .92rem; color: var(--muted); font-weight:
|
| 577 |
.boundary {
|
| 578 |
border-left: 5px solid var(--warn);
|
| 579 |
background: #fff7e8;
|
|
@@ -596,7 +996,7 @@ body,
|
|
| 596 |
.gradio-container th {
|
| 597 |
background: #eef4f8 !important;
|
| 598 |
color: #0f172a !important;
|
| 599 |
-
font-weight:
|
| 600 |
}
|
| 601 |
.gradio-container td {
|
| 602 |
background: #ffffff !important;
|
|
@@ -606,16 +1006,16 @@ body,
|
|
| 606 |
border-color: var(--line) !important;
|
| 607 |
}
|
| 608 |
@media (max-width: 900px) {
|
| 609 |
-
.
|
| 610 |
-
.hero-panel h1 { font-size:
|
| 611 |
}
|
| 612 |
@media (max-width: 620px) {
|
| 613 |
-
.
|
| 614 |
}
|
| 615 |
"""
|
| 616 |
|
| 617 |
|
| 618 |
-
with gr.Blocks(title="
|
| 619 |
gr.HTML(hero_html())
|
| 620 |
gr.HTML(plain_english_cards_html())
|
| 621 |
gr.HTML(metric_html())
|
|
@@ -624,57 +1024,65 @@ with gr.Blocks(title="Senator Financial Disclosure Explorer") as demo:
|
|
| 624 |
with gr.Tabs():
|
| 625 |
with gr.Tab("Start Here"):
|
| 626 |
gr.HTML(overview_intro_html())
|
| 627 |
-
with gr.Row():
|
| 628 |
-
gr.Plot(value=filing_delay_chart(), label="Filing delay: how late were public reports?")
|
| 629 |
-
gr.Plot(value=top_ticker_chart(), label="Ticker candidates: what appears in filings?")
|
| 630 |
gr.HTML(
|
| 631 |
-
"<div class='
|
| 632 |
-
"<
|
| 633 |
-
"<
|
| 634 |
"</div>"
|
| 635 |
)
|
| 636 |
|
| 637 |
-
with gr.Tab("
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
selector.change(wealth_table, inputs=selector, outputs=wealth_rows)
|
| 645 |
-
|
| 646 |
-
with gr.Tab("Disclosed Transactions"):
|
| 647 |
-
gr.HTML("<div class='section-intro' style='background:#ffffff;border:1px solid #cbd5e1;border-radius:10px;padding:15px 16px;margin:8px 0 14px;color:#334155;font-size:1rem;line-height:1.55;'><strong style='color:#0f172a;'>These are disclosed transaction rows.</strong> A row can be a purchase, sale, exchange, or another reported transaction type. The table does not know execution price, exact trade size, or private realized return.</div>")
|
| 648 |
-
with gr.Row():
|
| 649 |
-
search_box = gr.Textbox(label="Search asset, ticker, or filer ID", value="")
|
| 650 |
-
min_lag = gr.Slider(0, 400, value=0, step=5, label="Minimum filing lag days")
|
| 651 |
-
max_rows = gr.Slider(10, 250, value=50, step=10, label="Rows")
|
| 652 |
-
trade_rows = gr.Dataframe(value=trade_search("", 0, 50), label="Disclosed transaction rows", interactive=False, wrap=True)
|
| 653 |
-
search_box.change(trade_search, inputs=[search_box, min_lag, max_rows], outputs=trade_rows)
|
| 654 |
-
min_lag.change(trade_search, inputs=[search_box, min_lag, max_rows], outputs=trade_rows)
|
| 655 |
-
max_rows.change(trade_search, inputs=[search_box, min_lag, max_rows], outputs=trade_rows)
|
| 656 |
-
gr.Plot(value=trade_type_chart(), label="Transaction type by year")
|
| 657 |
-
|
| 658 |
-
with gr.Tab("Market Checks"):
|
| 659 |
-
gr.HTML(market_note_html())
|
| 660 |
-
gr.Plot(value=return_chart(), label="Exploratory market-context chart")
|
| 661 |
-
gr.Dataframe(value=return_handoff_display(), label="Plain-English market-check rows", interactive=False, wrap=True)
|
| 662 |
-
gr.HTML("<div class='boundary' style='border-left:5px solid #8a4b00;background:#fff7e8;color:#3f2a00;padding:14px 16px;border-radius:8px;margin:12px 0 20px;font-size:1rem;line-height:1.55;'><strong style='color:#2f1d00;'>Bottom line:</strong> these rows are blocked from public alpha claims. A positive cell is not proof of intent, illegality, causality, insider trading, or realized private return.</div>")
|
| 663 |
-
|
| 664 |
-
with gr.Tab("Receipts"):
|
| 665 |
-
gr.HTML("<div class='section-intro' style='background:#ffffff;border:1px solid #cbd5e1;border-radius:10px;padding:15px 16px;margin:8px 0 14px;color:#334155;font-size:1rem;line-height:1.55;'><strong style='color:#0f172a;'>Receipts are the audit trail.</strong> The files below show what was included, what was intentionally omitted, and where source or market-data gaps remain.</div>")
|
| 666 |
-
gr.Dataframe(value=source_gap_table(), label="Source-gap report", interactive=False, wrap=True)
|
| 667 |
-
gr.Dataframe(value=file_inventory(), label="Bundle inventory", interactive=False, wrap=True)
|
| 668 |
-
gr.File(
|
| 669 |
-
value=str(DATA / "public_release_manifest.json"),
|
| 670 |
-
label="Public release manifest",
|
| 671 |
-
interactive=False,
|
| 672 |
-
)
|
| 673 |
-
gr.File(
|
| 674 |
-
value=str(DATA / "dataset_bundle" / "return_tests.csv"),
|
| 675 |
-
label="Return-test summary CSV",
|
| 676 |
-
interactive=False,
|
| 677 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 678 |
|
| 679 |
|
| 680 |
if __name__ == "__main__":
|
|
|
|
| 82 |
|
| 83 |
def claim_label(value: str) -> str:
|
| 84 |
labels = {
|
| 85 |
+
"blocked_market_data_policy": "Not enough public market data for a market-beating claim",
|
| 86 |
+
"blocked": "Not enough public data for a market-beating claim",
|
| 87 |
}
|
| 88 |
return labels.get(str(value), str(value).replace("_", " "))
|
| 89 |
|
|
|
|
| 101 |
TRADES = read_jsonl_gz("trades.jsonl.gz")
|
| 102 |
WEALTH = read_jsonl_gz("net_worth_intervals.jsonl.gz")
|
| 103 |
FILINGS = read_jsonl_gz("filings.jsonl.gz")
|
| 104 |
+
SENATORS = pd.read_csv(DATASET / "senators.csv")
|
| 105 |
|
| 106 |
TRADES["transaction_year"] = pd.to_datetime(TRADES["transaction_date"], errors="coerce").dt.year
|
| 107 |
TRADES["filing_lag_days_num"] = pd.to_numeric(TRADES.get("filing_lag_days"), errors="coerce")
|
|
|
|
| 112 |
WEALTH[col] = pd.to_numeric(WEALTH.get(col), errors="coerce")
|
| 113 |
|
| 114 |
|
| 115 |
+
def title_name(value: str) -> str:
|
| 116 |
+
text = str(value or "").replace("-", " ").strip()
|
| 117 |
+
return " ".join(part.capitalize() for part in text.split())
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def build_filer_index() -> pd.DataFrame:
|
| 121 |
+
filing_counts = FILINGS.groupby("senator_id").size().rename("filings")
|
| 122 |
+
trade_counts = TRADES.groupby("senator_id").size().rename("transactions")
|
| 123 |
+
wealth_counts = WEALTH.groupby("senator_id").size().rename("wealth_ranges")
|
| 124 |
+
median_lag = TRADES.groupby("senator_id")["filing_lag_days_num"].median().round().rename("median_filing_lag_days")
|
| 125 |
+
ids = sorted(set(FILINGS["senator_id"]) | set(TRADES["senator_id"]) | set(WEALTH["senator_id"]) | set(SENATORS["senator_id"]))
|
| 126 |
+
frame = pd.DataFrame({"senator_id": ids})
|
| 127 |
+
frame = frame.merge(filing_counts, on="senator_id", how="left")
|
| 128 |
+
frame = frame.merge(trade_counts, on="senator_id", how="left")
|
| 129 |
+
frame = frame.merge(wealth_counts, on="senator_id", how="left")
|
| 130 |
+
frame = frame.merge(median_lag, on="senator_id", how="left")
|
| 131 |
+
roster = SENATORS[["senator_id", "display_name", "state", "party"]].copy()
|
| 132 |
+
frame = frame.merge(roster, on="senator_id", how="left")
|
| 133 |
+
|
| 134 |
+
filing_names = (
|
| 135 |
+
FILINGS.assign(
|
| 136 |
+
filing_display=(
|
| 137 |
+
FILINGS.get("filer_first_name", "").fillna("").map(title_name)
|
| 138 |
+
+ " "
|
| 139 |
+
+ FILINGS.get("filer_last_name", "").fillna("").map(title_name)
|
| 140 |
+
).str.strip()
|
| 141 |
+
)
|
| 142 |
+
.groupby("senator_id")["filing_display"]
|
| 143 |
+
.agg(lambda values: values.dropna().iloc[0] if len(values.dropna()) else "")
|
| 144 |
+
)
|
| 145 |
+
frame = frame.merge(filing_names.rename("filing_display"), on="senator_id", how="left")
|
| 146 |
+
frame["display_name"] = frame["display_name"].fillna(frame["filing_display"]).fillna("")
|
| 147 |
+
frame.loc[frame["display_name"].eq(""), "display_name"] = frame.loc[frame["display_name"].eq(""), "senator_id"]
|
| 148 |
+
for col in ["filings", "transactions", "wealth_ranges"]:
|
| 149 |
+
frame[col] = frame[col].fillna(0).astype(int)
|
| 150 |
+
frame["median_filing_lag_days"] = frame["median_filing_lag_days"].fillna("")
|
| 151 |
+
frame["state"] = frame["state"].fillna("")
|
| 152 |
+
frame["party"] = frame["party"].fillna("")
|
| 153 |
+
frame["search_text"] = (
|
| 154 |
+
frame["display_name"].astype(str)
|
| 155 |
+
+ " "
|
| 156 |
+
+ frame["senator_id"].astype(str)
|
| 157 |
+
+ " "
|
| 158 |
+
+ frame["state"].astype(str)
|
| 159 |
+
+ " "
|
| 160 |
+
+ frame["party"].astype(str)
|
| 161 |
+
).str.lower()
|
| 162 |
+
return frame.sort_values(["transactions", "filings", "display_name"], ascending=[False, False, True])
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
FILER_INDEX = build_filer_index()
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def name_for_id(senator_id: str) -> str:
|
| 169 |
+
match = FILER_INDEX[FILER_INDEX["senator_id"] == senator_id]
|
| 170 |
+
if match.empty:
|
| 171 |
+
return str(senator_id)
|
| 172 |
+
row = match.iloc[0]
|
| 173 |
+
suffix = ""
|
| 174 |
+
if row.get("state") or row.get("party"):
|
| 175 |
+
suffix = f" ({row.get('party', '')}-{row.get('state', '')})".replace(" (-", " (").replace("-)", ")")
|
| 176 |
+
return f"{row['display_name']}{suffix}"
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
PUBLIC_FILER_COLUMNS = [
|
| 180 |
+
"Name in records",
|
| 181 |
+
"State",
|
| 182 |
+
"Party",
|
| 183 |
+
"Filings",
|
| 184 |
+
"Transactions",
|
| 185 |
+
"Wealth ranges",
|
| 186 |
+
"Typical filing lag (days)",
|
| 187 |
+
]
|
| 188 |
+
|
| 189 |
+
PUBLIC_WEALTH_COLUMNS = [
|
| 190 |
+
"Report year",
|
| 191 |
+
"Low estimate",
|
| 192 |
+
"Midpoint estimate",
|
| 193 |
+
"High estimate",
|
| 194 |
+
"Range width",
|
| 195 |
+
"Open-ended top bracket",
|
| 196 |
+
"How to read it",
|
| 197 |
+
]
|
| 198 |
+
|
| 199 |
+
PUBLIC_TRADE_COLUMNS = [
|
| 200 |
+
"Filer",
|
| 201 |
+
"Reported transaction date",
|
| 202 |
+
"Public filing date",
|
| 203 |
+
"Filing lag days",
|
| 204 |
+
"Disclosure transaction type",
|
| 205 |
+
"Reported owner",
|
| 206 |
+
"Asset name from filing",
|
| 207 |
+
"Ticker text from filing",
|
| 208 |
+
"Amount low",
|
| 209 |
+
"Amount high",
|
| 210 |
+
"What this row does not prove",
|
| 211 |
+
]
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def resolve_filer(query: str) -> str:
|
| 215 |
+
q = str(query or "").strip().lower()
|
| 216 |
+
if q:
|
| 217 |
+
exact = FILER_INDEX[FILER_INDEX["senator_id"].str.lower().eq(q)]
|
| 218 |
+
if not exact.empty:
|
| 219 |
+
return str(exact.iloc[0]["senator_id"])
|
| 220 |
+
matches = FILER_INDEX[FILER_INDEX["search_text"].str.contains(q, regex=False)]
|
| 221 |
+
if not matches.empty:
|
| 222 |
+
return str(matches.iloc[0]["senator_id"])
|
| 223 |
+
wealth_ids = set(WEALTH["senator_id"])
|
| 224 |
+
matches = FILER_INDEX[FILER_INDEX["senator_id"].isin(wealth_ids)].sort_values(["wealth_ranges", "transactions"], ascending=[False, False])
|
| 225 |
+
return str(matches.iloc[0]["senator_id"]) if not matches.empty else str(FILER_INDEX.iloc[0]["senator_id"])
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def empty_state_figure(message: str):
|
| 229 |
+
fig = go.Figure()
|
| 230 |
+
fig.add_annotation(
|
| 231 |
+
text=message,
|
| 232 |
+
x=0.5,
|
| 233 |
+
y=0.5,
|
| 234 |
+
xref="paper",
|
| 235 |
+
yref="paper",
|
| 236 |
+
showarrow=False,
|
| 237 |
+
font=dict(size=16, color="#334155", family=FONT_FAMILY),
|
| 238 |
+
)
|
| 239 |
+
fig.update_layout(height=360, margin=dict(l=24, r=24, t=34, b=24))
|
| 240 |
+
return style_figure(fig)
|
| 241 |
+
|
| 242 |
+
|
| 243 |
def hero_html() -> str:
|
| 244 |
return """
|
| 245 |
+
<section class="hero-panel">
|
| 246 |
+
<div class="eyebrow">Official Senate disclosure records</div>
|
| 247 |
+
<h1>Senate stock disclosures, made easier to read</h1>
|
| 248 |
+
<p class="hero-answer">
|
| 249 |
+
Search public Senate filings for reported trades, asset and wealth ranges, filing dates, and links back to source records.
|
| 250 |
+
</p>
|
| 251 |
+
<div class="not-verdict">It is not an accusation and it does not prove motive, illegality, insider trading, or market-beating performance.</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
</section>
|
| 253 |
"""
|
| 254 |
|
|
|
|
| 256 |
def plain_english_cards_html() -> str:
|
| 257 |
cards = [
|
| 258 |
(
|
| 259 |
+
"Wealth is a range",
|
| 260 |
+
"Senate forms use dollar brackets, so this app shows low-to-high estimates, not exact net worth.",
|
| 261 |
"#e8f7f3",
|
| 262 |
"#0f766e",
|
| 263 |
),
|
| 264 |
(
|
| 265 |
+
"Transactions are disclosures",
|
| 266 |
+
"Rows come from public forms. They do not include exact execution prices or private profit.",
|
| 267 |
"#eef4ff",
|
| 268 |
"#3155b7",
|
| 269 |
),
|
| 270 |
(
|
| 271 |
+
"The math lives in Advanced",
|
| 272 |
+
"Benchmark charts are context only, so they stay out of the simple reading path.",
|
| 273 |
"#fff7e8",
|
| 274 |
"#8a4b00",
|
| 275 |
),
|
| 276 |
]
|
| 277 |
cells = "".join(
|
| 278 |
f"""
|
| 279 |
+
<div class="plain-card" style="background:{bg};border-color:{accent};">
|
| 280 |
+
<div class="plain-card-title" style="color:{accent};">{title}</div>
|
| 281 |
+
<div class="plain-card-body">{body}</div>
|
| 282 |
</div>
|
| 283 |
"""
|
| 284 |
for title, body, bg, accent in cards
|
| 285 |
)
|
| 286 |
+
return f"<div class='plain-cards'>{cells}</div>"
|
| 287 |
|
| 288 |
|
| 289 |
def metric_html() -> str:
|
| 290 |
counts = MANIFEST.get("counts", {})
|
| 291 |
dataset = counts.get("dataset", {})
|
|
|
|
| 292 |
items = [
|
| 293 |
+
(fmt_int(dataset.get("filings", "")), "public filings"),
|
| 294 |
+
(fmt_int(dataset.get("trades", "")), "reported trades"),
|
| 295 |
+
(fmt_int(dataset.get("net_worth_intervals", "")), "wealth ranges"),
|
|
|
|
|
|
|
|
|
|
| 296 |
]
|
| 297 |
cells = "".join(
|
| 298 |
+
f"<span class='count-pill'><strong>{value}</strong> {label}</span>"
|
| 299 |
+
for value, label in items
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
)
|
| 301 |
+
return f"<div class='count-strip'><span class='count-label'>Behind the app:</span>{cells}<span class='count-note'>More technical tables live in Advanced.</span></div>"
|
| 302 |
|
| 303 |
|
| 304 |
def boundary_html() -> str:
|
| 305 |
return """
|
| 306 |
+
<div class='boundary'>
|
| 307 |
+
<strong>Plain-English boundary:</strong> Public disclosure records can show what was reported and when.
|
| 308 |
+
They cannot, by themselves, prove why someone traded or whether a private return was earned.
|
|
|
|
| 309 |
</div>
|
| 310 |
"""
|
| 311 |
|
| 312 |
|
| 313 |
def overview_intro_html() -> str:
|
| 314 |
return """
|
| 315 |
+
<div class="simple-start">
|
| 316 |
+
<h2>Understand it in five seconds</h2>
|
| 317 |
+
<p>This is a search tool for public Senate financial disclosure records. It helps answer who filed, what was reported, when it became public, and where the original record trail lives.</p>
|
| 318 |
+
<div class="question-grid">
|
| 319 |
+
<div><strong>1. Search a senator</strong><span>See reported filings, trades, and wealth ranges.</span></div>
|
| 320 |
+
<div><strong>2. Search an asset</strong><span>Find transaction rows that mention a company, fund, or ticker text.</span></div>
|
| 321 |
+
<div><strong>3. Open Advanced</strong><span>Use this only for market context, source gaps, and downloads.</span></div>
|
| 322 |
</div>
|
| 323 |
</div>
|
| 324 |
"""
|
| 325 |
|
| 326 |
|
| 327 |
+
def public_source_summary_html() -> str:
|
| 328 |
+
return """
|
| 329 |
+
<div class="path-panel">
|
| 330 |
+
<strong>Best first step:</strong> search a senator by name, then open the original filing when you need the source.
|
| 331 |
+
<p>Public disclosure forms show what was reported and when. They usually do not show intent, exact dollar amounts, or the reason for a trade.</p>
|
| 332 |
+
</div>
|
| 333 |
+
"""
|
| 334 |
+
|
| 335 |
+
|
| 336 |
def market_note_html() -> str:
|
| 337 |
return """
|
| 338 |
<div class="section-intro" style="background:#f8fafc;border:1px solid #cbd5e1;border-radius:10px;padding:15px 16px;margin:12px 0;">
|
| 339 |
+
<div style="color:#0f172a;font-size:1.08rem;font-weight:800;margin-bottom:5px;">How to read this benchmark-context chart</div>
|
| 340 |
<div style="color:#334155;font-size:1rem;line-height:1.55;">
|
| 341 |
Bars above zero mean the disclosed transaction set moved more than a simple benchmark in that window under one assumption.
|
| 342 |
Bars below zero mean it moved less. Because this public bundle uses no-cost adjusted-close data, every row remains exploratory.
|
|
|
|
| 353 |
name=str(date_basis).capitalize(),
|
| 354 |
x=frame["window_label"],
|
| 355 |
y=frame["equal_weight_alpha_pct"],
|
| 356 |
+
customdata=frame[["claim_status_label", "n_events"]],
|
| 357 |
hovertemplate=(
|
| 358 |
"%{x}<br>"
|
| 359 |
"Benchmark difference: %{y:.3f}%<br>"
|
| 360 |
"Rows: %{customdata[1]}<br>"
|
| 361 |
+
"%{customdata[0]}<extra></extra>"
|
|
|
|
| 362 |
),
|
| 363 |
)
|
| 364 |
)
|
| 365 |
fig.add_hline(y=0, line_width=1, line_color="#555")
|
| 366 |
fig.update_layout(
|
| 367 |
+
title="Advanced benchmark context",
|
| 368 |
+
yaxis_title="Market move vs benchmark (%)",
|
| 369 |
xaxis_title="Market window after date",
|
| 370 |
barmode="group",
|
| 371 |
margin=dict(l=64, r=20, t=70, b=62),
|
|
|
|
| 414 |
counts.columns = ["ticker", "rows"]
|
| 415 |
fig = px.bar(counts, x="rows", y="ticker", orientation="h", color="rows", color_continuous_scale="Viridis")
|
| 416 |
fig.update_layout(
|
| 417 |
+
title="Stock-symbol text found most often in disclosures",
|
| 418 |
+
xaxis_title="Disclosure rows",
|
| 419 |
+
yaxis_title="Symbol text from filing",
|
| 420 |
margin=dict(l=80, r=20, t=60, b=50),
|
| 421 |
coloraxis_showscale=False,
|
| 422 |
)
|
|
|
|
| 431 |
def wealth_chart(senator_id: str):
|
| 432 |
frame = WEALTH[WEALTH["senator_id"] == senator_id].sort_values("report_year_num")
|
| 433 |
if frame.empty:
|
| 434 |
+
fig = go.Figure()
|
| 435 |
+
fig.add_annotation(
|
| 436 |
+
text="No wealth-range rows found for this filer.",
|
| 437 |
+
x=0.5,
|
| 438 |
+
y=0.5,
|
| 439 |
+
xref="paper",
|
| 440 |
+
yref="paper",
|
| 441 |
+
showarrow=False,
|
| 442 |
+
font=dict(size=16, color="#334155", family=FONT_FAMILY),
|
| 443 |
+
)
|
| 444 |
+
fig.update_layout(height=360, margin=dict(l=20, r=20, t=40, b=20))
|
| 445 |
+
return style_figure(fig)
|
| 446 |
fig = go.Figure()
|
| 447 |
fig.add_trace(
|
| 448 |
go.Scatter(
|
|
|
|
| 477 |
)
|
| 478 |
)
|
| 479 |
fig.update_layout(
|
| 480 |
+
title=f"Reported wealth range over time: {name_for_id(senator_id)}",
|
| 481 |
xaxis_title="Report year",
|
| 482 |
yaxis_title="Estimated range from disclosure brackets ($)",
|
| 483 |
margin=dict(l=70, r=20, t=60, b=50),
|
|
|
|
| 509 |
)
|
| 510 |
|
| 511 |
|
| 512 |
+
def filer_search(query: str, max_rows: int = 12) -> pd.DataFrame:
|
| 513 |
+
q = str(query or "").strip().lower()
|
| 514 |
+
if not q:
|
| 515 |
+
return pd.DataFrame(columns=PUBLIC_FILER_COLUMNS)
|
| 516 |
+
frame = FILER_INDEX.copy()
|
| 517 |
+
frame = frame[frame["search_text"].str.contains(q, regex=False)]
|
| 518 |
+
frame = frame.head(int(max_rows))
|
| 519 |
+
display = frame[["display_name", "state", "party", "filings", "transactions", "wealth_ranges", "median_filing_lag_days"]].copy()
|
| 520 |
+
return display.rename(
|
| 521 |
+
columns={
|
| 522 |
+
"display_name": "Name in records",
|
| 523 |
+
"state": "State",
|
| 524 |
+
"party": "Party",
|
| 525 |
+
"filings": "Filings",
|
| 526 |
+
"transactions": "Transactions",
|
| 527 |
+
"wealth_ranges": "Wealth ranges",
|
| 528 |
+
"median_filing_lag_days": "Typical filing lag (days)",
|
| 529 |
+
}
|
| 530 |
+
)
|
| 531 |
+
|
| 532 |
+
|
| 533 |
+
def filer_profile_html(query: str) -> str:
|
| 534 |
+
if not str(query or "").strip():
|
| 535 |
+
return """
|
| 536 |
+
<div class='profile-card empty-state'>
|
| 537 |
+
<h2>Search a senator to start</h2>
|
| 538 |
+
<p>Type a name, state, party, or filer ID. The result shows what appears in the public disclosure dataset, not a judgment about the person.</p>
|
| 539 |
+
</div>
|
| 540 |
+
"""
|
| 541 |
+
senator_id = resolve_filer(query)
|
| 542 |
+
match = FILER_INDEX[FILER_INDEX["senator_id"] == senator_id]
|
| 543 |
+
if match.empty:
|
| 544 |
+
return "<div class='profile-card'><strong>No match found.</strong> Try a name, state, or filer ID.</div>"
|
| 545 |
+
row = match.iloc[0]
|
| 546 |
+
lag = row.get("median_filing_lag_days", "")
|
| 547 |
+
lag_text = f"{int(lag)} days" if str(lag).strip() else "not enough transaction rows"
|
| 548 |
+
return f"""
|
| 549 |
+
<div class='profile-card'>
|
| 550 |
+
<div class='profile-kicker'>Selected public filer</div>
|
| 551 |
+
<h2>{name_for_id(senator_id)}</h2>
|
| 552 |
+
<div class='profile-grid'>
|
| 553 |
+
<span><strong>{fmt_int(row.get('filings', 0))}</strong> filings</span>
|
| 554 |
+
<span><strong>{fmt_int(row.get('transactions', 0))}</strong> transaction rows</span>
|
| 555 |
+
<span><strong>{fmt_int(row.get('wealth_ranges', 0))}</strong> wealth ranges</span>
|
| 556 |
+
<span><strong>{lag_text}</strong> typical filing lag</span>
|
| 557 |
+
</div>
|
| 558 |
+
</div>
|
| 559 |
+
"""
|
| 560 |
+
|
| 561 |
+
|
| 562 |
+
def filer_wealth_chart(query: str):
|
| 563 |
+
if not str(query or "").strip():
|
| 564 |
+
return empty_state_figure("Search a senator to see reported wealth ranges.")
|
| 565 |
+
return wealth_chart(resolve_filer(query))
|
| 566 |
+
|
| 567 |
+
|
| 568 |
+
def filer_wealth_table(query: str) -> pd.DataFrame:
|
| 569 |
+
if not str(query or "").strip():
|
| 570 |
+
return pd.DataFrame(columns=PUBLIC_WEALTH_COLUMNS)
|
| 571 |
+
return wealth_table(resolve_filer(query))
|
| 572 |
+
|
| 573 |
+
|
| 574 |
def trade_search(query: str, min_lag: int, max_rows: int) -> pd.DataFrame:
|
| 575 |
frame = TRADES.copy()
|
| 576 |
if query:
|
| 577 |
q = query.lower().strip()
|
| 578 |
+
matching_ids = FILER_INDEX[FILER_INDEX["search_text"].str.contains(q, regex=False)]["senator_id"].tolist()
|
| 579 |
frame = frame[
|
| 580 |
frame["asset_name"].fillna("").str.lower().str.contains(q, regex=False)
|
| 581 |
| frame["ticker_candidate"].fillna("").str.lower().str.contains(q, regex=False)
|
| 582 |
| frame["senator_id"].fillna("").str.lower().str.contains(q, regex=False)
|
| 583 |
+
| frame["senator_id"].isin(matching_ids)
|
| 584 |
]
|
| 585 |
frame = frame[frame["filing_lag_days_num"].fillna(-1) >= min_lag]
|
| 586 |
+
frame = frame.head(int(max_rows)).copy()
|
| 587 |
+
frame["filer_name"] = frame["senator_id"].fillna("").map(name_for_id)
|
| 588 |
cols = [
|
| 589 |
+
"filer_name",
|
| 590 |
"transaction_date",
|
| 591 |
"filing_date",
|
| 592 |
"filing_lag_days",
|
|
|
|
| 598 |
"amount_max",
|
| 599 |
"what_this_does_not_prove",
|
| 600 |
]
|
| 601 |
+
return frame[cols].rename(
|
| 602 |
columns={
|
| 603 |
+
"filer_name": "Filer",
|
| 604 |
"transaction_date": "Reported transaction date",
|
| 605 |
"filing_date": "Public filing date",
|
| 606 |
"filing_lag_days": "Filing lag days",
|
| 607 |
"transaction_type": "Disclosure transaction type",
|
| 608 |
"owner": "Reported owner",
|
| 609 |
"asset_name": "Asset name from filing",
|
| 610 |
+
"ticker_candidate": "Ticker text from filing",
|
| 611 |
"amount_min": "Amount low",
|
| 612 |
"amount_max": "Amount high",
|
| 613 |
"what_this_does_not_prove": "What this row does not prove",
|
|
|
|
| 615 |
)
|
| 616 |
|
| 617 |
|
| 618 |
+
def public_trade_search(query: str) -> pd.DataFrame:
|
| 619 |
+
if not str(query or "").strip():
|
| 620 |
+
return pd.DataFrame(columns=PUBLIC_TRADE_COLUMNS)
|
| 621 |
+
return trade_search(query, 0, 50)
|
| 622 |
+
|
| 623 |
+
|
| 624 |
def return_handoff_display() -> pd.DataFrame:
|
| 625 |
frame = RETURN_HANDOFF.copy()
|
| 626 |
if "date_basis" in frame:
|
|
|
|
| 639 |
"midpoint_weight_alpha_pct",
|
| 640 |
"bootstrap_ci_low_pct",
|
| 641 |
"bootstrap_ci_high_pct",
|
|
|
|
| 642 |
"claim_status",
|
| 643 |
"plain_english_status",
|
| 644 |
]
|
|
|
|
| 647 |
columns={
|
| 648 |
"date_basis": "Measured from",
|
| 649 |
"window": "Window",
|
| 650 |
+
"events": "Trade windows checked",
|
| 651 |
+
"senators": "Filers",
|
| 652 |
+
"tickers": "Stock symbols found",
|
| 653 |
"equal_weight_alpha_pct": "Equal-weight benchmark difference (%)",
|
| 654 |
"midpoint_weight_alpha_pct": "Midpoint-weight benchmark difference (%)",
|
| 655 |
"bootstrap_ci_low_pct": "Bootstrap low (%)",
|
| 656 |
"bootstrap_ci_high_pct": "Bootstrap high (%)",
|
| 657 |
+
"claim_status": "Status",
|
|
|
|
| 658 |
"plain_english_status": "Plain English status",
|
| 659 |
}
|
| 660 |
)
|
|
|
|
| 678 |
return pd.DataFrame(rows)
|
| 679 |
|
| 680 |
|
| 681 |
+
def audit_summary_html() -> str:
|
| 682 |
+
counts = MANIFEST.get("counts", {})
|
| 683 |
+
dataset = counts.get("dataset", {})
|
| 684 |
+
source_gaps = SOURCE_GAPS.get("source_gaps", [])
|
| 685 |
+
return f"""
|
| 686 |
+
<div class="audit-summary">
|
| 687 |
+
<h2>Source summary</h2>
|
| 688 |
+
<div class="audit-grid">
|
| 689 |
+
<div><strong>{fmt_int(dataset.get('source_artifacts', ''))}</strong><span>source artifacts tracked</span></div>
|
| 690 |
+
<div><strong>{fmt_int(len(source_gaps))}</strong><span>known source-gap notes</span></div>
|
| 691 |
+
<div><strong>No</strong><span>raw market-price file redistributed</span></div>
|
| 692 |
+
</div>
|
| 693 |
+
<p>Use the downloads below if you want to audit the package. Most visitors do not need this section.</p>
|
| 694 |
+
</div>
|
| 695 |
+
"""
|
| 696 |
+
|
| 697 |
+
|
| 698 |
CSS = """
|
| 699 |
:root {
|
| 700 |
--accent: #1b6b63;
|
|
|
|
| 704 |
--line: #cbd5e1;
|
| 705 |
--soft: #f8fafc;
|
| 706 |
--font: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
| 707 |
+
color-scheme: light;
|
| 708 |
}
|
| 709 |
html,
|
| 710 |
body,
|
|
|
|
| 715 |
.gradio-container .contain {
|
| 716 |
background: #f6f8fb !important;
|
| 717 |
color: var(--ink) !important;
|
| 718 |
+
color-scheme: light !important;
|
| 719 |
}
|
| 720 |
.dark .gradio-container,
|
| 721 |
.dark .gradio-container .main,
|
|
|
|
| 723 |
.dark .gradio-container .contain {
|
| 724 |
background: #f6f8fb !important;
|
| 725 |
color: var(--ink) !important;
|
| 726 |
+
color-scheme: light !important;
|
| 727 |
}
|
| 728 |
.gradio-container,
|
| 729 |
.gradio-container * {
|
|
|
|
| 734 |
letter-spacing: 0;
|
| 735 |
}
|
| 736 |
.gradio-container {
|
| 737 |
+
max-width: 1080px !important;
|
| 738 |
margin: auto;
|
| 739 |
color: var(--ink);
|
| 740 |
font-size: 16px;
|
| 741 |
line-height: 1.48;
|
| 742 |
padding: 0 18px 28px;
|
| 743 |
}
|
| 744 |
+
.hero-panel {
|
| 745 |
+
background: #ffffff;
|
| 746 |
+
border: 1px solid #cbd5e1;
|
| 747 |
+
border-radius: 14px;
|
| 748 |
+
padding: 26px 28px;
|
| 749 |
+
margin: 14px 0 14px;
|
| 750 |
+
box-shadow: 0 14px 34px rgba(15,23,42,.08);
|
| 751 |
+
}
|
| 752 |
+
.hero-panel .eyebrow {
|
| 753 |
+
color: #0f766e;
|
| 754 |
+
font-size: .86rem;
|
| 755 |
+
font-weight: 800;
|
| 756 |
+
letter-spacing: .04em;
|
| 757 |
+
text-transform: uppercase;
|
| 758 |
+
margin-bottom: 8px;
|
| 759 |
+
}
|
| 760 |
+
.hero-panel h1 {
|
| 761 |
+
color: #0f172a !important;
|
| 762 |
+
font-size: 2.55rem !important;
|
| 763 |
+
line-height: 1.04 !important;
|
| 764 |
+
font-weight: 800 !important;
|
| 765 |
+
margin: 0 0 10px !important;
|
| 766 |
+
}
|
| 767 |
+
.hero-answer {
|
| 768 |
+
color: #1f2937 !important;
|
| 769 |
+
font-size: 1.12rem !important;
|
| 770 |
+
line-height: 1.55 !important;
|
| 771 |
+
max-width: 830px;
|
| 772 |
+
margin: 0 !important;
|
| 773 |
+
}
|
| 774 |
+
.not-verdict {
|
| 775 |
+
display: inline-block;
|
| 776 |
+
margin-top: 14px;
|
| 777 |
+
color: #6b3b00;
|
| 778 |
+
background: #fff7e8;
|
| 779 |
+
border: 1px solid #e6bd7a;
|
| 780 |
+
border-radius: 999px;
|
| 781 |
+
padding: 8px 12px;
|
| 782 |
+
font-weight: 800;
|
| 783 |
+
}
|
| 784 |
+
.plain-cards {
|
| 785 |
+
display: grid;
|
| 786 |
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 787 |
+
gap: 12px;
|
| 788 |
+
margin: 14px 0;
|
| 789 |
+
}
|
| 790 |
+
.plain-card {
|
| 791 |
+
border: 1px solid;
|
| 792 |
+
border-radius: 12px;
|
| 793 |
+
padding: 16px;
|
| 794 |
+
min-height: 120px;
|
| 795 |
+
}
|
| 796 |
+
.plain-card-title {
|
| 797 |
+
font-size: 1.08rem;
|
| 798 |
+
font-weight: 800;
|
| 799 |
+
margin-bottom: 8px;
|
| 800 |
+
}
|
| 801 |
+
.plain-card-body {
|
| 802 |
+
color: #1f2937;
|
| 803 |
+
font-size: 1rem;
|
| 804 |
+
line-height: 1.45;
|
| 805 |
+
font-weight: 600;
|
| 806 |
+
}
|
| 807 |
+
.count-strip {
|
| 808 |
+
display: flex;
|
| 809 |
+
flex-wrap: wrap;
|
| 810 |
+
gap: 8px;
|
| 811 |
+
align-items: center;
|
| 812 |
+
background: #ffffff;
|
| 813 |
+
border: 1px solid #d7dee8;
|
| 814 |
+
border-radius: 12px;
|
| 815 |
+
padding: 12px 14px;
|
| 816 |
+
margin: 12px 0;
|
| 817 |
+
}
|
| 818 |
+
.count-label {
|
| 819 |
+
color: #334155;
|
| 820 |
+
font-weight: 800;
|
| 821 |
+
}
|
| 822 |
+
.count-pill {
|
| 823 |
+
background: #eef7f5;
|
| 824 |
+
color: #0f3f3a;
|
| 825 |
+
border: 1px solid #b8dcd6;
|
| 826 |
+
border-radius: 999px;
|
| 827 |
+
padding: 6px 10px;
|
| 828 |
+
font-weight: 700;
|
| 829 |
+
}
|
| 830 |
+
.count-pill strong {
|
| 831 |
+
color: #0f766e !important;
|
| 832 |
+
font-weight: 800 !important;
|
| 833 |
+
}
|
| 834 |
+
.count-note {
|
| 835 |
+
color: #64748b;
|
| 836 |
+
font-weight: 700;
|
| 837 |
+
}
|
| 838 |
+
.simple-start,
|
| 839 |
+
.path-panel,
|
| 840 |
+
.section-intro,
|
| 841 |
+
.profile-card {
|
| 842 |
+
background: #ffffff;
|
| 843 |
+
border: 1px solid #cbd5e1;
|
| 844 |
+
border-radius: 12px;
|
| 845 |
+
padding: 16px 18px;
|
| 846 |
+
margin: 10px 0 14px;
|
| 847 |
+
color: #1f2937;
|
| 848 |
+
}
|
| 849 |
+
.simple-start h2,
|
| 850 |
+
.profile-card h2 {
|
| 851 |
+
color: #0f172a !important;
|
| 852 |
+
font-size: 1.45rem !important;
|
| 853 |
+
font-weight: 800 !important;
|
| 854 |
+
line-height: 1.2 !important;
|
| 855 |
+
margin: 0 0 8px !important;
|
| 856 |
+
}
|
| 857 |
+
.question-grid,
|
| 858 |
+
.profile-grid,
|
| 859 |
+
.audit-grid {
|
| 860 |
+
display: grid;
|
| 861 |
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 862 |
+
gap: 10px;
|
| 863 |
+
margin-top: 12px;
|
| 864 |
+
}
|
| 865 |
+
.question-grid div,
|
| 866 |
+
.profile-grid span,
|
| 867 |
+
.audit-grid div {
|
| 868 |
+
background: #f8fafc;
|
| 869 |
+
border: 1px solid #d7dee8;
|
| 870 |
+
border-radius: 10px;
|
| 871 |
+
padding: 12px;
|
| 872 |
+
color: #1f2937;
|
| 873 |
+
}
|
| 874 |
+
.question-grid strong,
|
| 875 |
+
.profile-grid strong,
|
| 876 |
+
.audit-grid strong {
|
| 877 |
+
display: block;
|
| 878 |
+
color: #0f172a !important;
|
| 879 |
+
font-weight: 800 !important;
|
| 880 |
+
margin-bottom: 4px;
|
| 881 |
+
}
|
| 882 |
+
.question-grid span,
|
| 883 |
+
.profile-grid span,
|
| 884 |
+
.audit-grid span {
|
| 885 |
+
color: #334155;
|
| 886 |
+
font-weight: 700;
|
| 887 |
+
}
|
| 888 |
+
.audit-summary h2 {
|
| 889 |
+
color: #0f172a !important;
|
| 890 |
+
font-size: 1.35rem !important;
|
| 891 |
+
font-weight: 800 !important;
|
| 892 |
+
margin: 0 0 8px !important;
|
| 893 |
+
}
|
| 894 |
+
.profile-kicker {
|
| 895 |
+
color: #0f766e;
|
| 896 |
+
font-size: .84rem;
|
| 897 |
+
font-weight: 800;
|
| 898 |
+
letter-spacing: .04em;
|
| 899 |
+
text-transform: uppercase;
|
| 900 |
+
margin-bottom: 6px;
|
| 901 |
+
}
|
| 902 |
+
.gradio-container a,
|
| 903 |
+
.gradio-container a *,
|
| 904 |
+
.gradio-container strong,
|
| 905 |
+
.gradio-container b {
|
| 906 |
+
color: #0f172a !important;
|
| 907 |
+
}
|
| 908 |
.gradio-container h1,
|
| 909 |
.gradio-container .prose h1,
|
| 910 |
.gradio-container .markdown h1 {
|
|
|
|
| 927 |
.gradio-container button[role="tab"] {
|
| 928 |
color: #0f172a !important;
|
| 929 |
font-size: 1rem !important;
|
| 930 |
+
font-weight: 700 !important;
|
| 931 |
}
|
| 932 |
.gradio-container button[role="tab"][aria-selected="true"] {
|
| 933 |
color: var(--accent) !important;
|
|
|
|
| 943 |
font-size: 1rem !important;
|
| 944 |
background: #ffffff !important;
|
| 945 |
}
|
| 946 |
+
.gradio-container code {
|
| 947 |
+
color: #0f172a !important;
|
| 948 |
+
background: #eef4f8 !important;
|
| 949 |
+
border: 1px solid #d7dee8 !important;
|
| 950 |
+
border-radius: 6px !important;
|
| 951 |
+
padding: 1px 5px !important;
|
| 952 |
+
}
|
| 953 |
+
[role="listbox"],
|
| 954 |
+
[role="option"],
|
| 955 |
+
.gradio-container .options,
|
| 956 |
+
.gradio-container .option,
|
| 957 |
+
.gradio-container .svelte-select-list,
|
| 958 |
+
.gradio-container .dropdown-options {
|
| 959 |
+
background: #ffffff !important;
|
| 960 |
+
color: #111827 !important;
|
| 961 |
+
}
|
| 962 |
.gradio-container .block,
|
| 963 |
.gradio-container .form,
|
| 964 |
.gradio-container .panel,
|
| 965 |
.gradio-container .plot-container,
|
| 966 |
.gradio-container .table-wrap,
|
| 967 |
+
.gradio-container .dataframe,
|
| 968 |
+
.gradio-container .input-container,
|
| 969 |
+
.gradio-container .container {
|
| 970 |
background: #ffffff !important;
|
| 971 |
color: #111827 !important;
|
| 972 |
}
|
| 973 |
.metrics { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 12px; margin: 14px 0 18px; }
|
| 974 |
.metric { border: 1px solid var(--line); border-radius: 8px; padding: 13px 14px; background: #ffffff; }
|
| 975 |
.metric-value { font-size: 1.45rem; font-weight: 800; color: var(--accent); line-height: 1.18; overflow-wrap: anywhere; }
|
| 976 |
+
.metric-label { font-size: .92rem; color: var(--muted); font-weight: 700; margin-top: 5px; }
|
| 977 |
.boundary {
|
| 978 |
border-left: 5px solid var(--warn);
|
| 979 |
background: #fff7e8;
|
|
|
|
| 996 |
.gradio-container th {
|
| 997 |
background: #eef4f8 !important;
|
| 998 |
color: #0f172a !important;
|
| 999 |
+
font-weight: 700 !important;
|
| 1000 |
}
|
| 1001 |
.gradio-container td {
|
| 1002 |
background: #ffffff !important;
|
|
|
|
| 1006 |
border-color: var(--line) !important;
|
| 1007 |
}
|
| 1008 |
@media (max-width: 900px) {
|
| 1009 |
+
.plain-cards, .question-grid, .profile-grid, .audit-grid { grid-template-columns: 1fr !important; }
|
| 1010 |
+
.hero-panel h1 { font-size: 2rem !important; }
|
| 1011 |
}
|
| 1012 |
@media (max-width: 620px) {
|
| 1013 |
+
.plain-cards { grid-template-columns: 1fr !important; }
|
| 1014 |
}
|
| 1015 |
"""
|
| 1016 |
|
| 1017 |
|
| 1018 |
+
with gr.Blocks(title="Senate Disclosure Reader") as demo:
|
| 1019 |
gr.HTML(hero_html())
|
| 1020 |
gr.HTML(plain_english_cards_html())
|
| 1021 |
gr.HTML(metric_html())
|
|
|
|
| 1024 |
with gr.Tabs():
|
| 1025 |
with gr.Tab("Start Here"):
|
| 1026 |
gr.HTML(overview_intro_html())
|
|
|
|
|
|
|
|
|
|
| 1027 |
gr.HTML(
|
| 1028 |
+
"<div class='path-panel'>"
|
| 1029 |
+
"<div><strong>Most people should use the first two tabs.</strong></div>"
|
| 1030 |
+
"<p><b>Search Senator</b> answers, 'What did this filer report over time?' <b>Search Trades</b> answers, 'What transaction rows mention this company, fund, or ticker text?' <b>Advanced</b> keeps assumptions and receipts out of the way.</p>"
|
| 1031 |
"</div>"
|
| 1032 |
)
|
| 1033 |
|
| 1034 |
+
with gr.Tab("Search Senator"):
|
| 1035 |
+
gr.HTML("<div class='section-intro'><strong>Search a senator.</strong> The chart shows a reported range, not exact wealth.</div>")
|
| 1036 |
+
filer_query = gr.Textbox(
|
| 1037 |
+
label="Name, state, or filer ID",
|
| 1038 |
+
value="",
|
| 1039 |
+
placeholder="Type a senator name, state, party, or filer ID",
|
| 1040 |
+
show_label=True,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1041 |
)
|
| 1042 |
+
filer_results = gr.Dataframe(value=filer_search(""), label="Matching filers", interactive=False, wrap=True)
|
| 1043 |
+
filer_profile = gr.HTML(filer_profile_html(""))
|
| 1044 |
+
wealth_plot = gr.Plot(value=filer_wealth_chart(""), label="Reported wealth range", show_label=False)
|
| 1045 |
+
with gr.Accordion("Show the range rows", open=False):
|
| 1046 |
+
wealth_rows = gr.Dataframe(value=filer_wealth_table(""), label="Rows behind the chart", interactive=False, wrap=True)
|
| 1047 |
+
for event in (filer_query.input, filer_query.submit, filer_query.change):
|
| 1048 |
+
event(filer_search, inputs=filer_query, outputs=filer_results)
|
| 1049 |
+
event(filer_profile_html, inputs=filer_query, outputs=filer_profile)
|
| 1050 |
+
event(filer_wealth_chart, inputs=filer_query, outputs=wealth_plot)
|
| 1051 |
+
event(filer_wealth_table, inputs=filer_query, outputs=wealth_rows)
|
| 1052 |
+
|
| 1053 |
+
with gr.Tab("Search Trades"):
|
| 1054 |
+
gr.HTML("<div class='section-intro'><strong>Search transaction disclosures.</strong> These rows show what was reported on forms. They do not show exact execution price or private profit.</div>")
|
| 1055 |
+
search_box = gr.Textbox(label="Company, ticker, asset, or filer ID", value="", placeholder="Try: AAPL, Microsoft, SPY, Cruz")
|
| 1056 |
+
trade_rows = gr.Dataframe(value=public_trade_search(""), label="Matching transaction rows", interactive=False, wrap=True)
|
| 1057 |
+
for event in (search_box.input, search_box.submit, search_box.change):
|
| 1058 |
+
event(public_trade_search, inputs=search_box, outputs=trade_rows)
|
| 1059 |
+
|
| 1060 |
+
with gr.Tab("Advanced"):
|
| 1061 |
+
gr.HTML("<div class='section-intro'><strong>Advanced area.</strong> This is where the heavier charts, market assumptions, and receipts live.</div>")
|
| 1062 |
+
with gr.Accordion("Filing-delay chart", open=False):
|
| 1063 |
+
gr.Plot(value=filing_delay_chart(), show_label=False)
|
| 1064 |
+
with gr.Accordion("Stock-symbol text chart", open=False):
|
| 1065 |
+
gr.HTML("<div class='small-note'>Ticker text is parsed from disclosure forms. It is not always a verified security identifier.</div>")
|
| 1066 |
+
gr.Plot(value=top_ticker_chart(), show_label=False)
|
| 1067 |
+
with gr.Accordion("Benchmark context", open=False):
|
| 1068 |
+
gr.HTML(market_note_html())
|
| 1069 |
+
gr.Plot(value=return_chart(), show_label=False)
|
| 1070 |
+
gr.Dataframe(value=return_handoff_display(), label="Market-check rows", interactive=False, wrap=True)
|
| 1071 |
+
gr.HTML("<div class='boundary'><strong>Bottom line:</strong> these rows are not public alpha proof. A positive cell is not proof of intent, illegality, causality, insider trading, or realized private return.</div>")
|
| 1072 |
+
with gr.Accordion("Audit trail and downloads", open=False):
|
| 1073 |
+
gr.HTML(audit_summary_html())
|
| 1074 |
+
gr.Dataframe(value=source_gap_table(), label="Source-gap report", interactive=False, wrap=True)
|
| 1075 |
+
gr.Dataframe(value=file_inventory(), label="Bundle inventory", interactive=False, wrap=True)
|
| 1076 |
+
gr.File(
|
| 1077 |
+
value=str(DATA / "public_release_manifest.json"),
|
| 1078 |
+
label="Public release manifest",
|
| 1079 |
+
interactive=False,
|
| 1080 |
+
)
|
| 1081 |
+
gr.File(
|
| 1082 |
+
value=str(DATA / "dataset_bundle" / "return_tests.csv"),
|
| 1083 |
+
label="Return-test summary CSV",
|
| 1084 |
+
interactive=False,
|
| 1085 |
+
)
|
| 1086 |
|
| 1087 |
|
| 1088 |
if __name__ == "__main__":
|
space_bundle_manifest.json
CHANGED
|
@@ -8,8 +8,8 @@
|
|
| 8 |
"files": [
|
| 9 |
{
|
| 10 |
"path": "app.py",
|
| 11 |
-
"sha256": "
|
| 12 |
-
"byte_count":
|
| 13 |
},
|
| 14 |
{
|
| 15 |
"path": "data/audit/claim_boundary.json",
|
|
@@ -98,8 +98,8 @@
|
|
| 98 |
},
|
| 99 |
{
|
| 100 |
"path": "README.md",
|
| 101 |
-
"sha256": "
|
| 102 |
-
"byte_count":
|
| 103 |
},
|
| 104 |
{
|
| 105 |
"path": "requirements.txt",
|
|
@@ -107,5 +107,5 @@
|
|
| 107 |
"byte_count": 47
|
| 108 |
}
|
| 109 |
],
|
| 110 |
-
"manifest_hash": "
|
| 111 |
}
|
|
|
|
| 8 |
"files": [
|
| 9 |
{
|
| 10 |
"path": "app.py",
|
| 11 |
+
"sha256": "2cdc157335044caa3d49298cbd2618f787d304f08134774d9fa6abdad06c93b1",
|
| 12 |
+
"byte_count": 39823
|
| 13 |
},
|
| 14 |
{
|
| 15 |
"path": "data/audit/claim_boundary.json",
|
|
|
|
| 98 |
},
|
| 99 |
{
|
| 100 |
"path": "README.md",
|
| 101 |
+
"sha256": "5350c409b1ddf35423439b7aeb54f413df524f97c6698e4e90d28d2880a08ee9",
|
| 102 |
+
"byte_count": 488
|
| 103 |
},
|
| 104 |
{
|
| 105 |
"path": "requirements.txt",
|
|
|
|
| 107 |
"byte_count": 47
|
| 108 |
}
|
| 109 |
],
|
| 110 |
+
"manifest_hash": "0240d33cb57c5cf2224a610558dcfad56e3a96ff3bf4c0b0a6ee87c42205427f"
|
| 111 |
}
|