Skip to content

Fix optimal_limit_order hardcoded defaults, add IB per-instrument calibration - #40

Open
jay79-boop wants to merge 38 commits into
paperswithbacktest:mainfrom
jay79-boop:main
Open

Fix optimal_limit_order hardcoded defaults, add IB per-instrument calibration#40
jay79-boop wants to merge 38 commits into
paperswithbacktest:mainfrom
jay79-boop:main

Conversation

@jay79-boop

@jay79-boop jay79-boop commented Aug 1, 2026

Copy link
Copy Markdown

Summary

  • get_optimal_quote() had a dead if symbol == "demo" or True: branch that silently ignored its arguments and always used hardcoded demo parameters. Made mu/sigma/A/k/gamma/b/tick_size/average_trading_size real keyword arguments (same defaults as before), so callers can actually pass their own values.
  • Added per-instrument calibration in IBConnector: derives tick_size from live IB contract details and sigma from recent historical daily bars, falling back to the generic defaults when market data isn't available.
  • Updated the README's performance-analysis example to match the current run_strategy/generate_reports API, and documented how to run tests.
  • Added a yfinance fallback to load_dataset() for when neither PWB_API_KEY nor HF_ACCESS_TOKEN is set, scoped to Stocks-Daily-Price, ETFs-Daily-Price, Cryptocurrencies-Daily-Price, and Forex-Daily-Price (these have a clean yfinance equivalent). Bonds/Commodities/Indices still require a real token — no matching yfinance schema for the yield-to-price and FX-normalization logic those expect. get_pricing() is unaffected since it always loads the Universe-Daily-Price routing table first, which has no yfinance equivalent. This lowers the bar for anyone trying the library without applying for API access first.

Test plan

  • pytest tests/ -v — 31 passed (new suite covering calibration and quote logic, plus existing performance-metrics tests)
  • yfinance fallback verified live with zero credentials: AAPL/MSFT stocks, EURUSD forex, BTC/ETH crypto — correct OHLC data through the existing adjust/to-USD pipeline

🤖 Generated with Claude Code

jay79-boop and others added 30 commits August 1, 2026 04:34
…ibration

get_optimal_quote() had a dead `if symbol == "demo" or True:` branch that
silently ignored the symbol argument and always used hardcoded demo
parameters. Made mu/sigma/A/k/gamma/b/tick_size/average_trading_size real
keyword arguments with the same defaults, and added calibration in
IBConnector that derives tick_size and sigma from live IB contract details
and recent historical bars, falling back to the generic defaults when data
isn't available.

Adds a pytest suite (31 tests) covering the calibration and quote logic,
a GitHub Actions workflow to run it on push/PR, and README updates
documenting the new report-generation and test-running workflows.
Scoped to Stocks/ETFs/Crypto/Forex daily price datasets, which have a
clean yfinance equivalent. Bonds/Commodities/Indices still require a
real token (no matching yfinance schema for yield-to-price or FX
normalization). get_pricing() is unaffected since it always loads the
Universe-Daily-Price routing table first, which has no yfinance
equivalent either.

Verified with live symbols and zero credentials: AAPL/MSFT stocks,
EURUSD forex, BTC/ETH crypto all returned correct OHLC data through
the existing adjust/to-USD pipeline. Full 31-test suite still passes.
Maps the codebase into a queryable knowledge graph: 826 nodes and
1,330 edges across 64 labelled communities, built from AST extraction
over 72 code files plus semantic extraction over 9 docs and 2 images.

Outputs under graphify-out/:
  GRAPH_REPORT.md  audit report (god nodes, bridges, cohesion, gaps)
  graph.json       raw graph data
  graph.html       interactive standalone visualization

Machine-specific sidecars (.graphify_python, .graphify_root, cache/)
are gitignored since they hardcode absolute paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSqdYCWHYtsXmBA3ZCGny8
Web sessions start from a fresh container with none of the project's
dependencies installed, so pytest and black could not run without
repeating the setup by hand every session.

