Add Kalshi + Polymarket prediction market integration#68
Open
Paulo-BatistaFerraz wants to merge 33 commits into
Open
Add Kalshi + Polymarket prediction market integration#68Paulo-BatistaFerraz wants to merge 33 commits into
Paulo-BatistaFerraz wants to merge 33 commits into
Conversation
- vmOrderBook.cs: lock cumulative-depth X axes to 0-100 cents with title 'YES price (¢)' (binary-contract probability scale) - ucDepth1.xaml: rename 'Bids' → 'YES Bid', 'Asks' → 'YES Ask' headers (asks come from reflected NO bids per the plugin's mapping rule) - Disable VPIN, OTT_Ratio, MarketResilience studies — designed for continuous markets (informed-flow / toxic-trade detection); not meaningful on binary contracts. LOBImbalance kept (transfers cleanly). - Removed disabled study DLLs from runtime bin ScatterPoint patch from earlier session (oxyplot fork) carried forward.
New standalone window opened from a 'Kalshi Strikes' button on the Dashboard. Subscribes to HelperOrderBook (every venue's order books), filters to Kalshi tickers (prefix KX), parses 'EVENT-STRIKE' from the ticker, and renders a live grid: Event | Strike | Ticker | YES Bid | YES Ask | Spread | Mid Prob | Updated. Files added: - ViewModel/vmKalshiStrikeLadder.cs (KalshiStrikeRow + vmKalshiStrikeLadder) - View/KalshiStrikeLadderWindow.xaml + .cs (dark theme, materialdesign) Wired button + handler in Dashboard.xaml + Dashboard.xaml.cs. Verified: plugin loads, polls KXHIGHTATL-26APR29-B82.5 successfully. No errors in log. Strike ladder window opens cleanly.
- Revert hard-lock of cumulative-depth X axes to 0-100; auto-scale restored. Hard-lock broke non-Kalshi venues (KuCoin chart axes pinned to YES-price scale even when viewing BTC/USD etc). Conditional per-symbol locking deferred to Phase D. - Re-add 'Kalshi Strikes' button to Dashboard.xaml (previous edit did not persist — empty git diff for this file at Phase B commit time; handler in Dashboard.xaml.cs was already there). Companion change in visualhft-kalshi (separate repo): KalshiPlugin sets OrderBook.ProviderID=100, ProviderName='Kalshi' so the venue appears in VisualHFT's Providers' Status panel and is selectable as a venue throughout the app.
New View/KalshiLadderWindow + ViewModel/vmKalshiLadder show one market's full depth styled like kalshi.com: - Header card: Symbol, YES bid (green), NO bid (red, derived as 100 - YES ask), mid price, spread - Asks DataGrid (top, red): Price ¢ | Contracts | cumulative Total $ (/) - 'Mid' divider showing midprice - Bids DataGrid (bottom, green): Price ¢ | Contracts | cumulative Total $ Wired from Strike Ladder: double-click any row → opens KalshiLadderWindow for that ticker. Multiple ladders can be open simultaneously. Mirrors the kalshi.com price-ladder UX (cents, cumulative dollars, last-trade-style mid divider). Trade-button equivalents intentionally omitted — this is a read-only data view by design.
KalshiStrikeRow.Category derived from ticker prefix: KXHIGH/LOW/SNOW/RAIN → Weather; KXNBA/NFL/MLB → Sports; KXBTC/ETH → Crypto; KXWTI/BRENT/NGAS → Commodities; KXFED/RATE/CPI → Rates/Macro; KXSENATE/PRES → Politics; KXSP500/NASDAQ → Equities. New 'Cat' column at the leftmost position (sortable, color-coded). Lets you slice the ladder by domain when watching multiple market types.
Each row in the per-market ladder now shows a horizontal bar proportional to that level's contracts vs the largest visible level. Asks bars are translucent red, bids bars translucent green. This is the column-depth visual integrated with the price ladder — matches what kalshi.com shows behind each price level. KalshiLevelRow.BarWidth is recomputed on every poll based on the max contracts across all current bid+ask levels.
For each event group (rows sharing EventTicker), sort strikes ascending
and check every adjacent pair K_low < K_high for two violations:
1. Executable arb: yes_ask(K_low) < yes_bid(K_high). Free profit
= bid(K_high) - ask(K_low) cents per pair, regardless of outcome.
2. Theoretical violation: mid(K_low) < mid(K_high). Implied prob
ordering broken at midpoint, sub-spread (not directly tradable).
Each row exposes ArbEdgeCents (max executable across pairs) and
TheoMonotonicityGapCents (max theoretical gap). New 'Arb' column in
the grid renders +X.X¢ in red if executable, ~X.X¢ in yellow if
theoretical-only.
Strike numeric is parsed from the suffix token (T103.99, B82.5, T63)
to enable correct ordering across heterogeneous strike formats.
Scanner runs in BeginInvoke after every order book update, so it's
always current relative to the visible state.
This is the foundation for the future trading algorithm: visualize
free-money signals; algorithm picks them up later.
vmKalshiPMF + KalshiPMFWindow:
Subscribes to all Kalshi order books, filters by event-ticker prefix,
derives the PMF from the strike-ladder mids:
p_0 = 1 - P(>K_0) left tail
p_i = P(>K_{i-1}) - P(>K_i) interior bins
p_N = P(>K_N) right tail
Computes median strike via cumulative interpolation. Renders as
horizontal bars (peak = yellow, tails = gray, interior = blue).
Live updates as the strikes tick.
Strike ladder context menu — right-click any row to:
- Show implied PMF for this event
- Open ladder for this market
Ladder window polish — auto-scroll on each book update so:
- Best ask is always at the bottom (next to the mid divider)
- Best bid is always at the top (next to the mid divider)
The 'action' stays in view as the book updates, regardless of depth.
VisualHFT/Helpers/KalshiTradeHelper.cs: Self-contained RSA-PSS signer + HTTP client wired exclusively to demo (demo-api.kalshi.co). Replicates plugin's signing logic to avoid VisualHFT.csproj depending on the plugin assembly. Exposes PlaceLimitAsync(side, action, price, count) and CancelAsync(id). Hard-coded safety: count ∈ [1, MAX_COUNT=5], price ∈ [1, 99]¢, ticker must start with KX. KalshiLadderWindow: Adds a 'DEMO ORDER' panel below the bid grid. ComboBox for side (YES/NO) and action (Buy/Sell), TextBoxes for price and count, Submit + Cancel last buttons, status line. Tracks recently-placed order IDs so Cancel last works without the user needing to copy ids around. Trading + viewing are intentionally on different envs: - Polling stays on prod (rich orderbooks, read-only key) - Orders go to demo ($1000 fake balance, full trade scope) This avoids any path where a UI mistake hits prod with real money. Order placement endpoint and cancel both verified live on demo prior to wiring (POST /portfolio/orders → 201, DELETE → 200).
New button '📁 Events Browser' on Dashboard opens a tabbed window with one tab per real Kalshi category (16 of them, e.g. Sports / Elections / Crypto / Climate and Weather / Commodities). Each tab is a DataGrid: Event Ticker | Series | Title | Sub-title. Helpers/KalshiEventCatalog.cs: Self-contained signed REST client (mirrors KalshiTradeHelper pattern) that paginates GET /events?status=open&limit=200 through every cursor. Returns List<KalshiEventInfo>. Hits prod (richer universe). ViewModel/vmKalshiEventBrowser.cs: Loads the catalog async, groups by API's category field, sorts by count desc. ObservableCollection<KalshiCategoryGroup> with header counts. View/KalshiEventBrowserWindow.xaml + .cs: Dark-themed TabControl + DataGrid template per tab. Refresh button. Empty status text while loading; shows totals when done. ~6000 events across 16 categories on first load. Read-only browse — to add an event to the polling list you still edit plugin Tickers. Dynamic 'Watch this event' subscription is a future enhancement.
Helpers/KalshiBrowserPoller.cs (new):
Singleton poller in VisualHFT main project that watches a dynamic set
of Kalshi tickers. On each book update, calls
HelperOrderBook.Instance.UpdateData(book) — same bus the plugin uses.
Tagged with the same Kalshi ProviderID/Name so symbols merge under the
existing 'Kalshi' provider in the Provider/Symbol dropdown.
Hits prod, 1Hz baseline, 50-deep books.
KalshiEventCatalog.GetEventMarketsAsync(evt) — new method that fetches
the strike list for one event via /events/{ticker}?with_nested_markets=true.
KalshiEventBrowserWindow:
TabControl now wires MouseDoubleClick. On double-click of any event
row, the handler walks up to the DataGridRow, gets the KalshiEventInfo,
fetches its markets, and adds them to KalshiBrowserPoller.Instance.
Window title updates to show how many tickers are now being watched.
Workflow: open browser → double-click any event in any category → that
event's strikes immediately start populating the strike ladder and
appear in the main view's Provider/Symbol dropdown.
Log analysis showed catalog was hitting Kalshi's basic-tier rate limit
on page 5 (429 too_many_requests), giving up with only 1000 events
instead of the actual ~6000.
KalshiEventCatalog.FetchAllOpenAsync now:
- 200ms throttle between pages (= 5 req/s ceiling)
- Per-page retry on 429 with exp backoff (1s/2s/4s/8s/16s, max 5)
via FetchPageWithBackoffAsync
- Process-wide cache (5-min TTL) so reopening the browser is instant
- Refresh button calls InvalidateCache() to force a fresh fetch
First-load time goes from 'incomplete' to '~6s for full 6000 events'.
The plugin path auto-registers each new symbol with HelperSymbol via BasePluginDataRetriever.RaiseOnDataReceived. The browser poller pushes straight to HelperOrderBook, bypassing that — so symbols never appeared in VisualHFT's Provider/Symbol dropdown after watching. KalshiBrowserPoller.Watch now also calls HelperSymbol.Instance.UpdateData(t) the moment a new ticker is added. Symbols appear immediately, even before the first orderbook poll resolves.
Kalshi /events response has no liquidity info per event. Solution:
fetch all active /markets after events load, group by event_ticker,
sum open_interest_fp into a Dict<eventTicker, (oi, marketsCount)>.
KalshiEventCatalog.FetchEventLiquidityAsync paginates /markets with
the same throttle/backoff pattern as FetchAllOpenAsync. ~30 pages
typical, ~30-60s in background depending on active-market count.
KalshiEventInfo gains:
OpenInterest (double, set after liquidity load) + OpenInterestText
('5.0K', '1.2M', etc), MarketCount (int).
PropertyChanged notifications so the DataGrid updates live.
vmKalshiEventBrowser kicks off LoadLiquidityAsync on a background
task after events land. Once liquidity arrives, each category's
events get re-sorted by OI desc (ticker asc tiebreaker).
Window XAML: new 'OI' (yellow) + 'Mkts' columns at the leftmost
position, both right-aligned and sortable.
Two bugs surfaced by user screenshot showing only governance events
with OI=0:
1. KalshiEventCatalog.FetchEventLiquidityAsync used status=active in
the /markets query — Kalshi rejected with 400 ('cold market not
found' style error). Valid values are open/closed/settled. Switched
to status=open. Liquidity now actually loads.
2. vmKalshiStrikeLadder + KalshiBrowserPoller.Watch filtered by KX
prefix. Real Kalshi tickers include CONTROLH-2026, GOVPARTY*,
EUEXIT, etc. that pre-date the KX naming scheme. Filters now use
Kalshi ProviderID (100) for the ladder and length>=3 for the watch
list, so all Kalshi events are tradeable through the same flow.
Volume in events browser: KalshiEventInfo.Volume + VolumeText (5.0K/1.2M format). KalshiEventCatalog.FetchEventLiquidityAsync returns Dict<eventTicker, (oi, vol, markets)>; aggregates volume_fp same way as open_interest_fp. Browser DataGrid: new 'Volume' column with SortMemberPath=Volume. Watch List window (new): Dashboard '📋 Watch List' button opens KalshiWatchListWindow. vmKalshiWatchList subscribes to HelperOrderBook, filters by KalshiBrowserPoller.WatchedTickers + Kalshi providerId=100. Live grid: Ticker | Event | YES Bid | YES Ask | Mid | Spread | Updated | Remove. Manual 'Add ticker' input at the top calls KalshiBrowserPoller.Watch directly so additions flow to the same bus as double-click-from-browser. Remove button calls KalshiBrowserPoller.Unwatch and pulls the row from the table. Anything on the watch list automatically also shows in the Strike Ladder via shared HelperOrderBook subscription.
Helpers/KalshiViewRequest.cs (new): static event hub. Anything that wants the main vmOrderBook to display a specific Kalshi ticker calls KalshiViewRequest.Show(symbol, providerId). vmOrderBook subscribes in its ctor and on each fire dispatches to the UI thread, finds the matching Provider in _providers, sets SelectedProvider + SelectedSymbol so the chart populates. KalshiWatchListWindow: new MouseDoubleClick handler on the grid → KalshiViewRequest.Show(row.Ticker, 100). Cursor=Hand + tooltip. Strike Ladder kept on its existing double-click-opens-per-market-ladder behavior; can wire a separate keybind later if desired.
After WatchEventAsync subscribes the event's markets, fire KalshiViewRequest.Show(first_market, 100) so the user sees the chart populate immediately. Same UX as Watch List double-click. The other strikes for the event are still accessible via the Symbol dropdown.
WatchEventAsync now takes a loadChart bool. Plain double-click passes true (current behavior). New right-click menu on event rows offers: - 📋 Add to Watch List (no chart) - 📊 Watch + Load Chart - 📁 Show all markets in Strike Ladder Last option also opens the Strike Ladder window so the user can scan all strikes of the event side-by-side. SelectedEvent() walks the TabControl's selected content's visual tree to find the active grid + row across the dynamic per-category DataTemplate.
…ladder)
- TextBox 'Search' bound to vm.Search; FilteredRows is a CollectionView
with a Filter that keeps rows whose Ticker or EventTicker contains
the substring. Refresh on every keystroke.
- DataGrid right-click menu adds three actions:
📊 Open kalshi.com-style ladder window — KalshiLadderWindow per ticker
📈 Load in main chart — same as double-click
❌ Remove from Watch List
The kalshi-style ladder option fixes the user's complaint about KXBTCD
not displaying like kalshi.com — the standard ucDepth1 layout is
side-by-side bid/ask, while kalshi.com is asks-on-top with cumulative $.
KalshiLadderWindow already does the kalshi-style layout; this just
exposes it from the Watch List.
Header gains a 🔍 Search TextBox bound to vm.Search. Each KalshiCategoryGroup now also exposes FilteredEvents (an ICollectionView wrapping its Events). The view's filter checks event_ticker / series_ticker / title / sub_title for the search substring (case-insensitive). Setting vm.Search refreshes every group's FilteredEvents view, so typing in the box filters every category tab simultaneously without having to switch tabs. XAML: TabControl DataTemplate's DataGrid bound to FilteredEvents instead of Events.
Issue: when only 3 ask levels exist, the asks DataGrid (in a Grid row with Height='*') still claimed half the window and rendered the rows at the TOP, leaving a huge gap above the Mid divider. Fix: VerticalAlignment='Bottom' + MaxHeight=320 on AsksGrid so the rendered rows sit just above the Mid divider when sparse, and the spacer above absorbs the leftover height. Bids stay top-aligned. Also added MinHeight=120 to both Grid rows so neither side collapses when the other has tons of levels. Mirrors kalshi.com: asks descend toward the mid line, bids ascend away from it.
The previous layout used Grid rows with Height='*' for asks and bids, which split the window evenly even when one side had only 1-2 levels. Result: huge empty gaps in both halves for sparse books. New layout: DockPanel with header pinned top, order panel pinned bottom, and a single ScrollViewer in the middle containing [Asks DataGrid] [Mid divider] [Bids DataGrid] in a StackPanel. Both grids size to their content (ScrollViewer.CanContentScroll=False on the inner grids; the outer ScrollViewer takes over). When the book is sparse, the ladder is compact. When the book is deep, the outer ScrollViewer scrolls the whole stack. Mirrors kalshi.com.
KalshiBrowserPoller now also polls /markets/trades for the ticker the user has currently focused (set by KalshiViewRequest.Show whenever a double-click happens in Watch List / Events Browser / Strike Ladder). New trades are deduped by trade_id and pushed to HelperTrade.Instance.UpdateData — same bus the existing trade-tape panel binds to. Mapping: Price=yes_price_dollars*100, Size=count_fp, IsBuy=(taker_side=='yes'), Timestamp=created_time. Single-ticker only (the focused one) to avoid blowing past Kalshi's ~10 req/s basic-tier limit when 100+ tickers are watched. Switching focus seamlessly redirects the tape. Per-ticker dedupe set capped at 400 ids, then reseeded from the latest page so memory doesn't grow unbounded.
KalshiBrowserPoller.ComputeLoopDelayMs scales the loop sleep with watched-ticker count so total req/s stays under TargetReqPerSec=8 (safe headroom under Kalshi's basic-tier ~10 nominal). Symptom this fixes: with 100+ watched tickers we were issuing 100+ req/s and silently 429-ing on most of them, leaving stale data. Strike Ladder right-click menu now also offers: 📉 Load this ticker into main chart which fires KalshiViewRequest.Show — same hub used by Watch List double-click and Events Browser double-click. Consistent navigation across all three Kalshi-aware views.
The per-ticker ladder now has an opt-in cumulative depth chart above the book. Off by default and persisted per-user via the existing SettingsManager (new SettingKey.KALSHI_LADDER_DEPTH_CHART), so a fresh window shows the ladder full-height with no reserved space for a chart the user hasn't asked for. Toggle lives in the market header next to mid/spread; Ctrl+D toggles it when the window has focus, suppressed inside text inputs so order entry typing is never hijacked. Three unit-mode chips (contracts, notional $, % of total book) sit beside the toggle; selection is also persisted. Chart is drawn with OxyPlot StairStepSeries so the curve is discrete by construction (no smoothing). Y-axis is mirrored on both edges and clipped to the 90th percentile of cumulative depth so a single whale level far from the spread doesn't squash the near-spread shape flat — clipped levels are flagged with an inline overflow indicator.
Header was showing only the raw ticker (e.g. KXNHLGAME-25APR30PHI-PHI),
which is unreadable at a glance and makes it easy to confuse two open
ladders for the same event. Ladder now hits /markets/{ticker} once on
open (process-wide cached, best-effort — empty on failure so the ticker
remains the fallback) and shows the most distinctive human-readable
field as the big header line: subtitle, then yes_sub_title, then title.
The ticker (and parent question, when subtitle is the headline) are
demoted to a smaller gray secondary line below.
KalshiTradeHelper.ForDemo() previously hardcoded the demo PEM at an absolute path on the author's machine, which broke any clean clone. Now it resolves the path in this order: 1. KALSHI_DEMO_PEM environment variable 2. %USERPROFILE%\.visualhft\kalshi-demo.pem 3. The legacy hardcoded path (kept so the existing setup keeps working) Error message names both supported options when none of the candidates exists. Also adds README.Kalshi.md documenting the two-repo layout (this fork + the visualhft-kalshi plugin repo) and a short pointer to it from the top of README.md so anyone landing on the repo sees it immediately.
The three Kalshi helpers (TradeHelper, BrowserPoller, EventCatalog)
previously baked in:
- the author's prod and demo Kalshi access-key ids,
- absolute paths to the author's local PEM files.
Both are gone. A new shared resolver Helpers/KalshiCredentials.cs reads:
KALSHI_DEMO_KEY_ID / KALSHI_DEMO_PEM
KALSHI_PROD_KEY_ID / KALSHI_PROD_PEM
PEM env vars also fall back to %USERPROFILE%\.visualhft\kalshi-{demo,prod}.pem
if unset. Key ids are required (no defaults). Failure modes:
- Trade panel / Event catalog throw a clear error naming the missing
variable.
- Browser poller logs a warning and continues running with empty creds
(so view-only UX without prod creds doesn't crash the app).
README.Kalshi.md updated with the four env vars and softer framing for
sharing the fork upstream.
The companion bundle repo (Paulo-BatistaFerraz/VisualHFT-Kalshi) ships
the same Kalshi-aware fork as a vendored copy with a slightly cleaner
KalshiCredentials helper:
- Env vars use the *_PATH suffix:
KALSHI_DEMO_PEM_PATH, KALSHI_PROD_PEM_PATH (prev: *_PEM)
- Base URLs (DemoBase/ProdBase) live on KalshiCredentials, removing
duplication across TradeHelper / EventCatalog / BrowserPoller.
- Required-only API: Require() throws if missing, TryGetProd*() returns
null. No %USERPROFILE% fallback.
This commit ports those choices over so the standalone fork and the
bundle repo's vendored copy are consistent — same env vars, same setup
docs, same code shape.
README.Kalshi.md updated with the new env var names and a pointer to
the bundle repo for one-clone setup.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds first-class integration with two prediction-market venues (Kalshi + Polymarket) to VisualHFT, plus several supporting UI windows and SDK template improvements.
This is a large PR — happy to split into smaller, focused PRs if maintainers prefer. Opening this first as a single unit so the full scope is visible.
What's included
Kalshi integration
KalshiBrowserPoller,KalshiEventCatalog,KalshiCredentials,KalshiTradeHelper,KalshiViewRequesthelpersPolymarket integration
PolymarketBrowserPollerSDK template improvements
SDK-MarketConnectorTemplate: expanded JSON parser, exchange messages model, plugin settings improvements, template plugin scaffoldingSDK-StudyTemplate: README + viewmodel polishMisc
Notes
Kalshi*,Polymarket*) so it's easy to isolate or removeTest plan