Video 10: Kalshi Trading Bot Version 4
This fourth iteration expands the Version 3 Kalshi backend approach with two new capabilities: multi-agent execution and delta-aware entries. Version 3 moved order routing away from the front end and into Kalshi's backend API. Version 4 keeps that foundation, then extends it so the bot can evaluate several Kalshi 15 minute crypto markets in parallel across different assets while also accounting for how far the underlying market price is from the current target.
YouTube Video
Version 4 Focus
- Multi-agent execution: run the same trading logic across multiple assets in the Kalshi 15 minute crypto market so BTC, ETH, SOL, XRP, DOGE, HYPE, and BNB can each behave like their own bot lane.
- Delta: incorporate the underlying price distance from the current price before taking a trade, so entries can consider both contract price and how far the underlying market still has to move.
- Version 3 foundation: keep the backend API order routing, credential loading, retry handling, and position tracking from the previous version.
Latest Nightshark Build Required
This version requires downloading the latest Nightshark build.
Download Latest NightsharkAPI key Filename
apikey
Private Key Filename
privatekey
Nightshark Code
; =========================
; Strategy Configuration
; =========================
timeDelay := 8 ; activation window in minutes
Entry := 0.85 ; enter when side price >= 85c
Exit := 0.40 ; stop/exit when entered side price < 40c
Delta := 0 ; min |open15m - price| required alongside Entry
assets := ["BTC"] ; Options ["ETH"] ,["SOL"] ,["XRP"] , ["HYPE"] , ["BNB"] , ["DOGE"]
orderSize := 1
; =============================================================
; Kalshi Order API Config
; DO NOT TOUCH BELOW VALUES UNLESS YOU KNOW WHAT YOU ARE DOING
; =============================================================
enableLiveOrders := true
pollIntervalMs := 700
marketRefreshMs := 5000 ; refresh market/session metadata less often; orderbook still refreshes every loop
priceRefreshMs := 1500 ; throttle live asset price calls used only for LogStream
restDelay := 2000 ; ms to let order rest before checking position
entryDiff := 0.02 ; added to price on BUY orders (aggressive entry)
exitDiff := 0.02 ; subtracted from price on SELL orders (aggressive exit)
kalshiApiBase := "https://api.elections.kalshi.com"
kalshiCryptoExchangeIndex := 2
assetSeriesMap := { "BTC": "KXBTC15M", "ETH": "KXETH15M", "SOL": "KXSOL15M", "XRP": "KXXRP15M", "DOGE": "KXDOGE15M", "HYPE": "KXHYPE15M", "BNB": "KXBNB15M" }
kalshiApiKeyId := ""
kalshiApiKeyFile := "reactions/apikey.json"
kalshiPrivateKeyFile := "reactions/privatekey.json"
kalshiPrivateKeyPath := ""
kalshiApiKeyResolvedPath := ""
kalshiPrivateKeyResolvedPath := ""
kalshiKeyPassphrase := ""
kalshiSignerPath := ""
; Track one open position per asset.
positions := {}
assetPhase := {} ; WAIT_WINDOW | MONITORING | IN_POSITION
assetSessionQuarter := {} ; quarter index currently tracked per asset
assetLastPriceLogTick := {} ; throttled logging control
assetSnapshotCache := {}
assetMarketRefreshTick := {}
assetPriceCache := {}
assetPriceRefreshTick := {}
kalshiShardFundingSessionStatus := {}
kalshiShardPendingTransfers := {} ; retained across market changes until reconciled
kalshiAggregateBalanceSnapshot := ""
kalshiBalanceNextRequestTick := 0
kalshiBalanceRetryCount := 0
LoadKalshiCredentialsFromFiles()
if (kalshiSignerPath = "")
kalshiSignerPath := FindNightSharkSigner()
Log("Application Started | Entry: " Entry " | Exit: " Exit " | Delta: " Delta " | Window: last " timeDelay "m")
if (!StartupCredentialCheck())
return
loop {
for _, asset in assets {
snapshot := GetKalshiMarketSnapshot(asset)
if !IsObject(snapshot) {
if (PositionSessionEnded(asset))
MarkHeldToResolution(asset)
noDataKey := asset "|NO_DATA_" CurrentQuarterIndex()
if !IsObject(logOnceKeys)
logOnceKeys := {}
if !logOnceKeys.HasKey(noDataKey) {
logOnceKeys[noDataKey] := true
Log(asset " Waiting for market data")
}
continue
}
up := snapshot.up
down := snapshot.down
; Recompute minsLeft live from closeTime so cached snapshots reflect real elapsed time
minsLeft := SnapshotMinutesLeft(snapshot)
if (minsLeft = "")
minsLeft := MinutesRemainingInQuarter()
inEntryWindow := (minsLeft > 0 && minsLeft <= timeDelay)
sessionKey := (snapshot.marketTicker != "") ? snapshot.marketTicker : ((snapshot.closeTime != "") ? snapshot.closeTime : CurrentQuarterIndex())
if (PositionSessionEnded(asset, snapshot)) {
MarkHeldToResolution(asset)
continue
}
if (!assetSessionQuarter.HasKey(asset) || assetSessionQuarter[asset] != sessionKey) {
assetSessionQuarter[asset] := sessionKey
assetPhase[asset] := "WAIT_FUNDING"
assetLastPriceLogTick[asset] := 0
LogOnceReset(asset)
Log(asset " New session (" snapshot.marketTicker ", shard " kalshiCryptoExchangeIndex ")")
continue
}
if IsObject(positions[asset]) {
pos := positions[asset]
current := (pos.side = "UP") ? up : down
assetPhase[asset] := "IN_POSITION"
UpdateKalshiLogStream(asset, snapshot)
if (current < Exit) {
Log(asset " Stop-loss triggered @ " Fmt(current) " < " Fmt(Exit))
sellConfirmed := false
loop, 4 {
if (A_Index > 1) {
freshSnap := GetKalshiMarketSnapshot(asset)
if IsObject(freshSnap)
current := (pos.side = "UP") ? freshSnap.up : freshSnap.down
}
if SellPosition(asset, pos.side, current, "exit_below_" Exit) {
Log(asset " Position closed")
sellConfirmed := true
break
}
if (A_Index < 4) {
Log(asset " Exit retry " A_Index "/3 | " KalshiLastOrderApiSummary())
Sleep 1000
}
}
if (!sellConfirmed)
Log(asset " Exit order failed after 4 attempts | " KalshiLastOrderApiSummary())
positions.Delete(asset)
assetPhase[asset] := "STOPPED_OUT"
LogOnce(asset, "STOPPED_OUT", asset " Stopped out, waiting for next session")
}
continue
}
if (assetPhase[asset] = "STOPPED_OUT") {
LogOnce(asset, "STOPPED_OUT", asset " Stopped out, waiting for next session")
continue
}
if (assetPhase[asset] = "BUY_FAILED") {
LogOnce(asset, "BUY_FAILED", asset " Buy failed earlier, waiting for next session")
continue
}
if (assetPhase[asset] = "HAS_POSITION") {
LogOnce(asset, "HAS_POSITION", asset " Existing position, waiting for next session")
continue
}
if (assetPhase[asset] = "FUNDING_FAILED") {
LogOnce(asset, "FUNDING_FAILED", asset " Funding request rejected; waiting for next session")
continue
}
if (assetPhase[asset] = "WAIT_FUNDING") {
if (minsLeft = "" || minsLeft <= 0) {
LogOnce(asset, "FUNDING_WINDOW_ENDED", asset " Market closed while waiting for funding")
continue
}
fundingFailed := false
if (!EnsureKalshiCryptoShardFunding(asset, fundingFailed)) {
if (fundingFailed)
assetPhase[asset] := "FUNDING_FAILED"
continue
}
positionReadOk := false
existingPosition := HasExistingPosition(snapshot.marketTicker, positionReadOk)
if (!positionReadOk) {
LogOnce(asset, "FUNDING_POSITION_WAIT", asset " Funding ready; waiting for existing-position check")
continue
}
if (existingPosition) {
assetPhase[asset] := "HAS_POSITION"
Log(asset " Existing position found, waiting for next session")
} else {
assetPhase[asset] := "WAIT_WINDOW"
Log(asset " Shard funding ready")
}
; Refresh the market, quote and entry clock after a funding wait.
continue
}
; --- Waiting for entry window ---
if (!inEntryWindow) {
if (minsLeft <= 0)
LogOnce(asset, "SESSION_ENDED", asset " End of session, waiting for new market")
else
LogOnce(asset, "WAIT_WINDOW", asset " Waiting for last " timeDelay " minutes")
assetPhase[asset] := "WAIT_WINDOW"
continue
}
; --- Entry window open: monitoring prices ---
if (!assetPhase.HasKey(asset) || assetPhase[asset] != "MONITORING") {
assetPhase[asset] := "MONITORING"
Log(asset " Monitoring for entry > " Fmt(Entry) " | Delta >= " Delta)
}
; Live price + LiveDelta (|open15m - price|) is only needed once a current session is ready to monitor.
LiveDelta := UpdateKalshiLogStream(asset, snapshot)
side := ""
entryPrice := ""
if (up >= Entry || down >= Entry) {
if (LiveDelta != "" && LiveDelta >= Delta) {
if (up >= down) {
side := "UP"
entryPrice := up
} else {
side := "DOWN"
entryPrice := down
}
} else {
LogOnce(asset, "DELTA_LOW", asset " LiveDelta " FmtOrNA(LiveDelta) " < Delta " Delta ", skipping entry")
}
}
if (side = "")
continue
if (entryPrice > 0.98) {
LogOnce(asset, "PRICE_HIGH", asset " Price too high (" entryPrice "), waiting for drop")
continue
}
if !CanPlaceOrderNow(asset) {
LogOnce(asset, "NO_CREDS", asset " Missing credentials, cannot place orders")
continue
}
if HasExistingPosition(snapshot.marketTicker) {
assetPhase[asset] := "HAS_POSITION"
Log(asset " Existing position found, waiting for next session")
continue
}
Log(asset " " side " triggered @ " Fmt(entryPrice) " — placing order")
buyConfirmed := false
loop, 4 {
if (A_Index > 1 && HasExistingPosition(snapshot.marketTicker)) {
Log(asset " Position detected before retry " A_Index ", skipping")
buyConfirmed := true
break
}
if (A_Index > 1) {
freshSnap := GetKalshiMarketSnapshot(asset)
if IsObject(freshSnap)
entryPrice := (side = "UP") ? freshSnap.up : freshSnap.down
if (A_Index = 4)
entryPrice := 0.97
}
if BuyPosition(asset, snapshot.marketTicker, side, entryPrice) {
Log(asset " Position confirmed")
buyConfirmed := true
break
}
if (A_Index < 4) {
Log(asset " Entry retry " A_Index "/3 | " KalshiLastOrderApiSummary())
Sleep 1000
}
}
if (buyConfirmed) {
positions[asset] := { side: side, entry: entryPrice, ticker: snapshot.marketTicker, quarterIndex: CurrentQuarterIndex() }
assetPhase[asset] := "IN_POSITION"
assetLastPriceLogTick[asset] := 0
Log(asset " Monitoring stop-loss @ " Fmt(Exit))
} else {
assetPhase[asset] := "BUY_FAILED"
Log(asset " Entry order not filled after 4 attempts, skipping session | " KalshiLastOrderApiSummary())
}
}
Random, pollJitterMs, 0, 250
sleepMs := pollIntervalMs + pollJitterMs
Sleep %sleepMs%
}
LogOnce(asset, reason, msg) {
global logOnceKeys
if !IsObject(logOnceKeys)
logOnceKeys := {}
key := asset "|" reason
if (logOnceKeys.HasKey(key))
return
logOnceKeys[key] := true
Log(msg)
}
LogOnceReset(asset) {
global logOnceKeys
if !IsObject(logOnceKeys)
return
toRemove := []
for k, _ in logOnceKeys {
if (InStr(k, asset "|") = 1)
toRemove.Push(k)
}
for _, k in toRemove
logOnceKeys.Delete(k)
}
PositionSessionEnded(asset, snapshot := "") {
global positions
if !IsObject(positions[asset])
return false
pos := positions[asset]
if (IsObject(snapshot) && snapshot.marketTicker != "" && pos.HasKey("ticker") && pos.ticker != "" && snapshot.marketTicker != pos.ticker)
return true
if (pos.HasKey("quarterIndex") && CurrentQuarterIndex() != pos.quarterIndex)
return true
return false
}
MarkHeldToResolution(asset) {
global positions, assetPhase
if !IsObject(positions[asset])
return
pos := positions[asset]
Log(asset " " pos.side " held to resolution")
positions.Delete(asset)
ClearKalshiMarketCache(asset)
assetPhase[asset] := "WAIT_WINDOW"
}
UpdateKalshiLogStream(asset, snapshot) {
if !IsObject(snapshot)
return ""
priceObj := GetCachedKalshiPriceObj(asset)
livePrice := (IsObject(priceObj) && priceObj.HasKey("price") && priceObj.price > 0) ? priceObj.price : ""
LiveDelta := ""
if (livePrice != "" && IsObject(priceObj) && priceObj.HasKey("open15m") && priceObj.open15m > 0)
LiveDelta := Abs(priceObj.open15m - livePrice)
LogMonitoringStream(asset, snapshot, livePrice, LiveDelta)
return LiveDelta
}
LogMonitoringStream(asset, snapshot, livePrice := "", LiveDelta := "") {
if !IsObject(snapshot)
return
if (livePrice != "")
LogStream(asset, FmtOrNA(livePrice))
else
LogStream(asset, "n/a")
LogStream("up", FmtOrNA(snapshot.up))
LogStream("down", FmtOrNA(snapshot.down))
LogStream("LIVE DELTA", FmtLiveDeltaOrNA(LiveDelta))
}
GetCachedKalshiPriceObj(asset) {
global assetPriceCache, assetPriceRefreshTick, priceRefreshMs
nowTick := A_TickCount
if (assetPriceCache.HasKey(asset) && assetPriceRefreshTick.HasKey(asset)) {
if ((nowTick - assetPriceRefreshTick[asset]) < priceRefreshMs)
return assetPriceCache[asset]
}
priceObj := GetKalshiPrice(asset)
assetPriceRefreshTick[asset] := nowTick
if (IsObject(priceObj) && priceObj.HasKey("price") && priceObj.price > 0) {
assetPriceCache[asset] := priceObj
return priceObj
}
if (assetPriceCache.HasKey(asset))
return assetPriceCache[asset]
return ""
}
GetCachedKalshiAssetPrice(asset) {
obj := GetCachedKalshiPriceObj(asset)
if (IsObject(obj) && obj.HasKey("price"))
return obj.price
return ""
}
FmtOrNA(price) {
if (price = "")
return "n/a"
return Fmt(price)
}
FmtLiveDeltaOrNA(value) {
if (value = "")
return "n/a"
value := value + 0
if (Abs(value) >= 1)
return Round(value, 2)
return Round(value, 4)
}
Fmt(price) {
return Round(price + 0, 2)
}
BuyPosition(asset, marketTicker, side, price) {
global orderSize
return PlaceKalshiOrder("BUY", marketTicker, side, price, orderSize)
}
SellPosition(asset, side, price, reason, clientOrderId := "") {
global positions, orderSize
if !IsObject(positions[asset]) || !positions[asset].HasKey("ticker")
return true
return PlaceKalshiOrder("SELL", positions[asset].ticker, side, price, orderSize, clientOrderId)
}
HasExistingPosition(marketTicker, ByRef queryOk := false) {
global kalshiCryptoExchangeIndex
queryOk := false
if (marketTicker = "")
return false
res := KalshiSignedRequest("GET", "/trade-api/v2/portfolio/positions?ticker=" marketTicker "&exchange_index=" kalshiCryptoExchangeIndex)
if !IsObject(res) || (res.status != 200)
return false
queryOk := true
qty := JsonField(res.body, "position_fp")
if (qty = "")
qty := JsonField(res.body, "market_exposure_dollars")
return (qty != "" && Abs(qty + 0) > 0)
}
PlaceKalshiOrder(action, marketTicker, side, price, size, clientOrderId := "") {
global enableLiveOrders, kalshiApiKeyId, kalshiPrivateKeyPath, restDelay, entryDiff, exitDiff, kalshiLastOrderResult
kalshiLastOrderResult := { action: action, ticker: marketTicker, side: side, apiAction: "", apiSide: "", cents: "", price: "", contractCents: "", timeInForce: "", size: size, status: "", body: "", orderId: "", clientOrderId: clientOrderId, reason: "not submitted" }
if (!enableLiveOrders) {
kalshiLastOrderResult.reason := "live orders disabled"
return true
}
if (kalshiApiKeyId = "" || kalshiPrivateKeyPath = "") {
kalshiLastOrderResult.reason := "missing API credentials"
return false
}
if (marketTicker = "") {
kalshiLastOrderResult.reason := "missing market ticker"
return false
}
isBuy := (ToLower(action) = "buy")
contractPrice := price + 0
if (isBuy)
contractPrice := contractPrice + entryDiff
else
contractPrice := 0.01 ; IoC+reduce_only: sell at any price, exchange fills at best bid
contractCents := KalshiClampCents(Round(contractPrice * 100))
apiAction := ToLower(action)
apiSide := KalshiEventOrderSide(apiAction, side)
apiCents := KalshiEventOrderCents(apiAction, side, contractCents)
priceText := KalshiFixedPriceFromCents(apiCents)
countText := KalshiFixedCount(size)
timeInForce := isBuy ? "good_till_canceled" : "immediate_or_cancel"
reduceOnly := isBuy ? "false" : "true"
if (clientOrderId = "")
clientOrderId := BuildClientOrderId(marketTicker, apiAction, apiSide)
kalshiLastOrderResult.apiAction := apiAction
kalshiLastOrderResult.apiSide := apiSide
kalshiLastOrderResult.cents := apiCents
kalshiLastOrderResult.price := priceText
kalshiLastOrderResult.contractCents := contractCents
kalshiLastOrderResult.timeInForce := timeInForce
kalshiLastOrderResult.clientOrderId := clientOrderId
payload := "{"
. """ticker"":""" marketTicker ""","
. """side"":""" apiSide ""","
. """count"":""" countText ""","
. """price"":""" priceText ""","
. """time_in_force"":""" timeInForce ""","
. """self_trade_prevention_type"":""taker_at_cross"","
. """post_only"":false,"
. """cancel_order_on_pause"":false,"
. """reduce_only"":" reduceOnly ","
. """exchange_index"":2,"
. """client_order_id"":""" clientOrderId """"
. "}"
res := KalshiSignedRequest("POST", "/trade-api/v2/portfolio/events/orders", payload)
if !IsObject(res) {
kalshiLastOrderResult.reason := "signed request failed or no API response"
return false
}
kalshiLastOrderResult.status := res.status
kalshiLastOrderResult.body := res.body
orderId := JsonField(res.body, "order_id")
kalshiLastOrderResult.orderId := orderId
if (res.status = 401 || res.status = 403) {
kalshiLastOrderResult.reason := "auth rejected"
return false
}
if (res.status != 201 && res.status != 200) {
kalshiLastOrderResult.reason := "order rejected"
return false
}
kalshiLastOrderResult.reason := "order accepted"
if (isBuy)
Sleep %restDelay%
else
Sleep 150
hasPos := HasExistingPosition(marketTicker)
kalshiLastOrderResult.positionOpen := hasPos ? "yes" : "no"
if (isBuy && !hasPos && orderId != "") {
kalshiLastOrderResult.reason := "not filled; canceling resting order"
CancelKalshiOrder(orderId)
Sleep 500
hasPos := HasExistingPosition(marketTicker)
kalshiLastOrderResult.positionOpen := hasPos ? "yes" : "no"
if (hasPos)
{
kalshiLastOrderResult.reason := "filled during cancel race"
Log(marketTicker " Order filled during cancel race, treating as confirmed")
}
else
kalshiLastOrderResult.reason := "not filled; canceled resting order"
}
if (isBuy) {
if (hasPos && kalshiLastOrderResult.reason = "order accepted")
kalshiLastOrderResult.reason := "position confirmed"
else if (!hasPos && kalshiLastOrderResult.reason = "order accepted")
kalshiLastOrderResult.reason := "position not confirmed"
} else {
if (hasPos)
kalshiLastOrderResult.reason := "position still open after IoC sell"
else
kalshiLastOrderResult.reason := "position closed"
}
return (isBuy ? hasPos : !hasPos)
}
KalshiEventOrderSide(apiAction, contractSide) {
; V2 quotes everything from the YES book: bid=buy YES, ask=sell YES.
if (apiAction = "buy")
return (contractSide = "UP") ? "bid" : "ask"
return (contractSide = "UP") ? "ask" : "bid"
}
KalshiEventOrderCents(apiAction, contractSide, contractCents) {
contractCents := KalshiClampCents(contractCents)
if (contractSide = "UP")
return contractCents
return KalshiClampCents(100 - contractCents)
}
KalshiClampCents(cents) {
cents := Round(cents + 0)
if (cents < 1)
return 1
if (cents > 99)
return 99
return cents
}
KalshiFixedPriceFromCents(cents) {
cents := KalshiClampCents(cents)
return "0." KalshiPad2(cents) "00"
}
KalshiFixedCount(size) {
countCents := Round((size + 0) * 100)
whole := Floor(countCents / 100)
frac := Mod(countCents, 100)
return whole "." KalshiPad2(frac)
}
KalshiPad2(n) {
n := Round(n + 0)
if (n < 10)
return "0" n
return n
}
KalshiLastOrderApiSummary() {
global kalshiLastOrderResult
if !IsObject(kalshiLastOrderResult)
return "API no order attempt recorded"
action := KalshiResultValue(kalshiLastOrderResult, "action", "ORDER")
side := KalshiResultValue(kalshiLastOrderResult, "side", "")
apiSide := KalshiResultValue(kalshiLastOrderResult, "apiSide", "")
ticker := KalshiResultValue(kalshiLastOrderResult, "ticker", "")
cents := KalshiResultValue(kalshiLastOrderResult, "cents", "")
price := KalshiResultValue(kalshiLastOrderResult, "price", "")
contractCents := KalshiResultValue(kalshiLastOrderResult, "contractCents", "")
timeInForce := KalshiResultValue(kalshiLastOrderResult, "timeInForce", "")
size := KalshiResultValue(kalshiLastOrderResult, "size", "")
status := KalshiResultValue(kalshiLastOrderResult, "status", "")
body := KalshiResultValue(kalshiLastOrderResult, "body", "")
orderId := KalshiResultValue(kalshiLastOrderResult, "orderId", "")
clientOrderId := KalshiResultValue(kalshiLastOrderResult, "clientOrderId", "")
reason := KalshiResultValue(kalshiLastOrderResult, "reason", "")
orderText := action
if (side != "")
orderText .= " " side
if (apiSide != "")
orderText .= " -> " apiSide
if (ticker != "")
orderText .= " " ticker
if (price != "")
orderText .= " @" price
else if (cents != "")
orderText .= " @" cents "c"
if (size != "")
orderText .= " x" size
if (timeInForce != "")
orderText .= " " timeInForce
if (contractCents != "" && contractCents != cents)
orderText .= " contract@" contractCents "c"
if (status = "")
apiText := "API no response"
else
apiText := "API HTTP " status
includeBody := (status != 200 && status != 201)
apiMessage := KalshiApiMessage(body, includeBody)
if (apiMessage != "")
apiText .= " " apiMessage
if (orderId != "")
apiText .= " order_id=" KalshiShortLogText(orderId, 28)
if (reason != "")
apiText .= " | " reason
if (clientOrderId != "")
apiText .= " | cid=" KalshiShortLogText(clientOrderId, 36)
return orderText " | " apiText
}
KalshiApiMessage(body, includeBodyFallback := true) {
body := Trim(body)
if (body = "")
return ""
code := JsonField(body, "code")
message := JsonField(body, "message")
detail := JsonField(body, "detail")
errorText := JsonField(body, "error")
if (code != "" && message != "")
return "code=" KalshiCompactLogText(code, 40) " msg=" KalshiCompactLogText(message, 140)
if (message != "")
return "msg=" KalshiCompactLogText(message, 160)
if (detail != "")
return "detail=" KalshiCompactLogText(detail, 160)
if (errorText != "")
return "error=" KalshiCompactLogText(errorText, 160)
if (includeBodyFallback)
return "body=" KalshiCompactLogText(body, 180)
return ""
}
KalshiResultValue(result, key, fallback := "") {
if IsObject(result) && result.HasKey(key)
return result[key]
return fallback
}
KalshiShortLogText(s, maxLen := 36) {
return KalshiCompactLogText(s, maxLen)
}
KalshiCompactLogText(s, maxLen := 160) {
s := Trim(JsonUnescape(s))
s := StrReplace(s, "`r", " ")
s := StrReplace(s, "`n", " ")
s := StrReplace(s, A_Tab, " ")
s := RegExReplace(s, "\s+", " ")
if (StrLen(s) > maxLen)
return SubStr(s, 1, maxLen - 3) "..."
return s
}
NormalizeKalshiExchangeIndex(exchangeIndex) {
exchangeIndex := Trim(exchangeIndex)
if !RegExMatch(exchangeIndex, "^-?\d+$")
return -1
exchangeIndex += 0
return (exchangeIndex >= -1) ? exchangeIndex : -1
}
KalshiBalanceDollarsFromBody(body) {
balanceDollars := JsonField(body, "balance_dollars")
if !IsNumericStr(balanceDollars)
return ""
return balanceDollars + 0
}
GetKalshiBalanceSnapshot(exchangeIndex := "", maxAttempts := 1, forceRefresh := false) {
global kalshiAggregateBalanceSnapshot
snapshot := { ok: false, balance: "", status: "", body: "", userMissing: false, retryable: true, balances: {} }
endpoint := "/trade-api/v2/portfolio/balance"
if (exchangeIndex != "") {
exchangeIndex := NormalizeKalshiExchangeIndex(exchangeIndex)
if (exchangeIndex < 0) {
snapshot.retryable := false
return snapshot
}
endpoint .= "?exchange_index=" exchangeIndex
}
; Startup and funding checks share the same short-lived aggregate read.
if (!forceRefresh && IsObject(kalshiAggregateBalanceSnapshot) && (A_TickCount - kalshiAggregateBalanceSnapshot.tick) < 1100) {
if (exchangeIndex = "")
return kalshiAggregateBalanceSnapshot.Clone()
if kalshiAggregateBalanceSnapshot.balances.HasKey(exchangeIndex) {
snapshot.ok := true
snapshot.status := 200
snapshot.balance := kalshiAggregateBalanceSnapshot.balances[exchangeIndex]
return snapshot
}
}
attempt := 1
while (attempt <= maxAttempts) {
; KalshiSignedRequest paces every balance GET, including scoped reads.
res := KalshiSignedRequest("GET", endpoint)
if IsObject(res) {
snapshot.status := res.status
snapshot.body := res.body
snapshot.retryable := (res.status = 0 || res.status = 200 || res.status = 408 || res.status = 429 || res.status >= 500)
if (res.status = 200) {
balanceDollars := KalshiBalanceDollarsFromBody(res.body)
if (balanceDollars != "") {
snapshot.ok := true
snapshot.balance := balanceDollars + 0
snapshot.tick := A_TickCount
if (exchangeIndex = "") {
snapshot.balances := KalshiBalanceBreakdown(res.body)
kalshiAggregateBalanceSnapshot := snapshot.Clone()
}
return snapshot
}
}
errorCode := JsonField(res.body, "code")
if (exchangeIndex != "" && InStr(errorCode, "user_not_found") = 1) {
snapshot.ok := true
snapshot.balance := 0
snapshot.userMissing := true
return snapshot
}
if (!snapshot.retryable)
break
}
; The shared request scheduler supplies spacing and exponential backoff.
attempt++
}
return snapshot
}
KalshiBalanceBreakdown(body) {
balances := {}
if !RegExMatch(body, """balance_breakdown""\s*:\s*\[([^\]]*)\]", breakdown)
return balances
pos := 1
while (pos := RegExMatch(breakdown1, "\{[^{}]*\}", item, pos)) {
exchangeIndex := JsonField(item, "exchange_index")
balance := JsonField(item, "balance") ; breakdown balances are dollar strings
if (RegExMatch(exchangeIndex, "^\d+$") && IsNumericStr(balance))
balances[exchangeIndex + 0] := balance + 0
pos += StrLen(item)
}
return balances
}
KalshiBalanceForShard(aggregate, exchangeIndex) {
if (IsObject(aggregate) && aggregate.ok && aggregate.balances.HasKey(exchangeIndex))
return { ok: true, balance: aggregate.balances[exchangeIndex], status: 200 }
; Older responses or an unprovisioned shard may have no breakdown entry.
return GetKalshiBalanceSnapshot(exchangeIndex, 1)
}
KalshiFundingReadFailed(asset, exchangeIndex, snapshot, state, ByRef permanentFailure) {
permanentFailure := IsObject(snapshot) && !snapshot.retryable
if (permanentFailure)
state.status := "FAILED"
status := (IsObject(snapshot) && snapshot.status != "") ? snapshot.status : "no response"
LogOnce(asset, "FUNDING_READ_" exchangeIndex "_" status, asset " Balance read " (permanentFailure ? "rejected" : "unavailable; will retry") " | shard " exchangeIndex " | HTTP " status)
return false
}
NewKalshiTransferId() {
VarSetCapacity(guid, 16, 0)
if (DllCall("ole32\CoCreateGuid", "Ptr", &guid) != 0)
return ""
VarSetCapacity(guidText, 78, 0)
if (!DllCall("ole32\StringFromGUID2", "Ptr", &guid, "Ptr", &guidText, "Int", 39))
return ""
return Trim(StrGet(&guidText, "UTF-16"), "{}")
}
SubmitKalshiShardTransfer(asset, exchangeIndex, state, pending, ByRef permanentFailure) {
global kalshiShardPendingTransfers
wasAttempted := pending.attempted
pending.attempted := true
res := KalshiSignedRequest("POST", "/trade-api/v2/portfolio/intra_exchange_instance_transfer", pending.payload)
if (IsObject(res) && res.status = 200) {
pending.transferId := JsonField(res.body, "transfer_id")
Log(asset " Shard " exchangeIndex " transfer requested | client_transfer_id=" pending.clientTransferId " | transfer_id=" pending.transferId)
} else if (!IsObject(res) || res.status = 0 || res.status = 408 || res.status = 409 || res.status >= 500 || wasAttempted) {
; Any retry keeps the exact payload and client ID, including after a 409.
LogOnce(asset, "TRANSFER_AMBIGUOUS_" pending.clientTransferId, asset " Transfer outcome pending; will reconcile before another transfer")
} else {
; A definitive first rejection did not create a transfer.
kalshiShardPendingTransfers.Delete(exchangeIndex)
permanentFailure := (res.status != 429 && !InStr(JsonField(res.body, "code"), "insufficient"))
if (permanentFailure && !pending.baseFundingReady)
state.status := "FAILED"
LogOnce(asset, "TRANSFER_REJECTED_" res.status, asset " Shard transfer rejected | HTTP " res.status " " KalshiFundingApiMessage(res.body) (permanentFailure ? "" : " | will retry"))
}
if (pending.baseFundingReady) {
state.status := "READY"
permanentFailure := false
LogOnce(asset, "OPTIONAL_TRANSFER_PENDING", asset " Existing shard balance ready; additional reserve is best-effort")
return true
}
return false
}
ReconcileKalshiShardTransfer(asset, exchangeIndex, state, ByRef permanentFailure) {
global kalshiShardPendingTransfers
pending := kalshiShardPendingTransfers[exchangeIndex]
state.transferAttempted := true
aggregate := GetKalshiBalanceSnapshot("", 1, true)
if (!aggregate.ok)
return KalshiFundingReadFailed(asset, "all", aggregate, state, permanentFailure)
destination := KalshiBalanceForShard(aggregate, exchangeIndex)
if (!destination.ok)
return KalshiFundingReadFailed(asset, exchangeIndex, destination, state, permanentFailure)
transferComplete := false
historyExhausted := false
if (pending.transferId != "") {
res := KalshiSignedRequest("GET", "/trade-api/v2/portfolio/intra_exchange_instance_transfers/" KalshiQueryEncode(pending.transferId))
if (IsObject(res) && res.status = 200)
transferComplete := (JsonField(res.body, "status") = "complete")
} else {
; Recover a lost POST response by its client ID. Scan one page per retry.
endpoint := "/trade-api/v2/portfolio/intra_exchange_instance_transfers?limit=100"
if (pending.historyCursor != "")
endpoint .= "&cursor=" KalshiQueryEncode(pending.historyCursor)
res := KalshiSignedRequest("GET", endpoint)
if (IsObject(res) && res.status = 200) {
pos := 1
while (pos := RegExMatch(res.body, "\{[^{}]*\}", item, pos)) {
if (JsonField(item, "client_transfer_id") = pending.clientTransferId) {
pending.transferId := JsonField(item, "transfer_id")
transferComplete := (JsonField(item, "status") = "complete")
break
}
pos += StrLen(item)
}
pending.historyCursor := JsonField(res.body, "cursor")
historyExhausted := (pending.transferId = "" && pending.historyCursor = "")
}
}
if (transferComplete)
kalshiShardPendingTransfers.Delete(exchangeIndex)
fundingReady := (destination.balance + 0) >= (pending.requiredDollars - 0.000001)
targetReached := (destination.balance + 0) >= (pending.targetDollars - 0.000001)
if (fundingReady && (transferComplete || targetReached || pending.baseFundingReady)) {
state.status := "READY"
Log(asset " Shard " exchangeIndex " funding confirmed | $" KalshiFundingMoney(destination.balance) " available")
return true
}
if (transferComplete) {
LogOnce(asset, "TRANSFER_SPENT", asset " Transfer completed but shard balance is insufficient; will recheck funding")
return false
}
if (historyExhausted) {
; Kalshi deduplicates this UUID. Reusing the same request cannot top up twice.
return SubmitKalshiShardTransfer(asset, exchangeIndex, state, pending, permanentFailure)
}
LogOnce(asset, "TRANSFER_PENDING_" pending.clientTransferId, asset " Transfer pending confirmation; will retry without creating another transfer")
return false
}
KalshiScheduleNextBalanceRead(status) {
global kalshiBalanceNextRequestTick, kalshiBalanceRetryCount
delayMs := 1100
if (status = 0 || status = 408 || status = 429 || status >= 500) {
kalshiBalanceRetryCount := Min(kalshiBalanceRetryCount + 1, 5)
Random, balanceJitter, 0, 250
delayMs := (1100 * (2 ** (kalshiBalanceRetryCount - 1))) + balanceJitter
if (status = 429)
Log("Balance read rate-limited; retrying after " Round(delayMs / 1000.0, 2) " seconds")
} else {
kalshiBalanceRetryCount := 0
}
kalshiBalanceNextRequestTick := A_TickCount + delayMs
}
KalshiFundingMoney(value) {
return Format("{:.4f}", value + 0)
}
KalshiFundingApiMessage(body) {
code := JsonField(body, "code")
message := JsonField(body, "message")
if (code != "" || message != "")
return code " " message
return SubStr(body, 1, 180)
}
KalshiQueryEncode(value) {
value := StrReplace(value, "%", "%25")
value := StrReplace(value, "+", "%2B")
value := StrReplace(value, "/", "%2F")
value := StrReplace(value, "=", "%3D")
value := StrReplace(value, "&", "%26")
value := StrReplace(value, "?", "%3F")
value := StrReplace(value, "#", "%23")
value := StrReplace(value, " ", "%20")
return value
}
EnsureKalshiCryptoShardFunding(asset, ByRef permanentFailure := false) {
global enableLiveOrders, orderSize, assets, kalshiCryptoExchangeIndex, kalshiShardFundingSessionStatus, kalshiShardPendingTransfers
global kalshiBalanceNextRequestTick, kalshiAggregateBalanceSnapshot
permanentFailure := false
if (!enableLiveOrders)
return true
destinationExchangeIndex := NormalizeKalshiExchangeIndex(kalshiCryptoExchangeIndex)
if (destinationExchangeIndex = 0)
return true
if (destinationExchangeIndex < 0) {
permanentFailure := true
LogOnce(asset, "INVALID_SHARD", "ERROR: " asset " Market did not provide a valid exchange shard")
return false
}
; Include the date so a bot running overnight cannot reuse yesterday's state.
fundingSessionKey := destinationExchangeIndex "|" SubStr(CurrentUtcNowAhk(), 1, 8) "|" CurrentQuarterIndex()
if !kalshiShardFundingSessionStatus.HasKey(fundingSessionKey)
kalshiShardFundingSessionStatus[fundingSessionKey] := { status: "WAIT_FUNDING", retryTick: 0, transferAttempted: false }
state := kalshiShardFundingSessionStatus[fundingSessionKey]
if (state.status = "READY")
return true
if (state.status = "FAILED") {
permanentFailure := true
return false
}
if (A_TickCount < state.retryTick)
return false
; Defer in the strategy loop instead of sleeping through another asset's exit.
cacheFresh := IsObject(kalshiAggregateBalanceSnapshot) && (A_TickCount - kalshiAggregateBalanceSnapshot.tick) < 1100
if (A_TickCount < kalshiBalanceNextRequestTick && (!cacheFresh || kalshiShardPendingTransfers.HasKey(destinationExchangeIndex)))
return false
Random, fundingJitter, 0, 500
state.retryTick := A_TickCount + 5000 + fundingJitter
; A market change never discards a submitted or ambiguous transfer.
if kalshiShardPendingTransfers.HasKey(destinationExchangeIndex)
return ReconcileKalshiShardTransfer(asset, destinationExchangeIndex, state, permanentFailure)
aggregate := GetKalshiBalanceSnapshot("", 1)
if (!aggregate.ok)
return KalshiFundingReadFailed(asset, "all", aggregate, state, permanentFailure)
destination := KalshiBalanceForShard(aggregate, destinationExchangeIndex)
if (!destination.ok)
return KalshiFundingReadFailed(asset, destinationExchangeIndex, destination, state, permanentFailure)
enabledAssetCount := IsObject(assets) ? assets.Length() : 1
requiredDollars := (orderSize + 0) * Max(1, enabledAssetCount)
currentBalance := destination.balance + 0
baseFundingReady := (currentBalance >= (requiredDollars - 0.000001))
if (baseFundingReady && state.transferAttempted) {
state.status := "READY"
return true
}
; Keep the existing best-effort additional reserve for another running agent.
transferDollars := baseFundingReady ? orderSize + 0 : requiredDollars - currentBalance
transferCenticents := Ceil((transferDollars * 10000) - 0.000001)
transferDollars := transferCenticents / 10000.0
confirmationTargetDollars := currentBalance + transferDollars
source := KalshiBalanceForShard(aggregate, 0)
if (!source.ok || (source.balance + 0) < (transferDollars - 0.000001)) {
if (baseFundingReady) {
state.status := "READY"
LogOnce(asset, "OPTIONAL_RESERVE_UNAVAILABLE", asset " Shard already funded; additional agent reserve unavailable")
return true
}
if (!source.ok)
return KalshiFundingReadFailed(asset, 0, source, state, permanentFailure)
LogOnce(asset, "SHARD_INSUFFICIENT", asset " Shard balance insufficient; needs $" KalshiFundingMoney(transferDollars) " from shard 0, available $" KalshiFundingMoney(source.balance) "; will recheck")
return false
}
clientTransferId := NewKalshiTransferId()
if (clientTransferId = "") {
state.status := "FAILED"
permanentFailure := true
Log("ERROR: " asset " Could not create a duplicate-prevention ID for the shard transfer")
return false
}
payload := "{"
. """source"":""event_contract"","
. """destination"":""event_contract"","
. """client_transfer_id"":""" clientTransferId ""","
. """amount"":" transferCenticents ","
. """source_exchange_shard"":0,"
. """destination_exchange_shard"":" destinationExchangeIndex ","
. """source_subaccount"":0,"
. """destination_subaccount"":0"
. "}"
pending := { transferId: "", clientTransferId: clientTransferId, payload: payload, historyCursor: "", attempted: false, requiredDollars: requiredDollars, targetDollars: confirmationTargetDollars, baseFundingReady: baseFundingReady }
; Store before POST: a timeout may occur after Kalshi accepted the transfer.
state.transferAttempted := true
kalshiShardPendingTransfers[destinationExchangeIndex] := pending
return SubmitKalshiShardTransfer(asset, destinationExchangeIndex, state, pending, permanentFailure)
}
CancelKalshiOrder(orderId) {
global kalshiCryptoExchangeIndex
if (orderId = "")
return
KalshiSignedRequest("DELETE", "/trade-api/v2/portfolio/events/orders/" orderId "?exchange_index=" kalshiCryptoExchangeIndex)
}
LoadKalshiCredentialsFromFiles() {
global kalshiApiKeyId, kalshiApiKeyFile, kalshiPrivateKeyPath, kalshiPrivateKeyFile, kalshiApiKeyResolvedPath, kalshiPrivateKeyResolvedPath
kalshiApiKeyId := ""
kalshiApiKeyResolvedPath := ResolveCredentialPath(kalshiApiKeyFile)
if (kalshiApiKeyResolvedPath != "") {
FileRead, apiRaw, %kalshiApiKeyResolvedPath%
apiCode := JsonField(apiRaw, "code")
if (apiCode != "")
kalshiApiKeyId := Trim(JsonUnescape(apiCode))
}
kalshiPrivateKeyPath := ""
kalshiPrivateKeyResolvedPath := ResolveCredentialPath(kalshiPrivateKeyFile)
if (kalshiPrivateKeyResolvedPath != "") {
FileRead, keyRaw, %kalshiPrivateKeyResolvedPath%
keyCode := JsonField(keyRaw, "code")
if (keyCode != "") {
pemText := JsonUnescape(keyCode)
tempBase := A_Temp
if (tempBase = "")
tempBase := A_ScriptDir
tempPem := tempBase "\kalshi_ahk\privatekey_runtime.pem"
FileCreateDir, % tempBase "\kalshi_ahk"
FileDelete, %tempPem%
FileAppend, %pemText%, %tempPem%
kalshiPrivateKeyPath := tempPem
}
}
}
StartupCredentialCheck() {
global kalshiApiKeyId, kalshiPrivateKeyPath, kalshiSignerPath
apiReady := (kalshiApiKeyId != "")
keyReady := (kalshiPrivateKeyPath != "" && FileExist(kalshiPrivateKeyPath))
signerOut := RunAndCapture(QuoteForCmd(kalshiSignerPath) " version")
signerReady := InStr(signerOut, "NightShark Signer")
if (apiReady && keyReady && signerReady) {
snapshot := GetKalshiBalanceSnapshot("", 3, true)
if (snapshot.ok) {
Log("Credentials processed successfully | aggregate balance $" KalshiFundingMoney(snapshot.balance))
return true
}
if (snapshot.status = 401 || snapshot.status = 403) {
Log("ERROR: API key or private key is incorrect (HTTP " snapshot.status ")")
Log("Please fix your API key / private key and restart.")
} else {
Log("ERROR: Startup balance unavailable after retries | HTTP " snapshot.status " | stopping before live trading")
}
stopCode()
return false
}
if (!apiReady)
Log("ERROR: API key not loaded")
if (!keyReady)
Log("ERROR: Private key not loaded")
if (!signerReady)
Log("ERROR: NightShark signer not found")
Log("Please fix API credentials. Stopping script.")
stopCode()
return false
}
ResolveCredentialPath(pathSpec) {
if (pathSpec = "")
return ""
if FileExist(pathSpec)
return pathSpec
normalized := StrReplace(pathSpec, "\", "/")
if FileExist(normalized)
return normalized
relative := normalized
while (SubStr(relative, 1, 1) = "/" || SubStr(relative, 1, 1) = "\")
relative := SubStr(relative, 2)
fromScriptDir := A_ScriptDir "/" relative
if FileExist(fromScriptDir)
return fromScriptDir
fromScriptDirBackslash := StrReplace(fromScriptDir, "/", "\")
if FileExist(fromScriptDirBackslash)
return fromScriptDirBackslash
return ""
}
JsonUnescape(s) {
s := StrReplace(s, "\/", "/")
s := StrReplace(s, "\n", "`n")
s := StrReplace(s, "\r", "`r")
s := StrReplace(s, "\t", A_Tab)
s := StrReplace(s, Chr(92) Chr(34), Chr(34))
s := StrReplace(s, Chr(92) Chr(92), Chr(92))
return s
}
KalshiSignedRequest(method, endpointPath, bodyJson := "") {
global kalshiApiBase, kalshiApiKeyId, kalshiBalanceNextRequestTick, kalshiAggregateBalanceSnapshot
isBalanceRead := (method = "GET" && RegExMatch(endpointPath, "^/trade-api/v2/portfolio/balance(?:\?|$)"))
if (isBalanceRead) {
waitMs := kalshiBalanceNextRequestTick - A_TickCount
if (waitMs > 0)
Sleep %waitMs%
kalshiAggregateBalanceSnapshot := ""
} else if (method != "GET") {
kalshiAggregateBalanceSnapshot := ""
}
; Sign after waiting, so the timestamp stays fresh during a rate-limit backoff.
timestamp := CurrentTimeMillis()
signPath := endpointPath
if InStr(signPath, "?")
signPath := SubStr(signPath, 1, InStr(signPath, "?") - 1)
signature := SignKalshiMessage(timestamp method signPath)
if (signature = "")
return false
url := kalshiApiBase endpointPath
global httpSignedObj
if !IsObject(httpSignedObj)
httpSignedObj := ComObjCreate("WinHttp.WinHttpRequest.5.1")
http := httpSignedObj
http.Option(9) := 2048 | 8192
http.Open(method, url, false)
http.SetTimeouts(1000, 1000, 3000, 5000)
http.SetRequestHeader("User-Agent", "Mozilla/5.0")
http.SetRequestHeader("Cache-Control", "no-cache, no-store")
http.SetRequestHeader("Pragma", "no-cache")
http.SetRequestHeader("KALSHI-ACCESS-KEY", kalshiApiKeyId)
http.SetRequestHeader("KALSHI-ACCESS-TIMESTAMP", timestamp)
http.SetRequestHeader("KALSHI-ACCESS-SIGNATURE", signature)
http.SetRequestHeader("Content-Type", "application/json")
try {
if (bodyJson != "")
http.Send(bodyJson)
else
http.Send()
} catch e {
httpSignedObj := ""
if (isBalanceRead)
KalshiScheduleNextBalanceRead(0)
return false
}
res := { status: http.Status, body: http.ResponseText }
if (isBalanceRead)
KalshiScheduleNextBalanceRead(res.status)
return res
}
SignKalshiMessage(message) {
global kalshiPrivateKeyPath, kalshiKeyPassphrase, kalshiSignerPath
if (kalshiPrivateKeyPath = "" || !FileExist(kalshiPrivateKeyPath))
return ""
passArg := ""
if (kalshiKeyPassphrase != "")
passArg := " --passphrase " QuoteForCmd(kalshiKeyPassphrase)
signCmd := QuoteForCmd(kalshiSignerPath)
. " sign --key "
. QuoteForCmd(kalshiPrivateKeyPath)
. " --message "
. QuoteForCmd(message)
. passArg
out := Trim(RunAndCapture(signCmd))
if (SubStr(out, 1, 6) = "ERROR:")
return ""
return out
}
BuildClientOrderId(ticker, action, side) {
nowUtc := CurrentUtcNowAhk()
if (nowUtc = "")
nowUtc := A_Now
Random, r, 100000, 999999
marketToken := RegExReplace(ticker, "^KX([A-Z0-9]+)15M.*$", "$1")
if (marketToken = ticker || marketToken = "")
marketToken := SubStr(RegExReplace(ticker, "[^A-Za-z0-9]", ""), 1, 12)
clientOrderId := marketToken "-" action "-" side "-" nowUtc "-" A_TickCount "-" r
return SubStr(clientOrderId, 1, 64)
}
CurrentTimeMillis() {
epoch := CurrentUtcNowAhk()
if (epoch = "")
return ""
EnvSub, epoch, 19700101000000, Seconds
return epoch * 1000
}
CurrentUtcNowAhk() {
nowUtc := A_NowUTC
if (nowUtc != "")
return nowUtc
out := Trim(RunAndCapture("powershell -NoProfile -Command ""[DateTime]::UtcNow.ToString('yyyyMMddHHmmss')"""))
if RegExMatch(out, "^\d{14}$")
return out
return ""
}
QuoteForCmd(s) {
s := StrReplace(s, """", "\""")
return """" s """"
}
RunAndCapture(command) {
return RunCMD(command, A_ScriptDir)
}
ToLower(s) {
StringLower, out, s
return out
}
FindNightSharkSigner() {
candidates := [ A_ScriptDir "\nightshark-signer.exe"
, A_ScriptDir "/nightshark-signer.exe"
, A_WorkingDir "\nightshark-signer.exe"
, A_WorkingDir "/nightshark-signer.exe" ]
for _, p in candidates {
if FileExist(p)
return p
}
return A_ScriptDir "\nightshark-signer.exe"
}
CanPlaceOrderNow(asset) {
global enableLiveOrders, kalshiApiKeyId, kalshiPrivateKeyPath
if (!enableLiveOrders)
return true
if (kalshiApiKeyId != "" && kalshiPrivateKeyPath != "" && FileExist(kalshiPrivateKeyPath))
return true
return false
}
ShouldLogPrice(asset, minIntervalMs) {
global assetLastPriceLogTick
nowTick := A_TickCount
if !assetLastPriceLogTick.HasKey(asset) {
assetLastPriceLogTick[asset] := nowTick
return true
}
if ((nowTick - assetLastPriceLogTick[asset]) >= minIntervalMs) {
assetLastPriceLogTick[asset] := nowTick
return true
}
return false
}
MinutesRemainingInQuarter() {
utc := CurrentUtcNowAhk()
if (utc = "") {
; Fallback to local clock if UTC unavailable
currentMinute := A_Min + 0
remaining := 15 - Mod(currentMinute, 15)
remaining := remaining - ((A_Sec + 0) / 60.0)
return remaining
}
mm := SubStr(utc, 11, 2) + 0
ss := SubStr(utc, 13, 2) + 0
remaining := 15 - Mod(mm, 15) - (ss / 60.0)
return remaining
}
IsXMinRemaining(x) {
return (MinutesRemainingInQuarter() <= x)
}
CurrentQuarterIndex() {
utc := CurrentUtcNowAhk()
if (utc = "") {
totalMinutes := (A_Hour + 0) * 60 + (A_Min + 0)
return Floor(totalMinutes / 15)
}
hh := SubStr(utc, 9, 2) + 0
mm := SubStr(utc, 11, 2) + 0
totalMinutes := hh * 60 + mm
return Floor(totalMinutes / 15)
}
SnapshotMinutesLeft(snapshot) {
if !IsObject(snapshot)
return ""
if (snapshot.HasKey("closeTime") && snapshot.closeTime != "") {
minsLeft := MinutesUntilIsoUtc(snapshot.closeTime)
if (minsLeft != "")
return minsLeft
}
if (snapshot.HasKey("minutesLeft"))
return snapshot.minutesLeft
return ""
}
IsSnapshotExpired(snapshot, cutoffMinutes := 0) {
minsLeft := SnapshotMinutesLeft(snapshot)
return (minsLeft != "" && minsLeft <= cutoffMinutes)
}
ClearKalshiMarketCache(asset) {
global assetSnapshotCache, assetMarketRefreshTick
if (assetSnapshotCache.HasKey(asset))
assetSnapshotCache.Delete(asset)
if (assetMarketRefreshTick.HasKey(asset))
assetMarketRefreshTick.Delete(asset)
}
GetKalshiMarketSnapshot(asset, maxRetries := 4) {
global assetSeriesMap, assetSnapshotCache, assetMarketRefreshTick, marketRefreshMs
if !assetSeriesMap.HasKey(asset)
return false
series := assetSeriesMap[asset]
nowTick := A_TickCount
if (assetSnapshotCache.HasKey(asset) && assetMarketRefreshTick.HasKey(asset)) {
cached := assetSnapshotCache[asset]
if (IsObject(cached) && (nowTick - assetMarketRefreshTick[asset]) < marketRefreshMs) {
if (IsSnapshotExpired(cached, 0.5)) {
ClearKalshiMarketCache(asset)
} else {
if (RefreshKalshiBestAsks(cached))
return cached
return false
}
}
}
attempt := 1
while (attempt <= maxRetries) {
url := "https://api.elections.kalshi.com/trade-api/v2/markets?series_ticker=" series "&status=open&limit=1&_=" A_TickCount "-" attempt
body := HttpGet(url)
if (body != "") {
marketTicker := GetFirstNonEmptyJsonField(body, ["ticker"])
yesPrice := GetKalshiDollarPrice(body, "yes_ask_dollars", "yes_ask")
noPrice := GetKalshiDollarPrice(body, "no_ask_dollars", "no_ask")
closeIso := GetFirstNonEmptyJsonField(body, ["close_time", "expected_expiration_time", "expiration_time", "settlement_time"])
minsLeft := MinutesUntilIsoUtc(closeIso)
; Skip markets that already closed or close in < 30 seconds (stale/expired)
if (minsLeft != "" && minsLeft < 0.5) {
if (attempt < maxRetries) {
Sleep 200
attempt++
continue
}
}
if (marketTicker != "") {
obj := {}
obj.up := (yesPrice != "") ? yesPrice + 0 : ""
obj.down := (noPrice != "") ? noPrice + 0 : ""
obj.minutesLeft := minsLeft
obj.closeTime := closeIso
obj.marketTicker := marketTicker
assetSnapshotCache[asset] := obj
assetMarketRefreshTick[asset] := nowTick
if (RefreshKalshiBestAsks(obj) || (obj.up != "" && obj.down != ""))
return obj
}
}
if (attempt < maxRetries)
Sleep 200
attempt++
}
if (assetSnapshotCache.HasKey(asset)) {
cached := assetSnapshotCache[asset]
if IsObject(cached) {
if (IsSnapshotExpired(cached, 0.5)) {
ClearKalshiMarketCache(asset)
return false
}
if (RefreshKalshiBestAsks(cached))
return cached
}
}
return false
}
RefreshKalshiBestAsks(snapshot) {
if !IsObject(snapshot) || snapshot.marketTicker = ""
return false
url := "https://api.elections.kalshi.com/trade-api/v2/markets/" snapshot.marketTicker "/orderbook?depth=1&_=" A_TickCount
body := HttpGet(url)
if (body = "")
return false
yesBid := GetOrderbookBestBid(body, "yes_dollars")
noBid := GetOrderbookBestBid(body, "no_dollars")
updated := false
if (noBid != "") {
snapshot.up := Round(1 - (noBid + 0), 4)
updated := true
}
if (yesBid != "") {
snapshot.down := Round(1 - (yesBid + 0), 4)
updated := true
}
return updated
}
GetOrderbookBestBid(json, sideField) {
pattern := """" sideField """\s*:\s*\[\s*\[\s*""?(-?\d+(?:\.\d+)?)"
if RegExMatch(json, pattern, m)
return m1
return ""
}
GetKalshiDollarPrice(json, dollarField, centsField := "") {
val := GetFirstNonEmptyJsonField(json, [dollarField])
if (val != "")
return val
if (centsField != "") {
val := GetFirstNonEmptyJsonField(json, [centsField])
if (val != "" && IsNumericStr(val))
return (val + 0) / 100.0
}
return ""
}
GetFirstNonEmptyJsonField(json, fields, skipZero := false) {
for _, field in fields {
val := JsonField(json, field)
if (val != "") {
if (skipZero && IsNumericStr(val) && (val + 0) = 0)
continue
return val
}
}
return ""
}
IsNumericStr(s) {
s := Trim(s)
return RegExMatch(s, "^-?\d+(\.\d+)?$")
}
JsonField(json, field) {
pattern := """" field """\s*:\s*(""([^""]*)""|[^,}\s][^,}\r\n]*)"
result := ""
pos := 1
while (pos := RegExMatch(json, pattern, m, pos)) {
val := Trim(m1)
val := Trim(val, """")
if (val != "")
result := val
pos += StrLen(m)
}
return result
}
MinutesUntilIsoUtc(iso) {
ts := IsoUtcToAhk(iso)
if (ts = "")
return ""
nowUtc := CurrentUtcNowAhk()
if (nowUtc = "")
return ""
diff := ts
EnvSub, diff, %nowUtc%, Seconds
return diff / 60.0
}
IsoUtcToAhk(iso) {
if (iso = "")
return ""
if !RegExMatch(iso, "O)^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})", m)
return ""
return m1 m2 m3 m4 m5 m6
}