Add .claude/hooks/session-start.sh, registered in .claude/settings.json.
It mirrors the CI install (requirements-dev.txt) plus black, installing
into a project .venv because the container's system interpreter carries
Debian-managed packages such as cryptography that pip refuses to upgrade
in place. It exports PATH and PYTHONPATH so bare python/pytest/black
resolve to that venv rather than the system interpreter or isolated tool
shims, and runs asynchronously so the session is usable while packages
install. It is a no-op outside remote sessions.

Also fix a pre-existing CI failure. The Tests workflow had failed every
run since it was added in 7048aaf, including on main, with
"ModuleNotFoundError: No module named 'pwb_toolbox'" during collection:
the tests import the package from the repo root, pytest's prepend import
mode puts tests/ on sys.path rather than the root, and the workflow never
installs the package or sets PYTHONPATH. Setting pythonpath = ["."] under
[tool.pytest.ini_options] fixes CI and plain local runs alike.

Add CLAUDE.md documenting the layout, setup, commands, and test
conventions. No runtime code is changed.
These models were added in c2aab2d on
codex/add-risk-management-models-and-interface, four minutes after the
commit whose tree became the root of the rewritten history. The rewrite
therefore dropped them: neither RiskManagementModel nor TrailingStop
appeared anywhere in the tree, pwb_toolbox_legacy included.

Port the module to pwb_toolbox/backtesting/risk_models.py. The original
lived at pwb_toolbox/backtest/risk_models/__init__.py, a package that no
longer exists; a flat module matches the layout of its new neighbours
(commission.py, portfolio.py, universe.py). The logic is carried over
unchanged -- only black formatting differs -- since it depends on nothing
but dataclasses and typing. The models are re-exported from
pwb_toolbox.backtesting so they are reachable like the other building
blocks.

Tests come across too, dropping the sys.path insertion and stub_modules()
call they needed when the old package pulled heavy imports at __init__
time. Added alongside them: the base class raising NotImplementedError,
the package-level re-exports, the trailing stop ratcheting up with price,
and every model tolerating an empty position list without dividing by
zero.

Nothing calls these models yet -- the engine that would have consumed
them is gone, and wiring them into the Backtrader-based run_strategy is a
separate design question. Suite goes from 31 to 43 tests.
.vocab.txt is regenerated from graph.json on every query, so it is a
local cache rather than a tracked output.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSqdYCWHYtsXmBA3ZCGny8
…sts (#4)

is_tradable compared the current close to close[-2], skipping the most
recent bar, and its length guard required three bars when only two are
needed. Compare against close[-1], relax the guard to two bars, and add
tests/test_base_strategy.py covering both.

Co-Authored-By: Claude <noreply@anthropic.com>
Vendor the MIT-licensed ui-ux-pro-max skill into .claude/skills/ and register
21st.dev's 21st MCP (successor to Magic MCP) in .mcp.json.

The skill is a local CSV database — 84 UI styles, 192 color palettes, 74 font
pairings, 98 UX guidelines, 25 chart types across 22 stacks — queried by a
stdlib-only Python script with no network access and no API key. The installer
also drops six companion skills; those were pruned, since several of their
generators shell out to `npx shadcn` or image APIs.

The 21st MCP is an HTTP server authenticated with ${API_KEY_21ST}, expanded from
the environment so no key is committed. Claude Code reads that from its process
environment and never from .env, which .env.example and CLAUDE.md both spell out
because a filled-in dotfile is the obvious thing to try and is exactly what does
not work.

Also documents the NEEDS YOU convention: anything requiring the user goes in one
numbered block at the end of a reply rather than buried mid-paragraph, where it
reads as commentary and gets skipped.

The library itself is headless and unchanged — pytest passes 47/47.
The action-item block's shape was specified, but nothing said which shell its
commands ran in, or how the reader was meant to find the program to run them in.

- Target Windows PowerShell: no `curl | sed`, no `~`, no `&&`, and UTF-8 writes
  via `[IO.File]::AppendAllText` so em dashes and emoji survive on 5.1. Pin
  raw.githubusercontent URLs to a commit SHA so an unmerged branch cannot
  silently serve stale content.
- Every step names the program it goes into and how to open it; GUI steps name
  the window, menu path, and button text; say what success looks like where a
  command prints nothing. Adds a worked example.
- Never ask for a hand-edited placeholder. Use `Read-Host` for supplied values,
  so the paste runs verbatim and secrets stay out of shell history.
static/karaoke-box.html is a single-file web page that plays karaoke:
lyrics fill left-to-right in sync with a bouncing playhead while a
synthesized band plays underneath. No audio files and no network
requests — every sound is built at runtime from Web Audio oscillators
(a detuned triangle-pair guide voice, a filtered chord pad, a sine
bass, and noise-burst drums).

Ships six tracks: four public domain (Twinkle Twinkle, Row Row Row
Your Boat, Frère Jacques, Beethoven's Ode to Joy with Henry van Dyke's
1907 text) and two originals written for this page so no melody is
reproduced from memory. Songs are stored as [midi, beats, syllable]
triples; a melisma is a note with a null syllable, and swing is baked
into the durations so the audio and the lyric wipe share one timeline
and cannot drift.

Paste arbitrary lyrics and the page scores them: an English
syllabifier splits the text (respecting the writer's own hyphens),
then a seeded walk over a scale generates a melody with chords under
it. The seed is derived from the text, so the same lyrics always
produce the same tune, and a song round-trips through the URL hash.

Not imported by anything under pwb_toolbox/ and not collected by
`packages = find:`, so it stays out of the wheel and pytest never
touches it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197Pq328F76JxPk9fDWrDS3
docs/index.html exercises the skill end to end on a real page. Every visual
decision came from a query: surface treatment from --domain style, the
gold/violet palette from --domain color, the JetBrains Mono and IBM Plex Sans
pairing from --domain typography, and the reveal timings from --domain gsap.

The hero is an equity curve because that is the one artifact every backtest
produces. The six figures beneath it are computed in the browser from the same
synthetic series that is plotted, using the definitions in
pwb_toolbox.performance, and labelled as a demo rather than a track record.

Both typefaces are embedded as base64 woff2 instead of linked from a font CDN,
which accounts for most of the ~120 KB and buys a page that renders identically
offline, from file://, and under a strict CSP. Verified in Chromium: no console
errors, no failed requests, no horizontal overflow at 1280px or 390px, and both
themes resolving from tokens.

Named index.html so GitHub Pages can serve it from /docs unchanged.
Ports an EMA/RSI/StochRSI TradingView indicator into a backtestable signal.
backtrader has RSI and Stochastic but no Stochastic RSI, so StochasticRsi
builds it from RSI plus Highest/Lowest over the RSI line, validated against an
independent pandas implementation. EmaRsiStochRsiSignal emits the entry/exit
pair the existing portfolio strategies already consume.

Three things measurement changed about the obvious implementation:

The degenerate-window guard cannot be Pine's exact `high - low == 0`. Wilder
smoothing decays average gain and loss at the same rate, so a constant price
holds RSI fixed only to within float rounding (~1e-14, not 0). The exact test
misses, the division amplifies that residue across the full 0-100 range, and a
price that never moved reports a confident %K of 100. The guard is an epsilon.

"EMA rising" and "close above EMA" are one filter, not two:
EMA_t - EMA_{t-1} = alpha * (close_t - EMA_{t-1}), so they agreed on all 2981
bars measured. A single filter with a trend_lookback that does change behaviour
replaces the illusory choice.

exit_on_trend_break defaults off. Entry requires a rising EMA, so one down bar
closes the position right after opening it — measured, it cut average hold from
20.4 to 5.5 bars and win rate from 64% to 47%.

Tests validate the arithmetic and the plumbing, not profitability: they run on
simulated random walks, which contain no edge by construction.
Grok has no per-conversation export button, so getting chat history out means
either scraping a signed-in session or waiting on xAI's account data download.
This does both, into one layout: raw/ (payloads verbatim), markdown/ (readable),
merged/ (one document per topic), index.json.

`probe` prints one listing page for diagnosis, `render` rebuilds Markdown from
archived payloads offline, and `merge` groups repeated topics by TF-IDF cosine
similarity, collapsing byte-identical duplicates. `run_daily.sh` re-merges from
cron and stays quiet unless the grouping changed.

Verified against a real accounts.x.ai download: 29 conversations, 245 turns,
every message present verbatim, every timestamp parsed, no turn with an unknown
role. Merging reduced it to 22 documents with no false grouping. That run drove
most of the work — the real dump nests conversation metadata under a
`conversation` wrapper with turns beside it, wraps each turn again under
`response`, and uses MongoDB extended JSON for turn timestamps, which together
made it parse as nothing at all.

Three bugs the real data surfaced, each with a regression test: `convert`
archived a bare entry while `_load_raw` expected `pull`'s envelope, so `render`
re-read a converted export as 29 empty conversations; `merge` appended rather
than replaced, so re-running tripled the document count; and `run_daily.sh`
counted files with `ls` on a directory that does not exist before the first
merge, which under `set -e` killed it silently.

Field names in grok.com's internal endpoints move between builds, so nothing
assumes one spelling: accessors take candidate keys, endpoint paths fall through
a candidate list and are overridable, and every payload is archived verbatim.

Route 1 (`pull`) is unverified — those endpoints are unpublished and were never
run against a live account. `probe` checks that in one request.

Output goes to a gitignored grok-export/; only cookie names are ever logged.
Add a self-contained karaoke player under static/
Adds a workflow that deploys the landing page on every push to main touching
docs/. configure-pages runs with enablement: true so the first run turns Pages
on through the API — the repository setting does not have to be flipped by hand
first, which matters because that toggle is not reachable from a headless
session.

Deploys the docs/ directory only, not the repo root, so the workflow file and
package source are not published alongside the page.

docs/.nojekyll is insurance for the fallback source. "Deploy from a branch" does
run Jekyll, which would try to build the three .md files sitting next to
index.html; the Actions path serves the artifact verbatim and ignores the file.

Concurrency is grouped without cancel-in-progress: a half-finished Pages deploy
is worse than a slightly stale one.
The compose pane could only play a set of lyrics once. Now "Save to
songbook" gives it a track number and keeps it: saved songs appear
under a "Yours" divider, answer to the numeric remote like any other
track, and can be removed with a control beside the row.

An entry stores the lyric text, the vibe, and an optional title —
never the notes. composeSong is seeded from the text, so rehydrating
on load regenerates a byte-identical melody, which keeps a saved song
a few hundred bytes instead of a full note table.

Songbook rows are now built from DOM nodes rather than an innerHTML
string. Titles are user input and were being interpolated into markup;
a title containing a tag would have been parsed as one. Track codes
continue from the built-in six (007, 008, …), and the header count
follows along.

localStorage is wrapped: a browser that blocks it still gets the song
for the session, and the songbook says so rather than silently
dropping it on reload. Share links now carry the title alongside the
lyrics and vibe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197Pq328F76JxPk9fDWrDS3
Let the karaoke songbook hold your own songs
graphify resolves symbols by bare name with no module-origin check, which
produces two opposite errors on this repo:

  Defect A (under-linking): calls through an aliased local module import
  (`import pwb_toolbox.performance as pwb_perf` then `pwb_perf.cagr(...)`)
  are never emitted as edges. 23 such call sites exist and none are
  captured. The worst case is backtesting/portfolio.py, which calls 13
  functions in performance/ while the graph holds zero edges between them.

  Defect B (over-linking): a third-party name colliding with a repo symbol
  (`from datasets import load_dataset` vs pwb_toolbox/datasets/__init__.py)
  is matched to the local definition. 9 fake edges, all from HuggingFace
  `datasets`.

Net: 1,330 -> 1,344 edges, a 2.4% error surface. Degree rankings are
robust to this; betweenness is not - load_dataset() drops 86% once
corrected, so bridge findings need verification before use.

networkx is optional and only gates the betweenness section, so the audit
runs on a bare interpreter. Re-run after `graphify update` to measure drift.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSqdYCWHYtsXmBA3ZCGny8
Thirteen files had drifted out of black formatting. CI ran pytest but never
black, so nothing caught it. This reformats them and closes the hole that let
it happen — both halves together, since the gate alone would turn main red.

The reformat is formatting-only, verified by parsing every touched file before
and after and comparing ASTs, which are identical. The suite still passes at
170.

The new `format` job is separate from `test` rather than a step in it: as a
step, whichever ran first would mask the other's result, and a formatting nit
hiding a real test failure is the worse outcome.

black is pinned to 26.5.1 in requirements-dev.txt. Its stable style changes
with each January release, so an unpinned formatter would eventually fail CI on
code nobody touched, and a gate that fails for unrelated reasons is one people
learn to ignore. The workflow reads that pin out of requirements-dev.txt rather
than repeating it, so the two cannot drift and disagree about what "formatted"
means.

Scoped to the code this repo owns. pwb_toolbox_legacy/ is superseded and kept
as-is for reference; the vendored ui-ux-pro-max skill tracks upstream and is
restored by `uipro init`. Neither is in the gated scope.

Two knock-on fixes: the session-start hook installed black separately and
unpinned, which would have fetched a different version than CI enforces, so it
now comes from requirements-dev.txt; and CLAUDE.md said "CI does not check
formatting" and documented the narrower `black pwb_toolbox/` scope, both of
which are now wrong.
A Duet toggle splits the monitor into two lanes, one per singer. Lane A
keeps the rose-to-amber wipe; lane B gets a cool teal-to-periwinkle one,
so at a glance you can tell whose line is whose. The singer who is
waiting still sees their next line, held dim rather than sweeping, and
the look-ahead row is dropped because the lanes already carry it.

Each song says how it wants to be split. Twinkle, Ode to Joy, the blues
and the waltz alternate lines and finish together; the blues already had
call-and-response lines ("Yeah…", "Mm-hmm…") that become B's naturally.
Row Row Row Your Boat and Frère Jacques are rounds, so duet mode runs
them as actual canons: B sings the same words two bars behind A, which
extends the arrangement by the offset. Songs with no arrangement of
their own, including anything you write, fall back to alternating lines.

Where a line is marked for both singers, B takes a harmony — the nearest
chord tone above the melody, read off whatever chord is sounding
underneath, so it is consonant by construction rather than a fixed
interval. B's guide voice is filtered darker with a sub-octave partial
instead of an upper one, to keep the two apart by ear as well as eye.

The part badges are buttons: click A or B to drop that part from the
guide and sing it yourself while the other stays guided.

Rendering was one hardcoded line; it is now a lane structure the two
share. setLaneEntries paints synchronously rather than waiting on the
next animation frame — a background tab gets no frames, and the monitor
would otherwise sit on the previous song's words.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197Pq328F76JxPk9fDWrDS3
The workflow claimed enablement: true would turn Pages on via the API so the
repository setting never had to be touched. Run 1 disproved that:

  Get Pages site failed.    Error: Not Found
  Create Pages site failed. Error: Resource not accessible by integration

pages: write lets the default GITHUB_TOKEN deploy to a Pages site that already
exists; it does not let it create one, which needs admin rights the workflow
token does not carry. So Settings > Pages > Source > "GitHub Actions" is a
one-time manual prerequisite, and the comment and CLAUDE.md now say so instead
of promising otherwise.

Keeping the flag: once Pages exists the preceding GET succeeds and no create is
attempted, so it costs nothing, and it does work under a token with admin
rights.
A base class reached through a relative import is emitted as
uses/INFERRED/0.5 anchored to the import line, instead of
inherits/EXTRACTED/1.0 anchored to the class definition. Absolute imports
resolve correctly, so the graph holds 21 correct `inherits` edges while
all 11 BaseStrategy subclasses in backtesting/strategies.py are
downgraded. Topology is unaffected, but `inherits` queries silently miss
the entire modern package.

This is the same root cause as defects A and B: resolution keys off the
import form rather than the symbol.

Also reports dead base-class imports as a separate note. Following the
inheritance chain surfaced that pwb_toolbox_legacy imports 16 base
classes from pre-rename `pwb_toolbox.*` paths that no longer exist
(pwb_toolbox.datasets.raw, .perimeters, .raw.analysis, .knowledge_graph).
Those edges are correctly typed only because graphify fell back to
bare-name matching, so they describe intent rather than runnable code.
Flagged rather than fixed - the package is untested and unreferenced by
CI, so it may be archived deliberately.

Totals: 32 structural errors (2.41%) plus 11 mistyped (0.83%).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSqdYCWHYtsXmBA3ZCGny8
"Score me" opens the microphone and rates what it hears against the note
the song expects at that moment.

Pitch comes from a YIN detector rather than plain autocorrelation. A sung
note is rich in harmonics and bare autocorrelation octave-jumps on it;
YIN's cumulative mean normalised difference stays on the fundamental. The
buffer is halved in rate first, which quarters the work at no cost to a
range that tops out around 1 kHz, and the search is bounded to 70–1050 Hz.
A parabolic fit around the minimum gives sub-sample accuracy — measured
at 0 cents against a synthetic 440 Hz reference.

Comparison folds to the nearest octave, so a low voice answering a high
line is singing the note rather than missing it by twelve. Full credit
within a quarter-tone, tailing to nothing at two semitones.

A pitch ribbon shows the tune as bars with your voice plotted along it,
green where it lands. At the end a card gives the score out of 100, a
rank, how many notes landed, and the longest unbroken run in tune. The
run is measured in beats and converted through the current tempo, not
counted in animation frames, which would have read differently on a
120 Hz display. Notes are counted by onset rather than by pitch, so two
notes at the same pitch count as two.

In duet mode, whichever part you muted is the part that gets graded.

The microphone is never routed onward to the speakers — the page is
already playing a backing track, and connecting it would be a feedback
loop. Denial is handled explicitly: an embedded page is often refused the
microphone outright, and the page says so and stays usable rather than
failing silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197Pq328F76JxPk9fDWrDS3
CI runs `black --check --diff pwb_toolbox/ tools/ tests/` with the version
pinned in requirements-dev.txt (black==26.5.1). graph_audit.py was the only
file failing; verified locally with that exact version, 45 files now clean.

Behaviour is unchanged - the audit reports the same 23/9/11 counts after
reformatting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSqdYCWHYtsXmBA3ZCGny8
Pages is now served by Settings > Pages > Source > "Deploy from a branch",
main, /docs. GitHub builds that itself, so the workflow has nothing left to
do and cannot work anyway.

pages.yml never succeeded — six runs, all failing at configure-pages with

  Get Pages site failed.    Error: Not Found
  Create Pages site failed. Error: Resource not accessible by integration

The default GITHUB_TOKEN may not create a Pages site: pages: write covers
deploying to one that already exists, not creating one. And now that a
branch source is selected, actions/deploy-pages could not target it
regardless, since it only deploys where the build type is workflow. Left in
place it would keep marking every push that touches docs/ as failed.

docs/.nojekyll stays and matters more than before: the branch source does
run Jekyll, which would otherwise try to build the .md files sitting beside
index.html.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012MYpq7J61wGBjctqkKLP9T
Finishing a scored track now files the run: score, rank, which song, who
sang it, and when. The board sits under the songbook, sorted best first,
and each songbook row carries your personal best for that track.

An entry records the conditions too — duet and which part, and the tempo
when it is not 100% — because a run at 145% is not quite the same feat as
one at speed, and the row should say so rather than flatter the number.

Beating your own best on a track flags it on the result card, with the
score it beat. A first run says so instead, rather than claiming a
record over nothing.

The singer's name is a free text field, so more than one person can hold
places in a session; it persists alongside the scores. Both go through
the same guarded localStorage as the songbook, so a browser that blocks
storage still keeps the board for the session and says as much.

The board was originally stacked under the compose pane, which left the
left column half empty; it reads better beneath the songbook, and the two
are related anyway.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197Pq328F76JxPk9fDWrDS3
jay79-boop and others added 8 commits August 12, 2026 09:31
Add graphify knowledge graph and an audit tool for it

Maps the repo into a queryable knowledge graph (826 nodes, 1,330 edges,
64 communities) and adds tools/graph_audit.py, which validates the graph
against the repo's actual imports.

The audit found three systematic extraction defects, all from graphify
resolving symbols by bare name without a module-origin check: 23 missing
edges from aliased module calls, 9 fake edges from a third-party name
collision, and 11 mistyped inheritance edges. Degree rankings survive
correction; betweenness does not.
…on-9ci2wb

Publish docs/ from the branch instead of a workflow
tools/karaoke_server serves static/karaoke-box.html and a score API from
one origin, so several people posting from their own phones land on one
board. Standard library only, no new dependencies:

    python -m tools.karaoke_server

The page notices it was served by a board host, via an injected meta tag,
and connects on its own. A copy opened straight from disk finds no such
tag and makes no requests at all — which is also what keeps the published
artifact quiet, since it is sandboxed against outbound traffic and could
never reach a server anyway. Pointing a standalone page at a server is a
matter of pasting its address into the panel.

Nothing a client sends is trusted past its shape. Text is length-capped
and stripped of control characters, numbers are range-checked, and the
timestamp is assigned server-side, so a submission cannot backdate itself
to win a tie. Malformed runs get a 400 explaining why. Storage is a JSON
file written through a temporary and renamed into place, so an interrupted
write cannot leave a half-file; a corrupt or hand-edited file reads as
empty rather than taking the server down.

The page keeps its local board as the fallback and shows that instead
whenever a configured server is unreachable, saying so rather than
appearing empty. Clearing only ever affects this browser's copy — the
shared board is not ours to wipe.

The page is authored as an artifact fragment with no <html> around it, so
serving it raw would land in quirks mode and lay out wrongly. The server
splits it at the stylesheet and wraps it as a proper document.

Board rules are tested directly rather than over a socket, so the suite
stays offline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197Pq328F76JxPk9fDWrDS3
Add a shared leaderboard server for the karaoke page
Templates the market-close broadcast script that was being written by hand,
filling the four data segments from pwb_toolbox.datasets and leaving the
kicker to a human.

Three constraints drove the design:

Numbers never reach ElevenLabs as digits. TTS reads numerals by its own
rules, and on a markets script that is most of the runtime -- 4.09 comes
back as "four point zero nine" where the desk says "four-oh-nine". spoken.py
spells everything in broadcast idiom first ("six tenths of a percent", "a
hundred and forty points"), a test asserts the rendered script contains no
digit at all, and the CLI warns when one survives.

The generator reports moves and never invents causes. It has prices, not
press releases, so no line asserts why anything happened -- a template that
fills in "after the company beat expectations" would eventually broadcast
something false about a real company on a day nobody is reading the output.
The jokes are about post-hoc explanation instead, which needs no facts
beyond the move.

The disclaimer is a fixed string. Every other segment rotates from a bank
seeded by the session date, so a working week does not repeat itself while a
given day always renders identically.

market.py keeps collect() as the only networked function and everything else
pure over DataFrames, so the 100 new tests run offline like the rest of the
suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016phtezrnZSLL3u4PU8cQEG
The two segments carry nearly every number in the broadcast -- index
levels, breadth counts, two percentage moves, two closing prices -- and
they are also the segments that change most between sessions, so an
unfamiliar ticker spelling or an odd-sounding level surfaces there first.

Prints whole segments rather than a stripped-down figure list, jokes
included, so what you audition is what ships. Exits 1 when neither segment
has data, which makes it usable as a guard in a scheduled run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016phtezrnZSLL3u4PU8cQEG
…cription-6yx38q

Add daily market-close script generator for TTS avatars
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants