Compare commits

168 Commits
Author SHA1 Message Date
Theodore Song 2d210ffb93 Add feature-aware maker learning 2026-08-23 08:20:14 -04:00
Theodore Song 53141d9071 Expand executable protected opportunity search 2026-08-23 07:58:25 -04:00
Theodore Song 974a957f58 Adapt protected returns to capital lock duration 2026-08-22 21:42:47 -04:00
Theodore Song 2de28533d8 Prioritize capital-efficient protected trades 2026-08-22 21:21:17 -04:00
Theodore Song c5f59aa992 Keep autonomous cycles alive under state growth 2026-08-22 21:01:15 -04:00
github-actions[bot] f1799a2384 Retry protected agent routing deployment 2026-08-23 00:53:41 +00:00
Theodore Song e087d824ca Schedule protected routing deployment retry 2026-08-22 20:52:21 -04:00
Theodore Song 8e04e1bff7 Distribute protected strategies across agents 2026-08-22 20:46:51 -04:00
Theodore Song 1ea235b0cf Activate Build 124 autonomous cycles 2026-08-22 20:38:28 -04:00
Theodore Song 3183612d13 Deploy verified protected bundle opportunities 2026-08-22 20:37:21 -04:00
Theodore Song 05ebefaee0 Retry Build 124 as quota begins clearing 2026-08-22 20:23:13 -04:00
Theodore Song c261a1ae73 Exclude runtime state from Vercel deployments 2026-08-22 20:16:27 -04:00
Theodore Song dd7b376b44 Harden Build 124 release and report secured profit 2026-08-22 20:13:06 -04:00
Theodore Song 008801f179 Split protected bundle strategies across agents 2026-08-22 20:01:18 -04:00
Theodore Song 6853821dc9 Verify negative-risk conversions on chain 2026-08-22 19:55:15 -04:00
Theodore Song c15dc12323 Convert verified complete NO bundles immediately 2026-08-22 11:18:31 -04:00
Theodore Song 1e0921fc49 Recycle verified bundle profits early 2026-08-22 11:03:37 -04:00
Theodore Song d64810fc18 Require forward proof for sports capital 2026-08-22 09:08:00 -04:00
Theodore Song d32ce0de42 Quantify excluded learning outcomes 2026-08-22 08:48:04 -04:00
Theodore Song 605a5f2ae4 Isolate directional learning from retired pilots 2026-08-22 08:46:34 -04:00
Theodore Song 9e87d1f9f2 Verify bundle fees against CLOB V2 2026-08-22 08:40:51 -04:00
Theodore Song ee2d3edd72 Realize binary complement merges immediately 2026-08-22 08:32:53 -04:00
Theodore Song 4738767514 Scan executable binary complement bundles 2026-08-22 08:28:41 -04:00
Theodore Song 7ae2d6bdfc Diversify verified bundle search by event 2026-08-22 08:22:47 -04:00
Theodore Song a6c9fc1a14 Delay Build 121 retry past quota window 2026-08-22 08:15:49 -04:00
Theodore Song 0496bb1d9f Keep adaptive evidence read-only offline 2026-08-22 08:14:19 -04:00
Theodore Song d9e1e29a8d Reject slow locked-capital bundles 2026-08-22 08:04:26 -04:00
Theodore Song 933290290b Prioritize capital-efficient independent bundles 2026-08-22 07:45:05 -04:00
Theodore Song ca302b7168 Preserve raw learning ledger in fallback runner 2026-08-22 05:43:50 -04:00
Theodore Song 36d0dae5bd Compact fallback runtime transport 2026-08-22 04:48:58 -04:00
Theodore Song 4d4a4b6d4d Protect adaptive evidence in runtime snapshots 2026-08-22 04:42:05 -04:00
Theodore Song ffd3acb7d9 Defer Build 119 until Vercel quota resets 2026-08-22 03:08:32 -04:00
Theodore Song dcede02676 Add bounded six-hour adaptive probation 2026-08-22 03:02:35 -04:00
Theodore Song e3086ed5fb Reserve durable adaptive evidence capacity 2026-08-21 23:43:13 -04:00
Theodore Song 0084654f15 Retire stale deployment runners promptly 2026-08-21 23:25:08 -04:00
Theodore Song c52368dc9e Preserve learning and size verified bundles 2026-08-21 23:20:09 -04:00
Theodore Song 5e4355b850 Bound autonomous learning snapshots 2026-08-21 23:04:11 -04:00
Theodore Song 8ee554ea16 Serialize autonomous cycle chain 2026-08-21 22:52:12 -04:00
Theodore Song 2181d62104 Chain autonomous five-minute cycles 2026-08-21 22:48:09 -04:00
Theodore Song 26387e059f Reconstruct offline cache across cloud restarts 2026-08-21 22:38:55 -04:00
Theodore Song aa5885b295 Bound public suggestion diagnostics 2026-08-21 22:26:46 -04:00
Theodore Song 546f174276 Hold complete bundles at cost until settlement 2026-08-21 22:20:47 -04:00
Theodore Song 3152096057 Correct intact bundle portfolio marks 2026-08-21 22:16:42 -04:00
Theodore Song d375ed7fc3 Fix live bundle minimum execution 2026-08-21 22:11:07 -04:00
Theodore Song 7f39f6b6bb Bound autonomous snapshot diagnostics 2026-08-21 21:42:41 -04:00
Theodore Song 066cad13da Launch bounded sports contest exploration 2026-08-21 21:33:56 -04:00
Theodore Song c065ab5496 Bound fee-policy migration snapshot 2026-08-21 21:06:25 -04:00
Theodore Song 5c7c0440a7 Start fee-policy observations without migration delay 2026-08-21 21:00:38 -04:00
Theodore Song 4539456ac2 Grade adaptive signals with exact market fees 2026-08-21 20:56:40 -04:00
Theodore Song 98744e07ba Cap bundle orders to verified market depth 2026-08-21 20:16:35 -04:00
Theodore Song efd53a5f88 Price bundle opportunities with live depth and fees 2026-08-21 20:12:18 -04:00
Theodore Song ab165b0617 Replace invalid resolution bets with forward learning 2026-08-21 19:56:21 -04:00
Theodore Song 95947c4df6 Preserve bundle audit telemetry in shared state 2026-08-21 19:32:39 -04:00
Theodore Song 1da2ab3e29 Poll audited bundle opportunities every five minutes 2026-08-21 19:29:50 -04:00
Theodore Song a01571d5ee Broaden audited adaptive settlement windows 2026-08-21 19:19:40 -04:00
Theodore Song c0ce373833 Add audited adaptive resolution-window pilot 2026-08-21 18:56:58 -04:00
Theodore Song b956f50cbe Disable unproven shock capital pending forward evidence 2026-08-21 18:12:11 -04:00
Theodore Song 3efc5484c3 Harden shock strategy contract safety 2026-08-21 17:54:47 -04:00
Theodore Song 4513f266c5 Cluster adaptive shock exposure by underlying risk 2026-08-21 17:40:22 -04:00
Theodore Song 50db7ed7b3 Share adaptive evidence across autonomous agents 2026-08-21 17:34:23 -04:00
Theodore Song 22b5ddcee1 Resolve current runtime branch state 2026-08-21 17:21:09 -04:00
Theodore Song 84e0ef703b Serve the latest autonomous snapshot 2026-08-21 17:17:22 -04:00
Theodore Song 6e6477a4e3 Reject incomplete autonomous cycles 2026-08-21 17:13:24 -04:00
Theodore Song 83e4ea8203 Deploy hourly autonomous paper runtime 2026-08-21 17:10:26 -04:00
Theodore Song a10c9a304c Deploy adaptive cost-stressed shock strategy 2026-08-21 16:53:57 -04:00
Theodore Song 6f77c2c3bd Promote validated 24-hour shock fade 2026-08-21 16:31:43 -04:00
Theodore Song d59f7ace03 Add forward shock reversion pilot 2026-08-21 16:08:49 -04:00
Theodore Song ed140548d2 Retire failed maker capital path 2026-08-21 15:47:33 -04:00
Theodore Song 55ab1f02ea Bind capital adaptation to current evidence 2026-08-21 15:35:28 -04:00
Theodore Song 067b5f7ab8 Unblock scoped observation migration 2026-08-21 15:22:54 -04:00
Theodore Song 5a892e6b0d Broaden agent-scoped forward learning 2026-08-21 15:18:49 -04:00
Theodore Song f97b0d9e90 Expand agent-scoped learning capacity 2026-08-21 15:11:31 -04:00
Theodore Song 6f3982a4da Differentiate forward learning by agent 2026-08-21 15:08:42 -04:00
Theodore Song 03dbed706a Scan live deadline dominance bundles 2026-08-21 15:01:12 -04:00
Theodore Song 32f5d78374 Reuse verified directional evidence lineage 2026-08-21 14:51:50 -04:00
Theodore Song b932c9fc59 Preserve directional learning across releases 2026-08-21 14:48:07 -04:00
Theodore Song f49138e1ed Trade live threshold dominance bundles 2026-08-21 14:37:46 -04:00
Theodore Song 9fca4e303d Make sports forward learner quote aware 2026-08-20 18:27:39 -04:00
Theodore Song 39e893a553 Replace rejected sports pilot with forward shadow learner 2026-08-20 18:15:09 -04:00
Theodore Song 11ea832daf Grade only closed markets as settled 2026-08-20 18:00:52 -04:00
Theodore Song 7e75d98d8c Add adaptive sports favorite pilot 2026-08-20 17:31:41 -04:00
Theodore Song 5f14d012a5 Refresh Build 70 navigation badge 2026-08-19 16:29:21 -04:00
Theodore Song 007692f4e4 Preserve calibration counts in agent reports 2026-08-19 16:27:55 -04:00
Theodore Song b73057cc3d Retire stale holdings and add early loss veto 2026-08-19 16:21:06 -04:00
Theodore Song d7c6b8aa5e Refresh offline cache for build 68 2026-08-19 16:13:46 -04:00
Theodore Song 846b3fd5ba Share one-hour maker research across agents 2026-08-19 16:12:23 -04:00
Theodore Song b8415dc750 Remove emotion-driven agent sizing 2026-08-19 15:58:47 -04:00
Theodore Song ff337588be Fit reward audit within Vercel function limit 2026-08-19 15:42:12 -04:00
Theodore Song 6b1ab20439 Audit maker rewards before shadow quoting 2026-08-19 15:37:15 -04:00
Theodore Song b7b66d5c48 Gate maker exposure behind shadow evidence 2026-08-19 15:10:26 -04:00
Theodore Song bd326a2123 Learn from verified maker quote outcomes 2026-08-19 14:49:52 -04:00
Theodore Song e5efc85f65 Add evidence-gated paired maker strategy 2026-08-19 14:30:41 -04:00
Theodore Song c983c1e99f Require durable evidence before adaptive trades 2026-08-19 14:13:29 -04:00
Theodore Song b9d8d66264 Keep cloud outage visible after cycles 2026-08-19 13:52:01 -04:00
Theodore Song bf25c62871 Surface shared state provider failures 2026-08-19 13:49:01 -04:00
Theodore Song 720aa75d78 Classify nested Neon connection failures 2026-08-19 08:31:01 -04:00
Theodore Song f75e6b0877 Expose safe shared-state diagnostics 2026-08-19 08:29:48 -04:00
Theodore Song 45421c985a Harden database state failover diagnostics 2026-08-19 08:28:24 -04:00
Theodore Song 78b832f926 Diversify adaptive evidence and repair state failover 2026-08-19 08:26:13 -04:00
Theodore Song 0dbc6bf650 Support two-sided event bundle arbitrage 2026-08-19 08:16:34 -04:00
Theodore Song a5441e8659 Quarantine losing signals and add bundle arbitrage 2026-08-18 16:40:18 -04:00
Theodore Song c7abdf8d4d Verify accounting and rank current strategy 2026-08-18 13:05:10 -04:00
Theodore Song 97186482df Cluster adaptive evidence by event 2026-08-18 12:58:09 -04:00
Theodore Song a1fc52bc20 Preserve adaptive signals until grading 2026-08-18 12:54:16 -04:00
Theodore Song c20ac0e78f Cluster adaptive evidence by market 2026-08-18 12:50:42 -04:00
Theodore Song 7001366334 Prioritize settlement risk explanations 2026-08-18 12:44:37 -04:00
Theodore Song c78a13bbb7 Block path-dependent settlement barriers 2026-08-18 12:41:22 -04:00
Theodore Song 25622d2ee5 Block audited losing signal cohorts 2026-08-18 12:36:12 -04:00
Theodore Song 846d1f1482 Make historical risk gates adaptive 2026-08-18 11:01:30 -04:00
Theodore Song b768d92f36 Migrate renamed agent chat identity 2026-08-18 10:48:53 -04:00
Theodore Song ad11de39ad Align adaptation with audited outcomes 2026-08-18 10:45:58 -04:00
Theodore Song 7b81dbfa4b Coordinate high-risk agent exposure 2026-08-18 10:32:15 -04:00
Theodore Song 0db53c46f8 Grade signals beyond the active scan 2026-08-18 10:24:05 -04:00
Theodore Song aea8f5281d Preserve strategy evidence across builds 2026-08-18 10:19:12 -04:00
Theodore Song 8f89847cd6 Make stale offline cycles mark-only 2026-08-18 10:12:42 -04:00
Theodore Song bc5de3c013 Gate adaptation on repeatable evidence 2026-08-18 10:06:59 -04:00
Theodore Song 807eb1b506 Fix adaptive confidence and trading costs 2026-08-18 10:01:43 -04:00
Theodore Song c6c7dc30b4 Add settlement calibration audit 2026-08-18 09:58:55 -04:00
Theodore Song 36345e7cad Apply multi-horizon market priors 2026-08-18 09:52:06 -04:00
Theodore Song 38945b2045 Expand chronological signal audit 2026-08-18 09:50:46 -04:00
Theodore Song f341136d80 Apply history-tested risk priors 2026-08-18 09:45:45 -04:00
Theodore Song 901ad8a959 Add chronological signal evaluation 2026-08-18 09:43:29 -04:00
Theodore Song 8cdbcd00ba Remove lookahead from historical replay 2026-08-18 09:36:53 -04:00
Theodore Song 4b35b89f6d Align conviction capacity with risk budgets 2026-08-18 09:32:17 -04:00
Theodore Song 283a009e00 Explain every blocked agent entry 2026-08-18 08:44:12 -04:00
Theodore Song 9fea16988c Ignore tiny runners in agent overlap limits 2026-08-18 08:41:01 -04:00
Theodore Song dc9199a433 Cap binary settlement risk in adaptive engine 2026-08-18 08:36:55 -04:00
Theodore Song b906e9654d Keep cached cycles active in background tabs 2026-08-18 08:03:29 -04:00
Theodore Song e4e27c885a Add walk-forward signal calibration 2026-08-17 21:49:19 -04:00
Theodore Song 444fd10e79 Contain severe losing signal regimes early 2026-08-17 21:17:13 -04:00
Theodore Song 449cbdfd94 Add adaptive offline agent engine 2026-08-17 21:11:59 -04:00
Theodore Song ab333dd045 Replace forced exposure with confirmed signals 2026-08-09 15:47:06 -04:00
Theodore Song 0ef05ea734 Update exposure engine labels 2026-08-02 15:22:59 -04:00
Theodore Song cfecd2e78b Add durable portfolio exposure controller 2026-08-02 15:21:26 -04:00
Theodore Song 6233efc098 Move shared cycle state to Neon fallback 2026-08-02 14:45:06 -04:00
Theodore Song ecbdbf4cbf Use explicit Blob credentials for state sync 2026-08-02 14:41:49 -04:00
Theodore Song a4402134ff Restore resilient cloud state sync 2026-08-02 14:40:02 -04:00
Theodore Song e57d84af1d Let core agents join qualified probes 2026-08-01 17:54:42 -04:00
Theodore Song c7c8359f09 Fix audit churn and idle core portfolios 2026-08-01 17:49:15 -04:00
Theodore Song 8dc7c9506c Let aggressive agents press qualified signals 2026-08-01 12:18:14 -04:00
Theodore Song 313c4f9326 Add five aggressive strategy agents 2026-08-01 12:15:24 -04:00
Theodore Song 1effdd8373 Reject stale strategy data from old clients 2026-08-01 09:50:22 -04:00
Theodore Song 4630bd482b Add structured strategy audit records 2026-08-01 09:47:38 -04:00
Theodore Song 91a6c5678c Replace forced deployment with quality-first trading 2026-08-01 09:44:56 -04:00
Theodore Song 8a897b9a6b Let core agents share strong probe trades 2026-07-30 15:10:50 -04:00
Theodore Song 2a2f8af765 Increase core agent capital deployment 2026-07-30 15:06:51 -04:00
Theodore Song a352e86a2b Restore shared portfolio momentum metrics 2026-07-30 15:01:35 -04:00
Theodore Song 21f89a9fea Focus arena on five active core agents 2026-07-30 14:52:43 -04:00
Theodore Song f8e3799f21 Show rolling 24 hour portfolio changes 2026-07-29 16:11:47 -04:00
Theodore Song 5f57608ee7 Expand tiered trade-ready pool 2026-07-29 15:00:12 -04:00
Theodore Song 697830a782 Restore risk-capped agent deployment 2026-07-29 14:54:21 -04:00
Theodore Song cb4a539256 Add durable agent risk controls 2026-07-28 14:26:57 -04:00
Theodore Song 09b4eac736 Limit agent analysis to top 500 active markets 2026-07-27 15:55:08 -04:00
Theodore Song c7ce683db9 Read latest versioned cloud state 2026-07-27 15:45:51 -04:00
Theodore Song d04ed59088 Preserve suggestion quality fields 2026-07-27 15:40:26 -04:00
Theodore Song 82a1e0c831 Enable selective buys for agents 2026-07-27 15:37:55 -04:00
Theodore Song 67beab0144 Make top 5000 analysis visible 2026-07-27 15:27:48 -04:00
Theodore Song 3c39cfe9f3 Force visible top 5000 build 2026-07-26 21:13:02 -04:00
Theodore Song 744387199e Analyze top 5000 active markets 2026-07-26 20:10:39 -04:00
Theodore Song 49888c7b56 Fix agent hold rules and chart artifacts 2026-07-26 17:39:39 -04:00
Theodore Song e36ef12e6a Add evidence based agent scoring 2026-07-26 16:55:21 -04:00
Theodore Song 29f9dcf29b Reduce agent trade overlap 2026-07-26 16:39:30 -04:00
Theodore Song 1f1e0c39b8 Fill suggestion list with broad watch ideas 2026-07-26 16:29:23 -04:00
Theodore Song b5fed7980f Expand suggestions with scout ideas 2026-07-26 16:23:54 -04:00
Theodore Song 4b3e03a353 Use keyset pagination for full market scans 2026-07-26 16:15:48 -04:00
Theodore Song 734b989072 Expand active market scan and suggestions 2026-07-26 16:07:08 -04:00
Theodore Song 8d69619dad Tighten agent entry and recovery rules 2026-07-26 15:52:34 -04:00
Theodore Song 018d1d9294 Add SMTP support for trade emails 2026-07-26 14:13:34 -04:00
50 changed files with 23971 additions and 630 deletions
+7 -1
View File
@@ -85,11 +85,17 @@ CUSTOMER_SUPPORT_EMAIL=
RISK_ADMIN_WALLET=
# Trade email alerts
# Use either Resend directly or a webhook from Zapier/Make/another email automation.
# Use Resend, SMTP, or a webhook from Zapier/Make/another email automation.
RESEND_API_KEY=
TRADE_EMAIL_FROM=Poly Arena <alerts@yourdomain.com>
TRADE_EMAIL_REPLY_TO=
TRADE_EMAIL_WEBHOOK_URL=
SMTP_HOST=
SMTP_PORT=
SMTP_SECURE=true
SMTP_USER=
SMTP_PASS=
SMTP_FROM=
# Agent chat
OPENAI_API_KEY=
+62
View File
@@ -0,0 +1,62 @@
name: Autonomous paper cycle
on:
schedule:
- cron: "2,7,12,17,22,27,32,37,42,47,52,57 * * * *"
workflow_dispatch:
push:
branches: [main]
paths:
- index.html
- api/state.js
- scripts/run-autonomous-cycle.mjs
- .github/workflows/autonomous-cycle.yml
permissions:
contents: write
actions: write
concurrency:
group: autonomous-paper-cycle
cancel-in-progress: false
jobs:
cycle:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: node scripts/run-autonomous-cycle.mjs
env:
ARENA_URL: https://polymarket-site-eta.vercel.app
EXPECTED_BUILD: "128"
RUNTIME_BRANCH: runtime-state
RUNTIME_STATE_PATH: runtime/state.json
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Wait for the next five-minute slot
if: always()
shell: bash
run: |
now=$(date +%s)
next=$(( (now / 300 + 1) * 300 + 15 ))
delay=$(( next - now ))
echo "Next autonomous slot starts in ${delay}s"
sleep "$delay"
- name: Dispatch the next autonomous cycle
if: always()
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
curl --fail-with-body --silent --show-error \
--request POST \
--header "Accept: application/vnd.github+json" \
--header "Authorization: Bearer ${GITHUB_TOKEN}" \
--header "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/workflows/autonomous-cycle.yml/dispatches" \
--data '{"ref":"main"}'
@@ -0,0 +1,51 @@
name: Release expanded executable search after Vercel quota reset
on:
schedule:
- cron: "25 1-23/2 23 8 *"
- cron: "25 1-13/2 24 8 *"
workflow_dispatch:
permissions:
contents: write
concurrency:
group: release-protected-routing
cancel-in-progress: false
jobs:
release:
runs-on: ubuntu-latest
timeout-minutes: 35
steps:
- uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
- name: Trigger the deferred production deployment
env:
ARENA_URL: https://polymarket-site-eta.vercel.app
run: |
if curl --fail --silent --show-error "${ARENA_URL}/?search_preflight=${GITHUB_RUN_ID}" | grep -q 'BUNDLE_DEPTH_CANDIDATE_LIMIT=200'; then
echo "Expanded executable search is already live."
exit 0
fi
printf 'Expanded executable search deployment retry from run %s at %s\n' "$GITHUB_RUN_ID" "$(date -u +%FT%TZ)" > .expanded-search-release-trigger
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add .expanded-search-release-trigger
git commit -m "Retry expanded executable search deployment"
git push origin HEAD:main
- name: Verify the expanded search revision in production
env:
ARENA_URL: https://polymarket-site-eta.vercel.app
run: |
for attempt in $(seq 1 40); do
if curl --fail --silent --show-error "${ARENA_URL}/?search_wait=${GITHUB_RUN_ID}-${attempt}" | grep -q 'BUNDLE_DEPTH_CANDIDATE_LIMIT=200'; then
echo "Expanded executable search is live."
exit 0
fi
sleep 30
done
echo "The Vercel quota has not admitted this revision yet; a later scheduled retry will continue."
exit 1
+1
View File
@@ -0,0 +1 @@
Protected routing deployment retry from run 32609010526 at 2026-08-23T00:53:41Z
+667 -3
View File
@@ -14,8 +14,665 @@ Personal research mode:
https://polymarket-site-eta.vercel.app/personal.html
The site fetches live Polymarket markets, generates agent suggestions, lets you
run frequent paper cycles, and syncs the shared arena state through the Vercel
API when `BLOB_READ_WRITE_TOKEN` is configured.
run frequent paper cycles, and syncs the shared arena state through Neon or
Vercel Blob. Build 73 also installs an offline app shell and caches timestamped
market snapshots. During an outage, cycles continue locally; cached entries are
allowed for 90 minutes, older snapshots become mark-only, and all cached data
expires after 24 hours.
Offline cycles can apply calibration already earned from live observations, but
the evidence ledger is read-only: cached prices cannot grade pending signals,
expire horizons, or create new observations.
Build 128 targets five-minute slots with a serialized, self-chained GitHub Actions runtime.
The mutable snapshot stays on the dedicated `runtime-state` branch, which is
explicitly excluded from Vercel Git deployments through
`git.deploymentEnabled`. This prevents high-frequency state commits from
consuming Preview deployment quota while ordinary `main` source commits still
produce Production deployments.
It continues from the previous agent snapshot and runs the next due paper cycle
even when no browser is open. After each cycle attempt, one repo-scoped
workflow dispatch waits for the next five-minute boundary; one concurrency group
serializes active and pending runs, and both successful and failed runs hand off
after the next boundary while the cron schedule remains as recovery. The runtime writes a
sanitized snapshot to the `runtime-state` branch and `/api/state` uses that as a
read-only fallback while Neon or Vercel Blob is unavailable. The public snapshot
contains only `pma_agents_v2` and `pma_suggestions_v5`, with suggestions capped at
300 to keep cross-device loads small. The API preserves oldest-first pending
observations plus fee schedules, agent promotion scopes, and verified bundle
capacity from the bounded runtime instead of applying its older lossy compactor.
Public maker outcomes retain only calibration fields, action logs are bounded
to 48 entries per agent, and chart snapshots are evenly sampled once their
96-point public limit is reached so older dates remain represented. Repetitive
watch-only explanations are normalized without changing signal fields. If the
875 KB browser transport budget is reached, the compactor first protects 75 KB for
learning, reserves up to 144 of the newest completed directional outcomes, and
then gives the remaining evidence space to the oldest still-pending
observations. Only after display history reaches its documented floor can the
public suggestion list fall from 300 to its 240-item emergency floor. The headless
runner applies an additional 850 KB hard target before committing state, leaving
margin below the API's 900 KB limit.
Paper accounts, passwords, email settings,
investment allocations, chat history, wallet information, and live-money
settings are explicitly excluded. While that fallback is active, ordinary
phones and computers are display-only and cannot fork the public portfolio with
their local timer; only the headless runner advances it. The next autonomous
result is the shared public authority until managed storage is restored. The
headless browser also rebuilds a minimal timestamped market cache from those two
public items on every restart. A complete network outage can therefore continue
the existing 90-minute cached-entry and 24-hour mark-only policy without adding
another synchronized key; strategies that require fresh order-book depth remain
blocked until live connectivity returns. The five shock-strategy adopters now learn from one shared,
event-deduplicated forward ledger. A trade allocated to one agent therefore
teaches the other four without copying its cash or P&L; shared positive evidence
can promote bounded size and shared negative evidence disables the lane.
Active shock positions use a second underlying-risk key in addition to the
Polymarket event key. Related Ethereum or Bitcoin contracts cannot create
several simultaneous copies of one move, and outcomes from the same underlying
three-hour shock window count as one learner event.
Each Build 128 cycle also scans the 1,000 most-active Polymarket events for
complete negative-risk bundles and logically nested threshold or deadline
pairs, plus same-market YES/NO complements whose equal shares have a fixed
$1 redemption value. Polymarket's documented complete-set merge converts equal
YES and NO amounts directly back into collateral, so a verified binary complement
is merged and realized in the same paper cycle instead of waiting for resolution.
The official negative-risk adapter also converts a complete set of NO tokens into
`n - 1` units of collateral, less the event's `negRiskFeeBips`. Strategy 65 treats
that as a separate immediate path. The scanner reserves 40 of its 200 depth checks
for complete-NO candidates, then reads `getFeeBips` and `getQuestionCount` from the
official Polygon adapter for only those candidates. Every leg must expose one
consistent `negRiskMarketID`, the event leg count must equal the on-chain question
count, all asks and CLOB fee schedules must verify, and the post-conversion proceeds
must remain profitable. Missing or inconsistent adapter data cannot produce paper
P&L. Complete YES sets and logical dominance pairs are not convertible and continue
to use settlement or verified live-bid exits.
Bundle capital is split between five independent owners instead of bottlenecking one
portfolio: Value Hunter owns settlement complete sets, Momentum owns exact complete-NO
conversions, Breakout owns same-market binary merges, Tail Alpha owns exclusive NO
pairs, and The Diversifier owns threshold and deadline dominance spreads. A global underlying-event
claim prevents any owner from duplicating exposure already held by another.
Gasless CTF operations include merge transactions; see
[Positions & Tokens](https://docs.polymarket.com/concepts/positions-tokens) and
[Gasless Transactions](https://docs.polymarket.com/trading/gasless).
Gamma's market-specific fee flag replaces the old blanket 0.5-cent fee
reserve for markets declared fee-free. The scanner uses 120 independent-first
general structure checks and reserves 40 additional checks for same-market
complements. Structures are ranked by locked-capital efficiency, with
the best structure from each independent event checked before alternates from events
already represented. They are repriced
from batched CLOB asks from the equal-unit size needed for at least a $50
paper order up to a $1,000 verified-notional ceiling. The scanner applies each market's Gamma fee schedule at every
consumed ask level and checks the exact CLOB condition metadata for a matching
fee curve or fee-free state. Any opened position is capped to the exact equal-unit size
of the largest fill that stays profitable and passes this depth test; portfolio
cash and reserve limits can reduce it further. Ordinary protected entries use at
most 6% of equity, entries returning at least 0.20% per locked day use at most 8%,
and immediate merge or conversion paths use at most 10%. Strategy 65 ranks verified
structures by net return per expected locked day before raw edge, and caps active
cost from one underlying event at 12% of the owning portfolio's equity. This prevents
several related threshold pairs from monopolizing the non-directional book. New bundles must
clear a duration-aware daily hurdle: 0.03% through 7 days, 0.04% through 30 days,
0.05% through 90 days, and 0.075% beyond 90 days. Immediate merge and conversion
paths use a 0.01% hurdle. Exact executable sizing must also preserve at least a
0.10-cent per-unit safety margin and $0.50 of total locked profit. This admits a
small margin only when verified depth can scale it into meaningful profit, while
demanding substantially more from positions that immobilize capital for months. Existing paper bundles are
never retroactively enlarged against depth their original simulated fill would
already have consumed. Each live cycle also prices an atomic exit across every bid
level needed to sell every leg of each intact bundle. It verifies every condition's
exact fee schedule again and exits only when net proceeds realize at least 85% of
the remaining guaranteed settlement profit. Missing depth, mismatched fees, or a
smaller profit capture leave the guarantee intact; cached offline cycles cannot
execute this recycling path. A bundle can enter its designated protected owner only when that fee check
passes and the resulting worst-case payout clears the 0.10-cent per-unit buffer,
$0.50 total executable-profit floor, and 0.15% return floor. Top-of-book gaps, missing books,
incomplete fee schedules, and unavailable fee verification remain audit-only.
The Suggestions view stores scan, depth, fee, actionable, and closest
executable-margin counts so an empty lane is evidence rather than an ambiguous
failure.
Build 128 retains the directional learner's exact-fee policy, which replaced the blanket half-cent cost with
the market's Gamma fee schedule at both the entry and future checkpoint, plus a
separate half-cent round-trip slippage allowance. Fee-free markets pay only the
slippage allowance; an unavailable fee schedule gets a conservative four-cent
fee reserve and cannot look artificially profitable. Fee metadata survives
cloud compaction and offline caching. Unfinished observations from the prior fee
policy are replaced immediately, while completed historical outcomes remain
available at reduced weight. The exact-fee replay covered the top 1,000
active markets and 208 independent events: broad trends, reversals, and
favorite trends all had 90% upper bounds below zero at 12 hours. A separate
3,000-recently-closed-market study tested 297 one-decision-per-event settlement
rules and found zero robust positive rule. Strategy 65 retains online
directional evidence under fee policy 2. A separate probation gate can now use
capital after at least 12 current-policy independent events produce a net-of-cost
six-hour lower confidence bound above 1% for every required cohort feature.
The corresponding 500-market, 1,920-rule chronological audit selected zero
rules in validation at 6, 24, or 72 hours. No directional rule is preapproved;
probation and full sizing must be earned from new event-deduplicated forward
observations. Probation uses at most 0.5% of equity per position, 1% per-agent
total capital, one new
position per agent cycle, and one owner per Polymarket event across all agents.
Every probation position exits at the matching six-hour executable bid and keeps
the 18% stop policy active. Normal directional sizing remains locked until the
same cohort independently passes both the 24-hour and 72-hour promotion gates.
An August 23 exact-fee settlement calibration independently checked 5,000
resolved markets, 4,807 usable histories, and 7,854 observations across 1,400
fixed side, category, price-band, and 1/3/7/14/30-day rules. Zero rules cleared
even the event-clustered 95% training lower-bound gate. The three-day Sports NO
target fell to -44.14% in validation and remained negative in holdout, so it
cannot authorize capital. The reproducible result is stored in
`research/settlement-calibration-exact-fee-5000-audit.json`.
Build 128 retains Build 100's retirement of the old 3-6 day resolution-window
capital permission. That audit clustered confidence by event but still averaged
several correlated contracts inside each event, while production could choose
only one. The corrected replay chooses the highest-volume eligible contract per event and
rule. In the recent 3,000-market discovery sample, only the safe non-Sports
50%-55% NO rule four days before settlement survived every chronological gate:
145 events, a 38.42% mean, and a 26.12% lower 90% bound after one cent of cost.
The next disjoint 3,000-market block did not confirm it: the safe cohort's
holdout lower bound was -4.73%, and two chronological-third lower bounds were
negative. No 3-6 day rule is therefore approved for capital.
The remaining four-day candidate is a zero-capital forward learner. It accepts
only fixed-date, non-Sports contracts with a 50%-55% NO midpoint, at least
$15,000 total volume, $1,400 liquidity, and no more than a three-cent spread.
The modeled entry uses the executable NO ask plus 0.25 cents of slippage and
rejects more than one cent of friction. One observation is recorded per event
and graded only after Polymarket marks the market closed; offline snapshots
cannot invent settlement. Forty independent settled observations with a lower
90% confidence bound above 1% are required before 0.5%-of-equity paper
positions can begin. Existing positions from the retired version still follow
their precommitted settlement-only exit.
Build 94 also closes the contract-safety hole exposed by the first Strategy 3
paper positions. Shock Strategy 4 keeps the same audited accelerating
three-hour fade and fixed 12-hour executable exit, but it will not enter a
path-dependent barrier, an exact numeric range, or a market without at least
the full 12-hour holding period plus its two-hour grading tolerance remaining.
Older unsafe shock positions are unwound at an executable bid; older valid
positions continue to their recorded target and still reserve their underlying
risk so the new strategy cannot overlap them. Strategy 4 starts a fresh shared
forward ledger because its eligible contract universe is materially different.
Build 95 removes Strategy 4's initial capital permission after replaying the
production contract gate over 2,000 primary and 1,000 untouched active markets
at a two-cent modeled cost. The primary train, validation, chronological, and
event-holdout lower bounds remained positive, but the untouched chronological
event mean was negative and both its chronological and event-holdout confidence
bounds crossed zero. No category or entry-price refinement survived every
independent partition. Strategy 4 therefore starts shadow-only: 40 positive
event-deduplicated forward outcomes with a lower bound above 1% can qualify 1%
paper positions; 80 outcomes with a lower bound above 1.5% can raise size to
1.5%. Twenty convincingly losing events keep capital disabled.
Build 73 distinguishes a temporary order-book pause from settlement. Exact
market refreshes still mark paused positions to the latest published price, but
the engine cannot simulate a stop, policy exit, or settlement while
`acceptingOrders` is false and `closed` is still false. Only a closed market
books the final paper proceeds and contributes a completed learner outcome.
The live scan now continues through activity-ranked pages until it has the 500
most-active eligible Yes/No contracts. Markets whose actual outcome labels are
team names, Over/Under, or another pair are rejected instead of being silently
reinterpreted as Yes/No. The same semantic check applies to complete event
bundles and the offline evaluators.
Build 73 ranks the competition by each agent's return since Strategy 58 began.
Historical replay equity remains visible for context, but it no longer makes an
agent look like the current leader when the live adaptive strategy is losing.
Each agent learns bounded weights from its own v34+ trade outcomes across signal
type, setup quality, category, side, entry-price band, and time to resolution.
The learner shrinks small samples toward neutral, caps sizing changes to
0.68x-1.30x, and reserves
15% of candidates for deterministic exploration so a stale regime cannot become
permanent.
Strategy 58 treats each binary stake as capable of falling to zero even when the
18% stop cannot fill. New core positions are capped at 2.5%-4% of equity and
aggressive positions at 3%-5%, with lower limits for near-term, extreme-price,
reversal, and fast-moving setups. Oversized positions inherited from older
engines are reduced to the same loss budget during live marking.
The two-agent overlap guard counts only positions worth at least 1.25% of an
agent's equity, so tiny profit-lock runners do not block a new material trade.
A separate walk-forward ledger records every confirmed signal before its future
price is known, grades it at 6 hours for early loss vetoes and in separate
24-hour and 72-hour promotion windows, and combines
that broad market calibration with each agent's personal outcomes. This expands
the learning sample without forcing observation-only signals into portfolios or
backfilling future information into old decisions. Missed windows expire instead
of borrowing an arbitrarily later price. The 24-hour checkpoint matches the
minimum ordinary holding policy while the 72-hour checkpoint tests persistence;
stops and profit locks still act immediately from fresh prices. Positive
six-hour evidence cannot promote capital, while a mature negative six-hour
cohort can demote it.
The Build 69 re-audit loaded all 500 requested histories with no failures. The
six-hour family lost 1.27% net on average across 106 independent events, with
its full 90% interval below zero; no tested rule was robustly positive at 6,
12, 24, or 72 hours. Build 73 therefore uses six hours only to stop bad regimes
sooner. It also closes every stale pre-Strategy-58 directional holding at the
next fresh mark, including legacy records missing a signal label, while leaving
complete arbitrage bundles and paired maker inventory under their own accounting.
The initial seven-day chart seed is an approximate replay, not a live return.
It uses only prices available on each simulated date, computes daily and weekly
changes from those historical prices, disables unavailable hourly reversal data,
and labels the combined number as legacy/replay. Adaptive-strategy returns are the
clean live comparison.
Run `npm run evaluate:signals` to test the price-signal rules against one month
of hourly Polymarket history. The evaluator forms signals only from prior
one-hour, one-day, and one-week prices, marks them 6, 12, 24, and 72 hours later,
applies a conservative half-cent cost estimate, and reports a chronological
70/30 split plus three consecutive time segments. Results are clustered by
Polymarket event so repeated observations and correlated outcome contracts cannot
masquerade as broad evidence. Set `EVAL_MARKETS`, `EVAL_CONCURRENCY`, `EVAL_HORIZONS`, or
`EVAL_COST_CENTS` to change the audit. Set `EVAL_SUMMARY=1` for the compact,
decision-focused report.
Run `npm run evaluate:adaptive` for a stricter chronological search across 1,920
predefined price-action rules. It uses a 60/20/20 train, validation, and holdout
split and never selects a rule from the holdout period. The August 19 run loaded
all 300 requested histories and found no directional rule that passed both train
and validation at either 24 or 72 hours. Strategy 55 therefore keeps directional
signals in the walk-forward observation ledger until current, independent-event
evidence proves an edge.
Run `npm run evaluate:sports-favorites` for the retired pregame favorite audit.
Its 12-hour, 60%-75% cohort had positive point estimates but did not establish a
reliable confidence bound, so Build 128 no longer allocates capital to that rule.
Strategy 65 also tracks the exact-fee settlement calibration's three-day Sports
NO cohort as a zero-capital forward lane. The corrected 5,000-market run
groups every prop with the same dated contest slug, anchors the decision to the
published game start, and keeps only the highest-priced eligible NO contract per
real contest. Its 76 contests had positive point estimates in all four
chronological quarters; train and holdout 95% lower bounds were positive, but
validation returned only +1.78% with a wide negative lower bound. This is not a
proven edge. A follow-up search tested 60 rules on the most recent 5,000 eligible
resolved sports markets and again on the next older disjoint 5,000. No rule passed
train, validation, and untouched holdout in both archives. Favorite Backer therefore
opens no sports capital initially and records at most one contest-level observation
per cycle using the executable NO ask, exact Gamma taker fee, and 0.25-cent slippage.
Twenty independent forward settlements with a lower 90% confidence bound above
0.5% can promote 0.75% positions; 40 stronger outcomes can raise size to 1%. The
same ledger persists offline, but stale cache policy and verified settlement rules
still apply.
Run `npm run evaluate:settlement-calibration` for the stricter settlement-bias
search across up to 5,000 resolved markets. It uses a 60/20/20 chronological
split, a one-market-per-event limit, 95% event-clustered confidence bounds, four
stability windows, a 24-hour market-age minimum, and a recent non-flat price
history requirement. Before those activity and overlap controls, four sports
rules appeared to pass holdout because correlated props shared one event and
some histories contained inactive default prices. After correction, 11 of
1,400 rules passed training and zero passed validation. Strategy 58 therefore
does not install any other static side, category, price-band, or settlement-horizon bet.
Strategy 58 also removes the last emotion-driven sizing path. Agent mood and
leaderboard urgency remain visible in reports, but neither can increase capital.
A positive peer signal receives at most a 5% sizing lift, and only when both the
agent's realized-trade cohort and the independent walk-forward market cohort are
already promoted. One learner, popularity, or urgency alone leaves size at 1.00x.
Run `npm run evaluate:liquidity` to inspect live reward-scoring markets using
both public outcome books. It recomputes the minimum-size-adjusted midpoint,
upper-bounds competing maker score from visible qualifying depth, enforces the
$1 payout minimum, and reports one-leg loss beside the estimated reward share.
The estimate is a single snapshot, not earned income. Run `npm run evaluate:maker`
for the chronological fill-path audit; set
`MAKER_MARKETS`, `MAKER_HISTORY_DAYS`, `MAKER_CONCURRENCY`, or
`MAKER_EXIT_COST_CENTS` to change it and `MAKER_SUMMARY=1` for compact output.
The August 19 audit loaded both token histories for all 300 requested markets,
formed 5,477 non-overlapping observations, and tested 3,024 rules over 3, 6, 12,
and 24-hour horizons. Zero rules passed training, validation, or untouched
holdout. At three hours, the broad 0.5-cent quote-gap rule still lost 0.63% per
event in holdout; only 0.61% of observations completed both legs while 23.33%
produced adverse one-leg inventory. Wider quotes traded less but remained
negative. Strategy 58 therefore does not risk paper capital on an unproven
maker rule.
Run `npm run evaluate:reward-maker` to stress current reward-qualified books
against 30 days of token history. The August 19 comparison found zero passing
candidates at a three-hour exit window. At one hour, 2 of 24 current candidates
passed the chronological path screen at 25% of the present reward estimate, but
both did so without an adverse historical touch; this prioritizes shadow
research and does not authorize capital.
Maker research version 4 lets all ten agents split distinct zero-capital shadow
quote pairs selected by the shared reward-book audit. Token histories are fetched
in batches, so the learner no longer silently ignores quotes after the first ten
markets. A live cycle must still verify each touch from public CLOB price history
or a current book cross. After the first one-sided touch, the shadow engine now
buys the complement only when the current executable ask locks a positive margin
after modeled cost; otherwise it grades an immediate executable exit. It no
longer waits with adverse one-sided shadow inventory. Build 128 records seven
reproducible setup cohorts for every completed observation: category, spread,
reward yield, quote-price balance, recent movement, paired locked margin, and
visible competition. Shadow selection adds a bounded novelty score so the ten
agents rotate toward under-sampled configurations instead of repeatedly measuring
one dominant setup. A mature losing cohort vetoes risk; promotion would require
every matching cohort to clear its independent-event confidence and locked-fill
requirements, as well as a separately approved chronological backtest.
The August 21 expanded audit loaded both token histories for all 500 requested
markets, formed 7,335 observations, and tested 6,048 wait and immediate-hedge
rules. Zero passed training, validation, or untouched holdout. The separate
reward-assisted stress test also found zero passing candidates. Maker capital is
therefore disabled even if a small live cohort appears positive; those outcomes
remain research evidence and cannot override the failed independent backtest.
Existing paper inventory from prior maker versions is still reconciled honestly.
There is no capital-backed exploration lane, and the engine does not credit
hypothetical rewards.
See Polymarket's official [fees](https://docs.polymarket.com/trading/fees),
[maker rebates](https://docs.polymarket.com/market-makers/maker-rebates), and
[liquidity rewards](https://docs.polymarket.com/market-makers/liquidity-rewards)
documentation for the live venue rules this paper simulation approximates.
The latest 120-active-market audit produced 1,241 twelve-hour observations from
41 markets with no fetch failures. The broad rule averaged -1.35% net and was
negative in all three chronological segments. Reversals averaged -3.69%, with a
market-clustered 90% interval entirely below zero. Crypto and Sports were also
negative but covered only three and five markets. The 24-hour cohort improved to
-0.82% row mean and +1.31% market mean, with no rule robustly negative across all
segments. Strategy 54 therefore keeps reversal entries observation-only until
their recent signal and quality cohorts independently earn promotion, retains
their signals for paper grading, and evaluates adaptation at 24 and 72 hours.
The expanded active-market audit loaded history for 498 of the top 500 active
markets with no failures and produced 3,597 net-of-cost 24-hour outcomes across
142 markets. No tested follow or fade rule was robustly positive. Crypto trends
averaged -3.83% per observation and -3.99% per market; Sports trends averaged
-5.44% and -6.61%. Both stayed negative in every chronological segment and their
market-clustered 90% intervals were entirely below zero. Strategy 54 therefore
keeps Crypto and Sports trends observation-only while continuing to grade them.
The August 18 event-clustered rerun loaded 499 of 500 active markets and produced
3,894 twelve-hour observations across 156 markets and 102 independent events.
The broad mean was -1.14%, the event mean was -1.11%, and the event-clustered
90% interval stayed below zero. No tested category, side, price band, signal
strength, or combined feature cohort was robustly positive. Broad trends,
YES trends, favorite trends, strong trends, and hour-confirmed trends were all
robustly negative. Strategy 54 therefore makes every directional trend or
reversal observation-only until its own signal, side, and category cohorts each
earn positive promotion from recent independent events. This is a strategy reset,
so current adaptive returns begin from the portfolio equity at migration.
The final August 19 300-market rerun loaded all 300 eligible Yes/No price
histories without a failure and produced 2,598 twelve-hour observations across
64 independent events. The broad row mean was -0.65% and the event-cluster mean
was -1.71%. The 72-hour event-cluster mean was -3.59% with its 90% interval below
zero, and no tested directional rule was robustly positive. Strategy 54
therefore requires positive evidence at both 24 and 72 hours rather than
allowing one favorable short-horizon mark to authorize cash exposure.
A corrected 200-market audit paged through 197 markets with usable history and
1,912 twelve-hour outcomes. Reversals remained negative in every chronological
segment and averaged -4.13%. Sports trends were negative in train and test and
averaged -3.53% at 72 hours. Politics trends were the sole cohort with positive
row-level returns in all three 72-hour segments, but its market-cluster interval
still crossed zero; that supports a longer hold test, not a larger entry bet.
Strategy 54 gives previously opened Politics trend positions that 72-hour observation window before
ordinary signal exits. Stops, profit locks, settlement handling, and risk-budget
reductions remain immediate.
Strategy 65 retains exact Gamma entry and exit taker fees plus a half-cent
slippage allowance when grading each live walk-forward signal. Confidence uses the largest independent matching bucket,
not the sum of five overlapping feature buckets, and evidence from older engine
versions is down-weighted. This prevents a handful of duplicated observations
from authorizing larger positions or hiding a modest negative regime.
Strategy 54 adds uncertainty-aware, multi-horizon promotion and demotion. A
matching setup must accumulate at least eight effective independent-event
observations, including at least five from the current strategy, and agree across
at least two feature views at both the 24-hour and 72-hour checkpoints before it
can risk cash. Mixed or one-horizon evidence stays observation-only instead of
being mistaken for an edge.
Build 65 enforces the documented offline boundary end to end. Cached snapshots
under 90 minutes old may continue paper execution. Older snapshots remain usable
for valuation and chart snapshots for up to 24 hours, but cannot trigger entries,
stop-losses, gain-stops, risk rebalances, settlements, or policy exits. Network
requests have bounded timeouts so a weak connection falls back to cache instead
of leaving a cycle hanging indefinitely.
Build identity is separate from strategy lineage starting with build 42. The
service worker and deployment metadata advance with each code release, but
adaptive baselines, pending signal grades, and trade evidence remain in one strategy
lineage until the actual entry, sizing, or exit logic changes. Legacy build 40 and 41
records are migrated into the same strategy lineage without losing evidence.
Build 65 independently refreshes markets for due pending signals that have
left the current top-500 activity scan. Unavailable markets remain queued for a
bounded retry window. This prevents activity-rank survivorship from deciding
which wins and losses reach the adaptive calibration ledger.
Build 65 also allocates the 300 pending observation slots by evidence coverage.
Under-sampled signal/side/category cohorts are observed first, followed by
under-sampled independent events and market sides, with conviction used only as
a later tie-breaker. This prevents the same popular contracts from monopolizing
the ledger and gives the learner a realistic path to promote or reject more
diverse cohorts.
Build 65 retains safe shared-state provider diagnostics from both reads and
writes. A device now says `local only` when Neon is paused or a Blob credential
is rejected, instead of presenting a local browser save as a successful
cross-device sync. Completed cycle statuses retain that `local only` warning
until a cloud provider succeeds. Database URLs still fail over across configured Neon
aliases without exposing credentials in the API response.
Strategy 54 coordinates high-risk exploration globally. Near-term, extreme-price,
and other gap-prone positions may be held materially by only one agent, while
ordinary independently confirmed markets retain the two-agent cap. The robustly
negative Sports- and Crypto-trend cohorts cannot enter through exploration.
Reversal and short-dated NO signals remain observation-only until their own recent
feature cohorts pass the promotion gate.
Run `npm run evaluate:settlements` to evaluate fixed decisions made 1, 3, 7,
14, 30, and 90 days before known binary settlements. The audit uses one
observation per resolved market and horizon, includes losing contracts at zero,
applies the same half-cent cost assumption, clusters related contracts by event,
and requires positive event-clustered confidence bounds in train and test plus
positive results in three chronological segments before it calls a settlement
cohort robust. Environment variables beginning with
`SETTLEMENT_` control its market count, concurrency, horizons, and cost. Set
`SETTLEMENT_SUMMARY=1` for the compact report.
Run `npm run evaluate:neg-risk` to scan complete active negative-risk events for
whole-event YES or NO bundles using executable best asks/bids, per-leg costs, and
a minimum-liquidity requirement. An earlier 500-event audit found 33 complete
liquid negative-risk events and zero positive worst-case bundle returns after
costs. Midpoint price sums sometimes looked attractive, but executable spreads
removed the apparent edge. The August 18 rerun found 35 eligible events and one
three-leg NO bundle with a 0.25%
modeled margin after estimated costs. The final August 19 scan found 49 eligible
events and no currently actionable bundle; its closest complete bundle remained
0.25% negative after modeled costs. Strategy 54 can paper-trade either a
complete YES or complete NO bundle only from live executable prices, opens every
leg together, and holds
the hedge intact until settlement. It also requires at least a 0.15% modeled net
return so large bundles cannot tie up capital for a negligible absolute edge.
Cached bundle prices are never allowed to open positions.
Run `npm run evaluate:dominance` to inspect logically nested threshold contracts
using executable prices. The corrected August 19 scan tested 807 eligible pairs
across 500 active events and found zero positive pairs after estimated costs. The
August 20 rerun tested 888 eligible pairs and found one current candidate: YES on
MetaMask FDV above $2B plus NO on FDV above $3B cost 0.994 per pair after the
half-cent-per-leg execution allowance, against a guaranteed minimum $1 payout
when both contracts share the same event terms. Build 75 adds these same-event,
same-wording dominance pairs to Value Hunter's live scanner. Both legs must open
together, cached/offline prices cannot create an entry, and mismatched wording,
non-Yes/No labels, inadequate liquidity, and non-positive margins are rejected.
An earlier parser had mistaken Over/Under labels for Yes/No; the label-aware
scanner and live engine retain a regression test for that failure mode.
Build 78 expands the same-event dominance audit to explicit calendar deadlines.
For otherwise identical questions, buying NO on the earlier deadline and YES on
the later deadline guarantees at least one winning contract whether the event
happens early, happens between the dates, or never happens by the later date.
The August 21 audit found 198 valid deadline pairs among the 500 most-active
events and no pair with a positive executable margin after the half-cent-per-leg
allowance. The live scanner still monitors them every cycle and can open both
legs atomically when a positive gap appears. Mixed explicit/implicit years,
invalid dates, changed wording, non-Yes/No labels, stale quotes, and
non-positive margins are rejected.
Build 79 splits the forward directional learner by agent strategy. Every new
observation stores the agent IDs whose actual acceptance rules matched that
candidate. Graded outcomes retain the scope, and each agent builds an
event-clustered calibration from only its eligible opportunity universe. A
positive Momentum Chaser cohort can therefore promote for Momentum Chaser
without enabling the same trade for Value Hunter or the other agents; a losing
cohort can also veto one strategy without freezing all ten. The Suggestions tab
lists the agents that earned an aggregate promotion. Explicit empty scopes do
not leak to any agent, legacy unlabeled outcomes remain readable for migration,
and eligibility metadata survives local/offline and cloud-state compaction.
Build 80 expands the pending forward-research queue from 300 to 600 records and
the retained graded history from 500 to 1,000 outcomes. This keeps the 300
legacy observations through their remaining checkpoints while opening capacity
for strategy-scoped observations immediately. The larger history retains enough
independent events for per-agent 6-hour risk vetoes and 24-hour/72-hour
promotion confidence checks without relaxing execution, cost, or evidence
requirements.
Build 81 separates research eligibility from capital eligibility. Directional
trend and reversal candidates are tagged for every strategy whose broader
mandate would study them, including observation-only setups; actual positions
still require the strategy's stricter quality, edge, evidence, and forward
promotion gates. Sports-pilot and priced-bundle records are excluded from the
directional learner. Each agent report now audits the global pending queue as
legacy shared, strategy-tagged, unassigned, and tagged-for-this-agent counts so
learning differences are visible instead of inferred from a single total.
Build 82 fixes the migration edge discovered by the first Build 81 production
cycle: an explicitly unassigned Build 80 observation remains visible in the
queue audit but no longer blocks a new strategy-tagged observation for the same
market side. New records also retain the signal features used to assign their
research scope, making later audits reproducible.
Build 83 advances to Strategy 59 after a fresh return audit. A 300-market,
3,713-observation adaptive replay found no rule with a positive event-clustered
lower bound in train, validation, and untouched holdout at both 24 and 72 hours;
a separate 5,000-resolved-market settlement replay also produced no validated
holdout winner. Favorite-priced trend following was consistently net-negative
after cost across 6, 12, 24, and 72 hours, so that cohort is now hard-blocked.
Personal adaptation and position-size changes now use only closed Strategy 59
trades. Older trades remain visible as historical context but cannot promote a
personal cohort or increase current risk. Forward directional evidence from
Strategies 51-59 remains compatible because the signal and checkpoint policy
did not change.
Build 84 retires maker capital after the expanded 500-market audit found zero
validated winners among 6,048 wait and immediate-hedge rules. New maker work is
zero-capital lock-or-exit research: after one resting bid touches, the engine
records a complementary hedge only when the current executable ask locks a net
profit; otherwise it grades an immediate exit. A positive in-app cohort cannot
reactivate capital without a separately approved chronological backtest.
Build 85 adds a forward-only Shock Reversion lane after a 157,386-observation
hourly replay found a promising but not yet independently conclusive regime.
The agent detects an 8-point or larger three-hour YES-price move whose final
hour remains aligned, then observes the opposite side for exactly three hours.
Entry and exit use executable book prices plus a 0.25-cent slippage buffer.
The first 30 independent events use zero capital, missed exit windows expire
without borrowing a later price, and offline snapshots cannot create or grade
an observation. A positive 90% lower confidence bound above 0.5% is required
before 0.75%-of-equity paper positions can begin, with all concurrent shock
positions capped at 2% of equity.
Build 86 replaces that inconclusive three-hour exit with a narrower rule that
survived a corrected 2,000-market audit. Across 534,894 hourly observations,
the exact accelerating three-hour shock / 825% opposite-side longshot / 24-hour
exit rule retained positive 90% lower bounds in train, validation, chronological
holdout, and a sealed event-disjoint holdout at one-cent modeled cost. The four
partitions contained 69, 70, 104, and 56 independent events respectively. It
did not survive a two-cent stress test, so live candidate construction requires
entry friction of at most half a cent and grades the future exit at the actual
executable bid with another 0.25-cent slippage buffer. Backtest-approved paper
positions begin at 0.5% of equity and total shock exposure is capped at 3%.
Thirty positive forward events with a lower bound above 1% can raise individual
size to 1%; twelve convincingly losing events or a 2% strategy loss demote the
lane to zero capital. Strategy-1 outcomes do not contaminate Strategy-2 evidence.
Build 87 applies a harsher two-cent round-trip stress and replaces Strategy 2
with the stronger all-price-band rule: fade an accelerating move of at least
eight percentage points over three hours and exit after twelve hours. With each
return winsorized to the live learner's -100%/+200% range, its 90%
event-clustered lower bounds were +4.01% train, +4.55% validation, +3.63%
chronological holdout, and +1.08% on the sealed event-disjoint holdout, across
93, 85, 159, and 80 independent events. Entry still requires a live executable
price between 8% and 92%, no more than one cent of entry friction, and an extra
0.25-cent slippage buffer at exit. Initial paper positions are 1% of agent
equity, capped at 5% total per agent; 40 positive forward events can raise size
to 1.5%, while 20 convincingly losing events or a 3% strategy loss disable the
lane for that agent. All five aggressive agents may adopt it, but no event may
be repeated across agents and each agent can open at most two shock positions
per cycle. The service worker now uses Build 87 cache invalidation and
network-first refreshes, so an installed offline copy receives new builds when
it reconnects instead of continuing to serve stale Build 83 assets.
The exact Strategy 3 rule was then frozen and checked without retuning on the
next 1,000 eligible active markets (246,580 additional observations). Mean
return remained positive in train, validation, chronological holdout, and the
event-disjoint holdout. The independent event holdout retained a +0.37% 90%
lower bound across 61 events; train and chronological-holdout lower bounds were
slightly inconclusive at -0.17% and -0.89%. This is why Build 87 starts at 1%
rather than extrapolating the stronger first-universe result into large risk.
Resolved-market archive history was still unavailable, so the strategy remains
a demotable paper-trading lane rather than a return guarantee.
Build 77 separates the directional learner's evidence lineage from the global
strategy release. Code-history verification found the same trend/reversal
generator and 24-hour/72-hour grading policy in Strategy 51 through Strategy
59, so their forward, net-of-cost checkpoint observations remain compatible
even when an unrelated sports, maker, or bundle subsystem ships.
Older signal policies remain down-weighted and cannot satisfy the current-policy
promotion gate. This avoids repeatedly emptying a valid evidence set while
preserving the requirement for positive 24-hour and 72-hour results across
independent events. The app also requests an immediate catch-up cycle whenever
it regains focus, becomes visible, or reconnects; background browser timers can
still be suspended by the operating system when the app is closed.
The expanded event-clustered run loaded history for 498 of the 500 highest-volume
resolved markets with no fetch failures. No side, price band, category, trend,
or 1-90 day holding rule passed the required train/test confidence checks. In
particular, older YES/underdog gains
reversed in the recent test segment. The engine therefore does not install a
static settlement-direction boost from this audit.
The earlier 200-resolved-market audit found short-dated NO entries strongly
negative, but the 500-market rerun did not reproduce that loss in its newer test
segment. Strategy 54 therefore treats the result as a provisional prior instead
of a permanent ban: NO entries with 21 days or less remain observation-only until
the recent walk-forward calibration promotes their matching side and duration
cohorts. Exact numeric-range contracts are excluded from new entries because a
settlement jump can pass directly through an 18% stop; the live audit found that
this failure mode caused the largest latest-day loss.
Strategy 54 also excludes path-dependent barriers such as "reach $66,000," "hit
$90," and "dip to $62,000." These contracts can resolve abruptly as soon as the
barrier is touched, so a later hourly stop cannot reliably cap the loss. Fixed-date
level questions such as "above $66,000 on August 23" remain eligible.
Strategy 54 clusters live walk-forward observations by Polymarket event and checkpoint before
calculating confidence. Multiple six-hour snapshots and correlated outcome
markets from the same event are averaged into one effective outcome, so one
election or tournament cannot promote or demote an entire feature cohort.
Promotion requires agreement across two feature views at both 24-hour and
72-hour promotion horizons, while a mature negative six-hour view may veto risk.
Current-strategy support prevents old lineage data from authorizing a new rule.
The pending signal ledger keeps only one ungraded observation for each market and
side. When its bounded queue is full, it preserves the oldest evidence through
the 6-hour, 24-hour, and 72-hour grades and admits new signals in ranked order as space opens.
This prevents frequent cycles from evicting every signal shortly before maturity.
Paper accounts created with a password are also saved through the backend, so a
user can log in from another device and see the same paper portfolio, activity,
@@ -43,7 +700,14 @@ Pick one — all give you a public URL:
Use `.env.example` as the setup template.
- `BLOB_READ_WRITE_TOKEN` enables cross-device shared state.
- `DATABASE_URL` or `NEON_DATABASE_URL` enables Neon-backed shared state;
`BLOB_READ_WRITE_TOKEN` is the fallback provider. The backend accepts raw
Postgres URLs, quoted URLs, `DATABASE_URL=...`, and Neon dashboard
`psql 'postgresql://...'` copy formats, and prefers a syntactically valid
alias if the primary value is malformed. Shared-state reads and writes also
fail over across distinct configured database URLs when one has stale
credentials or points at an empty project. `/api/state` reports only
sanitized provider error codes when all stores are unavailable.
- `ACCOUNT_SESSION_SECRET` signs cloud paper-account sessions. If omitted, the
app falls back to the existing server secret/token, but production should use
a dedicated value.
+1 -1
View File
@@ -1,4 +1,4 @@
import { recordAuditEvent, recordUserConsent } from "./_db.js";
import { recordAuditEvent, recordUserConsent } from "../lib/db.js";
function activeVersions() {
return {
+1 -1
View File
@@ -1,4 +1,4 @@
import { recordAuditEvent } from "./_db.js";
import { recordAuditEvent } from "../lib/db.js";
async function notifyIncidentWebhook(payload) {
const url = process.env.INCIDENT_WEBHOOK_URL || process.env.SENTRY_WEBHOOK_URL;
+160 -3
View File
@@ -8,7 +8,9 @@ import {
recordRealFill,
updateRealPositionMark,
updateTradeTicketStatus,
} from "./_db.js";
} from "../lib/db.js";
import { auditLiquidity } from "../lib/liquidity-audit.js";
import nodemailer from "nodemailer";
const REQUIRED_ENV = [
["PRODUCTION_APP_URL", "Production app URL"],
@@ -173,6 +175,28 @@ function stackStatus() {
});
}
function tradeEmailStatus() {
const resend = Boolean(process.env.RESEND_API_KEY);
const webhook = Boolean(process.env.TRADE_EMAIL_WEBHOOK_URL);
const smtpMissing = ["SMTP_HOST", "SMTP_PORT", "SMTP_USER", "SMTP_PASS"].filter((key) => !process.env[key]);
const smtp = smtpMissing.length === 0;
const provider = resend ? "resend" : (smtp ? "smtp" : (webhook ? "webhook" : null));
return {
configured: Boolean(provider),
provider,
options: {
resend: { configured: resend, missing: resend ? [] : ["RESEND_API_KEY"] },
smtp: { configured: smtp, missing: smtpMissing },
webhook: { configured: webhook, missing: webhook ? [] : ["TRADE_EMAIL_WEBHOOK_URL"] },
},
missing_any_one_of: provider ? [] : [
"RESEND_API_KEY",
"or SMTP_HOST + SMTP_PORT + SMTP_USER + SMTP_PASS",
"or TRADE_EMAIL_WEBHOOK_URL",
],
};
}
function liveTradingReady() {
const requiredConfigured = providerStatus().every((x) => x.configured);
const liveFlagEnabled = process.env.LIVE_TRADING_ENABLED === "true";
@@ -214,6 +238,7 @@ function baseStatus() {
: "Live trading is locked until eligibility, payments, wallet/deposit-wallet signing, Polymarket CLOB credentials, audit storage, monitoring, and LIVE_TRADING_ENABLED=true are configured.",
providers,
provider_stack: stackStatus(),
trade_email: tradeEmailStatus(),
personal_requirements: personal,
launch_requirements: LAUNCH_REQUIREMENTS.map(([key, label]) => ({ key, label })),
webhooks: WEBHOOK_ROUTES.map(([provider, label, path]) => ({
@@ -336,6 +361,31 @@ async function sendTradeEmail({ to, events, appUrl, test }) {
return { status: 200, body: { ok: true, provider: "resend", id: data?.id || null, sent: events.length } };
}
if (process.env.SMTP_HOST && process.env.SMTP_PORT && process.env.SMTP_USER && process.env.SMTP_PASS) {
const port = Number(process.env.SMTP_PORT);
const secure = String(process.env.SMTP_SECURE || "").toLowerCase() === "true" || port === 465;
const from = process.env.TRADE_EMAIL_FROM || process.env.SMTP_FROM || process.env.SMTP_USER;
const replyTo = process.env.TRADE_EMAIL_REPLY_TO || process.env.CUSTOMER_SUPPORT_EMAIL || undefined;
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port,
secure,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
const info = await transporter.sendMail({
from,
to: email,
replyTo,
subject: payload.subject,
text: payload.text,
html: payload.html,
});
return { status: 200, body: { ok: true, provider: "smtp", id: info?.messageId || null, sent: events.length } };
}
if (process.env.TRADE_EMAIL_WEBHOOK_URL) {
const response = await fetch(process.env.TRADE_EMAIL_WEBHOOK_URL, {
method: "POST",
@@ -350,7 +400,8 @@ async function sendTradeEmail({ to, events, appUrl, test }) {
status: 501,
body: {
ok: false,
error: "Email alerts need RESEND_API_KEY plus TRADE_EMAIL_FROM, or TRADE_EMAIL_WEBHOOK_URL, in Vercel environment variables.",
error: "Email alerts need a delivery provider in Vercel: RESEND_API_KEY, or SMTP_HOST + SMTP_PORT + SMTP_USER + SMTP_PASS, or TRADE_EMAIL_WEBHOOK_URL.",
trade_email: tradeEmailStatus(),
},
};
}
@@ -600,10 +651,103 @@ function validateCapitalAction(action) {
return errors;
}
function stripTags(value) {
return String(value || "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
}
function decodeXml(value) {
return String(value || "")
.replace(/&amp;/g, "&")
.replace(/&quot;/g, "\"")
.replace(/&#39;/g, "'")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">");
}
function cleanMarketContextText(value, max = 220) {
return String(value || "").replace(/\s+/g, " ").trim().slice(0, max);
}
function contextQuery(market) {
const q = cleanMarketContextText(market?.question, 140)
.replace(/\bwill\b/ig, "")
.replace(/\?/g, "")
.replace(/\s+/g, " ")
.trim();
const cat = cleanMarketContextText(market?.category, 40);
if (cat === "Sports") return `${q} injury lineup odds`;
if (cat === "Politics") return `${q} poll election news`;
if (cat === "Crypto") return `${q} crypto market news`;
if (cat === "Economy") return `${q} economy fed inflation news`;
if (cat === "Pop Culture") return `${q} entertainment latest`;
return `${q} latest news`;
}
async function fetchNewsContext(market) {
const query = contextQuery(market);
if (!query || query.length < 8) return { source_count: 0, latest_title: "", query };
const url = `https://news.google.com/rss/search?q=${encodeURIComponent(query)}&hl=en-US&gl=US&ceid=US:en`;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 900);
try {
const response = await fetch(url, {
headers: { "User-Agent": "PolyArenaContext/1.0" },
signal: controller.signal,
});
if (!response.ok) return { source_count: 0, latest_title: "", query };
const xml = await response.text();
const items = [...xml.matchAll(/<item>([\s\S]*?)<\/item>/g)].slice(0, 5);
const titles = items.map((item) => {
const match = item[1].match(/<title><!\[CDATA\[([\s\S]*?)\]\]><\/title>|<title>([\s\S]*?)<\/title>/);
return cleanMarketContextText(decodeXml(stripTags(match ? (match[1] || match[2]) : "")), 160);
}).filter(Boolean);
return { source_count: titles.length, latest_title: titles[0] || "", query };
} catch (err) {
return { source_count: 0, latest_title: "", query, error: err?.name === "AbortError" ? "timeout" : "unavailable" };
} finally {
clearTimeout(timer);
}
}
async function handleMarketContext(body, res) {
const markets = (Array.isArray(body.markets) ? body.markets : []).slice(0, 120).map((market) => ({
id: cleanMarketContextText(market?.id, 80),
question: cleanMarketContextText(market?.question, 220),
category: cleanMarketContextText(market?.category, 60),
tags: Array.isArray(market?.tags) ? market.tags.slice(0, 6).map((tag) => cleanMarketContextText(tag, 40)) : [],
})).filter((market) => market.id && market.question);
const signals = {};
const batchSize = 24;
for (let i = 0; i < markets.length; i += batchSize) {
const batch = markets.slice(i, i + batchSize);
const results = await Promise.all(batch.map(fetchNewsContext));
results.forEach((signal, idx) => {
signals[batch[idx].id] = signal;
});
}
return res.status(200).json({ ok: true, count: Object.keys(signals).length, signals });
}
export default async function handler(req, res) {
res.setHeader("Cache-Control", "no-store");
if (req.method === "GET") {
if (req.query?.action === "liquidity") {
try {
const report = await auditLiquidity({
marketLimit: Number(req.query.limit || 60),
minHoursToEnd: Number(req.query.min_hours || 48),
});
res.setHeader("Cache-Control", "s-maxage=60, stale-while-revalidate=120");
return res.status(200).json({ ok: true, ...report });
} catch (error) {
return res.status(502).json({
ok: false,
error: "liquidity_audit_unavailable",
detail: String(error?.message || error),
});
}
}
if (req.query?.action === "tickets") {
const userId = String(req.query?.user_id || "local-readiness-user").trim();
const tickets = await listTradeTickets(userId, req.query?.limit || 50);
@@ -627,12 +771,21 @@ export default async function handler(req, res) {
if (body.action === "trade_email") {
const events = normalizeTradeEmailEvents(body.events);
const result = await sendTradeEmail({
let result;
try {
result = await sendTradeEmail({
to: body.to,
events,
appUrl: body.app_url,
test: Boolean(body.test),
});
} catch (err) {
return res.status(502).json({
ok: false,
error: err?.response || err?.message || "Email provider failed to send the alert.",
trade_email: tradeEmailStatus(),
});
}
if (result.body?.ok) {
await recordAuditEvent("TRADE_EMAIL_ALERT_SENT", {
to_domain: String(body.to || "").split("@")[1] || null,
@@ -648,6 +801,10 @@ export default async function handler(req, res) {
return handleAgentChat(body, res);
}
if (body.action === "market_context") {
return handleMarketContext(body, res);
}
if (body.action === "ticket") {
const ticket = normalizeTicket(body);
const ticketErrors = validateTicket(ticket);
+1 -1
View File
@@ -1,4 +1,4 @@
import { hasDatabase, providerEventSummary } from "./_db.js";
import { hasDatabase, providerEventSummary } from "../lib/db.js";
export default async function handler(req, res) {
res.setHeader("Cache-Control", "no-store");
+1 -1
View File
@@ -1,4 +1,4 @@
import { latestRiskProfile, recordAuditEvent, upsertRiskProfile } from "./_db.js";
import { latestRiskProfile, recordAuditEvent, upsertRiskProfile } from "../lib/db.js";
function validateProfile(profile) {
const errors = [];
+362 -26
View File
@@ -1,20 +1,229 @@
import { get, put } from "@vercel/blob";
import { get, list, put } from "@vercel/blob";
import { databaseConnectionDiagnostics, hasDatabase, readSharedAppState, writeSharedAppState } from "../lib/db.js";
const STATE_PATH = process.env.PMA_STATE_PATH || "shared/state.json";
const STATE_VERSION_PREFIX = process.env.PMA_STATE_VERSION_PREFIX || "shared/state-versions/";
const DATABASE_STATE_KEY = process.env.PMA_DATABASE_STATE_KEY || "polymarket-arena";
const GITHUB_RUNTIME_STATE_URL = process.env.PMA_GITHUB_RUNTIME_STATE_URL
|| "https://raw.githubusercontent.com/theodore-song/polymarket-analyst/runtime-state/runtime/state.json";
const GITHUB_RUNTIME_CONTENTS_URL = process.env.PMA_GITHUB_RUNTIME_CONTENTS_URL
|| "https://api.github.com/repos/theodore-song/polymarket-analyst/contents/runtime/state.json?ref=runtime-state";
const AGENTS_KEY = "pma_agents_v2";
const SUG_KEY = "pma_suggestions_v5";
const PAPER_KEY = "pma_paper_accounts_v1";
const LIVE_KEY = "pma_live_readiness_v1";
const AGENT_IDS = ["value", "momentum", "favorite", "longshot", "diversifier", "copycat", "whale1", "whale2", "whale3", "whale4"];
const LIMITS = { closed: 80, history: 160, snapshots: 240, suggestions: 120, paperHistory: 120, paperSnapshots: 120, audit: 120 };
const AGENT_IDS = ["value", "momentum", "favorite", "longshot", "diversifier", "catalyst", "reversal", "breakout", "tailalpha", "conviction"];
const LIMITS = { closed: 80, history: 160, snapshots: 240, suggestions: 900, paperHistory: 120, paperSnapshots: 120, audit: 120 };
const SIGNAL_LEDGER_LIMITS = { pending: 600, outcomes: 1000 };
const RUNTIME_ALLOWED_KEYS = new Set([AGENTS_KEY, SUG_KEY]);
function withBlobAuth(options = {}) {
const token = process.env.BLOB_READ_WRITE_TOKEN;
return token ? { ...options, token } : options;
}
export function providerErrorCode(error) {
if (!error) return "not_configured_or_not_attempted";
const nested = error.sourceError || error.cause;
const code = String(error.code || nested && (nested.code || nested.cause && nested.cause.code) || "").toUpperCase();
const message = `${String(error.message || error)} ${String(nested && nested.message || "")}`.toLowerCase();
if (/http status\s+402\b/.test(message) || /\b402 payment required\b/.test(message)) return "provider_payment_required";
if (code === "28P01" || code === "28000") return "authorization_failed";
if (code === "3D000") return "database_not_found";
if (code.startsWith("08")) return "connection_failed";
if (code === "ENOTFOUND" || code === "EAI_AGAIN") return "dns_failed";
if (code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT") return "timeout";
if (message.includes("invalid") && (message.includes("url") || message.includes("connection string"))) return "invalid_connection_string";
if (message.includes("password authentication") || message.includes("unauthorized") || /\b(?:401|403)\b/.test(message)) return "authorization_failed";
if (message.includes("enotfound") || message.includes("getaddrinfo") || message.includes("dns")) return "dns_failed";
if (message.includes("timeout") || message.includes("timed out")) return "timeout";
if (message.includes("fetch failed") || message.includes("connection") || message.includes("connect")) return "connection_failed";
return "unavailable";
}
function providerErrorMetadata(error) {
const nested = error && (error.sourceError || error.cause);
const name = String(error && error.name || "Error");
const nestedName = String(nested && nested.name || "none");
const code = String(error && (error.code || nested && (nested.code || nested.cause && nested.cause.code)) || "").toUpperCase();
const httpStatus = String(error && error.message || "").match(/HTTP status\s+(\d{3})/i);
return {
status: providerErrorCode(error),
error_class: /^[A-Za-z][A-Za-z0-9_]{0,39}$/.test(name) ? name : "Error",
source_error_class: /^[A-Za-z][A-Za-z0-9_]{0,39}$/.test(nestedName) ? nestedName : "none",
error_code: /^[A-Z0-9_]{1,32}$/.test(code) ? code : "none",
http_status: httpStatus ? Number(httpStatus[1]) : null,
};
}
async function readJsonBlob() {
const blob = await get(STATE_PATH, { access: "private" });
let databaseAvailable = false;
let databaseError = null;
if (hasDatabase()) {
try {
const row = await readSharedAppState(DATABASE_STATE_KEY);
databaseAvailable = true;
if (row && row.payload) return row.payload;
} catch (err) {
databaseError = err;
databaseAvailable = false;
}
}
let primaryError = null;
try {
const primary = await readBlobJson(STATE_PATH);
if (primary) return primary;
} catch (err) {
primaryError = err;
}
try {
const latest = await latestVersionedStateBlob();
if (latest) return latest;
} catch (err) {
if (!primaryError) primaryError = err;
}
let githubError = null;
try {
const runtime = await readGithubRuntimeState();
if (runtime) return runtime;
} catch (err) {
githubError = err;
}
if ((primaryError || githubError) && !databaseAvailable) {
const error = new Error("Shared state providers are unavailable");
error.providers = {
database: hasDatabase()
? { ...providerErrorMetadata(databaseError), ...databaseConnectionDiagnostics() }
: { status: "not_configured", candidates: 0, urls: [] },
blob: providerErrorMetadata(primaryError),
github_runtime: providerErrorMetadata(githubError),
};
throw error;
}
return null;
}
async function readGithubRuntimeState() {
if (!GITHUB_RUNTIME_CONTENTS_URL && !GITHUB_RUNTIME_STATE_URL) return null;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
try {
if (GITHUB_RUNTIME_CONTENTS_URL) {
const contentsUrl = new URL(GITHUB_RUNTIME_CONTENTS_URL);
contentsUrl.searchParams.set("runtime", `${Date.now()}-${Math.random().toString(36).slice(2)}`);
const contentsResponse = await fetch(contentsUrl, {
cache: "no-store",
headers: { Accept: "application/vnd.github+json", "Cache-Control": "no-cache", "User-Agent": "polymarket-arena-state" },
signal: controller.signal,
});
if (contentsResponse.ok) {
const file = await contentsResponse.json();
if (!file || typeof file.content !== "string") throw new Error("GitHub runtime contents response is missing file data");
const decoded = Buffer.from(file.content.replace(/\s/g, ""), "base64").toString("utf8");
return sanitizeGithubRuntimeState(JSON.parse(decoded));
}
if (contentsResponse.status !== 403 && contentsResponse.status !== 429 && contentsResponse.status !== 404) {
throw new Error(`GitHub runtime contents returned HTTP ${contentsResponse.status}`);
}
}
if (!GITHUB_RUNTIME_STATE_URL) return null;
const rawUrl = new URL(GITHUB_RUNTIME_STATE_URL);
rawUrl.searchParams.set("runtime", `${Date.now()}-${Math.random().toString(36).slice(2)}`);
const rawResponse = await fetch(rawUrl, {
cache: "no-store",
headers: { Accept: "application/json", "Cache-Control": "no-cache" },
signal: controller.signal,
});
if (rawResponse.status === 404) return null;
if (!rawResponse.ok) throw new Error(`GitHub runtime state returned HTTP ${rawResponse.status}`);
return sanitizeGithubRuntimeState(await rawResponse.json());
} finally {
clearTimeout(timeout);
}
}
async function persistState(state) {
let databaseSaved = false;
let databaseError = null;
if (hasDatabase()) {
try {
await writeSharedAppState(DATABASE_STATE_KEY, state);
databaseSaved = true;
} catch (err) {
databaseError = err;
}
}
let blobSaved = false;
let blobError = null;
try {
await put(STATE_PATH, JSON.stringify(state), withBlobAuth({
access: "private",
allowOverwrite: true,
contentType: "application/json",
cacheControlMaxAge: 0,
}));
blobSaved = true;
} catch (err) {
blobError = err;
}
if (!databaseSaved && !blobSaved) throw databaseError || blobError || new Error("No state provider is available");
return { databaseSaved, blobSaved };
}
async function readBlobJson(pathname) {
const blob = await get(pathname, withBlobAuth({
access: "private",
headers: { "cache-control": "no-cache" },
}));
if (!blob || blob.statusCode !== 200 || !blob.stream) return null;
const text = await new Response(blob.stream).text();
return text ? JSON.parse(text) : null;
}
async function latestVersionedStateBlob() {
let cursor;
let newest = null;
do {
const page = await list(withBlobAuth({ prefix: STATE_VERSION_PREFIX, limit: 1000, cursor }));
for (const blob of page.blobs || []) {
if (!newest || new Date(blob.uploadedAt).getTime() > new Date(newest.uploadedAt).getTime()) newest = blob;
}
cursor = page.cursor;
} while (cursor);
if (!newest) return null;
return readBlobJson(newest.pathname);
}
export function cycleVersion(cycle = "") {
const m = String(cycle).match(/\|[vs](\d+)$/);
return m ? Number(m[1]) : 0;
}
function openPositionCount(st) {
if (!st || !st.agents) return 0;
return AGENT_IDS.reduce((sum, id) => sum + (Array.isArray(st.agents[id]?.positions) ? st.agents[id].positions.length : 0), 0);
}
function shouldRejectStaleAgentWrite(currentAgents, incomingAgents) {
if (!currentAgents || !incomingAgents) return false;
const currentCycle = currentAgents.last_cycle_hour || "";
const incomingCycle = incomingAgents.last_cycle_hour || "";
if (!currentCycle || !incomingCycle) return false;
const currentVersion = cycleVersion(currentCycle);
const incomingVersion = cycleVersion(incomingCycle);
if (incomingVersion < currentVersion) return true;
if (incomingVersion === currentVersion && incomingCycle < currentCycle) return true;
const currentOpen = openPositionCount(currentAgents);
const incomingOpen = openPositionCount(incomingAgents);
return incomingVersion === currentVersion && incomingCycle === currentCycle && currentOpen > 0 && incomingOpen === 0;
}
function agentStateFromItems(items) {
if (!items || !items[AGENTS_KEY]) return null;
try {
@@ -24,6 +233,15 @@ function agentStateFromItems(items) {
}
}
function suggestionStateFromItems(items) {
if (!items || !items[SUG_KEY]) return null;
try {
return JSON.parse(items[SUG_KEY]);
} catch {
return null;
}
}
function compactPortfolio(p) {
if (!p || typeof p !== "object") return p;
const out = { ...p };
@@ -35,24 +253,62 @@ function compactPortfolio(p) {
return out;
}
function compactAgentState(st) {
export function compactAgentState(st) {
if (!st || typeof st !== "object") return st;
const out = { ...st, agents: {} };
for (const id of AGENT_IDS) {
out.agents[id] = compactPortfolio(st.agents && st.agents[id]);
}
out.whales = st.whales || {};
if (st.signal_ledger && typeof st.signal_ledger === "object") {
out.signal_ledger = {
pending: Array.isArray(st.signal_ledger.pending) ? st.signal_ledger.pending.slice(0, SIGNAL_LEDGER_LIMITS.pending) : [],
outcomes: Array.isArray(st.signal_ledger.outcomes) ? st.signal_ledger.outcomes.slice(-SIGNAL_LEDGER_LIMITS.outcomes) : [],
expired_ungraded: Number(st.signal_ledger.expired_ungraded || 0),
};
}
delete out.whales;
delete out.copycatLeader;
return out;
}
function compactSuggestion(s) {
export function compactSuggestion(s) {
if (!s || typeof s !== "object") return s;
return {
market_id: s.market_id, question: s.question, event: s.event, url: s.url, category: s.category,
market_id: s.market_id, question: s.question, event: s.event, event_key: s.event_key, url: s.url, category: s.category,
clob_yes: s.clob_yes, clob_no: s.clob_no, yes_price: s.yes_price, no_price: s.no_price,
fair_value: s.fair_value, edge: s.edge, side: s.side, entry_price: s.entry_price,
conviction: s.conviction, volume: s.volume, volume_24hr: s.volume_24hr,
days_to_resolution: s.days_to_resolution, drivers: s.drivers, rationale: s.rationale,
fair_value: s.fair_value, edge: s.edge, side: s.side, market_price: s.market_price, entry_price: s.entry_price,
net_edge: s.net_edge, friction: s.friction, chase_penalty: s.chase_penalty,
evidence_score: s.evidence_score, evidence_source_count: s.evidence_source_count, quality: s.quality,
conviction: s.conviction, volume: s.volume, volume_24hr: s.volume_24hr, liquidity: s.liquidity,
fees_enabled: s.fees_enabled, fee_schedule: s.fee_schedule,
spread: s.spread, price_change_1h: s.price_change_1h, price_change_1d: s.price_change_1d, price_change_1w: s.price_change_1w,
momentum_strength: s.momentum_strength, signal_strength: s.signal_strength, signal_confidence: s.signal_confidence,
signal_type: s.signal_type, trade_ready: s.trade_ready, entry_candidate: s.entry_candidate,
audited_observation_only: s.audited_observation_only, adaptive_promotion: s.adaptive_promotion,
adaptive_probation: s.adaptive_probation, probation_exit_hours: s.probation_exit_hours,
promoted_for_agents: s.promoted_for_agents,
watch_only: s.watch_only, jump_risk: s.jump_risk, requires_live: s.requires_live,
bundle_id: s.bundle_id, bundle_event_id: s.bundle_event_id, bundle_side: s.bundle_side, bundle_logic: s.bundle_logic,
bundle_immediate_merge: s.bundle_immediate_merge, bundle_conversion_candidate: s.bundle_conversion_candidate,
bundle_conversion_terms_verified: s.bundle_conversion_terms_verified, bundle_immediate_convert: s.bundle_immediate_convert,
neg_risk_market_id: s.neg_risk_market_id, neg_risk_fee_bips: s.neg_risk_fee_bips, neg_risk_metadata_fee_bips: s.neg_risk_metadata_fee_bips,
neg_risk_question_count: s.neg_risk_question_count,
conversion_verification_status: s.conversion_verification_status,
bundle_cost_per_unit: s.bundle_cost_per_unit, bundle_payout_per_unit: s.bundle_payout_per_unit,
bundle_settlement_payout_per_unit: s.bundle_settlement_payout_per_unit,
bundle_net_profit_per_unit: s.bundle_net_profit_per_unit,
bundle_capital_efficiency: s.bundle_capital_efficiency, bundle_min_daily_return: s.bundle_min_daily_return,
bundle_legs: s.bundle_legs, depth_verified: s.depth_verified, fees_verified: s.fees_verified,
fee_model: s.fee_model, execution_model: s.execution_model, execution_units: s.execution_units,
execution_notional: s.execution_notional, execution_total_profit: s.execution_total_profit, verification_status: s.verification_status,
days_to_resolution: s.days_to_resolution, end_date: s.end_date, game_start: s.game_start,
hours_to_start: s.hours_to_start, target_horizon_days: s.target_horizon_days,
resolution_week_strategy_version: s.resolution_week_strategy_version,
sports_contest_strategy_version: s.sports_contest_strategy_version, entry_fee: s.entry_fee,
pilot_prior: s.pilot_prior,
shock_move_1h: s.shock_move_1h, shock_prior_move_1h: s.shock_prior_move_1h, shock_move_3h: s.shock_move_3h,
shock_observed_at: s.shock_observed_at, shock_strategy_version: s.shock_strategy_version,
drivers: s.drivers, rationale: s.rationale,
};
}
@@ -89,6 +345,33 @@ function compactItems(items) {
return out;
}
export function sanitizeGithubRuntimeState(payload) {
if (!payload || typeof payload !== "object" || !payload.items || typeof payload.items !== "object") {
throw new Error("Invalid GitHub runtime state");
}
const items = {};
for (const key of RUNTIME_ALLOWED_KEYS) {
if (typeof payload.items[key] === "string") items[key] = payload.items[key];
}
const agents = agentStateFromItems(items);
if (!agents || !agents.agents) throw new Error("GitHub runtime state is missing agent portfolios");
const suggestions = suggestionStateFromItems(items);
items[AGENTS_KEY] = JSON.stringify(compactAgentState(agents));
if (suggestions) items[SUG_KEY] = JSON.stringify(compactSuggestions(suggestions));
return {
version: 1,
schema_version: Number(payload.schema_version || 1),
build_version: Number(payload.build_version || agents.engine_version || 0),
strategy_version: Number(payload.strategy_version || agents.strategy_version || 0),
generated_at: payload.generated_at || payload.updated_at || null,
updated_at: payload.updated_at || payload.generated_at || null,
last_cycle_hour: payload.last_cycle_hour || agents.last_cycle_hour || null,
source: "github-actions",
read_only: true,
items,
};
}
function conflictResponse(res, error, current) {
return res.status(409).json({ ok: false, error, state: current });
}
@@ -96,46 +379,99 @@ function conflictResponse(res, error, current) {
export default async function handler(req, res) {
res.setHeader("Cache-Control", "no-store");
try {
if (!process.env.BLOB_READ_WRITE_TOKEN && !process.env.VERCEL_OIDC_TOKEN) {
return res.status(503).json({ ok: false, error: "Cloud state is not configured" });
}
if (req.method === "GET") {
try {
const state = await readJsonBlob();
if (state && state.items) state.items = compactItems(state.items);
return res.status(200).json({ ok: true, state });
return res.status(200).json({
ok: true,
state,
degraded: false,
read_only: Boolean(state && state.read_only),
source: state && state.source || "managed-storage",
});
} catch (err) {
// A storage outage must not prevent the installed app from using its local paper state.
return res.status(200).json({
ok: true,
state: null,
degraded: true,
error: err && err.message ? err.message : "Cloud state provider unavailable",
providers: err && err.providers ? err.providers : undefined,
});
}
}
if (req.method === "POST") {
if (!hasDatabase() && !process.env.BLOB_READ_WRITE_TOKEN && !process.env.VERCEL_OIDC_TOKEN) {
return res.status(503).json({
ok: false,
degraded: true,
read_only: true,
source: "github-actions",
error: "Managed cloud state is not configured; autonomous shared state is read-only",
});
}
const body = typeof req.body === "string" ? JSON.parse(req.body || "{}") : (req.body || {});
if (!body || typeof body !== "object" || !body.items || typeof body.items !== "object") {
return res.status(400).json({ ok: false, error: "Invalid state payload" });
}
const current = await readJsonBlob();
let current;
try {
current = await readJsonBlob();
} catch (err) {
return res.status(503).json({
ok: false,
degraded: true,
retryable: true,
error: err && err.message ? err.message : "Cloud state provider unavailable",
providers: err && err.providers ? err.providers : undefined,
});
}
const incomingItems = { ...body.items };
const currentAgents = agentStateFromItems(current && current.items);
const incomingAgents = agentStateFromItems(body.items);
let incomingAgents = agentStateFromItems(incomingItems);
const staleAgentWrite = shouldRejectStaleAgentWrite(currentAgents, incomingAgents);
if (body.force && staleAgentWrite && current?.items?.[AGENTS_KEY]) {
incomingItems[AGENTS_KEY] = current.items[AGENTS_KEY];
incomingAgents = currentAgents;
}
const currentSuggestions = suggestionStateFromItems(current && current.items);
const incomingSuggestions = suggestionStateFromItems(incomingItems);
if (currentSuggestions && incomingSuggestions
&& Number(incomingSuggestions.engine_version || 0) < Number(currentSuggestions.engine_version || 0)) {
incomingItems[SUG_KEY] = current.items[SUG_KEY];
}
if (!body.force && currentAgents) {
const currentCycle = currentAgents.last_cycle_hour || "";
const incomingCycle = incomingAgents && incomingAgents.last_cycle_hour ? incomingAgents.last_cycle_hour : "";
if (currentCycle && (!incomingAgents || !incomingCycle)) {
return conflictResponse(res, "Cloud already has a cycle result; refusing unscheduled local state", current);
}
if (currentCycle && incomingCycle && incomingCycle < currentCycle) {
return conflictResponse(res, "Incoming state is older than the shared cloud result", current);
}
const sameCycle = currentCycle && currentCycle === incomingCycle;
const differentRun = currentAgents.last_run && incomingAgents.last_run && currentAgents.last_run !== incomingAgents.last_run;
if (sameCycle && differentRun) {
return conflictResponse(res, "This cycle already has a cloud result", current);
}
}
const state = { version: 1, updated_at: new Date().toISOString(), items: compactItems(body.items) };
await put(STATE_PATH, JSON.stringify(state), {
if (!body.force && staleAgentWrite) {
return conflictResponse(res, "Incoming state would replace newer active positions with stale cash-only data", current);
}
const state = { version: 1, updated_at: new Date().toISOString(), items: compactItems(incomingItems) };
const saved = await persistState(state);
if (saved.blobSaved && process.env.PMA_ENABLE_STATE_HISTORY === "true") {
const versionedPath = `${STATE_VERSION_PREFIX}${Date.now()}-${Math.random().toString(36).slice(2)}.json`;
try {
await put(versionedPath, JSON.stringify(state), withBlobAuth({
access: "private",
allowOverwrite: true,
allowOverwrite: false,
contentType: "application/json",
cacheControlMaxAge: 60,
});
cacheControlMaxAge: 0,
}));
} catch {
// The shared state is authoritative; backup retention must not block a cycle.
}
}
return res.status(200).json({ ok: true, state });
}
+2 -2
View File
@@ -1,5 +1,5 @@
import { recordProviderEvent } from "../_db.js";
import { firstHeader, parseJson, rawBody, safeHeaders, sendMethodNotAllowed, verifyHmacSignature } from "../_webhook.js";
import { recordProviderEvent } from "../../lib/db.js";
import { firstHeader, parseJson, rawBody, safeHeaders, sendMethodNotAllowed, verifyHmacSignature } from "../../lib/webhook.js";
const SIGNATURE_HEADERS = [
"circle-signature",
+2 -2
View File
@@ -1,6 +1,6 @@
import { Webhook } from "svix";
import { recordProviderEvent } from "../_db.js";
import { rawBody, parseJson, safeHeaders, sendMethodNotAllowed } from "../_webhook.js";
import { recordProviderEvent } from "../../lib/db.js";
import { rawBody, parseJson, safeHeaders, sendMethodNotAllowed } from "../../lib/webhook.js";
function eventType(payload) {
return payload.type || "clerk.webhook";
+2 -2
View File
@@ -1,5 +1,5 @@
import { recordProviderEvent } from "../_db.js";
import { firstHeader, parseJson, rawBody, safeHeaders, sendMethodNotAllowed, verifyHmacSignature } from "../_webhook.js";
import { recordProviderEvent } from "../../lib/db.js";
import { firstHeader, parseJson, rawBody, safeHeaders, sendMethodNotAllowed, verifyHmacSignature } from "../../lib/webhook.js";
const SIGNATURE_HEADERS = [
"x-hmac-signature",
+11
View File
@@ -0,0 +1,11 @@
const INTERVAL_MS = 60000;
postMessage({ type: "ready", at: Date.now() });
self.onmessage = event => {
if(event.data && event.data.type === "ping") postMessage({ type: "ready", at: Date.now() });
};
setInterval(() => {
postMessage({ type: "cycle", at: Date.now() });
}, INTERVAL_MS);
+5644 -566
View File
File diff suppressed because it is too large Load Diff
+106 -5
View File
@@ -1,10 +1,54 @@
import { neon } from "@neondatabase/serverless";
let sqlClient;
const sqlClients = new Map();
let schemaReady;
const sharedStateSchemasReady = new Set();
export function normalizeDatabaseUrl(raw) {
let value = String(raw || "").trim();
const assignment = value.match(/^(?:DATABASE_URL|NEON_DATABASE_URL)\s*=\s*([\s\S]+)$/i);
if (assignment) value = assignment[1].trim();
const psql = value.match(/^psql\s+(['"])([\s\S]+)\1\s*$/i);
if (psql) value = psql[2].trim();
else if ((value.startsWith("'") && value.endsWith("'")) || (value.startsWith('"') && value.endsWith('"'))) {
value = value.slice(1, -1).trim();
}
return value;
}
export function databaseUrls() {
const candidates = [process.env.DATABASE_URL, process.env.NEON_DATABASE_URL]
.map(normalizeDatabaseUrl).filter(Boolean);
const valid = candidates.filter(value => /^postgres(?:ql)?:\/\//i.test(value));
return [...new Set(valid.length ? valid : candidates)];
}
export function databaseUrl() {
return process.env.DATABASE_URL || process.env.NEON_DATABASE_URL || "";
return databaseUrls()[0] || "";
}
export function databaseConnectionDiagnostics() {
const candidates = databaseUrls();
return {
candidates: candidates.length,
urls: candidates.map(value => {
try {
const parsed = new URL(value);
return {
valid_postgres_url: /^postgres(?:ql)?:$/i.test(parsed.protocol),
neon_host: /(?:^|\.)neon\.tech$/i.test(parsed.hostname),
pooled_host: /-pooler(?:\.|$)/i.test(parsed.hostname),
has_username: Boolean(parsed.username),
has_password: Boolean(parsed.password),
has_database_name: parsed.pathname.length > 1,
sslmode: parsed.searchParams.get("sslmode") || "missing",
channel_binding: parsed.searchParams.get("channel_binding") || "missing",
};
} catch {
return { valid_postgres_url: false };
}
}),
};
}
export function hasDatabase() {
@@ -12,12 +56,69 @@ export function hasDatabase() {
}
export function sql() {
if (!sqlClient) {
const url = databaseUrl();
if (!url) throw new Error("Database is not configured");
sqlClient = neon(url);
return sqlForUrl(url);
}
return sqlClient;
function sqlForUrl(url) {
if (!sqlClients.has(url)) sqlClients.set(url, neon(url));
return sqlClients.get(url);
}
async function ensureSharedStateSchema(db, url) {
if (sharedStateSchemasReady.has(url)) return;
await db`
create table if not exists shared_app_state (
state_key text primary key,
payload jsonb not null,
updated_at timestamptz not null default now()
)
`;
sharedStateSchemasReady.add(url);
}
async function withSharedStateDatabase(operation, acceptResult = () => true) {
let lastError, lastResult, hadSuccess = false;
for (const url of databaseUrls()) {
try {
const db = sqlForUrl(url);
await ensureSharedStateSchema(db, url);
lastResult = await operation(db);
hadSuccess = true;
if (acceptResult(lastResult)) return lastResult;
} catch (error) {
lastError = error;
}
}
if (hadSuccess) return lastResult;
throw lastError || new Error("Database is not configured");
}
export async function readSharedAppState(stateKey) {
return withSharedStateDatabase(async db => {
const rows = await db`
select payload, updated_at
from shared_app_state
where state_key = ${stateKey}
limit 1
`;
return rows[0] || null;
}, result => result !== null);
}
export async function writeSharedAppState(stateKey, payload) {
return withSharedStateDatabase(async db => {
const rows = await db`
insert into shared_app_state (state_key, payload, updated_at)
values (${stateKey}, ${JSON.stringify(payload)}::jsonb, now())
on conflict (state_key) do update set
payload = excluded.payload,
updated_at = now()
returning updated_at
`;
return rows[0] || null;
});
}
export async function ensureSchema() {
+218
View File
@@ -0,0 +1,218 @@
const CLOB = "https://clob.polymarket.com";
const BOOK_BATCH_SIZE = 200;
async function fetchJson(url, options = {}, attempts = 3) {
let lastError;
for (let attempt = 0; attempt < attempts; attempt++) {
try {
const response = await fetch(url, {
...options,
signal: AbortSignal.timeout(30000),
headers: { accept: "application/json", ...(options.headers || {}) },
});
if (response.ok) return response.json();
lastError = new Error(`${response.status} ${response.statusText}`);
if (response.status !== 429 && response.status < 500) break;
} catch (error) {
lastError = error;
}
await new Promise((resolve) => setTimeout(resolve, 600 * (attempt + 1)));
}
throw lastError || new Error("request failed");
}
function activeRate(market) {
return (market.rewards_config || []).reduce((sum, row) => sum + Number(row.rate_per_day || 0), 0);
}
function categoryFor(question) {
const text = String(question || "").toLowerCase();
if (/election|president|senate|governor|mayor|minister|parliament|ceasefire|war|trump|congress/.test(text)) return "Politics";
if (/bitcoin|ethereum|crypto|btc|eth|solana|xrp|doge|token/.test(text)) return "Crypto";
if (/nba|nfl|mlb|nhl|soccer|football|tennis|ufc|boxing|tournament|championship|match|game/.test(text)) return "Sports";
if (/gdp|inflation|interest rate|fed|stock|nasdaq|s&p|oil|gold|gross margin|unemployment/.test(text)) return "Economy";
if (/movie|album|music|tv|views|award|ai lab|model release|code arena/.test(text)) return "Pop Culture";
return "Other";
}
async function fetchRewardMarkets(marketLimit, minHoursToEnd) {
const params = new URLSearchParams({ order_by: "rate_per_day", position: "DESC", page_size: "500" });
const response = await fetchJson(`${CLOB}/rewards/markets/multi?${params}`);
const cutoff = Date.now() + minHoursToEnd * 60 * 60 * 1000;
return (response.data || [])
.filter((market) => {
const tokens = market.tokens || [];
const labels = tokens.map((token) => String(token.outcome || "").trim().toLowerCase());
const endTime = Date.parse(market.end_date || "");
return labels[0] === "yes" && labels[1] === "no" && tokens.every((token) => token.token_id)
&& activeRate(market) >= 1 && Number(market.rewards_min_size) > 0
&& Number(market.rewards_max_spread) > 0 && Number.isFinite(endTime) && endTime >= cutoff;
})
.slice(0, marketLimit);
}
async function fetchBooks(tokenIds) {
const result = new Map();
for (let index = 0; index < tokenIds.length; index += BOOK_BATCH_SIZE) {
const batch = tokenIds.slice(index, index + BOOK_BATCH_SIZE);
const books = await fetchJson(`${CLOB}/books`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(batch.map((token_id) => ({ token_id }))),
});
for (const book of books || []) result.set(String(book.asset_id), book);
}
return result;
}
function levels(raw, side) {
return (raw || [])
.map((row) => ({ price: Number(row.price), size: Number(row.size) }))
.filter((row) => Number.isFinite(row.price) && Number.isFinite(row.size) && row.price > 0 && row.price < 1 && row.size > 0)
.sort((a, b) => side === "bid" ? b.price - a.price : a.price - b.price);
}
function adjustedLevel(rows, minimumSize) {
let cumulative = 0;
for (const row of rows) {
cumulative += row.size;
if (cumulative + 1e-9 >= minimumSize) return row.price;
}
return null;
}
function midpoint(book, minimumSize, ownBid) {
const bids = levels(book.bids, "bid");
if (ownBid) bids.push(ownBid);
bids.sort((a, b) => b.price - a.price);
const asks = levels(book.asks, "ask");
const bid = adjustedLevel(bids, minimumSize);
const ask = adjustedLevel(asks, minimumSize);
return Number.isFinite(bid) && Number.isFinite(ask) && bid < ask ? (bid + ask) / 2 : null;
}
function utility(maxDistance, distance) {
if (!(maxDistance > 0) || distance > maxDistance) return 0;
return ((maxDistance - distance) / maxDistance) ** 2;
}
function publicUtility(book, mid, maxDistance) {
return [...levels(book.bids, "bid"), ...levels(book.asks, "ask")]
.reduce((sum, row) => sum + utility(maxDistance, Math.abs(row.price - mid)) * row.size, 0);
}
function proposedBid(book) {
const bids = levels(book.bids, "bid");
const asks = levels(book.asks, "ask");
if (!bids.length || !asks.length) return null;
const tick = Number(book.tick_size || 0.01);
const bestBid = bids[0].price;
const bestAsk = asks[0].price;
const improved = bestAsk - bestBid >= tick * 3 ? bestBid + tick : bestBid;
return { price: Math.min(bestAsk - tick, improved), bestBid, bestAsk, tick };
}
function evaluateMarket(market, books) {
const [yesToken, noToken] = market.tokens;
const yesBook = books.get(String(yesToken.token_id));
const noBook = books.get(String(noToken.token_id));
if (!yesBook || !noBook) return null;
const yes = proposedBid(yesBook), no = proposedBid(noBook);
if (!yes || !no || yes.price <= 0 || no.price <= 0) return null;
const size = Math.max(Number(market.rewards_min_size), Number(yesBook.min_order_size || 0), Number(noBook.min_order_size || 0));
const maxDistance = Number(market.rewards_max_spread) / 100;
const yesMid = midpoint(yesBook, size, { price: yes.price, size });
const noMid = midpoint(noBook, size, { price: no.price, size });
if (!Number.isFinite(yesMid) || !Number.isFinite(noMid)) return null;
const ownQMin = Math.min(
utility(maxDistance, Math.abs(yes.price - yesMid)) * size,
utility(maxDistance, Math.abs(no.price - noMid)) * size,
);
if (!(ownQMin > 0)) return null;
const competitorUpperScore = publicUtility(yesBook, yesMid, maxDistance)
+ publicUtility(noBook, noMid, maxDistance);
const conservativeShareAtB1 = ownQMin / (ownQMin + competitorUpperScore);
const dailyRate = activeRate(market);
const estimatedDailyRewardFloor = dailyRate * conservativeShareAtB1;
const pairedCost = yes.price + no.price;
const capital = pairedCost * size;
const pairedFillProfit = (1 - pairedCost) * size;
const maximumOneLegLoss = Math.max(yes.price, no.price) * size;
const hoursToEnd = (Date.parse(market.end_date) - Date.now()) / 3600000;
const payoutEligible = estimatedDailyRewardFloor >= 1;
const shadowQualified = payoutEligible && pairedCost <= 1 && conservativeShareAtB1 >= 0.0025
&& estimatedDailyRewardFloor / Math.max(capital, 1) >= 0.001;
return {
audit_version: 1,
market_id: String(market.market_id),
condition_id: market.condition_id,
event_key: String(market.event_slug || market.market_slug || market.market_id),
question: market.question,
event: market.event_slug || "",
url: `https://polymarket.com/event/${market.event_slug || market.market_slug}`,
category: categoryFor(market.question),
clob_token_ids: [String(yesToken.token_id), String(noToken.token_id)],
reward_daily_rate: dailyRate,
reward_min_size: size,
reward_max_spread: Number(market.rewards_max_spread),
hours_to_end: hoursToEnd,
yes_quote: yes.price,
no_quote: no.price,
yes_adjusted_mid: yesMid,
no_adjusted_mid: noMid,
paired_cost: pairedCost,
required_capital: capital,
locked_profit: pairedFillProfit,
maximum_one_leg_loss: maximumOneLegLoss,
own_q_min: ownQMin,
competitor_upper_score: competitorUpperScore,
reward_share_floor: conservativeShareAtB1,
estimated_reward_floor_daily: estimatedDailyRewardFloor,
payout_eligible: payoutEligible,
shadow_qualified: shadowQualified,
spread: Math.max(yes.bestAsk - yes.bestBid, no.bestAsk - no.bestBid),
tick_size: Math.min(yes.tick, no.tick),
};
}
function roundRow(row) {
const rounded = { ...row };
for (const key of ["reward_daily_rate", "reward_min_size", "reward_max_spread", "hours_to_end", "yes_quote", "no_quote",
"yes_adjusted_mid", "no_adjusted_mid", "paired_cost", "required_capital", "locked_profit", "maximum_one_leg_loss",
"own_q_min", "competitor_upper_score", "reward_share_floor", "estimated_reward_floor_daily", "spread", "tick_size"]) {
rounded[key] = +Number(row[key]).toFixed(6);
}
return rounded;
}
export async function auditLiquidity({ marketLimit = 100, minHoursToEnd = 48 } = {}) {
const safeLimit = Math.max(10, Math.min(250, Number(marketLimit) || 100));
const safeHours = Math.max(1, Number(minHoursToEnd) || 48);
const markets = await fetchRewardMarkets(safeLimit, safeHours);
const books = await fetchBooks(markets.flatMap((market) => market.tokens.map((token) => String(token.token_id))));
const evaluated = markets.map((market) => evaluateMarket(market, books)).filter(Boolean);
const qualified = evaluated.filter((row) => row.shadow_qualified)
.sort((a, b) => (b.estimated_reward_floor_daily / Math.max(b.required_capital, 1))
- (a.estimated_reward_floor_daily / Math.max(a.required_capital, 1))
|| b.estimated_reward_floor_daily - a.estimated_reward_floor_daily);
return {
generated_at: new Date().toISOString(),
requested_markets: safeLimit,
minimum_hours_to_end: safeHours,
reward_markets: markets.length,
complete_book_pairs: evaluated.length,
shadow_qualified: qualified.length,
methodology: {
execution: "two resting BUY quotes, one on each complementary outcome; no fill or reward is credited",
midpoint: "minimum-qualifying-size adjusted midpoint recomputed with the proposed quote",
competition: "sum of public order utility upper-bounds competitors' aggregate Q_min at a uniform b=1",
reward_estimate: "single-snapshot lower-share estimate, not a guaranteed payout; $1 daily payout minimum enforced",
unresolved_risk: "in-game multipliers, future competition, queue priority, fills, and adverse selection require shadow evidence",
},
candidates: qualified.slice(0, 50).map(roundRow),
};
}
View File
+64 -3
View File
@@ -4,10 +4,15 @@
"requires": true,
"packages": {
"": {
"name": "polymarket-analyst",
"dependencies": {
"@neondatabase/serverless": "^1.1.0",
"@vercel/blob": "2.5.0",
"nodemailer": "^9.0.3",
"svix": "^1.96.1"
},
"devDependencies": {
"playwright": "^1.62.1"
}
},
"node_modules/@neondatabase/serverless": {
@@ -130,6 +135,21 @@
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
"license": "Unlicense"
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@@ -222,6 +242,15 @@
"node": ">=6"
}
},
"node_modules/nodemailer": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.3.tgz",
"integrity": "sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/npm-run-path": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
@@ -267,6 +296,38 @@
"node": ">=8"
}
},
"node_modules/playwright": {
"version": "1.62.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz",
"integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.62.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=20"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.62.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
"integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/retry": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
@@ -344,9 +405,9 @@
}
},
"node_modules/undici": {
"version": "6.27.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz",
"integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==",
"version": "6.28.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz",
"integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==",
"license": "MIT",
"engines": {
"node": ">=18.17"
+22
View File
@@ -1,7 +1,29 @@
{
"type": "module",
"scripts": {
"evaluate:neg-risk": "node scripts/evaluate-neg-risk.mjs",
"evaluate:dominance": "node scripts/evaluate-dominance.mjs",
"evaluate:adaptive": "node scripts/evaluate-adaptive.mjs",
"evaluate:liquidity": "node scripts/evaluate-liquidity.mjs",
"evaluate:maker": "node scripts/evaluate-maker.mjs",
"evaluate:reward-maker": "node scripts/evaluate-reward-assisted-maker.mjs",
"evaluate:signals": "node scripts/evaluate-signals.mjs",
"evaluate:shocks": "node scripts/evaluate-shock-regimes.mjs",
"evaluate:settlements": "node scripts/evaluate-settlements.mjs",
"evaluate:settlement-calibration": "node scripts/evaluate-settlement-calibration.mjs",
"evaluate:sports-favorites": "node scripts/evaluate-sports-favorites.mjs",
"test:server-state": "node scripts/test-server-state.mjs",
"test:shock-audit": "node scripts/test-shock-audit.mjs",
"test:offline-runtime": "node scripts/test-offline-runtime.mjs",
"test:autonomous-runtime": "node scripts/test-autonomous-runtime.mjs"
},
"dependencies": {
"@neondatabase/serverless": "^1.1.0",
"@vercel/blob": "2.5.0",
"nodemailer": "^9.0.3",
"svix": "^1.96.1"
},
"devDependencies": {
"playwright": "^1.62.1"
}
}
+31
View File
@@ -0,0 +1,31 @@
{
"strategy": 64,
"generated_at": "2026-08-22T12:17:23.498Z",
"requested_markets": 500,
"fetched_markets": 500,
"histories_with_data": 500,
"failures": 0,
"observations": 6115,
"methodology": {
"cost_cents": 0.5,
"observation_spacing_hours": 12,
"split": "60% train / 20% validation / 20% untouched holdout",
"cluster_unit": "Polymarket event",
"candidate_rules": 1920,
"promotion_gate": "Positive event-clustered 90% lower bound with minimum support in train, validation, and holdout"
},
"probation_rule_ids": [],
"durable_rule_ids": [],
"horizons": [
{"hours": 6, "train_rows": 3640, "validation_rows": 1226, "test_rows": 1223, "validation_selected": 0, "holdout_passed": 0},
{"hours": 24, "train_rows": 3482, "validation_rows": 875, "test_rows": 1223, "validation_selected": 0, "holdout_passed": 0},
{"hours": 72, "train_rows": 3043, "validation_rows": 215, "test_rows": 1223, "validation_selected": 0, "holdout_passed": 0}
],
"production_decision": {
"preapproved_directional_rules": [],
"six_hour_probation_requires_new_forward_evidence": true,
"full_directional_promotion_requires_new_forward_evidence": true,
"note": "No tested directional rule survived validation at any horizon. This active-market audit can still contain survivorship bias, so it cannot justify relaxing the live evidence gates."
},
"reproduce": "ADAPTIVE_MARKETS=500 ADAPTIVE_CONCURRENCY=12 npm run evaluate:adaptive"
}
+61
View File
@@ -0,0 +1,61 @@
{
"generatedAt": "2026-08-22T00:55:19.692Z",
"strategy": 61,
"verdict": "No tested directional, shock-fade, or near-settlement rule earned capital permission. Keep those lanes observation-only and retain only live depth-and-fee-verified complete bundles as immediately capital-enabled.",
"costModel": {
"directional": "Gamma entry and exit taker fee schedules plus 0.5 cents of round-trip slippage",
"settlement": "Gamma entry taker fee plus 0.5 cents of entry slippage; redemption has no modeled exit trade",
"unknownFeeSchedule": "4 cent conservative fee fallback"
},
"directional": {
"primary": {
"activeMarkets": 1000,
"activeMarketsSkipped": 0,
"eventsAt12Hours": 208,
"eventMeanAt12Hours": -0.03932,
"lower90At12Hours": -0.05078,
"upper90At12Hours": -0.02786,
"robustPositiveRulesAt12Hours": 0,
"robustNegativeRulesAt12Hours": 64
},
"independent": {
"activeMarkets": 1000,
"activeMarketsSkipped": 1000,
"eventsAt12Hours": 254,
"eventMeanAt12Hours": -0.05392,
"lower90At12Hours": -0.06326,
"upper90At12Hours": -0.04457,
"robustPositiveRulesByHorizon": {
"6": 0,
"12": 0,
"24": 0,
"72": 0
},
"robustNegativeRulesByHorizon": {
"6": 69,
"12": 66,
"24": 51,
"72": 34
}
}
},
"settlement": {
"recentResolvedMarkets": 3000,
"marketsWithHistory": 2916,
"testedRules": 297,
"horizonsDays": [1, 3, 7, 14, 30],
"robustPositiveRules": 0
},
"shockFade": {
"selectedOnIndependentCohort": "fade_h12_w3_m0.08_aligned_v1_All_All",
"freshPrimaryTrainLower90": 0.02099,
"freshPrimaryValidationLower90": 0.02641,
"freshPrimaryHoldoutLower90": -0.00751,
"freshPrimaryVerdict": "rejected"
},
"limitations": [
"CLOB price history is a midpoint proxy and does not reconstruct historical order-book depth.",
"Active-market cohorts are survivorship-biased, so positive results would still require resolved-market and forward validation.",
"The absence of a validated rule is evidence for retaining cash, not proof that no future opportunity can exist."
]
}
+70
View File
@@ -0,0 +1,70 @@
{
"generated_at": "2026-08-21T23:44:53.304Z",
"strategy": "resolution-window-no-50-55-forward-shadow-v3",
"selection": {
"discovery_markets_requested": 3000,
"discovery_markets_with_history": 2918,
"disjoint_holdout_markets_requested": 3000,
"disjoint_holdout_markets_with_history": 2394,
"disjoint_holdout_offset": 3000,
"order": "closedTime descending",
"horizon_days_tested": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 14, 21, 30],
"horizon_days_enabled": [],
"horizon_days_observed": [4],
"modeled_round_trip_cost_cents": 1,
"history_fidelity_minutes": 1440,
"decision_unit": "one highest-volume eligible contract per Polymarket event and rule",
"production_contract_filter": "non-Sports and no path-dependent barrier, numeric range, currency range, exact score, or post-count contract"
},
"discovery_rule": {
"side": "NO",
"midpoint_min_inclusive": 0.5,
"midpoint_max_exclusive": 0.55,
"horizon_days": 4,
"events": 145,
"event_mean_return": 0.3842272785,
"event_lower_90": 0.2612426599,
"train_events": 102,
"train_event_lower_90": 0.290934746,
"holdout_events": 46,
"holdout_event_lower_90": 0.0382807805,
"chronological_thirds_event_lower_90": [0.2464004239, 0.1620997356, 0.1521274303]
},
"disjoint_holdout": {
"events": 65,
"event_mean_return": 0.3545949134,
"event_lower_90": 0.1660920142,
"train_events": 49,
"train_event_lower_90": 0.1409018264,
"holdout_events": 16,
"holdout_event_lower_90": -0.047341047,
"chronological_thirds_event_lower_90": [0.2174015607, -0.352501396, -0.0402576867],
"passed_strict_gate": false
},
"production_constraints": {
"capital_enabled": false,
"safe_contract_only": true,
"sports_excluded": true,
"minimum_total_volume_usd": 15000,
"minimum_live_liquidity_usd": 1400,
"maximum_spread": 0.03,
"modeled_entry": "executable NO ask plus 0.25 cents slippage",
"maximum_entry_friction_cents": 1,
"observation_window_days": [3.5, 4.5],
"one_observation_per_event": true,
"promotion_events": 40,
"promotion_lower_90_minimum": 0.01,
"promoted_position_pct": 0.5,
"promoted_total_cap_pct": 3,
"exit": "verified market settlement only"
},
"finding": "The prior 3-6 day capital permission was invalid because it averaged multiple correlated contracts inside an event. After selecting one decision per event, only the four-day safe rule survived the recent discovery sample, and it failed the strict all-segment gate in a disjoint older sample. Production therefore records zero-capital forward outcomes only.",
"limitations": [
"Final volume is used to select one contract per event and was not known at the historical decision time.",
"The resolved archive cannot reconstruct contemporaneous historical liquidity or order-book depth.",
"Published midpoint history is not a fill guarantee.",
"Forward promotion would authorize bounded paper trading only and would not guarantee profit."
],
"reproduce_discovery": "SETTLEMENT_MARKETS=3000 SETTLEMENT_HORIZONS=1,2,3,4,5,6,7,8,9,10,14,21,30 SETTLEMENT_COST_CENTS=1 SETTLEMENT_ORDER=closedTime SETTLEMENT_ASCENDING=false SETTLEMENT_FINE_GRID=true SETTLEMENT_SURVIVORS_ONLY=1 node scripts/evaluate-settlements.mjs",
"reproduce_disjoint_holdout": "SETTLEMENT_MARKETS=3000 SETTLEMENT_SKIP=3000 SETTLEMENT_HORIZONS=4,5 SETTLEMENT_COST_CENTS=1 SETTLEMENT_ORDER=closedTime SETTLEMENT_ASCENDING=false SETTLEMENT_TARGET_ONLY=1 SETTLEMENT_TARGET_RULES=buy_no_50_55,buy_no_50_55_safe_non_sports,buy_safe_no_50_55 node scripts/evaluate-settlements.mjs"
}
@@ -0,0 +1,37 @@
{
"generated_at": "2026-08-23T00:10:38.029Z",
"strategy": "exact-fee settlement calibration",
"requested_markets": 5000,
"resolved_markets": 5000,
"histories_with_data": 4807,
"failures": 0,
"observations": 7854,
"tested_rules": 1400,
"train_passed": 0,
"validation_selected": 0,
"holdout_passed": 0,
"methodology": {
"selection": "most recently closed eligible Yes/No markets",
"horizons_days": [1, 3, 7, 14, 30],
"history_fidelity_minutes": 1440,
"maximum_price_staleness_hours": 36,
"modeled_entry_slippage_cents": 1,
"exact_gamma_entry_fee_schedules": true,
"split": "60% train / 20% validation / 20% untouched holdout",
"confidence": "event-clustered 95% lower bound",
"stability": "positive event mean in each of four chronological windows; at most one highest-entry market per underlying event or sports contest"
},
"partition_observations": {
"train": 4987,
"validation": 1419,
"holdout": 1448
},
"sports_no_3d_target": {
"train": {"events": 79, "event_mean": -0.00873, "lower_95": -0.11764},
"validation": {"events": 53, "event_mean": -0.44135, "lower_95": -0.62545},
"holdout": {"events": 34, "event_mean": -0.05382, "lower_95": -0.28345},
"stability_means": [0.00226, 0.01151, -0.36967, -0.18035],
"passes_holdout": false
},
"production_decision": "No static settlement-horizon, side, category, or price-band rule receives capital. Continue event-deduplicated zero-capital forward learning and require the existing promotion gates."
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,600 @@
{
"generatedAt": "2026-08-21T20:50:08.481Z",
"requestedMarkets": 1000,
"activeMarketsSkipped": 2000,
"markets": 1000,
"marketsWithHistory": 1000,
"fetchFailures": 0,
"medianHistoryCoverageDays": 11.53,
"maximumHistoryCoverageDays": 29.99,
"observations": 246580,
"testedBaseRules": 2560,
"trainWinners": 8,
"testedRefinements": 336,
"validationWinners": 8,
"holdoutPassed": 5,
"eventHoldoutObservations": 63020,
"eventHoldoutEvents": 134,
"eventHoldoutPassed": 3,
"externalRequestedMarkets": 100,
"externalMarkets": 100,
"externalMarketsWithHistory": 0,
"externalFetchFailures": 0,
"externalObservations": 0,
"archivePassed": 0,
"methodology": {
"requestedHistoryDays": 30,
"medianHistoryCoverageDays": 11.53,
"maximumHistoryCoverageDays": 29.99,
"observationSpacingHours": 3,
"horizons": [
3,
6,
12,
24
],
"windows": [
1,
3,
6,
24
],
"costCents": 2,
"entryPriceRange": [
0.08,
0.92
],
"returnWinsorization": [
-1,
2
],
"eventSplit": "25% deterministic event-disjoint holdout reserved before search",
"split": "Remaining events use 60% train / 20% validation / 20% untouched chronological holdout with future-mark purge",
"clusterUnit": "Polymarket event",
"refinementSource": "Only base rules with a positive train lower bound are refined by category and entry band",
"limitation": "Current active-market selection is survivorship biased; any holdout winner still requires resolved-market external validation"
},
"partitions": {
"train": 79592,
"validation": 50591,
"holdout": 45853
},
"exactStrategy3": {
"rule": {
"id": "fade_h12_w3_m0.08_accelerating_v1_All_All",
"baseId": "fade_h12_w3_m0.08_accelerating_v1",
"horizon": 12,
"window": 3,
"minMove": 0.08,
"direction": "fade",
"confirmation": "accelerating",
"maxVol": 1,
"category": "All",
"band": "All"
},
"train": {
"attempts": 252,
"markets": 87,
"events": 66,
"mean": 0.06705,
"eventMean": 0.0483,
"lower": -0.00168,
"upper": 0.09828,
"winRate": 0.49603
},
"validation": {
"attempts": 167,
"markets": 77,
"events": 67,
"mean": 0.11569,
"eventMean": 0.10467,
"lower": 0.03831,
"upper": 0.17102,
"winRate": 0.53892
},
"holdout": {
"attempts": 229,
"markets": 108,
"events": 91,
"mean": 0.08352,
"eventMean": 0.04391,
"lower": -0.0089,
"upper": 0.09672,
"winRate": 0.57642
},
"eventHoldout": {
"attempts": 257,
"markets": 72,
"events": 61,
"mean": 0.05872,
"eventMean": 0.05166,
"lower": 0.00367,
"upper": 0.09965,
"winRate": 0.53307
}
},
"candidates": [
{
"rule": {
"id": "fade_h24_w6_m0.08_accelerating_v1_All_All",
"baseId": "fade_h24_w6_m0.08_accelerating_v1",
"horizon": 24,
"window": 6,
"minMove": 0.08,
"direction": "fade",
"confirmation": "accelerating",
"maxVol": 1,
"category": "All",
"band": "All"
},
"passesHoldout": true,
"passesEventHoldout": true,
"passesArchive": false,
"train": {
"attempts": 317,
"markets": 94,
"events": 74,
"mean": 0.07218,
"eventMean": 0.07908,
"lower": 0.01652,
"upper": 0.14164,
"winRate": 0.52997
},
"validation": {
"attempts": 191,
"markets": 79,
"events": 69,
"mean": 0.11947,
"eventMean": 0.13775,
"lower": 0.06147,
"upper": 0.21403,
"winRate": 0.53927
},
"holdout": {
"attempts": 235,
"markets": 109,
"events": 91,
"mean": 0.11689,
"eventMean": 0.09368,
"lower": 0.03145,
"upper": 0.15591,
"winRate": 0.6
},
"eventHoldout": {
"attempts": 288,
"markets": 75,
"events": 65,
"mean": 0.08647,
"eventMean": 0.09967,
"lower": 0.03989,
"upper": 0.15945,
"winRate": 0.57639
},
"archive": {
"attempts": 0,
"markets": 0,
"events": 0,
"mean": 0,
"eventMean": 0,
"lower": 0,
"upper": 0,
"winRate": 0
}
},
{
"rule": {
"id": "fade_h24_w1_m0.08_accelerating_v1_All_All",
"baseId": "fade_h24_w1_m0.08_accelerating_v1",
"horizon": 24,
"window": 1,
"minMove": 0.08,
"direction": "fade",
"confirmation": "accelerating",
"maxVol": 1,
"category": "All",
"band": "All"
},
"passesHoldout": true,
"passesEventHoldout": true,
"passesArchive": false,
"train": {
"attempts": 219,
"markets": 77,
"events": 58,
"mean": 0.10882,
"eventMean": 0.09285,
"lower": 0.02031,
"upper": 0.16539,
"winRate": 0.58447
},
"validation": {
"attempts": 148,
"markets": 67,
"events": 59,
"mean": 0.16081,
"eventMean": 0.16966,
"lower": 0.08188,
"upper": 0.25744,
"winRate": 0.61486
},
"holdout": {
"attempts": 215,
"markets": 98,
"events": 82,
"mean": 0.13767,
"eventMean": 0.0892,
"lower": 0.0267,
"upper": 0.1517,
"winRate": 0.61395
},
"eventHoldout": {
"attempts": 219,
"markets": 69,
"events": 59,
"mean": 0.10033,
"eventMean": 0.07714,
"lower": 0.02104,
"upper": 0.13324,
"winRate": 0.58904
},
"archive": {
"attempts": 0,
"markets": 0,
"events": 0,
"mean": 0,
"eventMean": 0,
"lower": 0,
"upper": 0,
"winRate": 0
}
},
{
"rule": {
"id": "fade_h12_w6_m0.08_accelerating_v1_All_All",
"baseId": "fade_h12_w6_m0.08_accelerating_v1",
"horizon": 12,
"window": 6,
"minMove": 0.08,
"direction": "fade",
"confirmation": "accelerating",
"maxVol": 1,
"category": "All",
"band": "All"
},
"passesHoldout": true,
"passesEventHoldout": false,
"passesArchive": false,
"train": {
"attempts": 332,
"markets": 97,
"events": 76,
"mean": 0.07238,
"eventMean": 0.07011,
"lower": 0.01251,
"upper": 0.12772,
"winRate": 0.48795
},
"validation": {
"attempts": 211,
"markets": 85,
"events": 75,
"mean": 0.10268,
"eventMean": 0.08421,
"lower": 0.02454,
"upper": 0.14388,
"winRate": 0.51659
},
"holdout": {
"attempts": 271,
"markets": 124,
"events": 106,
"mean": 0.09294,
"eventMean": 0.07464,
"lower": 0.01562,
"upper": 0.13366,
"winRate": 0.5572
},
"eventHoldout": {
"attempts": 309,
"markets": 78,
"events": 67,
"mean": 0.03857,
"eventMean": 0.04684,
"lower": -0.00136,
"upper": 0.09503,
"winRate": 0.52427
},
"archive": {
"attempts": 0,
"markets": 0,
"events": 0,
"mean": 0,
"eventMean": 0,
"lower": 0,
"upper": 0,
"winRate": 0
}
},
{
"rule": {
"id": "fade_h12_w1_m0.08_accelerating_v1_All_All",
"baseId": "fade_h12_w1_m0.08_accelerating_v1",
"horizon": 12,
"window": 1,
"minMove": 0.08,
"direction": "fade",
"confirmation": "accelerating",
"maxVol": 1,
"category": "All",
"band": "All"
},
"passesHoldout": true,
"passesEventHoldout": false,
"passesArchive": false,
"train": {
"attempts": 234,
"markets": 80,
"events": 60,
"mean": 0.0916,
"eventMean": 0.06887,
"lower": 0.0019,
"upper": 0.13585,
"winRate": 0.54701
},
"validation": {
"attempts": 170,
"markets": 73,
"events": 65,
"mean": 0.13277,
"eventMean": 0.1205,
"lower": 0.04945,
"upper": 0.19155,
"winRate": 0.57647
},
"holdout": {
"attempts": 233,
"markets": 106,
"events": 90,
"mean": 0.11156,
"eventMean": 0.0626,
"lower": 0.014,
"upper": 0.11121,
"winRate": 0.59657
},
"eventHoldout": {
"attempts": 230,
"markets": 70,
"events": 60,
"mean": 0.07443,
"eventMean": 0.04833,
"lower": -0.00488,
"upper": 0.10153,
"winRate": 0.56522
},
"archive": {
"attempts": 0,
"markets": 0,
"events": 0,
"mean": 0,
"eventMean": 0,
"lower": 0,
"upper": 0,
"winRate": 0
}
},
{
"rule": {
"id": "fade_h24_w6_m0.08_aligned_v1_All_All",
"baseId": "fade_h24_w6_m0.08_aligned_v1",
"horizon": 24,
"window": 6,
"minMove": 0.08,
"direction": "fade",
"confirmation": "aligned",
"maxVol": 1,
"category": "All",
"band": "All"
},
"passesHoldout": true,
"passesEventHoldout": true,
"passesArchive": false,
"train": {
"attempts": 416,
"markets": 102,
"events": 82,
"mean": 0.0576,
"eventMean": 0.06209,
"lower": 0.00335,
"upper": 0.12083,
"winRate": 0.50721
},
"validation": {
"attempts": 248,
"markets": 88,
"events": 77,
"mean": 0.11198,
"eventMean": 0.13356,
"lower": 0.06518,
"upper": 0.20194,
"winRate": 0.54435
},
"holdout": {
"attempts": 308,
"markets": 119,
"events": 99,
"mean": 0.07857,
"eventMean": 0.06173,
"lower": 0.00882,
"upper": 0.11464,
"winRate": 0.56818
},
"eventHoldout": {
"attempts": 388,
"markets": 77,
"events": 67,
"mean": 0.08325,
"eventMean": 0.09039,
"lower": 0.02683,
"upper": 0.15395,
"winRate": 0.56186
},
"archive": {
"attempts": 0,
"markets": 0,
"events": 0,
"mean": 0,
"eventMean": 0,
"lower": 0,
"upper": 0,
"winRate": 0
}
},
{
"rule": {
"id": "fade_h24_w1_m0.08_any_v1_All_All",
"baseId": "fade_h24_w1_m0.08_any_v1",
"horizon": 24,
"window": 1,
"minMove": 0.08,
"direction": "fade",
"confirmation": "any",
"maxVol": 1,
"category": "All",
"band": "All"
},
"passesHoldout": false,
"passesEventHoldout": false,
"passesArchive": false,
"train": {
"attempts": 242,
"markets": 79,
"events": 60,
"mean": 0.09735,
"eventMean": 0.08032,
"lower": 0.00885,
"upper": 0.15178,
"winRate": 0.58678
},
"validation": {
"attempts": 164,
"markets": 73,
"events": 64,
"mean": 0.15066,
"eventMean": 0.1468,
"lower": 0.0652,
"upper": 0.2284,
"winRate": 0.59756
},
"holdout": {
"attempts": 248,
"markets": 103,
"events": 87,
"mean": 0.11871,
"eventMean": 0.05403,
"lower": -0.0077,
"upper": 0.11575,
"winRate": 0.58065
},
"eventHoldout": null,
"archive": null
},
{
"rule": {
"id": "fade_h24_w1_m0.08_aligned_v1_All_All",
"baseId": "fade_h24_w1_m0.08_aligned_v1",
"horizon": 24,
"window": 1,
"minMove": 0.08,
"direction": "fade",
"confirmation": "aligned",
"maxVol": 1,
"category": "All",
"band": "All"
},
"passesHoldout": false,
"passesEventHoldout": false,
"passesArchive": false,
"train": {
"attempts": 242,
"markets": 79,
"events": 60,
"mean": 0.09735,
"eventMean": 0.08032,
"lower": 0.00885,
"upper": 0.15178,
"winRate": 0.58678
},
"validation": {
"attempts": 164,
"markets": 73,
"events": 64,
"mean": 0.15066,
"eventMean": 0.1468,
"lower": 0.0652,
"upper": 0.2284,
"winRate": 0.59756
},
"holdout": {
"attempts": 248,
"markets": 103,
"events": 87,
"mean": 0.11871,
"eventMean": 0.05403,
"lower": -0.0077,
"upper": 0.11575,
"winRate": 0.58065
},
"eventHoldout": null,
"archive": null
},
{
"rule": {
"id": "fade_h12_w24_m0.08_accelerating_v1_All_All",
"baseId": "fade_h12_w24_m0.08_accelerating_v1",
"horizon": 12,
"window": 24,
"minMove": 0.08,
"direction": "fade",
"confirmation": "accelerating",
"maxVol": 1,
"category": "All",
"band": "All"
},
"passesHoldout": false,
"passesEventHoldout": false,
"passesArchive": false,
"train": {
"attempts": 581,
"markets": 115,
"events": 91,
"mean": 0.02848,
"eventMean": 0.03332,
"lower": 0.00168,
"upper": 0.06497,
"winRate": 0.44578
},
"validation": {
"attempts": 303,
"markets": 99,
"events": 89,
"mean": 0.09201,
"eventMean": 0.0833,
"lower": 0.02962,
"upper": 0.13698,
"winRate": 0.49835
},
"holdout": {
"attempts": 411,
"markets": 154,
"events": 133,
"mean": 0.04166,
"eventMean": 0.01876,
"lower": -0.02037,
"upper": 0.05789,
"winRate": 0.49392
},
"eventHoldout": null,
"archive": null
}
]
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,58 @@
{
"generated_at": "2026-08-22T12:55:00.000Z",
"strategy": "sports-contest-no-disjoint-10000-followup",
"production_strategy": 64,
"status": "zero-capital-forward-shadow",
"methodology": {
"cohorts": [
"most recent 5000 eligible resolved sports markets",
"next older disjoint 5000 eligible resolved sports markets"
],
"rules_tested_per_cohort": 60,
"lead_hours": [12, 18, 24, 30, 36],
"entry_bands": "grid from 0.55 through 0.95",
"modeled_cost_cents": 2,
"selection": "one highest-priced eligible favorite per contest",
"split": "60% train / 20% validation / 20% untouched holdout",
"strict_gate": "positive lower 90% confidence bound in train, validation, and untouched holdout"
},
"recent_5000": {
"train_rules_passing": 19,
"validation_rules_passing": 5,
"holdout_rules_passing": 0,
"closest_rule": {
"id": "lead30_0.55-0.75",
"train_mean": 0.12789,
"train_lower_90": 0.03068,
"validation_mean": 0.09454,
"validation_lower_90": 0.0101,
"holdout_mean": 0.09368,
"holdout_lower_90": -0.00833
}
},
"older_disjoint_5000": {
"train_rules_passing": 5,
"validation_rules_passing": 0,
"holdout_rules_passing": 0,
"closest_rule": {
"id": "lead30_0.55-0.75",
"train_mean": 0.01857,
"train_lower_90": -0.0588,
"validation_mean": 0.05567,
"validation_lower_90": -0.04222,
"holdout_mean": 0.08661,
"holdout_lower_90": -0.01431
},
"current_24_hour_broad_rule_segment_means": [-0.039, -0.026, -0.0097]
},
"decision": {
"capital_enabled": false,
"initial_position_pct": 0,
"promotion_events": 20,
"promotion_lower_90_minimum": 0.005,
"promoted_position_pct": 0.75,
"maximum_promotion_events": 40,
"maximum_position_pct": 1,
"reason": "No rule survived both disjoint archives with a positive lower confidence bound in every chronological partition. Recent positive point estimates are insufficient evidence for capital."
}
}
@@ -0,0 +1,78 @@
{
"generated_at": "2026-08-21T21:20:00.000Z",
"strategy": "sports-contest-no-three-day-exploration-v1",
"production_strategy": 62,
"status": "bounded-paper-exploration-not-proven",
"selection": {
"resolved_markets_requested": 5000,
"histories_with_data": 4731,
"side": "NO",
"entry_range": [0.03, 0.97],
"decision_horizon_days": 3,
"category": "Sports",
"contest_clustering": "dated Gamma market slug prefix, with game-start and normalized-title fallback",
"one_market_per_contest": "highest NO entry",
"decision_anchor": "published gameStartTime",
"modeled_entry": "historical midpoint plus exact Gamma taker fee plus 0.5-cent slippage",
"settlement_exit_fee": 0
},
"corrected_5000_market_result": {
"independent_contests": 76,
"train": {
"contests": 34,
"mean_return": 0.23457,
"lower_95": 0.057,
"win_rate": 0.912
},
"validation": {
"contests": 20,
"mean_return": 0.01776,
"lower_95": -0.19809,
"win_rate": 0.85
},
"holdout": {
"contests": 22,
"mean_return": 0.36683,
"lower_95": 0.12251,
"win_rate": 0.909
},
"chronological_quarter_means": [0.35259, 0.07435, 0.00466, 0.28046],
"passed_strict_gate": false,
"failure_reason": "Validation's 95% lower confidence bound crossed zero. The point estimate repeated, but the edge was not independently precise in every segment."
},
"disjoint_checks": {
"recent_3000": {
"train_mean": 0.03751,
"validation_mean": 0.16165,
"holdout_mean": 0.35035,
"holdout_lower_95": 0.07389,
"passed_strict_gate": false
},
"older_3000": {
"train_mean": 0.04042,
"validation_mean": 0.46699,
"holdout_mean": 0.48381,
"chronological_quarter_means": [-0.10883, 0.26995, 0.24012, 0.48381],
"passed_strict_gate": false,
"failure_reason": "The earliest older chronological quarter was negative."
}
},
"production_constraints": {
"paper_only": true,
"initial_position_pct": 0.5,
"promoted_position_pct": 0.75,
"maximum_position_pct": 1,
"total_lane_cap_pct": 3,
"maximum_new_contests_per_cycle": 1,
"promotion_events": 20,
"maximum_promotion_events": 40,
"suspension_events": 15,
"suspension_rule": "mean at or below -5%, upper 90% bound at or below zero, or realized lane loss at least 1% of starting portfolio",
"exact_entry_fee_required": true,
"unknown_fee_schedule_rejected": true,
"offline_fresh_cache_allowed": true,
"offline_stale_cache_mark_only": true,
"settlement_must_be_verified": true
},
"interpretation": "Repeated positive point estimates justify a small observable paper experiment, not a return promise. Capital size must respond only to independent forward contest settlements."
}
+220
View File
@@ -0,0 +1,220 @@
const GAMMA = "https://gamma-api.polymarket.com";
const CLOB = "https://clob.polymarket.com";
const MARKET_LIMIT = Math.max(50, Math.min(500, Number(process.env.ADAPTIVE_MARKETS || 300)));
const CONCURRENCY = Math.max(1, Math.min(12, Number(process.env.ADAPTIVE_CONCURRENCY || 6)));
const COST = Math.max(0, Math.min(0.05, Number(process.env.ADAPTIVE_COST_CENTS || 0.5) / 100));
const HOUR = 3600;
function parseJson(value) {
if (Array.isArray(value)) return value;
try { return JSON.parse(value || "[]"); } catch { return []; }
}
async function fetchJson(url, attempts = 3) {
let lastError;
for (let attempt = 0; attempt < attempts; attempt++) {
try {
const response = await fetch(url, { signal: AbortSignal.timeout(20000), headers: { accept: "application/json" } });
if (response.ok) return response.json();
lastError = new Error(`${response.status} ${response.statusText}`);
if (response.status !== 429 && response.status < 500) break;
} catch (error) { lastError = error; }
await new Promise((resolve) => setTimeout(resolve, 500 * (attempt + 1)));
}
throw lastError || new Error("request failed");
}
async function mapLimit(items, limit, task) {
const output = new Array(items.length);
let cursor = 0;
async function worker() {
while (cursor < items.length) {
const index = cursor++;
try { output[index] = await task(items[index]); }
catch (error) { output[index] = { error: error.message }; }
}
}
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
return output;
}
function categoryOf(raw) {
const text = `${raw.question || ""} ${(raw.tags || []).map((tag) => tag.slug || tag.label || "").join(" ")}`.toLowerCase();
if (/\b(election|president|politic|senate|congress|parliament|minister|governor|government|nominee|primary)\b/.test(text)) return "Politics";
if (/\b(bitcoin|crypto|ethereum|btc|eth|solana|xrp|token|stablecoin)\b/.test(text)) return "Crypto";
if (/\b(nba|nfl|nhl|mlb|soccer|football|baseball|basketball|tennis|ufc|boxing|championship|match|game|tournament|league)\b/.test(text)) return "Sports";
if (/\b(fed|inflation|gdp|recession|stock|company|economy|tariff|interest rate|unemployment|earnings)\b/.test(text)) return "Economy";
if (/\b(movie|music|album|box office|television|celebrity|award|gaming|youtube|stream)\b/.test(text)) return "Pop Culture";
return "Other";
}
async function fetchMarkets(limit) {
const markets = [], seen = new Set(), pageSize = 100;
for (let offset = 0; markets.length < limit && offset < limit * 4; offset += pageSize) {
const params = new URLSearchParams({ active: "true", closed: "false", archived: "false", include_tag: "true",
limit: String(pageSize), offset: String(offset), order: "volume24hr", ascending: "false" });
const page = await fetchJson(`${GAMMA}/markets?${params}`);
if (!Array.isArray(page) || !page.length) break;
for (const raw of page) {
const id = String(raw.id || ""), labels = parseJson(raw.outcomes).map((outcome) => String(outcome).trim().toLowerCase());
const tokenId = String(parseJson(raw.clobTokenIds)[0] || "");
if (!id || seen.has(id) || !tokenId || labels[0] !== "yes" || labels[1] !== "no") continue;
seen.add(id);
markets.push({ id, tokenId, question: raw.question || "", category: categoryOf(raw),
eventKey: String(raw.events?.[0]?.id || raw.events?.[0]?.slug || raw.eventId || id) });
if (markets.length >= limit) break;
}
if (page.length < pageSize) break;
}
return markets;
}
function atOrBefore(points, target) {
let lo = 0, hi = points.length - 1, answer = null;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (points[mid].t <= target) { answer = points[mid]; lo = mid + 1; }
else hi = mid - 1;
}
return answer;
}
function atOrAfter(points, target) {
let lo = 0, hi = points.length - 1, answer = null;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (points[mid].t >= target) { answer = points[mid]; hi = mid - 1; }
else lo = mid + 1;
}
return answer;
}
function observations(market, points) {
const rows = [], seenBuckets = new Set();
for (const current of points) {
const bucket = Math.floor(current.t / (12 * HOUR));
if (seenBuckets.has(bucket) || current.p < 0.08 || current.p > 0.92) continue;
const history = {};
let complete = true;
for (const hours of [1, 6, 24, 72, 168]) {
const prior = atOrBefore(points, current.t - hours * HOUR);
if (!prior || current.t - hours * HOUR - prior.t > 3 * HOUR) { complete = false; break; }
history[hours] = current.p - prior.p;
}
if (!complete) continue;
const future = {};
for (const hours of [6, 24, 72]) {
const next = atOrAfter(points, current.t + hours * HOUR);
if (next && next.t - (current.t + hours * HOUR) <= 3 * HOUR) future[hours] = next.p;
}
if (!Number.isFinite(future[24]) && !Number.isFinite(future[72])) continue;
seenBuckets.add(bucket);
rows.push({ marketId: market.id, eventKey: market.eventKey, category: market.category,
observedAt: current.t, price: current.p, moves: history, future });
}
return rows;
}
function bandOf(price) {
if (price < 0.3) return "longshot";
if (price < 0.7) return "mid";
return "favorite";
}
function tradeFor(row, rule, horizon) {
const move = row.moves[rule.lookback], magnitude = Math.abs(move);
if (!move || magnitude < rule.minMove || magnitude > rule.maxMove) return null;
const longSign = Math.sign(row.moves[24]), slowSign = Math.sign(row.moves[168]);
if (rule.agreement === "same" && (!longSign || longSign !== slowSign)) return null;
if (rule.agreement === "opposite" && (!longSign || longSign === slowSign)) return null;
if (rule.category !== "All" && row.category !== rule.category) return null;
const direction = Math.sign(move) * (rule.mode === "follow" ? 1 : -1);
const side = direction > 0 ? "YES" : "NO", entry = side === "YES" ? row.price : 1 - row.price;
const futureYes = row.future[horizon], exit = side === "YES" ? futureYes : 1 - futureYes;
if (!Number.isFinite(exit) || entry < 0.08 || entry > 0.92 || (rule.band !== "all" && bandOf(entry) !== rule.band)) return null;
return { eventKey: row.eventKey, marketId: row.marketId, observedAt: row.observedAt,
netReturn: exit / entry - 1 - COST / entry };
}
function summary(trades, confidence = 1.645) {
if (!trades.length) return { trades: 0, markets: 0, events: 0, mean: 0, eventMean: 0, lower: 0, upper: 0, winRate: 0 };
const buckets = new Map();
for (const trade of trades) {
const values = buckets.get(trade.eventKey) || [];
values.push(trade.netReturn); buckets.set(trade.eventKey, values);
}
const eventReturns = [...buckets.values()].map((values) => values.reduce((sum, value) => sum + value, 0) / values.length);
const eventMean = eventReturns.reduce((sum, value) => sum + value, 0) / eventReturns.length;
const variance = eventReturns.length > 1
? eventReturns.reduce((sum, value) => sum + (value - eventMean) ** 2, 0) / (eventReturns.length - 1) : 0;
const margin = confidence * Math.sqrt(variance / Math.max(1, eventReturns.length));
return { trades: trades.length, markets: new Set(trades.map((trade) => trade.marketId)).size, events: eventReturns.length,
mean: trades.reduce((sum, trade) => sum + trade.netReturn, 0) / trades.length,
eventMean, lower: eventMean - margin, upper: eventMean + margin,
winRate: trades.filter((trade) => trade.netReturn > 0).length / trades.length };
}
const rules = [];
for (const lookback of [6, 24, 72, 168]) {
for (const mode of ["follow", "fade"]) {
for (const [minMove, maxMove] of [[0.005, 0.04], [0.01, 0.08], [0.02, 0.15], [0.04, 1]]) {
for (const band of ["all", "longshot", "mid", "favorite"]) {
for (const category of ["All", "Politics", "Sports", "Crypto", "Other"]) {
for (const agreement of ["any", "same", "opposite"]) {
const id = `${mode}_${lookback}h_${minMove}-${maxMove}_${band}_${category}_${agreement}`;
rules.push({ id, lookback, mode, minMove, maxMove, band, category, agreement });
}
}
}
}
}
}
const markets = await fetchMarkets(MARKET_LIMIT);
const histories = await mapLimit(markets, CONCURRENCY, async (market) => {
const data = await fetchJson(`${CLOB}/prices-history?market=${encodeURIComponent(market.tokenId)}&interval=1m&fidelity=60`);
const points = (data.history || []).map((point) => ({ t: Number(point.t), p: Number(point.p) }))
.filter((point) => Number.isFinite(point.t) && Number.isFinite(point.p)).sort((a, b) => a.t - b.t);
return { rows: observations(market, points), points: points.length };
});
const rows = histories.filter((result) => result && !result.error).flatMap((result) => result.rows);
const times = rows.map((row) => row.observedAt).sort((a, b) => a - b);
const trainCut = times[Math.floor(times.length * 0.6)] || 0, validationCut = times[Math.floor(times.length * 0.8)] || 0;
function evaluateHorizon(horizon) {
const trainRows = rows.filter((row) => row.observedAt < trainCut && row.observedAt + horizon * HOUR < trainCut);
const validationRows = rows.filter((row) => row.observedAt >= trainCut && row.observedAt + horizon * HOUR < validationCut);
const testRows = rows.filter((row) => row.observedAt >= validationCut);
const selected = [];
for (const rule of rules) {
const train = summary(trainRows.map((row) => tradeFor(row, rule, horizon)).filter(Boolean));
if (train.trades < 80 || train.events < 15 || train.lower <= 0) continue;
const validation = summary(validationRows.map((row) => tradeFor(row, rule, horizon)).filter(Boolean));
if (validation.trades < 40 || validation.events < 10 || validation.lower <= 0) continue;
selected.push({ rule, train, validation });
}
const tested = selected.map((candidate) => {
const test = summary(testRows.map((row) => tradeFor(row, candidate.rule, horizon)).filter(Boolean));
return { ...candidate, test, passesHoldout: test.trades >= 40 && test.events >= 10 && test.lower > 0 };
}).sort((a, b) => (Number(b.passesHoldout) - Number(a.passesHoldout)) || b.test.lower - a.test.lower);
return { horizon, trainRows: trainRows.length, validationRows: validationRows.length, testRows: testRows.length,
validationSelected: selected.length, holdoutPassed: tested.filter((candidate) => candidate.passesHoldout).length,
candidates: tested.slice(0, 30) };
}
const compact = (stats) => Object.fromEntries(Object.entries(stats).map(([key, value]) => [key, Number.isFinite(value) ? +value.toFixed(5) : value]));
const horizons = [6, 24, 72].map(evaluateHorizon).map((result) => ({ ...result,
candidates: result.candidates.map((candidate) => ({ rule: candidate.rule, passesHoldout: candidate.passesHoldout,
train: compact(candidate.train), validation: compact(candidate.validation), test: compact(candidate.test) })) }));
const passedByHorizon = Object.fromEntries(horizons.map((result) => [result.horizon,
new Set(result.candidates.filter((candidate) => candidate.passesHoldout).map((candidate) => candidate.rule.id))]));
const probationRuleIds = [...passedByHorizon[6]];
const durableRuleIds = [...passedByHorizon[24]].filter((id) => passedByHorizon[72].has(id));
console.log(JSON.stringify({ generatedAt: new Date().toISOString(), requestedMarkets: MARKET_LIMIT, fetchedMarkets: markets.length,
historiesWithData: histories.filter((result) => result && !result.error && result.points).length,
failures: histories.filter((result) => result?.error).length, observations: rows.length,
methodology: { costCents: COST * 100, observationSpacingHours: 12, split: "60% train / 20% validation / 20% untouched holdout",
clusterUnit: "Polymarket event", candidateRules: rules.length,
promotionGate: "positive event-clustered 90% lower bound with minimum support in train, validation, and holdout" },
probationRuleIds, durableRuleIds, horizons }, null, 2));
+184
View File
@@ -0,0 +1,184 @@
const GAMMA = "https://gamma-api.polymarket.com";
const EVENT_LIMIT = Math.max(20, Math.min(1000, Number(process.env.DOMINANCE_EVENTS || 500)));
const COST_CENTS = Math.max(0, Math.min(5, Number(process.env.DOMINANCE_COST_CENTS || 0.5)));
const MIN_LIQUIDITY = Math.max(0, Number(process.env.DOMINANCE_MIN_LIQUIDITY || 1000));
const MIN_NET_PROFIT = Math.max(0, Number(process.env.DOMINANCE_MIN_NET_PROFIT || 0.003));
const MIN_NET_RETURN = Math.max(0, Number(process.env.DOMINANCE_MIN_NET_RETURN || 0.0015));
function parseJson(value) {
if (Array.isArray(value)) return value;
try { return JSON.parse(value || "[]"); } catch { return []; }
}
function number(value) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
async function fetchJson(url, attempts = 3) {
let lastError;
for (let attempt = 0; attempt < attempts; attempt++) {
try {
const response = await fetch(url, { signal: AbortSignal.timeout(20000), headers: { accept: "application/json" } });
if (response.ok) return response.json();
lastError = new Error(`${response.status} ${response.statusText}`);
if (response.status !== 429 && response.status < 500) break;
} catch (error) { lastError = error; }
await new Promise((resolve) => setTimeout(resolve, 500 * (attempt + 1)));
}
throw lastError || new Error("request failed");
}
async function fetchEvents(limit) {
const events = [], pageSize = 100;
for (let offset = 0; offset < limit; offset += pageSize) {
const size = Math.min(pageSize, limit - offset);
const params = new URLSearchParams({ active: "true", closed: "false", archived: "false",
limit: String(size), offset: String(offset), order: "volume24hr", ascending: "false" });
const page = await fetchJson(`${GAMMA}/events?${params}`);
if (!Array.isArray(page) || !page.length) break;
events.push(...page);
if (page.length < size) break;
}
return events.slice(0, limit);
}
const THRESHOLD_PATTERNS = [
{ direction: "above", outcomeMode: "over-under", regex: /(\bover\s*\/\s*under\s*(?:[$€£]\s*)?)([0-9]+(?:,[0-9]{3})*(?:\.[0-9]+)?)\s*(k|m|b|%|bps)?\b/i },
{ direction: "above", outcomeMode: "yes-no", regex: /(\b(?:above|over|at least|higher than|greater than)\s*(?:[$€£]\s*)?)([0-9]+(?:,[0-9]{3})*(?:\.[0-9]+)?)\s*(k|m|b|%|bps)?\b/i },
{ direction: "below", outcomeMode: "yes-no", regex: /(\b(?:below|(?<!\/)under|at most|lower than|less than)\s*(?:[$€£]\s*)?)([0-9]+(?:,[0-9]{3})*(?:\.[0-9]+)?)\s*(k|m|b|%|bps)?\b/i },
];
const MONTHS = Object.freeze({ january: 1, february: 2, march: 3, april: 4, may: 5, june: 6,
july: 7, august: 8, september: 9, october: 10, november: 11, december: 12 });
const DEADLINE_PATTERN = /\b((?:on\s+or\s+)?(?:by|before)\s+(?:the\s+)?)(january|february|march|april|may|june|july|august|september|october|november|december)\s+(\d{1,2})(?:st|nd|rd|th)?(?:,?\s+(\d{4}))?\b/i;
function parseThreshold(question) {
const text = String(question || "").trim();
for (const pattern of THRESHOLD_PATTERNS) {
const match = pattern.regex.exec(text);
if (!match) continue;
const rawValue = Number(match[2].replaceAll(",", ""));
const suffix = String(match[3] || "").toLowerCase();
const multiplier = suffix === "k" ? 1e3 : suffix === "m" ? 1e6 : suffix === "b" ? 1e9 : 1;
const value = rawValue * multiplier;
if (!Number.isFinite(value)) continue;
const valueStart = match.index + match[1].length, valueEnd = valueStart + match[2].length;
const stem = `${text.slice(0, valueStart)}{threshold}${text.slice(valueEnd)}`.toLowerCase().replace(/\s+/g, " ").trim();
return { direction: pattern.direction, outcomeMode: pattern.outcomeMode, value, stem };
}
return null;
}
function parseDeadline(question) {
const text = String(question || "").trim(), match = DEADLINE_PATTERN.exec(text);
if (!match) return null;
const month = MONTHS[String(match[2]).toLowerCase()], day = Number(match[3]), year = match[4] ? Number(match[4]) : null;
if (!month || !Number.isInteger(day) || day < 1 || day > new Date(Date.UTC(year || 2024, month, 0)).getUTCDate()) return null;
const dateStart = match.index + match[1].length, dateEnd = match.index + match[0].length;
const stem = `${text.slice(0, dateStart)}{deadline}${text.slice(dateEnd)}`.toLowerCase().replace(/\s+/g, " ").trim();
return { direction: "deadline", value: year ? year * 10000 + month * 100 + day : month * 100 + day,
yearMode: year ? "explicit-year" : "implicit-year", stem };
}
function quoteMarket(raw, event) {
const threshold = parseThreshold(raw.question);
const tokens = parseJson(raw.clobTokenIds).map(String), prices = parseJson(raw.outcomePrices).map(number);
const outcomes = parseJson(raw.outcomes).map((outcome) => String(outcome).trim().toLowerCase());
const bid = number(raw.bestBid), ask = number(raw.bestAsk), liquidity = number(raw.liquidityNum || raw.liquidity) || 0;
const expectedOutcomes = threshold?.outcomeMode === "over-under" ? ["over", "under"] : ["yes", "no"];
if (!threshold || tokens.length !== 2 || prices.length !== 2 || outcomes[0] !== expectedOutcomes[0] || outcomes[1] !== expectedOutcomes[1] || !tokens[0] || !tokens[1]
|| bid == null || ask == null || bid < 0 || ask > 1 || ask < bid || liquidity < MIN_LIQUIDITY
|| raw.closed || raw.active === false || raw.acceptingOrders === false) return null;
return { ...threshold, marketId: String(raw.id || ""), question: raw.question || "", yesBid: bid, yesAsk: ask,
yesMid: prices[0], noMid: prices[1], yesToken: tokens[0], noToken: tokens[1], liquidity,
url: event.slug ? `https://polymarket.com/event/${event.slug}` : "" };
}
function evaluateEvent(event) {
const quotes = (Array.isArray(event.markets) ? event.markets : []).map((market) => quoteMarket(market, event)).filter(Boolean);
const groups = new Map();
for (const quote of quotes) {
const key = `${quote.direction}|${quote.stem}`;
const group = groups.get(key) || [];
group.push(quote); groups.set(key, group);
}
const candidates = [];
for (const group of groups.values()) {
if (group.length < 2 || new Set(group.map((quote) => quote.value)).size !== group.length) continue;
const ordered = [...group].sort((a, b) => a.value - b.value);
for (let left = 0; left < ordered.length - 1; left++) {
for (let right = left + 1; right < ordered.length; right++) {
const lower = ordered[left], higher = ordered[right];
const superset = lower.direction === "above" ? lower : higher;
const subset = lower.direction === "above" ? higher : lower;
const yesEntry = superset.yesAsk + COST_CENTS / 100;
const noEntry = 1 - subset.yesBid + COST_CENTS / 100;
const cost = yesEntry + noEntry, profit = 1 - cost, netReturn = cost > 0 ? profit / cost : 0;
candidates.push({ eventId: String(event.id || ""), title: event.title || "", direction: lower.direction,
supersetThreshold: superset.value, subsetThreshold: subset.value, cost, payout: 1, profit, netReturn,
minimumLiquidity: Math.min(superset.liquidity, subset.liquidity), url: superset.url,
legs: [{ marketId: superset.marketId, question: superset.question, side: "YES", entry: yesEntry },
{ marketId: subset.marketId, question: subset.question, side: "NO", entry: noEntry }] });
}
}
}
return candidates;
}
function evaluateDeadlineEvent(event) {
const quotes = (Array.isArray(event.markets) ? event.markets : []).map((raw) => {
const deadline = parseDeadline(raw.question), tokens = parseJson(raw.clobTokenIds).map(String);
const prices = parseJson(raw.outcomePrices).map(number), outcomes = parseJson(raw.outcomes).map((outcome) => String(outcome).trim().toLowerCase());
const bid = number(raw.bestBid), ask = number(raw.bestAsk), liquidity = number(raw.liquidityNum || raw.liquidity) || 0;
if (!deadline || tokens.length !== 2 || prices.length !== 2 || outcomes[0] !== "yes" || outcomes[1] !== "no" || !tokens[0] || !tokens[1]
|| bid == null || ask == null || bid < 0 || ask > 1 || ask < bid || liquidity < MIN_LIQUIDITY
|| raw.closed || raw.active === false || raw.acceptingOrders === false) return null;
return { ...deadline, marketId: String(raw.id || ""), question: raw.question || "", yesBid: bid, yesAsk: ask,
yesMid: prices[0], noMid: prices[1], yesToken: tokens[0], noToken: tokens[1], liquidity,
url: event.slug ? `https://polymarket.com/event/${event.slug}` : "" };
}).filter(Boolean);
const groups = new Map();
for (const quote of quotes) {
const key = `${quote.yearMode}|${quote.stem}`, group = groups.get(key) || [];
group.push(quote); groups.set(key, group);
}
const candidates = [];
for (const group of groups.values()) {
if (group.length < 2 || new Set(group.map((quote) => quote.value)).size !== group.length) continue;
const ordered = [...group].sort((a, b) => a.value - b.value);
for (let left = 0; left < ordered.length - 1; left++) {
for (let right = left + 1; right < ordered.length; right++) {
const earlier = ordered[left], later = ordered[right];
const noEntry = 1 - earlier.yesBid + COST_CENTS / 100, yesEntry = later.yesAsk + COST_CENTS / 100;
const cost = noEntry + yesEntry, profit = 1 - cost, netReturn = cost > 0 ? profit / cost : 0;
candidates.push({ eventId: String(event.id || ""), title: event.title || "", direction: "deadline",
earlierDeadline: earlier.value, laterDeadline: later.value, cost, payout: 1, profit, netReturn,
minimumLiquidity: Math.min(earlier.liquidity, later.liquidity), url: earlier.url,
legs: [{ marketId: earlier.marketId, question: earlier.question, side: "NO", entry: noEntry },
{ marketId: later.marketId, question: later.question, side: "YES", entry: yesEntry }] });
}
}
}
return candidates;
}
const events = await fetchEvents(EVENT_LIMIT);
const thresholdCandidates = events.flatMap(evaluateEvent), deadlineCandidates = events.flatMap(evaluateDeadlineEvent);
const candidates = [...thresholdCandidates, ...deadlineCandidates].sort((a, b) => b.netReturn - a.netReturn);
const actionable = candidates.filter((candidate) => candidate.profit >= MIN_NET_PROFIT && candidate.netReturn >= MIN_NET_RETURN);
const actionableThresholds = actionable.filter((candidate) => candidate.direction !== "deadline");
const actionableDeadlines = actionable.filter((candidate) => candidate.direction === "deadline");
const compact = (candidate) => ({ eventId: candidate.eventId, title: candidate.title, direction: candidate.direction,
supersetThreshold: candidate.supersetThreshold, subsetThreshold: candidate.subsetThreshold,
earlierDeadline: candidate.earlierDeadline, laterDeadline: candidate.laterDeadline,
cost: +candidate.cost.toFixed(4), payout: candidate.payout, profit: +candidate.profit.toFixed(4),
netReturn: +candidate.netReturn.toFixed(4), minimumLiquidity: +candidate.minimumLiquidity.toFixed(2),
url: candidate.url, legs: candidate.legs.map((leg) => ({ ...leg, entry: +leg.entry.toFixed(4) })) });
console.log(JSON.stringify({ generatedAt: new Date().toISOString(), requestedEvents: EVENT_LIMIT, fetchedEvents: events.length,
eligibleDominancePairs: candidates.length, thresholdPairs: thresholdCandidates.length, deadlinePairs: deadlineCandidates.length,
actionablePairs: actionable.length, actionableThresholdPairs: actionableThresholds.length, actionableDeadlinePairs: actionableDeadlines.length,
estimatedCostCentsPerLeg: COST_CENTS,
minimumLiquidityPerLeg: MIN_LIQUIDITY, minimumNetProfitPerPair: MIN_NET_PROFIT, minimumNetReturn: MIN_NET_RETURN,
actionable: actionable.slice(0, 50).map(compact), bestObserved: candidates.slice(0, 20).map(compact),
bestDeadlineObserved: deadlineCandidates.sort((a, b) => b.netReturn - a.netReturn).slice(0, 20).map(compact) }, null, 2));
+8
View File
@@ -0,0 +1,8 @@
import { auditLiquidity } from "../lib/liquidity-audit.js";
const report = await auditLiquidity({
marketLimit: Number(process.env.LIQUIDITY_MARKETS || 100),
minHoursToEnd: Number(process.env.LIQUIDITY_MIN_HOURS || 48),
});
console.log(JSON.stringify(report, null, 2));
+263
View File
@@ -0,0 +1,263 @@
const GAMMA = "https://gamma-api.polymarket.com";
const CLOB = "https://clob.polymarket.com";
const MARKET_LIMIT = Math.max(100, Math.min(500, Number(process.env.MAKER_MARKETS || 300)));
const HISTORY_DAYS = Math.max(14, Math.min(30, Number(process.env.MAKER_HISTORY_DAYS || 30)));
const CONCURRENCY = Math.max(1, Math.min(8, Number(process.env.MAKER_CONCURRENCY || 4)));
const EXIT_COST = Math.max(0, Math.min(0.05, Number(process.env.MAKER_EXIT_COST_CENTS || 0.5) / 100));
const SUMMARY_ONLY = process.env.MAKER_SUMMARY === "1";
const HOUR = 3600;
function parseJson(value) {
if (Array.isArray(value)) return value;
try { return JSON.parse(value || "[]"); } catch { return []; }
}
async function fetchJson(url, options = {}, attempts = 3) {
let lastError;
for (let attempt = 0; attempt < attempts; attempt++) {
try {
const response = await fetch(url, { ...options, signal: AbortSignal.timeout(30000),
headers: { accept: "application/json", ...(options.headers || {}) } });
if (response.ok) return response.json();
lastError = new Error(`${response.status} ${response.statusText}`);
if (response.status !== 429 && response.status < 500) break;
} catch (error) { lastError = error; }
await new Promise((resolve) => setTimeout(resolve, 600 * (attempt + 1)));
}
throw lastError || new Error("request failed");
}
async function mapLimit(items, limit, task) {
const output = new Array(items.length);
let cursor = 0;
async function worker() {
while (cursor < items.length) {
const index = cursor++;
try { output[index] = await task(items[index], index); }
catch (error) { output[index] = { error: error.message }; }
}
}
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
return output;
}
function categoryOf(raw) {
const text = `${raw.question || ""} ${(raw.tags || []).map((tag) => tag.slug || tag.label || "").join(" ")}`.toLowerCase();
if (/\b(election|president|politic|senate|congress|parliament|minister|governor|government|nominee|primary)\b/.test(text)) return "Politics";
if (/\b(bitcoin|crypto|ethereum|btc|eth|solana|xrp|token|stablecoin)\b/.test(text)) return "Crypto";
if (/\b(nba|nfl|nhl|mlb|soccer|football|baseball|basketball|tennis|ufc|boxing|championship|match|game|tournament|league)\b/.test(text)) return "Sports";
if (/\b(fed|inflation|gdp|recession|stock|company|economy|tariff|interest rate|unemployment|earnings)\b/.test(text)) return "Economy";
return "Other";
}
async function fetchMarkets(limit) {
const markets = [], seen = new Set();
for (let offset = 0; markets.length < limit && offset < limit * 5; offset += 100) {
const params = new URLSearchParams({ active: "true", closed: "false", archived: "false", include_tag: "true",
limit: "100", offset: String(offset), order: "volume24hr", ascending: "false" });
const page = await fetchJson(`${GAMMA}/markets?${params}`);
if (!Array.isArray(page) || !page.length) break;
for (const raw of page) {
const labels = parseJson(raw.outcomes).map((value) => String(value).trim().toLowerCase());
const tokens = parseJson(raw.clobTokenIds).map(String), id = String(raw.id || "");
const reward = (raw.clobRewards || []).reduce((sum, row) => sum + Number(row.rewardsDailyRate || 0), 0);
if (!id || seen.has(id) || labels[0] !== "yes" || labels[1] !== "no" || tokens.length !== 2 || reward < 1) continue;
seen.add(id);
markets.push({ id, tokens, category: categoryOf(raw), question: raw.question || "",
eventKey: String(raw.events?.[0]?.id || raw.events?.[0]?.slug || raw.eventId || id) });
if (markets.length >= limit) break;
}
if (page.length < 100) break;
}
return markets;
}
function chunks(items, size) {
const output = [];
for (let index = 0; index < items.length; index += size) output.push(items.slice(index, index + size));
return output;
}
async function fetchHistories(markets) {
const tokenRows = markets.flatMap((market) => market.tokens.map((token) => ({ token, marketId: market.id })));
const tokenChunks = chunks(tokenRows, 20);
const responses = await mapLimit(tokenChunks, CONCURRENCY, async (chunk) => fetchJson(`${CLOB}/batch-prices-history`, {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ markets: chunk.map((row) => row.token), interval: "1m", fidelity: 60 })
}));
const history = {};
responses.forEach((response) => {
if (response?.history) Object.entries(response.history).forEach(([token, points]) => {
history[token] = (points || []).map((point) => ({ t: Number(point.t), p: Number(point.p) }))
.filter((point) => Number.isFinite(point.t) && Number.isFinite(point.p)).sort((a, b) => a.t - b.t);
});
});
return { history, failures: responses.filter((response) => response?.error).length,
failureMessages: [...new Set(responses.filter((response) => response?.error).map((response) => response.error))].slice(0, 5) };
}
function atOrBefore(points, target) {
let result = null;
for (const point of points) {
if (point.t > target) break;
result = point;
}
return result;
}
function atOrAfter(points, target) {
return points.find((point) => point.t >= target) || null;
}
function observations(market, yesPoints, noPoints) {
const rows = [], seen = new Set();
for (const yes of yesPoints) {
const bucket = Math.floor(yes.t / (24 * HOUR));
if (seen.has(bucket)) continue;
const no = atOrBefore(noPoints, yes.t + HOUR);
if (!no || Math.abs(no.t - yes.t) > 2 * HOUR || yes.p < 0.08 || yes.p > 0.92) continue;
const prior = yesPoints.filter((point) => point.t >= yes.t - 24 * HOUR && point.t <= yes.t);
const futureYes = yesPoints.filter((point) => point.t > yes.t && point.t <= yes.t + 24 * HOUR);
const futureNo = noPoints.filter((point) => point.t > yes.t && point.t <= yes.t + 24 * HOUR);
if (prior.length < 12 || futureYes.length < 12 || futureNo.length < 12) continue;
const endYes = atOrBefore(yesPoints, yes.t + 24 * HOUR), endNo = atOrBefore(noPoints, yes.t + 24 * HOUR);
if (!endYes || !endNo || yes.t + 24 * HOUR - Math.min(endYes.t, endNo.t) > 2 * HOUR) continue;
seen.add(bucket);
const prices = prior.map((point) => point.p);
rows.push({ marketId: market.id, eventKey: market.eventKey, category: market.category, observedAt: yes.t,
yes: yes.p, no: no.p, priorRange: Math.max(...prices) - Math.min(...prices), futureYes, futureNo,
endYes: endYes.p, endNo: endNo.p });
}
return rows;
}
function simulate(row, rule) {
if (row.priorRange > rule.maxPriorRange || (rule.category !== "All" && row.category !== rule.category)) return null;
if (rule.band === "mid" && (row.yes < 0.2 || row.yes > 0.8)) return null;
if (rule.band === "tails" && row.yes >= 0.2 && row.yes <= 0.8) return null;
const yesQuote = row.yes - rule.gap, noQuote = row.no - rule.gap, reserved = yesQuote + noQuote;
if (yesQuote < 0.02 || noQuote < 0.02 || reserved >= 0.995) return null;
const horizonEnd = row.observedAt + rule.horizon * HOUR;
const futureYes = row.futureYes.filter((point) => point.t <= horizonEnd), futureNo = row.futureNo.filter((point) => point.t <= horizonEnd);
const endYes = atOrBefore(futureYes, horizonEnd), endNo = atOrBefore(futureNo, horizonEnd);
if (!endYes || !endNo || horizonEnd - Math.min(endYes.t, endNo.t) > 2 * HOUR) return null;
const yesFillPoint = futureYes.find((point) => point.p <= yesQuote) || null;
const noFillPoint = futureNo.find((point) => point.p <= noQuote) || null;
const yesFill = Boolean(yesFillPoint), noFill = Boolean(noFillPoint);
let pnl = 0, status = "unfilled";
if (rule.mode === "immediate-hedge" && (yesFill || noFill)) {
const yesFirst = yesFillPoint && (!noFillPoint || yesFillPoint.t < noFillPoint.t);
const noFirst = noFillPoint && (!yesFillPoint || noFillPoint.t < yesFillPoint.t);
if (!yesFirst && !noFirst) {
pnl = 1 - reserved;
status = "locked";
} else {
const first = yesFirst ? yesFillPoint : noFillPoint;
const firstQuote = yesFirst ? yesQuote : noQuote;
const otherPoints = yesFirst ? futureNo : futureYes;
const other = atOrAfter(otherPoints, first.t);
const hedgeCost = other ? firstQuote + other.p + EXIT_COST : Infinity;
if (hedgeCost < 1) {
pnl = 1 - hedgeCost;
status = "hedged-lock";
} else {
pnl = first.p - firstQuote - EXIT_COST;
status = "immediate-exit";
}
}
} else if (yesFill && noFill) { pnl = 1 - reserved; status = "locked"; }
else if (yesFill) { pnl = endYes.p - yesQuote - EXIT_COST; status = "single-exit"; }
else if (noFill) { pnl = endNo.p - noQuote - EXIT_COST; status = "single-exit"; }
return { marketId: row.marketId, eventKey: row.eventKey, observedAt: row.observedAt, pnl,
netReturn: pnl / reserved, status };
}
function summary(rows) {
if (!rows.length) return { attempts: 0, markets: 0, events: 0, pnl: 0, mean: 0, eventMean: 0, lower: 0, upper: 0, lockedRate: 0, adverseRate: 0 };
const events = new Map();
rows.forEach((row) => {
const values = events.get(row.eventKey) || [];
values.push(row.netReturn); events.set(row.eventKey, values);
});
const eventReturns = [...events.values()].map((values) => values.reduce((sum, value) => sum + value, 0) / values.length);
const eventMean = eventReturns.reduce((sum, value) => sum + value, 0) / eventReturns.length;
const variance = eventReturns.length > 1 ? eventReturns.reduce((sum, value) => sum + (value - eventMean) ** 2, 0) / (eventReturns.length - 1) : 0;
const margin = 1.645 * Math.sqrt(variance / Math.max(1, eventReturns.length));
return { attempts: rows.length, markets: new Set(rows.map((row) => row.marketId)).size, events: eventReturns.length,
pnl: rows.reduce((sum, row) => sum + row.pnl, 0), mean: rows.reduce((sum, row) => sum + row.netReturn, 0) / rows.length,
eventMean, lower: eventMean - margin, upper: eventMean + margin,
lockedRate: rows.filter((row) => row.status === "locked" || row.status === "hedged-lock").length / rows.length,
adverseRate: rows.filter((row) => row.status === "single-exit" || row.status === "immediate-exit").length / rows.length };
}
const rules = [];
for (const mode of ["wait", "immediate-hedge"]) {
for (const horizon of [3, 6, 12, 24]) {
for (const gap of [0.005, 0.01, 0.015, 0.02, 0.025, 0.03, 0.04]) {
for (const maxPriorRange of [0.02, 0.04, 0.06, 0.08, 0.12, 0.2]) {
for (const band of ["all", "mid", "tails"]) {
for (const category of ["All", "Politics", "Sports", "Crypto", "Economy", "Other"])
rules.push({ id: `${mode}_h${horizon}_gap${gap}_range${maxPriorRange}_${band}_${category}`, mode, horizon, gap, maxPriorRange, band, category });
}
}
}
}
}
const markets = await fetchMarkets(MARKET_LIMIT), fetched = await fetchHistories(markets);
const rows = markets.flatMap((market) => observations(market, fetched.history[market.tokens[0]] || [], fetched.history[market.tokens[1]] || []));
const timestamps = rows.map((row) => row.observedAt).sort((a, b) => a - b), trainCut = timestamps[Math.floor(timestamps.length * 0.6)] || 0,
validationCut = timestamps[Math.floor(timestamps.length * 0.8)] || 0;
const partitions = {
train: rows.filter((row) => row.observedAt < trainCut && row.observedAt + 24 * HOUR < trainCut),
validation: rows.filter((row) => row.observedAt >= trainCut && row.observedAt + 24 * HOUR < validationCut),
holdout: rows.filter((row) => row.observedAt >= validationCut)
};
const evaluated = rules.map((rule) => {
const train = summary(partitions.train.map((row) => simulate(row, rule)).filter(Boolean));
const validation = summary(partitions.validation.map((row) => simulate(row, rule)).filter(Boolean));
const holdout = summary(partitions.holdout.map((row) => simulate(row, rule)).filter(Boolean));
const trainPassed = train.attempts >= 80 && train.events >= 15 && train.lower > 0;
const validationPassed = trainPassed && validation.attempts >= 35 && validation.events >= 8 && validation.lower > 0;
return { rule, train, validation, holdout, trainPassed, validationPassed,
passesHoldout: validationPassed && holdout.attempts >= 35 && holdout.events >= 8 && holdout.lower > 0 };
});
const candidates = evaluated.filter((candidate) => candidate.validationPassed);
const compact = (stats) => Object.fromEntries(Object.entries(stats).map(([key, value]) => [key, Number.isFinite(value) ? +value.toFixed(5) : value]));
candidates.sort((a, b) => Number(b.passesHoldout) - Number(a.passesHoldout) || b.holdout.lower - a.holdout.lower);
const nearMisses = evaluated.filter((candidate) => candidate.train.attempts >= 80 && candidate.validation.attempts >= 35 && candidate.holdout.attempts >= 35)
.sort((a, b) => Math.min(b.train.lower, b.validation.lower) - Math.min(a.train.lower, a.validation.lower)).slice(0, 15);
const broadByGap = evaluated.filter((candidate) => candidate.rule.maxPriorRange === 0.2 && candidate.rule.band === "all" && candidate.rule.category === "All");
const report = { generatedAt: new Date().toISOString(), requestedMarkets: MARKET_LIMIT, rewardMarkets: markets.length,
marketsWithBothHistories: markets.filter((market) => fetched.history[market.tokens[0]]?.length && fetched.history[market.tokens[1]]?.length).length,
batchFailures: fetched.failures, batchFailureMessages: fetched.failureMessages, observations: rows.length, testedRules: rules.length,
methodology: { historyDays: HISTORY_DAYS, quoteHorizonHours: [3, 6, 12, 24], modes: ["wait", "immediate-hedge"], observationSpacingHours: 24, exitCostCents: EXIT_COST * 100,
split: "60% train / 20% validation / 20% untouched holdout", clusterUnit: "Polymarket event",
fillProxy: "public CLOB token price touched the resting bid after placement",
hedgeProxy: "first complementary token history point plus exit cost; production must use a fresh executable ask",
noFillPnl: 0 },
partitionRows: Object.fromEntries(Object.entries(partitions).map(([key, value]) => [key, value.length])),
trainPassed: evaluated.filter((candidate) => candidate.trainPassed).length, validationSelected: candidates.length,
holdoutPassed: candidates.filter((candidate) => candidate.passesHoldout).length,
broadByGap: broadByGap.map((candidate) => ({ horizon: candidate.rule.horizon, gap: candidate.rule.gap, train: compact(candidate.train), validation: compact(candidate.validation), holdout: compact(candidate.holdout) })),
nearMisses: nearMisses.map((candidate) => ({ rule: candidate.rule, train: compact(candidate.train), validation: compact(candidate.validation), holdout: compact(candidate.holdout) })),
candidates: candidates.slice(0, 30).map((candidate) => ({ rule: candidate.rule, passesHoldout: candidate.passesHoldout,
train: compact(candidate.train), validation: compact(candidate.validation), holdout: compact(candidate.holdout) })) };
if (SUMMARY_ONLY) {
const summarizeCandidate = (candidate) => ({ rule: candidate.rule, passesHoldout: candidate.passesHoldout,
train: { attempts: candidate.train.attempts, events: candidate.train.events, lower: +candidate.train.lower.toFixed(5), eventMean: +candidate.train.eventMean.toFixed(5) },
validation: { attempts: candidate.validation.attempts, events: candidate.validation.events, lower: +candidate.validation.lower.toFixed(5), eventMean: +candidate.validation.eventMean.toFixed(5) },
holdout: { attempts: candidate.holdout.attempts, events: candidate.holdout.events, lower: +candidate.holdout.lower.toFixed(5), eventMean: +candidate.holdout.eventMean.toFixed(5), lockedRate: +candidate.holdout.lockedRate.toFixed(4), adverseRate: +candidate.holdout.adverseRate.toFixed(4) } });
console.log(JSON.stringify({ generatedAt: report.generatedAt, requestedMarkets: report.requestedMarkets,
rewardMarkets: report.rewardMarkets, marketsWithBothHistories: report.marketsWithBothHistories,
batchFailures: report.batchFailures, observations: report.observations, testedRules: report.testedRules,
partitionRows: report.partitionRows, trainPassed: report.trainPassed, validationSelected: report.validationSelected,
holdoutPassed: report.holdoutPassed,
broadByGap: broadByGap.map((candidate) => ({ mode: candidate.rule.mode, horizon: candidate.rule.horizon, gap: candidate.rule.gap,
trainLower: +candidate.train.lower.toFixed(5), validationLower: +candidate.validation.lower.toFixed(5),
holdoutMean: +candidate.holdout.eventMean.toFixed(5), holdoutLower: +candidate.holdout.lower.toFixed(5),
holdoutLockedRate: +candidate.holdout.lockedRate.toFixed(4), holdoutAdverseRate: +candidate.holdout.adverseRate.toFixed(4) })),
nearMisses: nearMisses.slice(0, 5).map(summarizeCandidate),
candidates: candidates.slice(0, 10).map(summarizeCandidate) }, null, 2));
} else console.log(JSON.stringify(report, null, 2));
+89
View File
@@ -0,0 +1,89 @@
const GAMMA = "https://gamma-api.polymarket.com";
const EVENT_LIMIT = Math.max(20, Math.min(1000, Number(process.env.NEG_RISK_EVENTS || 300)));
const COST_CENTS = Math.max(0, Math.min(5, Number(process.env.NEG_RISK_COST_CENTS || 0.5)));
const MIN_LIQUIDITY = Math.max(0, Number(process.env.NEG_RISK_MIN_LIQUIDITY || 1000));
const MIN_NET_PROFIT = Math.max(0, Number(process.env.NEG_RISK_MIN_NET_PROFIT || 0.001));
const MIN_NET_RETURN = Math.max(0, Number(process.env.NEG_RISK_MIN_NET_RETURN || 0.0015));
async function fetchJson(url, attempts = 3) {
let lastError;
for (let attempt = 0; attempt < attempts; attempt++) {
try {
const response = await fetch(url, { signal: AbortSignal.timeout(20000), headers: { accept: "application/json" } });
if (response.ok) return response.json();
lastError = new Error(`${response.status} ${response.statusText}`);
if (response.status !== 429 && response.status < 500) break;
} catch (error) { lastError = error; }
await new Promise((resolve) => setTimeout(resolve, 500 * (attempt + 1)));
}
throw lastError || new Error("request failed");
}
function parseJson(value) {
if (Array.isArray(value)) return value;
try { return JSON.parse(value || "[]"); } catch { return []; }
}
function number(value) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
async function fetchEvents(limit) {
const events = [], pageSize = 100;
for (let offset = 0; offset < limit; offset += pageSize) {
const size = Math.min(pageSize, limit - offset);
const params = new URLSearchParams({ active: "true", closed: "false", archived: "false",
limit: String(size), offset: String(offset), order: "volume24hr", ascending: "false" });
const page = await fetchJson(`${GAMMA}/events?${params}`);
if (!Array.isArray(page) || !page.length) break;
events.push(...page);
if (page.length < size) break;
}
return events.slice(0, limit);
}
function evaluateEvent(event) {
if (!event?.negRisk || event.enableNegRisk === false) return null;
const allMarkets = Array.isArray(event.markets) ? event.markets : [];
if (allMarkets.length < 2 || allMarkets.some((market) => market.closed || market.active === false || market.acceptingOrders === false)) return null;
const legs = allMarkets.map((market) => {
const outcomes = parseJson(market.outcomes).map((outcome) => String(outcome).trim().toLowerCase());
const prices = parseJson(market.outcomePrices).map(number);
return { id: String(market.id || ""), question: market.question || "", yes: prices[0],
binaryLabels: outcomes[0] === "yes" && outcomes[1] === "no",
bid: number(market.bestBid), ask: number(market.bestAsk), liquidity: number(market.liquidityNum || market.liquidity) || 0 };
});
if (legs.some((leg) => !leg.binaryLabels || !leg.id || leg.yes == null || leg.bid == null || leg.ask == null
|| leg.bid < 0 || leg.ask > 1 || leg.ask < leg.bid || leg.liquidity < MIN_LIQUIDITY)) return null;
const count = legs.length, costPerLeg = COST_CENTS / 100;
const yesCost = legs.reduce((sum, leg) => sum + leg.ask, 0) + count * costPerLeg;
const yesProfit = 1 - yesCost;
const noCost = count - legs.reduce((sum, leg) => sum + leg.bid, 0) + count * costPerLeg;
const noProfit = count - 1 - noCost;
const yesReturn = yesCost > 0 ? yesProfit / yesCost : 0;
const noReturn = noCost > 0 ? noProfit / noCost : 0;
const side = yesReturn >= noReturn ? "YES_BUNDLE" : "NO_BUNDLE";
return { eventId: String(event.id || ""), title: event.title || "", slug: event.slug || "", markets: count,
minimumLiquidity: Math.min(...legs.map((leg) => leg.liquidity)), side,
executableCost: side === "YES_BUNDLE" ? yesCost : noCost,
worstCasePayout: side === "YES_BUNDLE" ? 1 : count - 1,
netProfitPerBundle: side === "YES_BUNDLE" ? yesProfit : noProfit,
netReturn: side === "YES_BUNDLE" ? yesReturn : noReturn,
theoreticalYesSum: legs.reduce((sum, leg) => sum + leg.yes, 0), legs };
}
const events = await fetchEvents(EVENT_LIMIT);
const evaluated = events.map(evaluateEvent).filter(Boolean).sort((a, b) => b.netReturn - a.netReturn);
const actionable = evaluated.filter((event) => event.netProfitPerBundle >= MIN_NET_PROFIT && event.netReturn >= MIN_NET_RETURN);
const compact = (event) => ({ eventId: event.eventId, title: event.title, markets: event.markets, side: event.side,
executableCost: +event.executableCost.toFixed(4), worstCasePayout: event.worstCasePayout,
netProfitPerBundle: +event.netProfitPerBundle.toFixed(4), netReturn: +event.netReturn.toFixed(4),
minimumLiquidity: +event.minimumLiquidity.toFixed(2), theoreticalYesSum: +event.theoreticalYesSum.toFixed(4),
url: event.slug ? `https://polymarket.com/event/${event.slug}` : "" });
console.log(JSON.stringify({ generatedAt: new Date().toISOString(), requestedEvents: EVENT_LIMIT,
fetchedEvents: events.length, eligibleNegativeRiskEvents: evaluated.length, actionableBundles: actionable.length,
estimatedCostCentsPerLeg: COST_CENTS, minimumLiquidityPerLeg: MIN_LIQUIDITY,
minimumNetProfitPerBundle: MIN_NET_PROFIT, minimumNetReturn: MIN_NET_RETURN,
actionable: actionable.slice(0, 50).map(compact), bestObserved: evaluated.slice(0, 20).map(compact) }, null, 2));
+167
View File
@@ -0,0 +1,167 @@
import { auditLiquidity } from "../lib/liquidity-audit.js";
const CLOB = "https://clob.polymarket.com";
const MARKET_LIMIT = Math.max(10, Math.min(250, Number(process.env.REWARD_MAKER_MARKETS || 100)));
const HISTORY_DAYS = Math.max(14, Math.min(30, Number(process.env.REWARD_MAKER_HISTORY_DAYS || 30)));
const HORIZON_HOURS = Math.max(1, Math.min(24, Number(process.env.REWARD_MAKER_HORIZON_HOURS || 3)));
const EXIT_COST = Math.max(0, Math.min(0.05, Number(process.env.REWARD_MAKER_EXIT_COST_CENTS || 1) / 100));
const SUMMARY_ONLY = process.env.REWARD_MAKER_SUMMARY === "1";
const HOUR = 3600;
async function fetchJson(url, options = {}, attempts = 4) {
let lastError;
for (let attempt = 0; attempt < attempts; attempt++) {
try {
const response = await fetch(url, { ...options, signal: AbortSignal.timeout(30000),
headers: { accept: "application/json", ...(options.headers || {}) } });
if (response.ok) return response.json();
lastError = new Error(`${response.status} ${response.statusText}`);
if (response.status !== 429 && response.status < 500) break;
} catch (error) { lastError = error; }
await new Promise((resolve) => setTimeout(resolve, 600 * (attempt + 1)));
}
throw lastError || new Error("request failed");
}
function atOrBefore(points, target) {
let lo = 0, hi = points.length - 1, answer = null;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (points[mid].t <= target) { answer = points[mid]; lo = mid + 1; }
else hi = mid - 1;
}
return answer;
}
async function fetchHistories(candidates) {
const tokenIds = [...new Set(candidates.flatMap((candidate) => candidate.clob_token_ids || []).map(String))];
const history = {};
for (let index = 0; index < tokenIds.length; index += 20) {
const batch = tokenIds.slice(index, index + 20);
const response = await fetchJson(`${CLOB}/batch-prices-history`, {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ markets: batch, interval: "1m", fidelity: 60 }),
});
Object.entries(response.history || {}).forEach(([token, points]) => {
history[token] = (points || []).map((point) => ({ t: Number(point.t), p: Number(point.p) }))
.filter((point) => Number.isFinite(point.t) && Number.isFinite(point.p)).sort((a, b) => a.t - b.t);
});
}
return history;
}
function firstTouch(points, start, end, quote) {
return points.find((point) => point.t > start && point.t <= end && point.p <= quote) || null;
}
function observations(candidate, yesPoints, noPoints) {
const rows = [], seen = new Set(), cutoff = Date.now() / 1000 - HISTORY_DAYS * 24 * HOUR;
const yesDistance = Math.max(Number(candidate.tick_size || 0.01), Number(candidate.yes_adjusted_mid) - Number(candidate.yes_quote));
const noDistance = Math.max(Number(candidate.tick_size || 0.01), Number(candidate.no_adjusted_mid) - Number(candidate.no_quote));
const size = Number(candidate.reward_min_size), endBuffer = HORIZON_HOURS * HOUR;
for (const yes of yesPoints) {
if (yes.t < cutoff || yes.t + endBuffer > Date.now() / 1000) continue;
const bucket = Math.floor(yes.t / (24 * HOUR));
if (seen.has(bucket)) continue;
const no = atOrBefore(noPoints, yes.t + HOUR);
if (!no || Math.abs(no.t - yes.t) > 2 * HOUR || yes.p <= 0.04 || yes.p >= 0.96 || no.p <= 0.04 || no.p >= 0.96) continue;
const priorYes = yesPoints.filter((point) => point.t >= yes.t - 24 * HOUR && point.t <= yes.t);
if (priorYes.length < 12 || Math.max(...priorYes.map((point) => point.p)) - Math.min(...priorYes.map((point) => point.p)) > 0.08) continue;
const yesQuote = yes.p - yesDistance, noQuote = no.p - noDistance, pairedCost = yesQuote + noQuote;
if (yesQuote < 0.02 || noQuote < 0.02 || pairedCost >= 0.995) continue;
const horizonEnd = yes.t + endBuffer;
const yesTouch = firstTouch(yesPoints, yes.t, horizonEnd, yesQuote);
const noTouch = firstTouch(noPoints, yes.t, horizonEnd, noQuote);
const endYes = atOrBefore(yesPoints, horizonEnd), endNo = atOrBefore(noPoints, horizonEnd);
if (!endYes || !endNo || horizonEnd - Math.min(endYes.t, endNo.t) > 2 * HOUR) continue;
seen.add(bucket);
let pnl = 0, status = "unfilled";
if (yesTouch && noTouch) { pnl = (1 - pairedCost) * size; status = "locked"; }
else if (yesTouch) { pnl = (endYes.p - yesQuote - EXIT_COST) * size; status = "single-exit"; }
else if (noTouch) { pnl = (endNo.p - noQuote - EXIT_COST) * size; status = "single-exit"; }
const firstFillAt = Math.min(yesTouch?.t || Infinity, noTouch?.t || Infinity);
const activeHours = Number.isFinite(firstFillAt) ? Math.max(0, Math.min(HORIZON_HOURS, (firstFillAt - yes.t) / HOUR)) : HORIZON_HOURS;
rows.push({ observedAt: yes.t, pnl, status, activeHours, capital: pairedCost * size });
}
return rows;
}
function summarize(rows, dailyReward, rewardHaircut) {
if (!rows.length) return { observations: 0, mean: 0, lower90: 0, pnl: 0, reward: 0, net: 0,
lockedRate: 0, adverseRate: 0, activeDays: 0, breakEvenRewardDaily: 0 };
const results = rows.map((row) => {
const reward = dailyReward * rewardHaircut * row.activeHours / 24;
return { ...row, reward, net: row.pnl + reward, netReturn: (row.pnl + reward) / Math.max(1, row.capital) };
});
const values = results.map((row) => row.netReturn), mean = values.reduce((sum, value) => sum + value, 0) / values.length;
const variance = values.length > 1 ? values.reduce((sum, value) => sum + (value - mean) ** 2, 0) / (values.length - 1) : 0;
const lower90 = mean - 1.645 * Math.sqrt(variance / values.length);
const pnl = results.reduce((sum, row) => sum + row.pnl, 0), activeDays = results.reduce((sum, row) => sum + row.activeHours / 24, 0);
return { observations: rows.length, mean, lower90, pnl,
reward: results.reduce((sum, row) => sum + row.reward, 0), net: results.reduce((sum, row) => sum + row.net, 0),
lockedRate: rows.filter((row) => row.status === "locked").length / rows.length,
adverseRate: rows.filter((row) => row.status === "single-exit").length / rows.length,
activeDays, breakEvenRewardDaily: Math.max(0, -pnl / Math.max(activeDays, 1e-9)) };
}
function split(rows) {
const ordered = [...rows].sort((a, b) => a.observedAt - b.observedAt);
return {
train: ordered.slice(0, Math.floor(ordered.length * 0.60)),
validation: ordered.slice(Math.floor(ordered.length * 0.60), Math.floor(ordered.length * 0.80)),
holdout: ordered.slice(Math.floor(ordered.length * 0.80)),
};
}
const compact = (stats) => Object.fromEntries(Object.entries(stats).map(([key, value]) => [key, Number.isFinite(value) ? +value.toFixed(5) : value]));
const audit = await auditLiquidity({ marketLimit: MARKET_LIMIT, minHoursToEnd: 48 });
const histories = await fetchHistories(audit.candidates);
const candidates = audit.candidates.map((candidate) => {
const rows = observations(candidate, histories[String(candidate.clob_token_ids[0])] || [], histories[String(candidate.clob_token_ids[1])] || []);
const partitions = split(rows), floor = Number(candidate.estimated_reward_floor_daily || 0);
const stress = Object.fromEntries([0, 0.25, 0.5, 1].map((haircut) => [haircut, {
all: compact(summarize(rows, floor, haircut)), train: compact(summarize(partitions.train, floor, haircut)),
validation: compact(summarize(partitions.validation, floor, haircut)), holdout: compact(summarize(partitions.holdout, floor, haircut)),
}]));
const conservative = stress[0.25], passesStress = rows.length >= 20 && partitions.validation.length >= 4 && partitions.holdout.length >= 4
&& conservative.all.lower90 > 0 && conservative.train.mean > 0 && conservative.validation.mean > 0 && conservative.holdout.mean > 0;
return { market_id: candidate.market_id, question: candidate.question, url: candidate.url, category: candidate.category,
required_capital: candidate.required_capital, estimated_reward_floor_daily: floor,
reward_share_floor: candidate.reward_share_floor, maximum_one_leg_loss: candidate.maximum_one_leg_loss,
observations: rows.length, passes_stressed_path_gate: passesStress,
reward_floor_to_break_even: conservative.all.breakEvenRewardDaily > 0 ? floor / conservative.all.breakEvenRewardDaily : null,
stress };
}).sort((a, b) => Number(b.passes_stressed_path_gate) - Number(a.passes_stressed_path_gate)
|| Number(b.reward_floor_to_break_even || 0) - Number(a.reward_floor_to_break_even || 0));
const observedCandidates = candidates.filter((candidate) => candidate.observations > 0);
const outputCandidates = SUMMARY_ONLY ? observedCandidates.slice(0, 12).map((candidate) => ({
market_id: candidate.market_id,
question: candidate.question,
estimated_reward_floor_daily: candidate.estimated_reward_floor_daily,
observations: candidate.observations,
passes_stressed_path_gate: candidate.passes_stressed_path_gate,
reward_floor_to_break_even: candidate.reward_floor_to_break_even,
no_reward: {
pnl: candidate.stress[0].all.pnl,
lower90: candidate.stress[0].all.lower90,
adverseRate: candidate.stress[0].all.adverseRate,
},
reward_at_25pct: {
net: candidate.stress[0.25].all.net,
lower90: candidate.stress[0.25].all.lower90,
trainMean: candidate.stress[0.25].train.mean,
validationMean: candidate.stress[0.25].validation.mean,
holdoutMean: candidate.stress[0.25].holdout.mean,
},
})) : candidates;
console.log(JSON.stringify({ generatedAt: new Date().toISOString(), requestedRewardMarkets: MARKET_LIMIT,
auditedRewardMarkets: audit.reward_markets, shadowQualified: audit.shadow_qualified, evaluatedCandidates: candidates.length,
candidatesWithHistory: observedCandidates.length,
stressedPathPassed: candidates.filter((candidate) => candidate.passes_stressed_path_gate).length,
methodology: { historyDays: HISTORY_DAYS, horizonHours: HORIZON_HOURS, exitCostCents: EXIT_COST * 100,
rewardStress: "25% of the current public-book reward-share floor", rewardAccrual: "stops at the first simulated fill",
fillProxy: "hourly CLOB token price touches a resting bid", split: "60% / 20% / 20% chronological path stability",
limitation: "Repeated paths from a current market are not independent events and cannot authorize capital; this gate only prioritizes live shadow observations." },
candidates: outputCandidates }, null, 2));
+255
View File
@@ -0,0 +1,255 @@
const GAMMA = "https://gamma-api.polymarket.com";
const CLOB = "https://clob.polymarket.com";
const MARKET_LIMIT = Math.max(500, Math.min(5000, Number(process.env.CALIBRATION_MARKETS || 5000)));
const MARKET_SKIP = Math.max(0, Math.min(10000, Number(process.env.CALIBRATION_SKIP || 0)));
const CONCURRENCY = Math.max(1, Math.min(12, Number(process.env.CALIBRATION_CONCURRENCY || 10)));
const COST = Math.max(0, Math.min(0.10, Number(process.env.CALIBRATION_COST_CENTS || 1) / 100));
const EXACT_GAMMA_FEES = process.env.CALIBRATION_EXACT_GAMMA_FEES === "1";
const HORIZONS = [...new Set(String(process.env.CALIBRATION_HORIZONS || "1,3,7,14,30")
.split(",").map(Number).filter((value) => Number.isFinite(value) && value >= 1 && value <= 180))].sort((a, b) => a - b);
const TARGET_RULE_IDS = String(process.env.CALIBRATION_TARGET_RULES || "3d_no_0.03-0.97_sports")
.split(",").map((value) => value.trim()).filter(Boolean);
const DAY = 86400;
function parseJson(value) {
if (Array.isArray(value)) return value;
try { return JSON.parse(value || "[]"); } catch { return []; }
}
function timestamp(value) {
const parsed = Date.parse(String(value || "").replace(" ", "T").replace(/\+00$/, "Z"));
return Number.isFinite(parsed) ? parsed / 1000 : null;
}
function categoryOf(raw) {
const tags = [...(raw.tags || []), ...(raw.events || []).flatMap((event) => event.tags || [])]
.map((tag) => `${tag.slug || ""} ${tag.label || ""}`).join(" ");
const text = `${raw.category || ""} ${raw.question || ""} ${raw.sportsMarketType || ""} ${tags}`.toLowerCase();
if (/\b(election|president|politic|senate|congress|parliament|minister|governor|government|nominee|primary)\b/.test(text)) return "Politics";
if (/\b(bitcoin|crypto|ethereum|btc|eth|solana|xrp|token|stablecoin)\b/.test(text)) return "Crypto";
if (/\b(sports?|soccer|football|basketball|baseball|tennis|hockey|cricket|golf|boxing|ufc|nba|nfl|nhl|mlb|fifa|epl|match|game|tournament)\b/.test(text)) return "Sports";
if (/\b(fed|inflation|gdp|recession|stock|company|economy|tariff|interest rate|unemployment|earnings|ipo)\b/.test(text)) return "Economy";
if (/\b(movie|music|album|box office|television|celebrity|award|gaming|youtube|stream)\b/.test(text)) return "Pop Culture";
return "Other";
}
function feeScheduleOf(raw) {
const rate = Number(raw?.feeSchedule?.rate), exponent = Number(raw?.feeSchedule?.exponent);
return Number.isFinite(rate) && rate >= 0 && Number.isFinite(exponent) && exponent > 0
? { rate, exponent } : null;
}
function takerFeePerShare(schedule, price) {
const p = Number(price), rate = Number(schedule?.rate), exponent = Number(schedule?.exponent);
if (!(p > 0 && p < 1) || !Number.isFinite(rate) || rate < 0 || !Number.isFinite(exponent) || exponent <= 0) return null;
return rate * Math.pow(p * (1 - p), exponent);
}
function sportsContestKey(raw, gameStartAt) {
const slug = String(raw?.slug || "").toLowerCase();
const datedPrefix = slug.match(/^(.+?-\d{4}-\d{2}-\d{2})(?:-|$)/)?.[1];
if (datedPrefix) return `sports:${datedPrefix}`;
const start = Number.isFinite(gameStartAt) ? String(gameStartAt) : "unknown-start";
const title = String(raw?.question || "").toLowerCase().split(":")[0]
.replace(/\b(will|win|exact score|leading at halftime|to score first)\b/g, " ").replace(/[^a-z0-9]+/g, " ").trim();
return `sports:${start}:${title || raw?.id || "unknown"}`;
}
async function fetchJson(url, attempts = 4) {
let lastError;
for (let attempt = 0; attempt < attempts; attempt++) {
try {
const response = await fetch(url, { signal: AbortSignal.timeout(30000), headers: { accept: "application/json" } });
if (response.ok) return response.json();
lastError = new Error(`${response.status} ${response.statusText}`);
if (response.status !== 429 && response.status < 500) break;
} catch (error) { lastError = error; }
await new Promise((resolve) => setTimeout(resolve, 600 * (attempt + 1)));
}
throw lastError || new Error("request failed");
}
async function mapLimit(items, limit, task) {
const output = new Array(items.length);
let cursor = 0;
async function worker() {
while (cursor < items.length) {
const index = cursor++;
try { output[index] = await task(items[index]); }
catch (error) { output[index] = { error: error.message }; }
}
}
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
return output;
}
async function fetchResolvedMarkets(limit, skip = 0) {
const markets = [], seen = new Set();
const targetCount = limit + skip;
let cursor = "";
while (markets.length < targetCount) {
const params = new URLSearchParams({ closed: "true", order: "closedTime", ascending: "false", limit: "100", include_tag: "true" });
if (cursor) params.set("after_cursor", cursor);
const payload = await fetchJson(`${GAMMA}/markets/keyset?${params}`), page = payload?.markets;
if (!Array.isArray(page) || !page.length) break;
for (const raw of page) {
const id = String(raw.id || ""), labels = parseJson(raw.outcomes).map((value) => String(value).trim().toLowerCase());
const outcomes = parseJson(raw.outcomePrices).map(Number), tokens = parseJson(raw.clobTokenIds).map(String);
const finalYes = outcomes[0] >= 0.99 && outcomes[1] <= 0.01 ? 1 : outcomes[1] >= 0.99 && outcomes[0] <= 0.01 ? 0 : null;
const closedAt = timestamp(raw.closedTime || raw.endDate), createdAt = timestamp(raw.createdAt), gameStartAt = timestamp(raw.gameStartTime);
if (!id || seen.has(id) || labels[0] !== "yes" || labels[1] !== "no" || tokens.length !== 2 || finalYes == null || !closedAt || !createdAt) continue;
seen.add(id);
const category = categoryOf(raw), eventKey = category === "Sports" ? sportsContestKey(raw, gameStartAt) : String(raw.events?.[0]?.id || id);
markets.push({ id, question: raw.question || "", eventKey, tokenId: tokens[0],
finalYes, closedAt, createdAt, gameStartAt, decisionAnchor:category === "Sports" && gameStartAt ? gameStartAt : closedAt,
category, feeSchedule: feeScheduleOf(raw) });
if (markets.length >= targetCount) break;
}
if (page.length < 100 || !payload.next_cursor || payload.next_cursor === cursor) break;
cursor = payload.next_cursor;
}
return markets.slice(skip, skip + limit);
}
function atOrBefore(points, target) {
let lo = 0, hi = points.length - 1, answer = null;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (points[mid].t <= target) { answer = points[mid]; lo = mid + 1; }
else hi = mid - 1;
}
return answer;
}
function observations(market, points) {
return HORIZONS.flatMap((horizonDays) => {
const decisionAt = market.decisionAnchor - horizonDays * DAY, point = atOrBefore(points, decisionAt);
const recent = points.filter((candidate) => candidate.t >= decisionAt - 7 * DAY && candidate.t <= decisionAt);
const recentRange = recent.length ? Math.max(...recent.map((candidate) => candidate.p)) - Math.min(...recent.map((candidate) => candidate.p)) : 0;
if (!point || decisionAt < market.createdAt + DAY || decisionAt - point.t > 36 * 3600
|| recent.length < 2 || recentRange < 0.005 || point.p <= 0.03 || point.p >= 0.97) return [];
return ["YES", "NO"].map((side) => {
const entry = side === "YES" ? point.p : 1 - point.p;
const won = side === (market.finalYes ? "YES" : "NO");
const fee = EXACT_GAMMA_FEES ? takerFeePerShare(market.feeSchedule, entry) : null;
const entryCost = COST + (EXACT_GAMMA_FEES && Number.isFinite(fee) ? fee : 0);
return { marketId: market.id, eventKey: market.eventKey, question: market.question, category: market.category,
closedAt: market.closedAt,gameStartAt:market.gameStartAt,decisionAt, horizonDays, side, entry, favorite: entry >= 0.5, won,
entryCost, exactFeeSchedule: EXACT_GAMMA_FEES && market.feeSchedule != null,
netReturn: (won ? 1 : 0) / entry - 1 - entryCost / entry };
});
});
}
function tradesFor(rows, rule) {
const grouped = new Map();
rows.filter((row) => matches(row, rule)).forEach((row) => {
const current = grouped.get(row.eventKey);
if (!current || row.entry > current.entry || (row.entry === current.entry && row.marketId < current.marketId)) {
grouped.set(row.eventKey, row);
}
});
return [...grouped.values()];
}
function summarize(rows, confidence = 1.96) {
if (!rows.length) return { trades: 0, events: 0, mean: 0, eventMean: 0, lower: 0, upper: 0, winRate: 0 };
const buckets = new Map();
rows.forEach((row) => { const values = buckets.get(row.eventKey) || []; values.push(row.netReturn); buckets.set(row.eventKey, values); });
const eventReturns = [...buckets.values()].map((values) => values.reduce((sum, value) => sum + value, 0) / values.length);
const eventMean = eventReturns.reduce((sum, value) => sum + value, 0) / eventReturns.length;
const variance = eventReturns.length > 1
? eventReturns.reduce((sum, value) => sum + (value - eventMean) ** 2, 0) / (eventReturns.length - 1) : 0;
const margin = confidence * Math.sqrt(variance / Math.max(1, eventReturns.length));
return { trades: rows.length, events: eventReturns.length,
mean: rows.reduce((sum, row) => sum + row.netReturn, 0) / rows.length,
eventMean, lower: eventMean - margin, upper: eventMean + margin,
winRate: rows.filter((row) => row.won).length / rows.length };
}
const PRICE_RANGES = [
[0.03, 0.97], [0.05, 0.25], [0.10, 0.30], [0.20, 0.40], [0.30, 0.50],
[0.40, 0.60], [0.50, 0.70], [0.60, 0.80], [0.70, 0.90], [0.75, 0.95],
];
const CATEGORIES = ["All", "Politics", "Sports", "Crypto", "Economy", "Pop Culture", "Other"];
const rules = [];
for (const horizonDays of HORIZONS) {
for (const side of ["YES", "NO", "FAVORITE", "UNDERDOG"]) {
for (const [minEntry, maxEntry] of PRICE_RANGES) {
for (const category of CATEGORIES) {
rules.push({ id: `${horizonDays}d_${side.toLowerCase()}_${minEntry}-${maxEntry}_${category.toLowerCase().replace(/\s+/g, "-")}`,
horizonDays, side, minEntry, maxEntry, category });
}
}
}
}
function matches(row, rule) {
return row.horizonDays === rule.horizonDays && row.entry >= rule.minEntry && row.entry < rule.maxEntry
&& (rule.category === "All" || row.category === rule.category)
&& (rule.side === row.side || (rule.side === "FAVORITE" && row.favorite) || (rule.side === "UNDERDOG" && !row.favorite));
}
function splitByTime(rows) {
const times = [...new Set(rows.map((row) => row.closedAt))].sort((a, b) => a - b);
const trainCut = times[Math.floor(times.length * 0.60)] || 0, validationCut = times[Math.floor(times.length * 0.80)] || 0;
return {
train: rows.filter((row) => row.closedAt < trainCut),
validation: rows.filter((row) => row.closedAt >= trainCut && row.closedAt < validationCut),
holdout: rows.filter((row) => row.closedAt >= validationCut),
trainCut, validationCut,
};
}
function stabilityWindows(rows) {
const times = [...new Set(rows.map((row) => row.closedAt))].sort((a, b) => a - b);
const cuts = [0, 0.25, 0.5, 0.75, 1].map((fraction) => times[Math.min(times.length - 1, Math.floor(times.length * fraction))] || 0);
return Array.from({ length: 4 }, (_, index) => rows.filter((row) => row.closedAt >= cuts[index]
&& (index === 3 || row.closedAt < cuts[index + 1])));
}
const markets = await fetchResolvedMarkets(MARKET_LIMIT, MARKET_SKIP);
const histories = await mapLimit(markets, CONCURRENCY, async (market) => {
const data = await fetchJson(`${CLOB}/prices-history?market=${encodeURIComponent(market.tokenId)}&interval=max&fidelity=1440`);
const points = (data.history || []).map((point) => ({ t: Number(point.t), p: Number(point.p) }))
.filter((point) => Number.isFinite(point.t) && Number.isFinite(point.p)).sort((a, b) => a.t - b.t);
return { market, points, observations: observations(market, points) };
});
const usable = histories.filter((row) => row && !row.error && row.points.length), rows = usable.flatMap((row) => row.observations);
const partitions = splitByTime(rows), windows = stabilityWindows(rows);
const evaluated = rules.map((rule) => {
const train = summarize(tradesFor(partitions.train, rule));
const validation = summarize(tradesFor(partitions.validation, rule));
const holdoutRows = tradesFor(partitions.holdout, rule), holdout = summarize(holdoutRows);
const stable = windows.map((window) => summarize(tradesFor(window, rule)));
const trainPassed = train.trades >= 100 && train.events >= 50 && train.lower > 0;
const validationPassed = trainPassed && validation.trades >= 40 && validation.events >= 20 && validation.lower > 0
&& stable.every((summary) => summary.trades >= 20 && summary.events >= 10 && summary.eventMean > 0);
const passesHoldout = validationPassed && holdout.trades >= 40 && holdout.events >= 20 && holdout.lower > 0;
return { rule, train, validation, holdout, stable, trainPassed, validationPassed, passesHoldout, holdoutRows };
});
const selected = evaluated.filter((row) => row.validationPassed)
.sort((a, b) => Number(b.passesHoldout) - Number(a.passesHoldout) || b.holdout.lower - a.holdout.lower);
const compact = (stats) => Object.fromEntries(Object.entries(stats).map(([key, value]) => [key, Number.isFinite(value) ? +value.toFixed(5) : value]));
const candidate = (row) => ({ rule: row.rule, train: compact(row.train), validation: compact(row.validation),
holdout: compact(row.holdout), stabilityMeans: row.stable.map((summary) => +summary.eventMean.toFixed(5)), passesHoldout: row.passesHoldout });
console.log(JSON.stringify({ generatedAt: new Date().toISOString(), requestedMarkets: MARKET_LIMIT, resolvedMarkets: markets.length,
historiesWithData: usable.length, failures: histories.filter((row) => row?.error).length, observations: rows.length / 2,
methodology: { selection: "most recently closed eligible Yes/No markets", horizonsDays: HORIZONS,
selectionOffset: MARKET_SKIP,historyFidelityMinutes: 1440, maximumPriceStalenessHours: 36,
modeledEntrySlippageCents: COST * 100,exactGammaEntryFeeSchedules:EXACT_GAMMA_FEES,settlementRedemptionExitFeeCents:0,
split: "60% train / 20% validation / 20% untouched holdout", confidence: "event-clustered 95% lower bound",
stability: "positive event mean in each of four chronological windows; at most one highest-entry market per underlying event or sports contest",
sportsTiming:"published gameStartTime minus horizon; sports contracts sharing the dated contest slug prefix form one cluster",
activityGate: "market open for at least 24h with at least two recent observations and a 0.5-cent seven-day price range", testedRules: rules.length,
note: "No final volume or settlement outcome enters rule features. Market selection remains a closure-time cohort, and midpoint-plus-fee-plus-slippage is still an execution approximation." },
partitionRows: { train: partitions.train.length / 2, validation: partitions.validation.length / 2, holdout: partitions.holdout.length / 2 },
trainPassed: evaluated.filter((row) => row.trainPassed).length, validationSelected: selected.length,
holdoutPassed: selected.filter((row) => row.passesHoldout).length, candidates: selected.slice(0, 25).map(candidate),
targets: evaluated.filter((row) => TARGET_RULE_IDS.includes(row.rule.id)).map(candidate),
holdoutExamples: (selected.find((row) => row.passesHoldout)?.holdoutRows || []).slice(0, 15)
.map((row) => ({ marketId: row.marketId, question: row.question, category: row.category, side: row.side,
entry: +row.entry.toFixed(4), won: row.won, netReturn: +row.netReturn.toFixed(4),
decisionAt: new Date(row.decisionAt * 1000).toISOString(), closedAt: new Date(row.closedAt * 1000).toISOString() }))
}, null, 2));
+388
View File
@@ -0,0 +1,388 @@
const GAMMA = "https://gamma-api.polymarket.com";
const CLOB = "https://clob.polymarket.com";
const MARKET_LIMIT = Math.max(20, Math.min(3000, Number(process.env.SETTLEMENT_MARKETS || 200)));
const MARKET_SKIP = Math.max(0, Math.min(10000, Number(process.env.SETTLEMENT_SKIP || 0)));
const CONCURRENCY = Math.max(1, Math.min(12, Number(process.env.SETTLEMENT_CONCURRENCY || 6)));
const HORIZON_DAYS = [...new Set(String(process.env.SETTLEMENT_HORIZONS || "1,3,7,14,30,90").split(",")
.map(Number).filter((value) => Number.isFinite(value) && value >= 1 && value <= 365))].sort((a, b) => a - b);
const COST_CENTS = Math.max(0, Math.min(5, Number(process.env.SETTLEMENT_COST_CENTS || 0.5)));
const EXACT_GAMMA_FEES = process.env.SETTLEMENT_EXACT_GAMMA_FEES === "1";
const FINE_GRID = String(process.env.SETTLEMENT_FINE_GRID || "false").toLowerCase() === "true";
const SELECTION_ORDER = ["volumeNum", "closedTime", "createdAt", "id"].includes(process.env.SETTLEMENT_ORDER)
? process.env.SETTLEMENT_ORDER : "volumeNum";
const SELECTION_ASCENDING = String(process.env.SETTLEMENT_ASCENDING || "false").toLowerCase() === "true";
const DAY = 86400;
function parseJson(value) {
if (Array.isArray(value)) return value;
try { return JSON.parse(value || "[]"); } catch { return []; }
}
function toTimestamp(value) {
const parsed = Date.parse(String(value || "").replace(" ", "T").replace(/\+00$/, "Z"));
return Number.isFinite(parsed) ? parsed / 1000 : null;
}
function categoryOf(raw) {
const text = `${raw.category || ""} ${raw.question || ""} ${(raw.events || []).flatMap((event) => event.tags || [])
.map((tag) => tag.slug || tag.label || "").join(" ")}`.toLowerCase();
if (/\b(election|president|politic|senate|congress|parliament|minister|governor|government|nominee|primary)\b/.test(text)) return "Politics";
if (/\b(bitcoin|crypto|ethereum|btc|eth|solana|xrp|token|stablecoin)\b/.test(text)) return "Crypto";
if (/\b(nba|nfl|nhl|mlb|soccer|football|baseball|basketball|tennis|ufc|boxing|championship|match|game|tournament|league)\b/.test(text)) return "Sports";
if (/\b(fed|inflation|gdp|recession|stock|company|economy|tariff|interest rate|unemployment|earnings)\b/.test(text)) return "Economy";
if (/\b(movie|music|album|box office|television|celebrity|award|gaming|youtube|stream)\b/.test(text)) return "Pop Culture";
return "Other";
}
function feeScheduleOf(raw) {
const rate = Number(raw?.feeSchedule?.rate), exponent = Number(raw?.feeSchedule?.exponent);
return Number.isFinite(rate) && rate >= 0 && Number.isFinite(exponent) && exponent > 0
? { rate, exponent } : null;
}
function takerFeePerShare(schedule, price) {
const p = Number(price), rate = Number(schedule?.rate), exponent = Number(schedule?.exponent);
if (!(p > 0 && p < 1) || !Number.isFinite(rate) || rate < 0 || !Number.isFinite(exponent) || exponent <= 0) return null;
return rate * Math.pow(p * (1 - p), exponent);
}
function hardSettlementJumpRisk(raw) {
const text = `${raw?.question || ""} ${raw?.events?.[0]?.title || ""}`.toLowerCase();
const numericRange = /\b\d+(?:\.\d+)?\s*(?:%|percent)?\s*(?:-||—|to)\s*\d+(?:\.\d+)?\s*(?:%|percent|votes?|points?|seats?|bps|basis points?|tweets?|posts?|goals?)(?![a-z])/;
const currencyRange = /[$€£]\d+(?:\.\d+)?\s*(?:-||—|to)\s*[$€£]?\d+(?:\.\d+)?/;
const betweenRange = /\bbetween\s+[$€£]?\d+(?:\.\d+)?\s*(?:%|percent)?\s+(?:and|to)\s+[$€£]?\d+(?:\.\d+)?/;
const pathDependentBarrier = /\b(?:reach|hit|touch|dip(?:\s+(?:to|below))?|rise\s+(?:to|above)|fall\s+(?:to|below))\b[^?]{0,45}(?:[$€£]\s*)?\d[\d,]*(?:\.\d+)?\s*(?:%|percent|k|m|b|points?|bps|basis points?)?/;
return numericRange.test(text) || currencyRange.test(text) || betweenRange.test(text) || pathDependentBarrier.test(text)
|| /\bexact score\b|\bscore:\s*\d|\bposts? \d+-\d+|\bnumber of (tweets|posts)\b/.test(text);
}
async function fetchJson(url, options = {}, attempts = 3) {
let lastError;
for (let attempt = 0; attempt < attempts; attempt++) {
try {
const response = await fetch(url, { ...options, signal: AbortSignal.timeout(25000),
headers: { accept: "application/json", ...(options.headers || {}) } });
if (response.ok) return response.json();
lastError = new Error(`${response.status} ${response.statusText}`);
if (response.status !== 429 && response.status < 500) break;
} catch (error) { lastError = error; }
await new Promise((resolve) => setTimeout(resolve, 500 * (attempt + 1)));
}
throw lastError || new Error("request failed");
}
async function mapLimit(items, limit, task) {
const output = new Array(items.length);
let cursor = 0;
async function worker() {
while (cursor < items.length) {
const index = cursor++;
try { output[index] = await task(items[index], index); }
catch (error) { output[index] = { error: error.message }; }
}
}
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
return output;
}
function atOrBefore(points, target) {
let lo = 0, hi = points.length - 1, answer = null;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (points[mid].t <= target) { answer = points[mid]; lo = mid + 1; }
else hi = mid - 1;
}
return answer;
}
function priceBand(price) {
if (price < 0.25) return "longshot";
if (price < 0.55) return "mid";
if (price < 0.78) return "favorite";
return "heavy-favorite";
}
function confirmedTrendAt(points, target, current) {
const dayPoint = atOrBefore(points, target - DAY), weekPoint = atOrBefore(points, target - 7 * DAY);
if (!dayPoint || !weekPoint || target - DAY - dayPoint.t > 36 * 3600 || target - 7 * DAY - weekPoint.t > 36 * 3600) return null;
const dayMove = current.p - dayPoint.p, weekMove = current.p - weekPoint.p;
const daySign = Math.sign(dayMove), weekSign = Math.sign(weekMove);
const confirmed = daySign && daySign === weekSign && Math.abs(dayMove) >= 0.006 && Math.abs(weekMove) >= 0.012
&& Math.abs(dayMove) <= 0.08 && Math.abs(weekMove) <= 0.18;
if (!confirmed) return null;
return { side: daySign > 0 ? "YES" : "NO", dayMove, weekMove,
strong: Math.abs(dayMove) >= 0.015 && Math.abs(weekMove) >= 0.03,
moderate: Math.abs(dayMove) <= 0.03 && Math.abs(weekMove) <= 0.10 };
}
async function fetchResolvedMarkets(limit, skip = 0) {
const raw = [], seen = new Set(), pageSize = 100;
const targetCount = limit + skip;
let cursor = "";
while (raw.length < targetCount) {
const params = new URLSearchParams({ closed: "true", order: SELECTION_ORDER, ascending: String(SELECTION_ASCENDING),
limit: String(pageSize) });
if (cursor) params.set("after_cursor", cursor);
const payload = await fetchJson(`${GAMMA}/markets/keyset?${params}`), page = payload?.markets;
if (!Array.isArray(page) || !page.length) break;
for (const market of page) {
const id = String(market.id || ""), labels = parseJson(market.outcomes).map((outcome) => String(outcome).trim().toLowerCase());
const outcomes = parseJson(market.outcomePrices).map(Number);
const tokens = parseJson(market.clobTokenIds), closedAt = toTimestamp(market.closedTime || market.endDate);
const resolved = outcomes.length === 2 && outcomes.every(Number.isFinite)
&& ((outcomes[0] >= 0.99 && outcomes[1] <= 0.01) || (outcomes[1] >= 0.99 && outcomes[0] <= 0.01));
if (!id || seen.has(id) || !resolved || labels[0] !== "yes" || labels[1] !== "no" || tokens.length !== 2 || !closedAt) continue;
seen.add(id); raw.push({ id, question: market.question || "", category: categoryOf(market),
eventId: String(market.events?.[0]?.id || id),
tokenId: String(tokens[0]), finalYes: outcomes[0] >= 0.99 ? 1 : 0, closedAt,
safeContract: !hardSettlementJumpRisk(market), feeSchedule: feeScheduleOf(market),
volume: Number(market.volumeNum || market.volume || 0) });
if (raw.length >= targetCount) break;
}
if (page.length < pageSize || !payload.next_cursor || payload.next_cursor === cursor) break;
cursor = payload.next_cursor;
}
return raw.slice(skip, skip + limit);
}
function evaluateMarket(market, points) {
const rows = [];
for (const horizonDays of HORIZON_DAYS) {
const target = market.closedAt - horizonDays * DAY, point = atOrBefore(points, target);
const maximumStaleness = Math.max(36 * 3600, horizonDays * DAY * 0.15);
if (!point || target - point.t > maximumStaleness || point.p <= 0.03 || point.p >= 0.97) continue;
const yesEntry = point.p, noEntry = 1 - point.p, favoriteSide = yesEntry >= noEntry ? "YES" : "NO";
const winningSide = market.finalYes ? "YES" : "NO", trend = confirmedTrendAt(points, target, point);
for (const side of ["YES", "NO"]) {
const entry = side === "YES" ? yesEntry : noEntry, final = side === winningSide ? 1 : 0;
const fee = EXACT_GAMMA_FEES ? takerFeePerShare(market.feeSchedule, entry) : null;
const entryCost = COST_CENTS / 100 + (EXACT_GAMMA_FEES && Number.isFinite(fee) ? fee : 0);
const netReturn = final / entry - 1 - entryCost / entry;
rows.push({ marketId: market.id, eventId: market.eventId, question: market.question, category: market.category,
safeContract: market.safeContract, closedAt: market.closedAt, volume: market.volume,
horizonDays, side, favorite: side === favoriteSide, winner: side === winningSide,
trend: Boolean(trend && trend.side === side), trendSide: trend?.side || null,
dayMove: trend?.dayMove || 0, weekMove: trend?.weekMove || 0,
strongTrend: Boolean(trend?.strong), moderateTrend: Boolean(trend?.moderate),
entry, band: priceBand(entry), entryCost, exactFeeSchedule: EXACT_GAMMA_FEES && market.feeSchedule != null, netReturn });
}
}
return rows;
}
function median(values) {
const sorted = [...values].sort((a, b) => a - b), mid = Math.floor(sorted.length / 2);
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
}
function summarize(rows) {
if (!rows.length) return { count: 0, events: 0, mean: 0, median: 0, winRate: 0, lower90: 0, upper90: 0,
eventMean: 0, eventLower90: 0, eventUpper90: 0, worst: 0, best: 0 };
const values = rows.map((row) => row.netReturn), mean = values.reduce((sum, value) => sum + value, 0) / values.length;
const variance = values.length > 1 ? values.reduce((sum, value) => sum + (value - mean) ** 2, 0) / (values.length - 1) : 0;
const margin90 = 1.645 * Math.sqrt(variance / values.length);
const eventBuckets = new Map();
rows.forEach((row) => {
const bucket = eventBuckets.get(row.eventId) || [];
bucket.push(row.netReturn); eventBuckets.set(row.eventId, bucket);
});
const eventReturns = [...eventBuckets.values()].map((bucket) => bucket.reduce((sum, value) => sum + value, 0) / bucket.length);
const eventMean = eventReturns.reduce((sum, value) => sum + value, 0) / eventReturns.length;
const eventVariance = eventReturns.length > 1
? eventReturns.reduce((sum, value) => sum + (value - eventMean) ** 2, 0) / (eventReturns.length - 1) : 0;
const eventMargin90 = 1.645 * Math.sqrt(eventVariance / eventReturns.length);
return { count: values.length, mean, median: median(values), winRate: rows.filter((row) => row.winner).length / rows.length,
lower90: mean - margin90, upper90: mean + margin90, events: eventReturns.length,
eventMean, eventLower90: eventMean - eventMargin90, eventUpper90: eventMean + eventMargin90,
worst: Math.min(...values), best: Math.max(...values) };
}
const SAFE_PRICE_BANDS = Object.freeze([
{ label: "50_55", min: 0.50, max: 0.55 },
{ label: "55_60", min: 0.55, max: 0.60 },
{ label: "60_70", min: 0.60, max: 0.70 },
{ label: "70_80", min: 0.70, max: 0.80 },
{ label: "80_90", min: 0.80, max: 0.90 },
{ label: "90_95", min: 0.90, max: 0.95 },
{ label: "95_97", min: 0.95, max: 0.97 },
]);
const FINE_PRICE_BANDS = Object.freeze(Array.from({ length: 18 }, (_, index) => {
const min = 0.05 + index * 0.05, max = min + 0.05;
return { label: `${Math.round(min * 100)}_${Math.round(max * 100)}`, min, max };
}));
const FINE_CATEGORIES = Object.freeze(["Politics", "Crypto", "Economy", "Pop Culture", "Other"]);
const FINE_RULES = FINE_GRID ? FINE_PRICE_BANDS.flatMap((band) => [
{ name: `grid_safe_yes_${band.label}`, test: (row) => row.side === "YES" && row.entry >= band.min && row.entry < band.max
&& row.category !== "Sports" && row.safeContract },
{ name: `grid_safe_no_${band.label}`, test: (row) => row.side === "NO" && row.entry >= band.min && row.entry < band.max
&& row.category !== "Sports" && row.safeContract },
{ name: `grid_safe_favorite_${band.label}`, test: (row) => row.favorite && row.entry >= band.min && row.entry < band.max
&& row.category !== "Sports" && row.safeContract },
...FINE_CATEGORIES.flatMap((category) => [
{ name: `grid_safe_yes_${band.label}_${category.toLowerCase().replace(/\s+/g, "_")}`,
test: (row) => row.side === "YES" && row.entry >= band.min && row.entry < band.max && row.category === category && row.safeContract },
{ name: `grid_safe_no_${band.label}_${category.toLowerCase().replace(/\s+/g, "_")}`,
test: (row) => row.side === "NO" && row.entry >= band.min && row.entry < band.max && row.category === category && row.safeContract },
]),
]) : [];
const RULES = [
{ name: "buy_favorite", test: (row) => row.favorite },
{ name: "buy_heavy_favorite", test: (row) => row.favorite && row.entry >= 0.78 },
{ name: "buy_60_78_favorite", test: (row) => row.favorite && row.entry >= 0.60 && row.entry < 0.78 },
{ name: "buy_55_60_favorite", test: (row) => row.favorite && row.entry >= 0.55 && row.entry < 0.60 },
{ name: "buy_underdog", test: (row) => !row.favorite },
{ name: "buy_yes", test: (row) => row.side === "YES" },
{ name: "buy_no", test: (row) => row.side === "NO" },
{ name: "buy_no_favorite", test: (row) => row.side === "NO" && row.favorite },
{ name: "buy_no_45_50", test: (row) => row.side === "NO" && row.entry >= 0.45 && row.entry < 0.50 },
{ name: "buy_no_55_90", test: (row) => row.side === "NO" && row.entry >= 0.55 && row.entry < 0.90 },
{ name: "buy_no_50_55", test: (row) => row.side === "NO" && row.entry >= 0.50 && row.entry < 0.55 },
{ name: "buy_no_50_55_non_sports", test: (row) => row.side === "NO" && row.entry >= 0.50 && row.entry < 0.55 && row.category !== "Sports" },
{ name: "buy_no_50_55_safe_non_sports", test: (row) => row.side === "NO" && row.entry >= 0.50 && row.entry < 0.55
&& row.category !== "Sports" && row.safeContract },
{ name: "buy_no_55_60", test: (row) => row.side === "NO" && row.entry >= 0.55 && row.entry < 0.60 },
{ name: "buy_no_60_90", test: (row) => row.side === "NO" && row.entry >= 0.60 && row.entry < 0.90 },
{ name: "buy_no_90_95", test: (row) => row.side === "NO" && row.entry >= 0.90 && row.entry < 0.95 },
{ name: "buy_no_95_99", test: (row) => row.side === "NO" && row.entry >= 0.95 && row.entry < 0.99 },
{ name: "buy_no_90_99", test: (row) => row.side === "NO" && row.entry >= 0.90 && row.entry < 0.99 },
{ name: "follow_trend", test: (row) => row.trend },
{ name: "follow_trend_yes", test: (row) => row.trend && row.side === "YES" },
{ name: "follow_trend_no", test: (row) => row.trend && row.side === "NO" },
{ name: "follow_trend_favorite", test: (row) => row.trend && row.favorite },
{ name: "follow_trend_underdog", test: (row) => row.trend && !row.favorite },
{ name: "follow_strong_trend", test: (row) => row.trend && row.strongTrend },
{ name: "follow_moderate_trend", test: (row) => row.trend && row.moderateTrend },
...SAFE_PRICE_BANDS.flatMap((band) => [
{ name: `buy_safe_favorite_${band.label}`, test: (row) => row.favorite && row.entry >= band.min && row.entry < band.max
&& row.category !== "Sports" && row.safeContract },
{ name: `buy_safe_no_${band.label}`, test: (row) => row.side === "NO" && row.entry >= band.min && row.entry < band.max
&& row.category !== "Sports" && row.safeContract },
]),
...["Politics", "Sports", "Crypto", "Economy", "Pop Culture", "Other"].flatMap((category) => [
{ name: `buy_favorite_${category.toLowerCase().replace(/\s+/g, "_")}`, test: (row) => row.favorite && row.category === category },
{ name: `buy_underdog_${category.toLowerCase().replace(/\s+/g, "_")}`, test: (row) => !row.favorite && row.category === category },
{ name: `buy_no_50_55_${category.toLowerCase().replace(/\s+/g, "_")}`,
test: (row) => row.side === "NO" && row.entry >= 0.50 && row.entry < 0.55 && row.category === category },
{ name: `follow_trend_${category.toLowerCase().replace(/\s+/g, "_")}`, test: (row) => row.trend && row.category === category },
]),
...FINE_RULES,
];
function selectEventDecisions(rows, rule) {
const selected = new Map();
for (const row of rows) {
if (!rule.test(row)) continue;
const current = selected.get(row.eventId);
if (!current || row.volume > current.volume
|| (row.volume === current.volume && row.marketId.localeCompare(current.marketId) < 0)) {
selected.set(row.eventId, row);
}
}
return [...selected.values()];
}
function evaluateRules(rows) {
return Object.fromEntries(RULES.map((rule) => [rule.name, summarize(selectEventDecisions(rows, rule))]));
}
function chronologicalEvaluation(rows) {
const ordered = [...rows].sort((a, b) => a.closedAt - b.closedAt);
const splitTime = ordered[Math.floor(ordered.length * 0.70)]?.closedAt || 0;
const cut1 = ordered[Math.floor(ordered.length / 3)]?.closedAt || 0;
const cut2 = ordered[Math.floor(ordered.length * 2 / 3)]?.closedAt || 0;
const train = ordered.filter((row) => row.closedAt < splitTime), test = ordered.filter((row) => row.closedAt >= splitTime);
const thirds = [ordered.filter((row) => row.closedAt < cut1),
ordered.filter((row) => row.closedAt >= cut1 && row.closedAt < cut2),
ordered.filter((row) => row.closedAt >= cut2)];
const pooled = evaluateRules(ordered), trainRules = evaluateRules(train), testRules = evaluateRules(test), thirdRules = thirds.map(evaluateRules);
const robustRules = Object.fromEntries(RULES.map((rule) => {
const segments = thirdRules.map((result) => result[rule.name]), all = pooled[rule.name];
const enoughData = all.events >= 15 && segments.every((segment) => segment.count >= 15 && segment.events >= 5)
&& trainRules[rule.name].count >= 30 && trainRules[rule.name].events >= 10
&& testRules[rule.name].count >= 15 && testRules[rule.name].events >= 5;
const allPositive = enoughData && all.eventLower90 > 0 && trainRules[rule.name].eventLower90 > 0
&& testRules[rule.name].eventLower90 > 0 && segments.every((segment) => segment.eventLower90 > 0);
const allNegative = enoughData && all.eventUpper90 < 0 && trainRules[rule.name].eventUpper90 < 0
&& testRules[rule.name].eventUpper90 < 0 && segments.every((segment) => segment.eventUpper90 < 0);
return [rule.name, { enoughData, allPositive, allNegative, pooled: all, train: trainRules[rule.name], test: testRules[rule.name], segments }];
}));
return { splitTime: splitTime ? new Date(splitTime * 1000).toISOString() : null, trainCount: train.length,
testCount: test.length, train: trainRules, test: testRules, thirds: thirdRules, robustRules };
}
const markets = await fetchResolvedMarkets(MARKET_LIMIT, MARKET_SKIP);
const histories = await mapLimit(markets, CONCURRENCY, async (market) => {
const data = await fetchJson(`${CLOB}/prices-history?market=${encodeURIComponent(market.tokenId)}&interval=max&fidelity=1440`);
const points = (data.history || []).map((point) => ({ t: Number(point.t), p: Number(point.p) }))
.filter((point) => Number.isFinite(point.t) && Number.isFinite(point.p)).sort((a, b) => a.t - b.t);
return { market, points, rows: evaluateMarket(market, points) };
});
const successful = histories.filter((result) => result && !result.error && result.points.length);
const rows = successful.flatMap((result) => result.rows);
const report = {
generatedAt: new Date().toISOString(), requestedMarkets: MARKET_LIMIT, resolvedMarkets: markets.length,
marketsWithHistory: successful.length, failures: histories.filter((result) => result?.error).length,
methodology: { horizonDays: HORIZON_DAYS, estimatedEntrySlippageCents: COST_CENTS,
exactGammaFeeSchedules: EXACT_GAMMA_FEES, settlementRedemptionExitFeeCents: 0,
historyFidelityMinutes: 1440,
fineGrid:FINE_GRID,
testedRules:RULES.length,
marketSelection: `${SELECTION_ORDER} ${SELECTION_ASCENDING ? "ascending" : "descending"}`,
marketSelectionOffset: MARKET_SKIP,
clusterUnit: "one highest-volume eligible contract per event and rule",
note: "Each rule uses only daily prices available at or before the decision horizon and a subsequently published binary settlement. Trend replays require aligned one-day and one-week direction under the production move bounds. Each rule makes at most one capital decision per event, choosing the highest-volume eligible contract with a stable market-ID tie-breaker. Strict robust rules require positive or negative event-level 90% bounds in pooled data, chronological train, chronological holdout, and every chronological third. Markets are selected by resolved volume, so results still carry historical-selection and execution-model limitations." },
horizons: Object.fromEntries(HORIZON_DAYS.map((horizon) => {
const horizonRows = rows.filter((row) => row.horizonDays === horizon);
return [horizon, { observations: horizonRows.length / 2, chronological: chronologicalEvaluation(horizonRows) }];
})),
};
const compact = process.env.SETTLEMENT_SUMMARY === "1";
const compactStats = (stats = {}) => ({ count: stats.count || 0, events: stats.events || 0,
mean: stats.mean || 0, eventMean: stats.eventMean || 0, eventLower90: stats.eventLower90 || 0,
eventUpper90: stats.eventUpper90 || 0, winRate: stats.winRate || 0 });
const compactRules = (rules = {}) => Object.fromEntries(Object.entries(rules)
.filter(([, result]) => result.enoughData && (result.allPositive || result.allNegative))
.map(([name, result]) => [name, { direction: result.allPositive ? "positive" : "negative",
pooled: compactStats(result.pooled), train: compactStats(result.train), test: compactStats(result.test),
segments: (result.segments || []).map(compactStats) }]));
const TARGET_RULE_NAMES = String(process.env.SETTLEMENT_TARGET_RULES || "buy_no_favorite,buy_no_50_55,buy_no_55_90,buy_no_60_90,buy_no_90_95,buy_no_95_99,buy_no_90_99")
.split(",").map((name) => name.trim()).filter(Boolean);
const targetRuleStats = (chronological, names = TARGET_RULE_NAMES) => Object.fromEntries(names.map((name) => {
const result = chronological.robustRules[name] || {};
return [name, { enoughData: Boolean(result.enoughData), allPositive: Boolean(result.allPositive), allNegative: Boolean(result.allNegative),
pooled: compactStats(result.pooled), train: compactStats(result.train), test: compactStats(result.test),
segments: (result.segments || []).map(compactStats) }];
}));
const summary = {
generatedAt: report.generatedAt, requestedMarkets: report.requestedMarkets, resolvedMarkets: report.resolvedMarkets,
marketsWithHistory: report.marketsWithHistory, failures: report.failures,
horizons: Object.fromEntries(Object.entries(report.horizons).map(([days, value]) => [days, {
observations: value.observations,
favorite: compactStats(value.chronological.train.buy_favorite),
favoriteTest: compactStats(value.chronological.test.buy_favorite),
underdog: compactStats(value.chronological.train.buy_underdog),
underdogTest: compactStats(value.chronological.test.buy_underdog),
yes: compactStats(value.chronological.train.buy_yes),
yesTest: compactStats(value.chronological.test.buy_yes),
no: compactStats(value.chronological.train.buy_no),
noTest: compactStats(value.chronological.test.buy_no),
trend: compactStats(value.chronological.train.follow_trend),
trendTest: compactStats(value.chronological.test.follow_trend),
targetRules: targetRuleStats(value.chronological),
robustRules: compactRules(value.chronological.robustRules),
}])),
};
const targetOnly = process.env.SETTLEMENT_TARGET_ONLY === "1";
const targetSummary = { generatedAt: summary.generatedAt, requestedMarkets: summary.requestedMarkets,
resolvedMarkets: summary.resolvedMarkets, marketsWithHistory: summary.marketsWithHistory, failures: summary.failures,
horizons: Object.fromEntries(Object.entries(summary.horizons).map(([days, value]) => [days, { observations: value.observations, targetRules: value.targetRules }])) };
const survivorsOnly = process.env.SETTLEMENT_SURVIVORS_ONLY === "1";
const survivorsSummary = { generatedAt: report.generatedAt, requestedMarkets: report.requestedMarkets,
resolvedMarkets: report.resolvedMarkets, marketsWithHistory: report.marketsWithHistory, failures: report.failures,
methodology: report.methodology,
horizons: Object.fromEntries(Object.entries(summary.horizons).map(([days, value]) => [days, {
observations: value.observations, robustRules: value.robustRules,
}])) };
console.log(JSON.stringify(survivorsOnly ? survivorsSummary : (targetOnly ? targetSummary : (compact ? summary : report)), null, 2));
+449
View File
@@ -0,0 +1,449 @@
import fs from "node:fs";
const GAMMA = "https://gamma-api.polymarket.com";
const CLOB = "https://clob.polymarket.com";
const MARKET_LIMIT = Math.max(100, Math.min(2000, Number(process.env.SHOCK_MARKETS || 500)));
const ACTIVE_SKIP = Math.max(0, Math.min(10000, Number(process.env.SHOCK_ACTIVE_SKIP || 0)));
const EXTERNAL_MARKET_LIMIT = Math.max(100, Math.min(2000, Number(process.env.SHOCK_EXTERNAL_MARKETS || 1000)));
const HISTORY_DAYS = Math.max(14, Math.min(30, Number(process.env.SHOCK_HISTORY_DAYS || 30)));
const CONCURRENCY = Math.max(1, Math.min(8, Number(process.env.SHOCK_CONCURRENCY || 6)));
const COST = Math.max(0, Math.min(0.05, Number(process.env.SHOCK_COST_CENTS || 0.5) / 100));
const EXACT_GAMMA_FEES = process.env.SHOCK_EXACT_GAMMA_FEES === "1";
const MIN_ENTRY_PRICE = Math.max(0.02, Math.min(0.40, Number(process.env.SHOCK_MIN_ENTRY_PRICE || 0.08)));
const MAX_ENTRY_PRICE = Math.max(0.60, Math.min(0.98, Number(process.env.SHOCK_MAX_ENTRY_PRICE || 0.92)));
const CACHE_FILE = String(process.env.SHOCK_CACHE_FILE || "").trim();
const OUTPUT_FILE = String(process.env.SHOCK_OUTPUT_FILE || "").trim();
const SUMMARY_ONLY = process.env.SHOCK_SUMMARY === "1";
const SUMMARY_CANDIDATES = Math.max(0, Math.min(10, Number(process.env.SHOCK_SUMMARY_CANDIDATES || 10)));
const CLOSED_ORDER = String(process.env.SHOCK_CLOSED_ORDER || "closedTime").trim() || "closedTime";
const CLOSED_ASCENDING = process.env.SHOCK_CLOSED_ASCENDING === "true";
const HOUR = 3600;
const STRATEGY4_MINIMUM_RUNWAY_HOURS = 14;
const HORIZONS = [3, 6, 12, 24];
const WINDOWS = [1, 3, 6, 24];
const MIN_MOVES = [0.005, 0.01, 0.02, 0.04, 0.08];
const CONFIRMATIONS = ["any", "aligned", "opposed", "accelerating"];
const MAX_VOLS = [0.01, 0.02, 0.04, 1];
const CATEGORIES = ["All", "Politics", "Sports", "Crypto", "Economy", "Pop Culture", "Other"];
const PRICE_BANDS = ["All", "longshot", "lower-mid", "center", "upper-mid", "favorite"];
function parseJson(value) {
if (Array.isArray(value)) return value;
try { return JSON.parse(value || "[]"); } catch { return []; }
}
function feeScheduleOf(raw) {
if (raw?.feesEnabled === false) return { rate: 0, exponent: 1, takerOnly: true };
const rate = Number(raw?.feeSchedule?.rate), exponent = Number(raw?.feeSchedule?.exponent);
return raw?.feesEnabled === true && Number.isFinite(rate) && rate >= 0 && Number.isFinite(exponent) && exponent > 0
? { rate, exponent, takerOnly: raw.feeSchedule.takerOnly !== false } : null;
}
function takerFeePerShare(schedule, price) {
const p = Number(price);
return schedule && Number.isFinite(p) && p > 0 && p < 1
? Number(schedule.rate) * Math.pow(p * (1 - p), Number(schedule.exponent || 1)) : null;
}
async function fetchJson(url, options = {}, attempts = 3) {
let lastError;
for (let attempt = 0; attempt < attempts; attempt++) {
try {
const response = await fetch(url, { ...options, signal: AbortSignal.timeout(30000),
headers: { accept: "application/json", ...(options.headers || {}) } });
if (response.ok) return response.json();
lastError = new Error(`${response.status} ${response.statusText}`);
if (response.status !== 429 && response.status < 500) break;
} catch (error) { lastError = error; }
await new Promise((resolve) => setTimeout(resolve, 600 * (attempt + 1)));
}
throw lastError || new Error("request failed");
}
async function mapLimit(items, limit, task) {
const output = new Array(items.length);
let cursor = 0;
async function worker() {
while (cursor < items.length) {
const index = cursor++;
try { output[index] = await task(items[index], index); }
catch (error) { output[index] = { error: error.message }; }
}
}
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
return output;
}
function categoryOf(raw) {
const text = `${raw.question || ""} ${(raw.tags || []).map((tag) => tag.slug || tag.label || "").join(" ")}`.toLowerCase();
if (/\b(election|president|politic|senate|congress|parliament|minister|governor|government|nominee|primary)\b/.test(text)) return "Politics";
if (/\b(bitcoin|crypto|ethereum|btc|eth|solana|xrp|token|stablecoin)\b/.test(text)) return "Crypto";
if (/\b(nba|nfl|nhl|mlb|soccer|football|baseball|basketball|tennis|ufc|boxing|championship|match|game|tournament|league)\b/.test(text)) return "Sports";
if (/\b(fed|inflation|gdp|recession|stock|company|economy|tariff|interest rate|unemployment|earnings)\b/.test(text)) return "Economy";
if (/\b(movie|music|album|celebrity|award|television|gaming|game release)\b/.test(text)) return "Pop Culture";
return "Other";
}
function hardSettlementJumpRisk(row) {
const text = `${row?.question || ""} ${row?.eventTitle || ""}`.toLowerCase();
const numericRange = /\b\d+(?:\.\d+)?\s*(?:%|percent)?\s*(?:-|\u2013|\u2014|to)\s*\d+(?:\.\d+)?\s*(?:%|percent|votes?|points?|seats?|bps|basis points?|tweets?|posts?|goals?)(?![a-z])/;
const currencyRange = /[$\u20ac\u00a3]\d+(?:\.\d+)?\s*(?:-|\u2013|\u2014|to)\s*[$\u20ac\u00a3]?\d+(?:\.\d+)?/;
const betweenRange = /\bbetween\s+[$\u20ac\u00a3]?\d+(?:\.\d+)?\s*(?:%|percent)?\s+(?:and|to)\s+[$\u20ac\u00a3]?\d+(?:\.\d+)?/;
const pathBarrier = /\b(?:reach|hit|touch|dip(?:\s+(?:to|below))?|rise\s+(?:to|above)|fall\s+(?:to|below))\b[^?]{0,45}(?:[$\u20ac\u00a3]\s*)?\d[\d,]*(?:\.\d+)?\s*(?:%|percent|k|m|b|points?|bps|basis points?)?/;
return numericRange.test(text) || currencyRange.test(text) || betweenRange.test(text) || pathBarrier.test(text)
|| /\bexact score\b|\bscore:\s*\d|\bposts? \d+-\d+|\bnumber of (tweets|posts)\b/.test(text);
}
function contractSafeEligible(row, horizonHours) {
return !row.hardSettlementJumpRisk && Number.isFinite(row.endTs)
&& row.endTs - row.observedAt >= (Number(horizonHours) + 2) * HOUR;
}
async function fetchMarkets(limit, universe = "active") {
const markets = [], seen = new Set();
if (universe === "closed") {
let cursor = "";
while (markets.length < limit) {
const params = new URLSearchParams({ closed: "true", order: CLOSED_ORDER, ascending: String(CLOSED_ASCENDING), limit: "100", include_tag: "true" });
if (cursor) params.set("after_cursor", cursor);
const payload = await fetchJson(`${GAMMA}/markets/keyset?${params}`), page = payload?.markets;
if (!Array.isArray(page) || !page.length) break;
for (const raw of page) {
const labels = parseJson(raw.outcomes).map((value) => String(value).trim().toLowerCase());
const tokens = parseJson(raw.clobTokenIds).map(String), outcomes = parseJson(raw.outcomePrices).map(Number), id = String(raw.id || "");
const resolved = outcomes.length === 2 && outcomes.every(Number.isFinite)
&& ((outcomes[0] >= 0.99 && outcomes[1] <= 0.01) || (outcomes[1] >= 0.99 && outcomes[0] <= 0.01));
if (!id || seen.has(id) || !resolved || labels[0] !== "yes" || labels[1] !== "no" || tokens.length !== 2) continue;
seen.add(id);
const endTs = Date.parse(raw.endDate || raw.events?.[0]?.endDate || "") / 1000;
markets.push({ id, token: tokens[0], category: categoryOf(raw), question: raw.question || "", feeSchedule: feeScheduleOf(raw),
eventTitle: raw.events?.[0]?.title || "", endTs: Number.isFinite(endTs) ? endTs : null,
eventKey: String(raw.events?.[0]?.id || raw.events?.[0]?.slug || raw.eventId || id) });
if (markets.length >= limit) break;
}
if (page.length < 100 || !payload.next_cursor || payload.next_cursor === cursor) break;
cursor = payload.next_cursor;
}
return markets;
}
let cursor = "", skipped = 0;
while (markets.length < limit) {
const params = new URLSearchParams({ active: "true", closed: "false", archived: "false", include_tag: "true",
limit: "100", order: "volume24hr", ascending: "false" });
if (cursor) params.set("after_cursor", cursor);
const payload = await fetchJson(`${GAMMA}/markets/keyset?${params}`), page = payload?.markets;
if (!Array.isArray(page) || !page.length) break;
for (const raw of page) {
const labels = parseJson(raw.outcomes).map((value) => String(value).trim().toLowerCase());
const tokens = parseJson(raw.clobTokenIds).map(String), id = String(raw.id || "");
if (!id || seen.has(id) || labels[0] !== "yes" || labels[1] !== "no" || tokens.length !== 2) continue;
if (skipped < ACTIVE_SKIP) { skipped++; continue; }
seen.add(id);
const endTs = Date.parse(raw.endDate || raw.events?.[0]?.endDate || "") / 1000;
markets.push({ id, token: tokens[0], category: categoryOf(raw), question: raw.question || "", feeSchedule: feeScheduleOf(raw),
eventTitle: raw.events?.[0]?.title || "", endTs: Number.isFinite(endTs) ? endTs : null,
eventKey: String(raw.events?.[0]?.id || raw.events?.[0]?.slug || raw.eventId || id) });
if (markets.length >= limit) break;
}
if (page.length < 100 || !payload.next_cursor || payload.next_cursor === cursor) break;
cursor = payload.next_cursor;
}
return markets;
}
function chunks(items, size) {
const output = [];
for (let index = 0; index < items.length; index += size) output.push(items.slice(index, index + size));
return output;
}
async function fetchHistories(markets, { lookbackDays = HISTORY_DAYS } = {}) {
const now = Math.floor(Date.now() / 1000);
const windows = [];
if (lookbackDays == null) windows.push(null);
else for (let end = now; end > now - lookbackDays * 86400; end -= 15 * 86400)
windows.push({ start: Math.max(now - lookbackDays * 86400, end - 15 * 86400), end });
const jobs = windows.flatMap((window) => chunks(markets, 20).map((chunk) => ({ window, chunk })));
const responses = await mapLimit(jobs, CONCURRENCY, async ({ window, chunk }) => fetchJson(`${CLOB}/batch-prices-history`, {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ markets: chunk.map((market) => market.token), fidelity: 60,
...(window == null ? { interval: "max" } : { start_ts: window.start, end_ts: window.end }) })
}));
const history = {};
responses.forEach((response) => {
if (!response?.history) return;
Object.entries(response.history).forEach(([token, points]) => {
history[token] = [...(history[token] || []), ...(points || []).map((point) => ({ t: Number(point.t), p: Number(point.p) }))
.filter((point) => Number.isFinite(point.t) && Number.isFinite(point.p))];
});
});
Object.entries(history).forEach(([token, points]) => {
history[token] = [...new Map(points.sort((a, b) => a.t - b.t).map((point) => [point.t, point])).values()];
});
return { history, failures: responses.filter((response) => response?.error).length };
}
function atOrBefore(points, target) {
let lo = 0, hi = points.length - 1, answer = null;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (points[mid].t <= target) { answer = points[mid]; lo = mid + 1; }
else hi = mid - 1;
}
return answer;
}
function atOrAfter(points, target) {
let lo = 0, hi = points.length - 1, answer = null;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (points[mid].t >= target) { answer = points[mid]; hi = mid - 1; }
else lo = mid + 1;
}
return answer;
}
function stddev(values) {
if (values.length < 2) return 0;
const mean = values.reduce((sum, value) => sum + value, 0) / values.length;
return Math.sqrt(values.reduce((sum, value) => sum + (value - mean) ** 2, 0) / (values.length - 1));
}
function stableHash(value) {
let hash = 2166136261;
for (const char of String(value)) { hash ^= char.charCodeAt(0); hash = Math.imul(hash, 16777619); }
return hash >>> 0;
}
function priceBand(price) {
if (price < 0.25) return "longshot";
if (price < 0.45) return "lower-mid";
if (price <= 0.55) return "center";
if (price <= 0.75) return "upper-mid";
return "favorite";
}
function observations(market, points) {
const rows = [], seen = new Set();
for (const current of points) {
const bucket = Math.floor(current.t / (3 * HOUR));
if (seen.has(bucket) || current.p < 0.05 || current.p > 0.95) continue;
const past = Object.fromEntries(WINDOWS.map((window) => [window, atOrBefore(points, current.t - window * HOUR)]));
const priorOne = atOrBefore(points, current.t - HOUR), priorTwo = atOrBefore(points, current.t - 2 * HOUR);
if (WINDOWS.some((window) => !past[window]) || !priorOne || !priorTwo || current.t - past[24].t > 26 * HOUR) continue;
const prior = points.filter((point) => point.t >= current.t - 24 * HOUR && point.t <= current.t);
if (prior.length < 12) continue;
const hourlyReturns = [];
for (let index = 1; index < prior.length; index++) hourlyReturns.push(prior[index].p - prior[index - 1].p);
const features = { moves: Object.fromEntries(WINDOWS.map((window) => [window, current.p - past[window].p])),
oneMove: current.p - priorOne.p, priorOneMove: priorOne.p - priorTwo.p, volatility: stddev(hourlyReturns) };
let captured = false;
for (const horizon of HORIZONS) {
const future = atOrAfter(points, current.t + horizon * HOUR);
if (!future || future.t - (current.t + horizon * HOUR) > 2 * HOUR) continue;
rows.push({ marketId: market.id, eventKey: market.eventKey, category: market.category, question: market.question,
feeSchedule: market.feeSchedule,
endTs: Number(market.endTs), hardSettlementJumpRisk: hardSettlementJumpRisk(market), observedAt: current.t,
evaluatedAt: future.t, horizon, price: current.p, futurePrice: future.p, ...features });
captured = true;
}
if (captured) seen.add(bucket);
}
return rows;
}
function confirmationPass(row, move, confirmation) {
const direction = Math.sign(move), oneDirection = Math.sign(row.oneMove);
if (confirmation === "any") return true;
if (confirmation === "aligned") return direction !== 0 && oneDirection === direction;
if (confirmation === "opposed") return direction !== 0 && oneDirection === -direction;
return direction !== 0 && oneDirection === direction && Math.abs(row.oneMove) >= Math.abs(row.priorOneMove);
}
function simulate(row, rule) {
if (row.horizon !== rule.horizon || row.volatility > rule.maxVol) return null;
if (rule.contractGate === "strategy4" && !contractSafeEligible(row, rule.horizon)) return null;
if (rule.category !== "All" && row.category !== rule.category) return null;
const move = row.moves[rule.window];
if (Math.abs(move) < rule.minMove || !confirmationPass(row, move, rule.confirmation)) return null;
const followedSide = Math.sign(move) > 0 ? "YES" : "NO";
const side = rule.direction === "continue" ? followedSide : followedSide === "YES" ? "NO" : "YES";
const entry = side === "YES" ? row.price : 1 - row.price, exit = side === "YES" ? row.futurePrice : 1 - row.futurePrice;
if (entry < MIN_ENTRY_PRICE || entry > MAX_ENTRY_PRICE) return null;
const band = priceBand(entry);
if (rule.band !== "All" && band !== rule.band) return null;
const entryFee = EXACT_GAMMA_FEES ? takerFeePerShare(row.feeSchedule, entry) : null;
const exitFee = EXACT_GAMMA_FEES ? takerFeePerShare(row.feeSchedule, exit) : null;
const roundTripCost = EXACT_GAMMA_FEES && Number.isFinite(entryFee) && Number.isFinite(exitFee) ? entryFee + exitFee : COST;
const rawNetReturn = (exit - entry - roundTripCost) / entry;
const netReturn = Math.max(-1, Math.min(2, rawNetReturn));
return { marketId: row.marketId, eventKey: row.eventKey, observedAt: row.observedAt, evaluatedAt: row.evaluatedAt,
category: row.category, band, side, entry, exit, roundTripCost, exactFeeSchedule: EXACT_GAMMA_FEES && Number.isFinite(entryFee) && Number.isFinite(exitFee), netReturn };
}
function summary(rows) {
if (!rows.length) return { attempts: 0, markets: 0, events: 0, mean: 0, eventMean: 0, lower: 0, upper: 0, winRate: 0 };
const events = new Map();
rows.forEach((row) => { const bucket = events.get(row.eventKey) || []; bucket.push(row.netReturn); events.set(row.eventKey, bucket); });
const eventReturns = [...events.values()].map((values) => values.reduce((sum, value) => sum + value, 0) / values.length);
const eventMean = eventReturns.reduce((sum, value) => sum + value, 0) / eventReturns.length;
const variance = eventReturns.length > 1 ? eventReturns.reduce((sum, value) => sum + (value - eventMean) ** 2, 0) / (eventReturns.length - 1) : 0;
const margin = 1.645 * Math.sqrt(variance / Math.max(1, eventReturns.length));
return { attempts: rows.length, markets: new Set(rows.map((row) => row.marketId)).size, events: eventReturns.length,
mean: rows.reduce((sum, row) => sum + row.netReturn, 0) / rows.length, eventMean, lower: eventMean - margin, upper: eventMean + margin,
winRate: rows.filter((row) => row.netReturn > 0).length / rows.length };
}
const baseRules = [];
for (const horizon of HORIZONS) for (const window of WINDOWS) for (const minMove of MIN_MOVES)
for (const direction of ["continue", "fade"]) for (const confirmation of CONFIRMATIONS) for (const maxVol of MAX_VOLS)
baseRules.push({ id: `${direction}_h${horizon}_w${window}_m${minMove}_${confirmation}_v${maxVol}`,
baseId: `${direction}_h${horizon}_w${window}_m${minMove}_${confirmation}_v${maxVol}`, horizon, window, minMove,
direction, confirmation, maxVol, category: "All", band: "All" });
let markets, fetched;
if (CACHE_FILE && fs.existsSync(CACHE_FILE)) {
const cached = JSON.parse(fs.readFileSync(CACHE_FILE, "utf8"));
if (Number(cached.activeSkip || 0) !== ACTIVE_SKIP) throw new Error(`Shock cache active-skip mismatch: expected ${ACTIVE_SKIP}`);
markets = cached.markets || []; fetched = { history: cached.history || {}, failures: Number(cached.failures || 0) };
} else {
markets = await fetchMarkets(MARKET_LIMIT); fetched = await fetchHistories(markets);
if (CACHE_FILE) fs.writeFileSync(CACHE_FILE, JSON.stringify({ activeSkip: ACTIVE_SKIP, markets, history: fetched.history, failures: fetched.failures }));
}
const rows = markets.flatMap((market) => observations(market, fetched.history[market.token] || []));
const eventHoldoutRows = rows.filter((row) => stableHash(row.eventKey) % 4 === 0);
const developmentRows = rows.filter((row) => stableHash(row.eventKey) % 4 !== 0);
const timestamps = [...new Set(developmentRows.map((row) => row.observedAt))].sort((a, b) => a - b);
const trainCut = timestamps[Math.floor(timestamps.length * 0.6)] || 0, validationCut = timestamps[Math.floor(timestamps.length * 0.8)] || 0;
const partitions = { train: developmentRows.filter((row) => row.observedAt < trainCut && row.evaluatedAt < trainCut),
validation: developmentRows.filter((row) => row.observedAt >= trainCut && row.evaluatedAt < validationCut),
holdout: developmentRows.filter((row) => row.observedAt >= validationCut) };
const partitionByHorizon = Object.fromEntries(Object.entries(partitions).map(([name, partitionRows]) => [name,
Object.fromEntries(HORIZONS.map((horizon) => [horizon, partitionRows.filter((row) => row.horizon === horizon)]))]));
const simulationCache = Object.fromEntries(Object.keys(partitions).map((name) => [name, new Map()]));
function simulatedRows(rule, partition) {
const key = `${rule.baseId || rule.id}|${rule.contractGate || "all"}`;
if (!simulationCache[partition].has(key)) {
const baseRule = { ...rule, category: "All", band: "All" };
simulationCache[partition].set(key, (partitionByHorizon[partition][rule.horizon] || []).map((row) => simulate(row, baseRule)).filter(Boolean));
}
return simulationCache[partition].get(key).filter((row) => (rule.category === "All" || row.category === rule.category)
&& (rule.band === "All" || row.band === rule.band));
}
function evaluate(rule, partition) { return summary(simulatedRows(rule, partition)); }
const trainWinners = baseRules.map((rule) => ({ rule, train: evaluate(rule, "train") }))
.filter((candidate) => candidate.train.attempts >= 100 && candidate.train.events >= 20 && candidate.train.lower > 0);
const refinedRules = [];
for (const candidate of trainWinners) {
for (const category of CATEGORIES)
for (const band of PRICE_BANDS)
refinedRules.push({ ...candidate.rule, id: `${candidate.rule.id}_${category}_${band}`, category, band });
}
const uniqueRefined = [...new Map(refinedRules.map((rule) => [rule.id, rule])).values()];
const validationWinners = uniqueRefined.map((rule) => ({ rule, train: evaluate(rule, "train"), validation: evaluate(rule, "validation") }))
.filter((candidate) => candidate.train.attempts >= 80 && candidate.train.events >= 15 && candidate.train.lower > 0
&& candidate.validation.attempts >= 35 && candidate.validation.events >= 8 && candidate.validation.lower > 0);
const holdout = validationWinners.map((candidate) => ({ ...candidate, holdout: evaluate(candidate.rule, "holdout") }))
.map((candidate) => ({ ...candidate, passesHoldout: candidate.holdout.attempts >= 35 && candidate.holdout.events >= 8 && candidate.holdout.lower > 0 }))
.sort((a, b) => Number(b.passesHoldout) - Number(a.passesHoldout) || b.holdout.lower - a.holdout.lower);
const finalists = holdout.filter((candidate) => candidate.passesHoldout);
finalists.forEach((candidate) => {
candidate.eventHoldout = summary(eventHoldoutRows.map((row) => simulate(row, candidate.rule)).filter(Boolean));
candidate.passesEventHoldout = candidate.eventHoldout.attempts >= 35 && candidate.eventHoldout.events >= 8
&& candidate.eventHoldout.mean > 0 && candidate.eventHoldout.lower > 0;
});
let external = { markets: [], histories: {}, failures: 0, rows: [] };
if (finalists.length) {
const activeEvents = new Set(markets.map((market) => market.eventKey));
external.markets = (await fetchMarkets(EXTERNAL_MARKET_LIMIT, "closed")).filter((market) => !activeEvents.has(market.eventKey));
const externalFetched = await fetchHistories(external.markets, { lookbackDays: null });
external.histories = externalFetched.history;
external.failures = externalFetched.failures;
external.rows = external.markets.flatMap((market) => observations(market, external.histories[market.token] || []));
finalists.forEach((candidate) => {
candidate.archive = summary(external.rows.map((row) => simulate(row, candidate.rule)).filter(Boolean));
candidate.passesArchive = candidate.archive.attempts >= 100 && candidate.archive.events >= 20
&& candidate.archive.mean > 0 && candidate.archive.lower > 0;
});
}
const compact = (stats) => Object.fromEntries(Object.entries(stats).map(([key, value]) => [key, Number.isFinite(value) ? +value.toFixed(5) : value]));
const exactStrategy3Rule = { id: "fade_h12_w3_m0.08_accelerating_v1_All_All", baseId: "fade_h12_w3_m0.08_accelerating_v1",
horizon: 12, window: 3, minMove: 0.08, direction: "fade", confirmation: "accelerating", maxVol: 1, category: "All", band: "All" };
const exactStrategy3 = { rule: exactStrategy3Rule,
train: compact(evaluate(exactStrategy3Rule, "train")), validation: compact(evaluate(exactStrategy3Rule, "validation")),
holdout: compact(evaluate(exactStrategy3Rule, "holdout")),
eventHoldout: compact(summary(eventHoldoutRows.map((row) => simulate(row, exactStrategy3Rule)).filter(Boolean))) };
const exactStrategy4Rule = { ...exactStrategy3Rule, id: "fade_h12_w3_m0.08_accelerating_v1_contract_safe_v4", contractGate: "strategy4" };
const exactStrategy4 = { rule: exactStrategy4Rule,
train: compact(evaluate(exactStrategy4Rule, "train")), validation: compact(evaluate(exactStrategy4Rule, "validation")),
holdout: compact(evaluate(exactStrategy4Rule, "holdout")),
eventHoldout: compact(summary(eventHoldoutRows.map((row) => simulate(row, exactStrategy4Rule)).filter(Boolean))) };
const strategy4Refinements = CATEGORIES.flatMap((category) => PRICE_BANDS.map((band) => {
const rule = { ...exactStrategy4Rule, id: `${exactStrategy4Rule.id}_${category}_${band}`, category, band };
return { rule, train: compact(evaluate(rule, "train")), validation: compact(evaluate(rule, "validation")),
holdout: compact(evaluate(rule, "holdout")),
eventHoldout: compact(summary(eventHoldoutRows.map((row) => simulate(row, rule)).filter(Boolean))) };
}));
const exactPrimaryWinner24Rule = { ...exactStrategy3Rule, id: "fade_h24_w3_m0.08_accelerating_v1_All_All",
baseId: "fade_h24_w3_m0.08_accelerating_v1", horizon: 24 };
const exactPrimaryWinner24 = { rule: exactPrimaryWinner24Rule,
train: compact(evaluate(exactPrimaryWinner24Rule, "train")), validation: compact(evaluate(exactPrimaryWinner24Rule, "validation")),
holdout: compact(evaluate(exactPrimaryWinner24Rule, "holdout")),
eventHoldout: compact(summary(eventHoldoutRows.map((row) => simulate(row, exactPrimaryWinner24Rule)).filter(Boolean))) };
const exactContractSafe24Rule = { ...exactPrimaryWinner24Rule,
id: "fade_h24_w3_m0.08_accelerating_v1_contract_safe", contractGate: "strategy4" };
const exactContractSafe24 = { rule: exactContractSafe24Rule,
train: compact(evaluate(exactContractSafe24Rule, "train")), validation: compact(evaluate(exactContractSafe24Rule, "validation")),
holdout: compact(evaluate(exactContractSafe24Rule, "holdout")),
eventHoldout: compact(summary(eventHoldoutRows.map((row) => simulate(row, exactContractSafe24Rule)).filter(Boolean))) };
const coverageDays = Object.values(fetched.history).map((points) => points.length > 1 ? (points.at(-1).t - points[0].t) / 86400 : 0).sort((a, b) => a - b);
const medianCoverageDays = coverageDays.length ? coverageDays[Math.floor(coverageDays.length / 2)] : 0;
const candidateRow = (candidate) => ({ rule: candidate.rule, passesHoldout: candidate.passesHoldout,
passesEventHoldout: Boolean(candidate.passesEventHoldout), passesArchive: Boolean(candidate.passesArchive),
train: compact(candidate.train), validation: compact(candidate.validation), holdout: compact(candidate.holdout),
eventHoldout: candidate.eventHoldout ? compact(candidate.eventHoldout) : null, archive: candidate.archive ? compact(candidate.archive) : null });
const report = { generatedAt: new Date().toISOString(), requestedMarkets: MARKET_LIMIT, activeMarketsSkipped: ACTIVE_SKIP, markets: markets.length,
marketsWithHistory: markets.filter((market) => fetched.history[market.token]?.length).length, fetchFailures: fetched.failures,
medianHistoryCoverageDays: +medianCoverageDays.toFixed(2), maximumHistoryCoverageDays: +(coverageDays.at(-1) || 0).toFixed(2),
observations: rows.length, testedBaseRules: baseRules.length, trainWinners: trainWinners.length, testedRefinements: uniqueRefined.length,
validationWinners: validationWinners.length, holdoutPassed: holdout.filter((candidate) => candidate.passesHoldout).length,
eventHoldoutObservations: eventHoldoutRows.length, eventHoldoutEvents: new Set(eventHoldoutRows.map((row) => row.eventKey)).size,
eventHoldoutPassed: finalists.filter((candidate) => candidate.passesEventHoldout).length,
externalRequestedMarkets: finalists.length ? EXTERNAL_MARKET_LIMIT : 0, externalMarkets: external.markets.length,
externalMarketsWithHistory: external.markets.filter((market) => external.histories[market.token]?.length).length,
externalFetchFailures: external.failures, externalObservations: external.rows.length,
archivePassed: finalists.filter((candidate) => candidate.passesArchive).length,
methodology: { requestedHistoryDays: HISTORY_DAYS, medianHistoryCoverageDays: +medianCoverageDays.toFixed(2),
maximumHistoryCoverageDays: +(coverageDays.at(-1) || 0).toFixed(2), observationSpacingHours: 3, horizons: HORIZONS, windows: WINDOWS,
costCents: COST * 100, exactGammaFeeSchedules: EXACT_GAMMA_FEES, feeFallbackCents: COST * 100,
entryPriceRange: [MIN_ENTRY_PRICE, MAX_ENTRY_PRICE], returnWinsorization: [-1, 2],
eventSplit: "25% deterministic event-disjoint holdout reserved before search",
split: "Remaining events use 60% train / 20% validation / 20% untouched chronological holdout with future-mark purge",
clusterUnit: "Polymarket event", refinementSource: "Only base rules with a positive train lower bound are refined by category and entry band",
closedArchiveOrder: `${CLOSED_ORDER} ${CLOSED_ASCENDING ? "ascending" : "descending"}`,
strategy4ContractGate: `Reject path barriers and exact numeric ranges; require ${STRATEGY4_MINIMUM_RUNWAY_HOURS} hours to market end at entry`,
limitation: "Current active-market selection is survivorship biased; any holdout winner still requires resolved-market external validation" },
partitions: Object.fromEntries(Object.entries(partitions).map(([key, value]) => [key, value.length])), exactStrategy3, exactStrategy4,
strategy4Refinements, exactPrimaryWinner24, exactContractSafe24,
candidates: holdout.slice(0, 30).map(candidateRow) };
const output = SUMMARY_ONLY ? { generatedAt: report.generatedAt, activeMarketsSkipped: report.activeMarketsSkipped, markets: report.markets,
marketsWithHistory: report.marketsWithHistory, fetchFailures: report.fetchFailures, observations: report.observations,
medianHistoryCoverageDays: report.medianHistoryCoverageDays, maximumHistoryCoverageDays: report.maximumHistoryCoverageDays,
testedBaseRules: report.testedBaseRules, trainWinners: report.trainWinners, testedRefinements: report.testedRefinements,
validationWinners: report.validationWinners, holdoutPassed: report.holdoutPassed,
eventHoldoutObservations: report.eventHoldoutObservations, eventHoldoutEvents: report.eventHoldoutEvents,
eventHoldoutPassed: report.eventHoldoutPassed, externalRequestedMarkets: report.externalRequestedMarkets,
externalMarkets: report.externalMarkets, externalMarketsWithHistory: report.externalMarketsWithHistory,
externalFetchFailures: report.externalFetchFailures, externalObservations: report.externalObservations,
archivePassed: report.archivePassed, partitions: report.partitions, exactStrategy3: report.exactStrategy3, exactStrategy4: report.exactStrategy4,
strategy4Refinements: report.strategy4Refinements,
exactPrimaryWinner24: report.exactPrimaryWinner24, exactContractSafe24: report.exactContractSafe24,
candidates: report.candidates.slice(0, SUMMARY_CANDIDATES) } : report;
const serialized = JSON.stringify(output, null, 2);
if (OUTPUT_FILE) fs.writeFileSync(OUTPUT_FILE, serialized + "\n");
else console.log(serialized);
+361
View File
@@ -0,0 +1,361 @@
const GAMMA = "https://gamma-api.polymarket.com";
const CLOB = "https://clob.polymarket.com";
const MARKET_LIMIT = Math.max(10, Math.min(1000, Number(process.env.EVAL_MARKETS || 80)));
const ACTIVE_SKIP = Math.max(0, Math.min(5000, Number(process.env.EVAL_SKIP || 0)));
const CONCURRENCY = Math.max(1, Math.min(12, Number(process.env.EVAL_CONCURRENCY || 6)));
const HORIZONS = [...new Set(String(process.env.EVAL_HORIZONS || "6,12,24,72").split(",")
.map(Number).filter((value) => Number.isFinite(value) && value >= 1 && value <= 168))].sort((a, b) => a - b);
const COST_CENTS = Math.max(0, Math.min(5, Number(process.env.EVAL_COST_CENTS || 0.5)));
const EXACT_GAMMA_FEES = process.env.EVAL_EXACT_GAMMA_FEES === "1";
const HOUR = 3600;
const CATEGORY_RULES = [
["Politics", ["politics", "election", "elections", "us-politics", "geopolitics", "trump", "government", "congress", "policy", "democrats", "republicans"]],
["Crypto", ["crypto", "bitcoin", "ethereum", "btc", "eth", "solana", "defi", "stablecoin", "xrp"]],
["Sports", ["sports", "soccer", "football", "nba", "nfl", "mlb", "nhl", "tennis", "basketball", "baseball", "ufc", "boxing", "golf", "f1"]],
["Economy", ["economy", "business", "fed", "inflation", "interest-rates", "gdp", "jobs", "recession", "stocks", "earnings", "tariffs"]],
["Pop Culture", ["pop-culture", "entertainment", "movies", "music", "tv", "awards", "celebrity", "gaming", "ai"]],
];
function parseJson(value) {
if (Array.isArray(value)) return value;
try { return JSON.parse(value || "[]"); } catch { return []; }
}
function categoryOf(raw) {
const tags = (Array.isArray(raw.tags) ? raw.tags : []).map((tag) => String(tag.slug || tag.label || "").toLowerCase());
return CATEGORY_RULES.find(([, keys]) => tags.some((tag) => keys.includes(tag)))?.[0] || "Other";
}
function feeScheduleOf(raw) {
const rate = Number(raw?.feeSchedule?.rate), exponent = Number(raw?.feeSchedule?.exponent);
return Number.isFinite(rate) && rate >= 0 && Number.isFinite(exponent) && exponent > 0
? { rate, exponent } : null;
}
function takerFeePerShare(schedule, price) {
const p = Number(price), rate = Number(schedule?.rate), exponent = Number(schedule?.exponent);
if (!(p > 0 && p < 1) || !Number.isFinite(rate) || rate < 0 || !Number.isFinite(exponent) || exponent <= 0) return null;
return rate * Math.pow(p * (1 - p), exponent);
}
async function fetchJson(url, attempts = 3) {
let lastError;
for (let attempt = 0; attempt < attempts; attempt++) {
try {
const response = await fetch(url, { signal: AbortSignal.timeout(20000), headers: { accept: "application/json" } });
if (response.ok) return response.json();
lastError = new Error(`${response.status} ${response.statusText}`);
if (response.status !== 429 && response.status < 500) break;
} catch (error) { lastError = error; }
await new Promise((resolve) => setTimeout(resolve, 500 * (attempt + 1)));
}
throw lastError || new Error("request failed");
}
async function mapLimit(items, limit, task) {
const output = new Array(items.length);
let cursor = 0;
async function worker() {
while (cursor < items.length) {
const index = cursor++;
try { output[index] = await task(items[index], index); }
catch (error) { output[index] = { error: error.message }; }
}
}
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
return output;
}
function atOrBefore(points, target) {
let lo = 0, hi = points.length - 1, answer = null;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (points[mid].t <= target) { answer = points[mid]; lo = mid + 1; }
else hi = mid - 1;
}
return answer;
}
function atOrAfter(points, target) {
let lo = 0, hi = points.length - 1, answer = null;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (points[mid].t >= target) { answer = points[mid]; hi = mid - 1; }
else lo = mid + 1;
}
return answer;
}
function signalAt(points, index) {
const current = points[index], hour = atOrBefore(points, current.t - HOUR);
const day = atOrBefore(points, current.t - 24 * HOUR), week = atOrBefore(points, current.t - 7 * 24 * HOUR);
if (!hour || !day || !week || current.t - week.t > 8 * 24 * HOUR) return null;
const hourMove = current.p - hour.p, dayMove = current.p - day.p, weekMove = current.p - week.p;
const daySign = Math.sign(dayMove), weekSign = Math.sign(weekMove), hourSign = Math.sign(hourMove);
const trend = daySign && daySign === weekSign && Math.abs(dayMove) >= 0.006 && Math.abs(weekMove) >= 0.012
&& Math.abs(dayMove) <= 0.08 && Math.abs(weekMove) <= 0.18
&& (!hourSign || hourSign === daySign || Math.abs(hourMove) < 0.008);
const reversal = daySign && Math.abs(dayMove) >= 0.04 && Math.abs(dayMove) <= 0.18
&& hourSign === -daySign && Math.abs(hourMove) >= 0.004
&& (!weekSign || weekSign !== daySign || Math.abs(weekMove) < Math.abs(dayMove) * 1.6);
if (!trend && !reversal) return null;
const sign = reversal ? -daySign : daySign;
return { type: reversal ? "reversal" : "trend", side: sign > 0 ? "YES" : "NO", hourMove, dayMove, weekMove };
}
function priceBand(price) {
if (price < 0.25) return "longshot";
if (price < 0.55) return "mid";
if (price < 0.78) return "favorite";
return "heavy-favorite";
}
function evaluateMarket(market, points) {
const outcomes = [];
let previousBucket = null;
for (let index = 0; index < points.length; index++) {
const current = points[index], bucket = Math.floor(current.t / (6 * HOUR));
if (bucket === previousBucket || current.p < 0.08 || current.p > 0.92) continue;
const signal = signalAt(points, index);
if (!signal) continue;
const entry = signal.side === "YES" ? current.p : 1 - current.p;
const fadeEntry = signal.side === "YES" ? 1 - current.p : current.p;
if (entry <= 0.02 || entry >= 0.98) continue;
let captured = false;
for (const horizonHours of HORIZONS) {
const future = atOrAfter(points, current.t + horizonHours * HOUR);
if (!future || future.t - (current.t + horizonHours * HOUR) > 3 * HOUR) continue;
const exit = signal.side === "YES" ? future.p : 1 - future.p;
const fadeExit = signal.side === "YES" ? 1 - future.p : future.p;
const grossReturn = exit / entry - 1;
const entryFee = EXACT_GAMMA_FEES ? takerFeePerShare(market.feeSchedule, entry) : null;
const exitFee = EXACT_GAMMA_FEES ? takerFeePerShare(market.feeSchedule, exit) : null;
const roundTripCost = COST_CENTS / 100
+ (EXACT_GAMMA_FEES && Number.isFinite(entryFee) && Number.isFinite(exitFee) ? entryFee + exitFee : 0);
const netReturn = grossReturn - roundTripCost / entry;
const fadeEntryFee = EXACT_GAMMA_FEES ? takerFeePerShare(market.feeSchedule, fadeEntry) : null;
const fadeExitFee = EXACT_GAMMA_FEES ? takerFeePerShare(market.feeSchedule, fadeExit) : null;
const fadeRoundTripCost = COST_CENTS / 100
+ (EXACT_GAMMA_FEES && Number.isFinite(fadeEntryFee) && Number.isFinite(fadeExitFee) ? fadeEntryFee + fadeExitFee : 0);
const fadeNetReturn = fadeEntry > 0.02 && fadeEntry < 0.98
? fadeExit / fadeEntry - 1 - fadeRoundTripCost / fadeEntry : null;
outcomes.push({ marketId: market.id, eventKey: market.eventKey, question: market.question, category: market.category,
type: signal.type, side: signal.side, band: priceBand(entry), entry, exit, horizonHours,
grossReturn, netReturn, fadeNetReturn, roundTripCost, exactFeeSchedule: EXACT_GAMMA_FEES && market.feeSchedule != null,
hourMove: signal.hourMove, dayMove: signal.dayMove, weekMove: signal.weekMove,
observedAt: current.t, evaluatedAt: future.t });
captured = true;
}
if (captured) previousBucket = bucket;
}
return outcomes;
}
function median(values) {
const sorted = [...values].sort((a, b) => a - b), mid = Math.floor(sorted.length / 2);
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
}
function summarize(rows, field = "netReturn") {
if (!rows.length) return { count: 0, markets: 0, events: 0, mean: 0, median: 0, winRate: 0, worst: 0, best: 0, marketMean: 0, lower90: 0, upper90: 0 };
const returns = rows.map((row) => row[field]).filter(Number.isFinite);
if (!returns.length) return { count: 0, markets: 0, events: 0, mean: 0, median: 0, winRate: 0, worst: 0, best: 0, marketMean: 0, lower90: 0, upper90: 0 };
const eventBuckets = new Map();
rows.forEach((row) => {
const value = row[field];
if (!Number.isFinite(value)) return;
const key = row.eventKey || row.marketId;
const bucket = eventBuckets.get(key) || [];
bucket.push(value); eventBuckets.set(key, bucket);
});
const marketReturns = [...eventBuckets.values()].map((values) => values.reduce((sum, value) => sum + value, 0) / values.length);
const marketMean = marketReturns.reduce((sum, value) => sum + value, 0) / Math.max(1, marketReturns.length);
const variance = marketReturns.length > 1
? marketReturns.reduce((sum, value) => sum + (value - marketMean) ** 2, 0) / (marketReturns.length - 1) : 0;
const margin90 = 1.645 * Math.sqrt(variance / Math.max(1, marketReturns.length));
return { count: returns.length,
mean: returns.reduce((sum, value) => sum + value, 0) / returns.length,
median: median(returns), winRate: returns.filter((value) => value > 0).length / returns.length,
worst: Math.min(...returns), best: Math.max(...returns), markets: new Set(rows.map((row) => row.marketId)).size, events: marketReturns.length,
marketMean, lower90: marketMean - margin90, upper90: marketMean + margin90 };
}
function grouped(rows, key) {
return Object.fromEntries([...new Set(rows.map((row) => row[key]))].sort().map((value) => [value, summarize(rows.filter((row) => row[key] === value))]));
}
const RULES = [
{ name: "follow_all", field: "netReturn", test: () => true },
{ name: "follow_trend", field: "netReturn", test: (row) => row.type === "trend" },
{ name: "follow_trend_no", field: "netReturn", test: (row) => row.type === "trend" && row.side === "NO" },
{ name: "follow_trend_yes", field: "netReturn", test: (row) => row.type === "trend" && row.side === "YES" },
{ name: "follow_trend_mid", field: "netReturn", test: (row) => row.type === "trend" && row.band === "mid" },
{ name: "follow_trend_favorites", field: "netReturn", test: (row) => row.type === "trend" && ["favorite", "heavy-favorite"].includes(row.band) },
{ name: "follow_trend_non_longshot", field: "netReturn", test: (row) => row.type === "trend" && row.band !== "longshot" },
{ name: "follow_strong_trend", field: "netReturn", test: (row) => row.type === "trend" && Math.abs(row.dayMove) >= 0.015 && Math.abs(row.weekMove) >= 0.03 },
{ name: "follow_moderate_trend", field: "netReturn", test: (row) => row.type === "trend" && Math.abs(row.dayMove) <= 0.03 && Math.abs(row.weekMove) <= 0.10 },
{ name: "follow_hour_confirmed_trend", field: "netReturn", test: (row) => row.type === "trend" && Math.sign(row.hourMove) === Math.sign(row.dayMove) },
...["Politics", "Sports", "Crypto", "Economy", "Pop Culture", "Other"].map((category) => ({
name: `follow_trend_${category.toLowerCase().replace(/\s+/g, "_")}`, field: "netReturn",
test: (row) => row.type === "trend" && row.category === category,
})),
{ name: "follow_reversal", field: "netReturn", test: (row) => row.type === "reversal" },
{ name: "fade_trend", field: "fadeNetReturn", test: (row) => row.type === "trend" },
{ name: "fade_trend_yes_move", field: "fadeNetReturn", test: (row) => row.type === "trend" && row.side === "YES" },
{ name: "fade_trend_no_move", field: "fadeNetReturn", test: (row) => row.type === "trend" && row.side === "NO" },
{ name: "fade_trend_mid", field: "fadeNetReturn", test: (row) => row.type === "trend" && row.band === "mid" },
{ name: "fade_trend_favorites", field: "fadeNetReturn", test: (row) => row.type === "trend" && ["favorite", "heavy-favorite"].includes(row.band) },
{ name: "fade_trend_longshots", field: "fadeNetReturn", test: (row) => row.type === "trend" && row.band === "longshot" },
{ name: "fade_strong_trend", field: "fadeNetReturn", test: (row) => row.type === "trend" && Math.abs(row.dayMove) >= 0.015 && Math.abs(row.weekMove) >= 0.03 },
{ name: "fade_moderate_trend", field: "fadeNetReturn", test: (row) => row.type === "trend" && Math.abs(row.dayMove) <= 0.03 && Math.abs(row.weekMove) <= 0.10 },
...["Politics", "Sports", "Crypto", "Economy", "Pop Culture", "Other"].map((category) => ({
name: `fade_trend_${category.toLowerCase().replace(/\s+/g, "_")}`, field: "fadeNetReturn",
test: (row) => row.type === "trend" && row.category === category,
})),
];
const COMBINATION_CATEGORIES = ["Politics", "Sports", "Crypto", "Economy", "Pop Culture", "Other"];
const COMBINATION_BANDS = ["longshot", "mid", "favorite", "heavy-favorite"];
for (const category of COMBINATION_CATEGORIES) {
const slug = category.toLowerCase().replace(/\s+/g, "_");
for (const side of ["YES", "NO"]) {
RULES.push({ name: `follow_trend_${slug}_${side.toLowerCase()}`, field: "netReturn",
test: (row) => row.type === "trend" && row.category === category && row.side === side });
RULES.push({ name: `follow_reversal_${slug}_${side.toLowerCase()}`, field: "netReturn",
test: (row) => row.type === "reversal" && row.category === category && row.side === side });
for (const band of COMBINATION_BANDS) {
RULES.push({ name: `follow_trend_${slug}_${side.toLowerCase()}_${band.replace("-", "_")}`, field: "netReturn",
test: (row) => row.type === "trend" && row.category === category && row.side === side && row.band === band });
}
}
}
for (const side of ["YES", "NO"]) {
for (const band of COMBINATION_BANDS) {
RULES.push({ name: `follow_trend_${side.toLowerCase()}_${band.replace("-", "_")}`, field: "netReturn",
test: (row) => row.type === "trend" && row.side === side && row.band === band });
RULES.push({ name: `follow_reversal_${side.toLowerCase()}_${band.replace("-", "_")}`, field: "netReturn",
test: (row) => row.type === "reversal" && row.side === side && row.band === band });
}
}
RULES.push(
{ name: "follow_strong_trend_yes", field: "netReturn", test: (row) => row.type === "trend" && row.side === "YES" && Math.abs(row.dayMove) >= 0.015 && Math.abs(row.weekMove) >= 0.03 },
{ name: "follow_strong_trend_no", field: "netReturn", test: (row) => row.type === "trend" && row.side === "NO" && Math.abs(row.dayMove) >= 0.015 && Math.abs(row.weekMove) >= 0.03 },
{ name: "follow_hour_confirmed_trend_yes", field: "netReturn", test: (row) => row.type === "trend" && row.side === "YES" && Math.sign(row.hourMove) === Math.sign(row.dayMove) },
{ name: "follow_hour_confirmed_trend_no", field: "netReturn", test: (row) => row.type === "trend" && row.side === "NO" && Math.sign(row.hourMove) === Math.sign(row.dayMove) },
);
function evaluateRules(rows) {
return Object.fromEntries(RULES.map((rule) => [rule.name, summarize(rows.filter(rule.test), rule.field)]));
}
function chronologicalEvaluation(rows) {
const ordered = [...rows].sort((a, b) => a.observedAt - b.observedAt);
const splitTime = ordered[Math.floor(ordered.length * 0.70)]?.observedAt || 0;
const train = ordered.filter((row) => row.observedAt < splitTime), test = ordered.filter((row) => row.observedAt >= splitTime);
const cut1 = ordered[Math.floor(ordered.length / 3)]?.observedAt || 0;
const cut2 = ordered[Math.floor(ordered.length * 2 / 3)]?.observedAt || 0;
const thirds = [ordered.filter((row) => row.observedAt < cut1),
ordered.filter((row) => row.observedAt >= cut1 && row.observedAt < cut2),
ordered.filter((row) => row.observedAt >= cut2)];
const thirdRules = thirds.map(evaluateRules), pooled = evaluateRules(ordered);
const trainRules = evaluateRules(train), testRules = evaluateRules(test);
const robustRules = Object.fromEntries(RULES.map((rule) => {
const segments = thirdRules.map((result) => result[rule.name]);
const trainStats = trainRules[rule.name], testStats = testRules[rule.name], pooledStats = pooled[rule.name];
const enoughData = segments.every((segment) => segment.count >= 20 && segment.events >= 5);
const trainTestPositive = trainStats.count >= 40 && testStats.count >= 20
&& trainStats.mean > 0 && trainStats.marketMean > 0 && testStats.mean > 0 && testStats.marketMean > 0;
const trainTestNegative = trainStats.count >= 40 && testStats.count >= 20
&& trainStats.mean < 0 && trainStats.marketMean < 0 && testStats.mean < 0 && testStats.marketMean < 0;
const allPositive = enoughData && trainTestPositive && pooledStats.lower90 > 0
&& segments.every((segment) => segment.mean > 0 && segment.marketMean > 0);
const allNegative = enoughData && trainTestNegative && pooledStats.upper90 < 0
&& segments.every((segment) => segment.mean < 0 && segment.marketMean < 0);
return [rule.name, { enoughData, allPositive, allNegative,
minimumSegmentMean: Math.min(...segments.map((segment) => segment.mean)),
maximumSegmentMean: Math.max(...segments.map((segment) => segment.mean)), pooled: pooledStats }];
}));
return { splitTime: splitTime ? new Date(splitTime * 1000).toISOString() : null,
trainCount: train.length, testCount: test.length, train: trainRules, test: testRules,
thirds: thirdRules, robustRules };
}
async function fetchActiveMarkets(limit, skip = 0) {
const markets = [], seen = new Set(), pageSize = 100;
let eligibleSkipped = 0, cursor = "";
while (markets.length < limit) {
const params = new URLSearchParams({ active: "true", closed: "false", archived: "false", include_tag: "true",
limit: String(pageSize), order: "volume24hr", ascending: "false" });
if (cursor) params.set("after_cursor", cursor);
const payload = await fetchJson(`${GAMMA}/markets/keyset?${params}`), page = payload?.markets;
if (!Array.isArray(page) || !page.length) break;
for (const market of page) {
const id = String(market.id || ""), labels = parseJson(market.outcomes).map((outcome) => String(outcome).trim().toLowerCase());
if (!id || seen.has(id) || labels[0] !== "yes" || labels[1] !== "no") continue;
if (eligibleSkipped < skip) { seen.add(id); eligibleSkipped++; continue; }
seen.add(id); markets.push(market);
if (markets.length >= limit) break;
}
if (page.length < pageSize || !payload.next_cursor || payload.next_cursor === cursor) break;
cursor = payload.next_cursor;
}
return markets.slice(0, limit);
}
const rawMarkets = await fetchActiveMarkets(MARKET_LIMIT, ACTIVE_SKIP);
const markets = rawMarkets.map((raw) => ({ id: String(raw.id), question: raw.question || "", category: categoryOf(raw),
binaryLabels: parseJson(raw.outcomes).map((outcome) => String(outcome).trim().toLowerCase()),
eventKey: String(raw.events?.[0]?.id || raw.events?.[0]?.slug || raw.eventId || raw.id),
tokenId: String(parseJson(raw.clobTokenIds)[0] || ""), feeSchedule: feeScheduleOf(raw) })).filter((market) => market.id && market.tokenId
&& market.binaryLabels[0] === "yes" && market.binaryLabels[1] === "no");
const histories = await mapLimit(markets, CONCURRENCY, async (market) => {
const data = await fetchJson(`${CLOB}/prices-history?market=${encodeURIComponent(market.tokenId)}&interval=1m&fidelity=60`);
const points = (data.history || []).map((point) => ({ t: Number(point.t), p: Number(point.p) }))
.filter((point) => Number.isFinite(point.t) && Number.isFinite(point.p)).sort((a, b) => a.t - b.t);
return { market, points, outcomes: evaluateMarket(market, points) };
});
const successful = histories.filter((result) => result && !result.error && result.points.length);
const outcomes = successful.flatMap((result) => result.outcomes);
const primaryHorizon = HORIZONS.includes(12) ? 12 : HORIZONS[0];
const primaryOutcomes = outcomes.filter((row) => row.horizonHours === primaryHorizon);
const report = {
generatedAt: new Date().toISOString(), marketLimit: MARKET_LIMIT, activeMarketsSkipped: ACTIVE_SKIP, marketsWithHistory: successful.length,
methodology: { horizonHours: HORIZONS, primaryHorizon, observationBucketHours: 6, historyInterval: "1m", fidelityMinutes: 60,
estimatedRoundTripSlippageCents: COST_CENTS, exactGammaFeeSchedules: EXACT_GAMMA_FEES, clusterUnit: "event",
note: "Current active-market selection and current category tags are a survivorship-biased proxy; signal inputs and future marks are time-ordered without lookahead. Confidence intervals cluster correlated markets by Polymarket event." },
overall: summarize(primaryOutcomes), byType: grouped(primaryOutcomes, "type"), byCategory: grouped(primaryOutcomes, "category"),
byBand: grouped(primaryOutcomes, "band"), bySide: grouped(primaryOutcomes, "side"),
chronologicalSplit: chronologicalEvaluation(primaryOutcomes),
horizons: Object.fromEntries(HORIZONS.map((horizon) => {
const rows = outcomes.filter((row) => row.horizonHours === horizon);
return [horizon, { overall: summarize(rows), chronological: chronologicalEvaluation(rows) }];
})),
failures: histories.filter((result) => result?.error).length,
};
const compact = process.env.EVAL_SUMMARY === "1";
const compactStats = (stats = {}) => ({ count: stats.count || 0, markets: stats.markets || 0, events: stats.events || 0,
mean: stats.mean || 0, marketMean: stats.marketMean || 0, lower90: stats.lower90 || 0, upper90: stats.upper90 || 0,
winRate: stats.winRate || 0 });
const compactRules = (rules = {}) => Object.fromEntries(Object.entries(rules)
.filter(([, result]) => result.enoughData && (result.allPositive || result.allNegative))
.map(([name, result]) => [name, { direction: result.allPositive ? "positive" : "negative",
minimumSegmentMean: result.minimumSegmentMean, maximumSegmentMean: result.maximumSegmentMean,
pooled: compactStats(result.pooled) }]));
const summary = {
generatedAt: report.generatedAt, marketLimit: report.marketLimit, activeMarketsSkipped: report.activeMarketsSkipped,
marketsWithHistory: report.marketsWithHistory,
primaryHorizon: report.methodology.primaryHorizon, failures: report.failures,
overall: compactStats(report.overall),
byType: Object.fromEntries(Object.entries(report.byType).map(([key, value]) => [key, compactStats(value)])),
byCategory: Object.fromEntries(Object.entries(report.byCategory).map(([key, value]) => [key, compactStats(value)])),
byBand: Object.fromEntries(Object.entries(report.byBand).map(([key, value]) => [key, compactStats(value)])),
bySide: Object.fromEntries(Object.entries(report.bySide).map(([key, value]) => [key, compactStats(value)])),
train: compactStats(report.chronologicalSplit.train.follow_all),
test: compactStats(report.chronologicalSplit.test.follow_all),
robustRules: compactRules(report.chronologicalSplit.robustRules),
horizons: Object.fromEntries(Object.entries(report.horizons).map(([hours, value]) => [hours, {
overall: compactStats(value.overall), robustRules: compactRules(value.chronological.robustRules),
}])),
};
console.log(JSON.stringify(compact ? summary : report, null, 2));
+198
View File
@@ -0,0 +1,198 @@
const GAMMA = "https://gamma-api.polymarket.com";
const CLOB = "https://clob.polymarket.com";
const MARKET_LIMIT = Math.max(500, Math.min(5000, Number(process.env.SPORTS_FAVORITE_MARKETS || 3000)));
const MARKET_SKIP = Math.max(0, Math.min(20000, Number(process.env.SPORTS_FAVORITE_SKIP || 0)));
const CONCURRENCY = Math.max(1, Math.min(12, Number(process.env.SPORTS_FAVORITE_CONCURRENCY || 10)));
const COST = Math.max(0, Math.min(0.10, Number(process.env.SPORTS_FAVORITE_COST_CENTS || 5) / 100));
const MAX_STALENESS_HOURS = Math.max(1, Math.min(12, Number(process.env.SPORTS_FAVORITE_MAX_STALENESS_HOURS || 3)));
const EVENT_LEGS = Math.max(1, Math.min(4, Number(process.env.SPORTS_FAVORITE_EVENT_LEGS || 1)));
const COST_GRID = String(process.env.SPORTS_FAVORITE_COST_GRID_CENTS || "0,1,2,3,4,5")
.split(",").map(Number).filter((value) => Number.isFinite(value) && value >= 0 && value <= 10).map((value) => value / 100);
const HOUR = 3600;
function parseJson(value) {
if (Array.isArray(value)) return value;
try { return JSON.parse(value || "[]"); } catch { return []; }
}
function timestamp(value) {
const parsed = Date.parse(String(value || "").replace(" ", "T").replace(/\+00$/, "Z"));
return Number.isFinite(parsed) ? parsed / 1000 : null;
}
async function fetchJson(url, attempts = 4) {
let lastError;
for (let attempt = 0; attempt < attempts; attempt++) {
try {
const response = await fetch(url, { signal: AbortSignal.timeout(30000), headers: { accept: "application/json" } });
if (response.ok) return response.json();
lastError = new Error(`${response.status} ${response.statusText}`);
if (response.status !== 429 && response.status < 500) break;
} catch (error) { lastError = error; }
await new Promise((resolve) => setTimeout(resolve, 700 * (attempt + 1)));
}
throw lastError || new Error("request failed");
}
async function mapLimit(items, limit, task) {
const output = new Array(items.length);
let cursor = 0;
async function worker() {
while (cursor < items.length) {
const index = cursor++;
try { output[index] = await task(items[index]); }
catch (error) { output[index] = { error: error.message }; }
}
}
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
return output;
}
function sportsMarket(raw) {
const tags = [...(raw.tags || []), ...(raw.events || []).flatMap((event) => event.tags || [])]
.map((tag) => `${tag.slug || ""} ${tag.label || ""}`).join(" ");
const text = `${raw.question || ""} ${raw.sportsMarketType || ""} ${tags}`.toLowerCase();
return /\b(sports?|soccer|football|basketball|baseball|tennis|hockey|cricket|golf|boxing|ufc|nba|nfl|nhl|mlb|fifa|epl|match|game|tournament)\b/.test(text);
}
function gameStart(raw) {
const event = raw.events?.[0] || {};
return timestamp(raw.gameStartTime || raw.eventStartTime || event.startTime || event.eventDate);
}
async function fetchMarkets(limit, skip = 0) {
const markets = [], seen = new Set();
let eligibleSeen = 0;
let cursor = "";
while (markets.length < limit) {
const params = new URLSearchParams({ closed: "true", order: "closedTime", ascending: "false", limit: "100", include_tag: "true" });
if (cursor) params.set("after_cursor", cursor);
const payload = await fetchJson(`${GAMMA}/markets/keyset?${params}`), page = payload?.markets;
if (!Array.isArray(page) || !page.length) break;
for (const raw of page) {
const id = String(raw.id || ""), labels = parseJson(raw.outcomes).map((value) => String(value).trim().toLowerCase());
const outcomes = parseJson(raw.outcomePrices).map(Number), tokens = parseJson(raw.clobTokenIds).map(String);
const finalYes = outcomes[0] >= 0.99 && outcomes[1] <= 0.01 ? 1 : outcomes[1] >= 0.99 && outcomes[0] <= 0.01 ? 0 : null;
const startsAt = gameStart(raw), closedAt = timestamp(raw.closedTime || raw.endDate);
if (!id || seen.has(id) || labels[0] !== "yes" || labels[1] !== "no" || tokens.length !== 2 || finalYes == null
|| !startsAt || !closedAt || !sportsMarket(raw)) continue;
seen.add(id);
if (eligibleSeen++ < skip) continue;
markets.push({ id, question: raw.question || "", eventKey: String(raw.events?.[0]?.id || id), event: raw.events?.[0]?.title || "",
tokenId: tokens[0], finalYes, startsAt, closedAt, marketType: String(raw.sportsMarketType || "unknown") });
if (markets.length >= limit) break;
}
if (page.length < 100 || !payload.next_cursor || payload.next_cursor === cursor) break;
cursor = payload.next_cursor;
}
return markets;
}
function atOrBefore(points, target) {
let lo = 0, hi = points.length - 1, answer = null;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (points[mid].t <= target) { answer = points[mid]; lo = mid + 1; }
else hi = mid - 1;
}
return answer;
}
function observation(market, points, leadHours) {
const target = market.startsAt - leadHours * HOUR, point = atOrBefore(points, target);
if (!point || target - point.t > MAX_STALENESS_HOURS * HOUR || point.p <= 0.03 || point.p >= 0.97) return null;
const side = point.p >= 0.5 ? "YES" : "NO", entry = side === "YES" ? point.p : 1 - point.p;
const won = side === (market.finalYes ? "YES" : "NO"), netReturn = (won ? 1 : 0) / entry - 1 - COST / entry;
return { marketId: market.id, eventKey: market.eventKey, question: market.question, event: market.event,
marketType: market.marketType, startsAt: market.startsAt, closedAt: market.closedAt, leadHours, side, entry, won, netReturn };
}
function summarize(rows) {
if (!rows.length) return { trades: 0, events: 0, mean: 0, eventMean: 0, lower: 0, upper: 0, winRate: 0 };
const events = new Map();
rows.forEach((row) => { const bucket = events.get(row.eventKey) || []; bucket.push(row.netReturn); events.set(row.eventKey, bucket); });
const eventReturns = [...events.values()].map((values) => values.reduce((sum, value) => sum + value, 0) / values.length);
const eventMean = eventReturns.reduce((sum, value) => sum + value, 0) / eventReturns.length;
const variance = eventReturns.length > 1 ? eventReturns.reduce((sum, value) => sum + (value - eventMean) ** 2, 0) / (eventReturns.length - 1) : 0;
const margin = 1.645 * Math.sqrt(variance / Math.max(1, eventReturns.length));
return { trades: rows.length, events: eventReturns.length, mean: rows.reduce((sum, row) => sum + row.netReturn, 0) / rows.length,
eventMean, lower: eventMean - margin, upper: eventMean + margin, winRate: rows.filter((row) => row.won).length / rows.length };
}
const rules = [];
for (const leadHours of [12, 18, 24, 30, 36]) {
for (const minEntry of [0.55, 0.60, 0.65, 0.70]) {
for (const maxEntry of [0.75, 0.85, 0.95]) {
if (minEntry >= maxEntry) continue;
rules.push({ id: `lead${leadHours}_${minEntry}-${maxEntry}`, leadHours, minEntry, maxEntry });
}
}
}
const markets = await fetchMarkets(MARKET_LIMIT, MARKET_SKIP);
const histories = await mapLimit(markets, CONCURRENCY, async (market) => {
const data = await fetchJson(`${CLOB}/prices-history?market=${encodeURIComponent(market.tokenId)}&interval=max&fidelity=60`);
const points = (data.history || []).map((point) => ({ t: Number(point.t), p: Number(point.p) }))
.filter((point) => Number.isFinite(point.t) && Number.isFinite(point.p)).sort((a, b) => a.t - b.t);
return { market, points };
});
const usable = histories.filter((row) => row && !row.error && row.points.length);
const times = usable.map((row) => row.market.startsAt).sort((a, b) => a - b);
const trainCut = times[Math.floor(times.length * 0.6)] || 0, validationCut = times[Math.floor(times.length * 0.8)] || 0;
const partitions = {
train: usable.filter((row) => row.market.startsAt < trainCut),
validation: usable.filter((row) => row.market.startsAt >= trainCut && row.market.startsAt < validationCut),
holdout: usable.filter((row) => row.market.startsAt >= validationCut),
};
function tradesFor(partition, rule) {
const eligible = partition.map(({ market, points }) => observation(market, points, rule.leadHours)).filter(Boolean)
.filter((row) => row.entry >= rule.minEntry && row.entry < rule.maxEntry);
const events = new Map();
eligible.forEach((row) => {
const rows = events.get(row.eventKey) || [];
rows.push(row);events.set(row.eventKey, rows);
});
return [...events.values()].flatMap((rows) => rows.sort((a, b) => b.entry - a.entry || a.marketId.localeCompare(b.marketId)).slice(0, EVENT_LEGS));
}
const evaluated = rules.map((rule) => {
const trainRows = tradesFor(partitions.train, rule), validationRows = tradesFor(partitions.validation, rule), holdoutRows = tradesFor(partitions.holdout, rule);
const train = summarize(trainRows), validation = summarize(validationRows), holdout = summarize(holdoutRows);
const trainPassed = train.trades >= 60 && train.events >= 25 && train.lower > 0;
const validationPassed = trainPassed && validation.trades >= 25 && validation.events >= 10 && validation.lower > 0;
const passesHoldout = validationPassed && holdout.trades >= 25 && holdout.events >= 10 && holdout.lower > 0;
return { rule, train, validation, holdout, trainPassed, validationPassed, passesHoldout, holdoutRows };
});
const selected = evaluated.filter((row) => row.validationPassed).sort((a, b) => Number(b.passesHoldout) - Number(a.passesHoldout) || b.holdout.lower - a.holdout.lower);
const baseline = evaluated.find((row) => row.rule.leadHours === 24 && row.rule.minEntry === 0.55 && row.rule.maxEntry === 0.95);
const retiredPilot = evaluated.find((row) => row.rule.leadHours === 24 && row.rule.minEntry === 0.60 && row.rule.maxEntry === 0.85);
const forwardResearch = evaluated.find((row) => row.rule.leadHours === 12 && row.rule.minEntry === 0.60 && row.rule.maxEntry === 0.75);
const compact = (stats) => Object.fromEntries(Object.entries(stats).map(([key, value]) => [key, Number.isFinite(value) ? +value.toFixed(5) : value]));
const candidate = (row) => ({ rule: row.rule, train: compact(row.train), validation: compact(row.validation), holdout: compact(row.holdout), passesHoldout: row.passesHoldout });
function costCurve(rule) {
if (!rule) return [];
const raw = Object.fromEntries(Object.entries(partitions).map(([name, partition]) => [name, tradesFor(partition, rule)]));
return COST_GRID.map((cost) => ({ modeledCostCents: cost * 100, ...Object.fromEntries(Object.entries(raw).map(([name, rows]) => [name,
compact(summarize(rows.map((row) => ({ ...row, netReturn: (row.won ? 1 : 0) / row.entry - 1 - cost / row.entry }))))])) }));
}
const closest = evaluated.filter((row) => row.train.events >= 25 && row.validation.events >= 10 && row.holdout.events >= 10)
.sort((a, b) => Math.min(b.train.lower, b.validation.lower, b.holdout.lower)
- Math.min(a.train.lower, a.validation.lower, a.holdout.lower));
console.log(JSON.stringify({ generatedAt: new Date().toISOString(), requestedMarkets: MARKET_LIMIT, skippedEligibleMarkets: MARKET_SKIP, sportsMarkets: markets.length,
historiesWithData: usable.length, failures: histories.filter((row) => row?.error).length,
methodology: { selection: "most recently closed eligible Yes/No sports markets", decisionAnchor: "published game start",
historyFidelityMinutes: 60, maxPriceStalenessHours: MAX_STALENESS_HOURS, modeledCostCents: COST * 100,
split: "60% train / 20% validation / 20% untouched holdout", clusterUnit: `one equal-dollar position in each event's highest-priced eligible favorite (event legs ${EVENT_LEGS})`, testedRules: rules.length },
partitionMarkets: Object.fromEntries(Object.entries(partitions).map(([key, value]) => [key, value.length])),
trainPassed: evaluated.filter((row) => row.trainPassed).length, validationSelected: selected.length,
holdoutPassed: selected.filter((row) => row.passesHoldout).length, baseline: baseline ? candidate(baseline) : null,
retiredPilot: retiredPilot ? candidate(retiredPilot) : null,
forwardResearch: forwardResearch ? candidate(forwardResearch) : null,
costSensitivity: { retiredPilot: costCurve(retiredPilot?.rule), forwardResearch: costCurve(forwardResearch?.rule) },
closestCandidates: closest.slice(0, 15).map(candidate),
candidates: selected.slice(0, 20).map(candidate),
holdoutExamples: (selected.find((row) => row.passesHoldout)?.holdoutRows || []).slice(0, 12)
.map((row) => ({ question: row.question, event: row.event, side: row.side, entry: +row.entry.toFixed(4), won: row.won, netReturn: +row.netReturn.toFixed(4) }))
}, null, 2));
+385
View File
@@ -0,0 +1,385 @@
import { chromium } from "playwright";
import { pathToFileURL } from "node:url";
export const AGENTS_KEY = "pma_agents_v2";
export const SUGGESTIONS_KEY = "pma_suggestions_v5";
export const ALLOWED_RUNTIME_KEYS = Object.freeze([AGENTS_KEY, SUGGESTIONS_KEY]);
const FORBIDDEN_RUNTIME_KEYS = Object.freeze([
"pma_paper_accounts_v1",
"pma_trade_email_alerts_v1",
"pma_invest_allocations_v1",
"pma_live_readiness_v1",
"pma_agent_chat_v1",
"pma_paid_agent_chat_v1",
]);
const MAX_STATE_BYTES = 900_000;
const TRANSPORT_HISTORY_LIMIT = 24;
const TRANSPORT_SNAPSHOT_LIMIT = 96;
const TRANSPORT_MAKER_OUTCOME_LIMIT = 30;
const TRANSPORT_TARGET_BYTES = 850_000;
const TRANSPORT_HISTORY_FLOOR = 12;
const TRANSPORT_MAKER_OUTCOME_FLOOR = 12;
const TRANSPORT_SUGGESTION_FLOOR = 240;
const TRANSPORT_SIGNAL_OUTCOME_RESERVE = 144;
const TRANSPORT_SNAPSHOT_FALLBACKS = Object.freeze([72, 48, 24]);
function sampleSnapshots(rows, limit = TRANSPORT_SNAPSHOT_LIMIT) {
const list = Array.isArray(rows) ? rows : [];
if (list.length <= limit) return list;
const recentCount = Math.min(24, limit);
const older = list.slice(0, -recentCount);
const olderSlots = limit - recentCount;
const sampled = [];
for (let i = 0; i < olderSlots; i += 1) {
const index = olderSlots === 1 ? older.length - 1 : Math.round(i * (older.length - 1) / (olderSlots - 1));
if (older[index] && sampled.at(-1) !== older[index]) sampled.push(older[index]);
}
return sampled.concat(list.slice(-recentCount)).slice(-limit);
}
function compactHistoryRow(row) {
if (!row || typeof row !== "object") return row;
return {
date: row.date,
action: row.action,
question: typeof row.question === "string" ? row.question.slice(0, 140) : row.question,
side: row.side,
detail: typeof row.detail === "string" ? row.detail.slice(0, 220) : row.detail,
};
}
function compactPublicSuggestion(suggestion) {
if (!suggestion || typeof suggestion !== "object") return suggestion;
const out = { ...suggestion };
if (suggestion.trade_ready || suggestion.adaptive_probation || suggestion.bundle_id) {
out.drivers = Array.isArray(suggestion.drivers) ? suggestion.drivers.slice(0, 1).map(value => String(value).slice(0, 80)) : [];
out.rationale = typeof suggestion.rationale === "string"
? suggestion.rationale.slice(0, 180)
: "Trade-ready opportunity passed the active execution gate.";
} else {
out.drivers = [];
out.rationale = suggestion.jump_risk
? "Watch only: settlement-gap risk prevents a protected entry."
: suggestion.signal_type && suggestion.signal_type !== "none"
? "Watch only: gathering independent forward evidence before capital is enabled."
: "Watch only: no independently confirmed direction yet.";
delete out.clob_yes;
delete out.clob_no;
}
return out;
}
function compactDecision(decision) {
if (!decision || typeof decision !== "object") return decision;
const out = { ...decision };
if (out.makerProfile && typeof out.makerProfile === "object") {
out.makerProfile = { version: out.makerProfile.version, outcomes: out.makerProfile.outcomes, global: out.makerProfile.global };
}
if (out.learning && typeof out.learning === "object") {
out.learning = { ...out.learning };
delete out.learning.buckets;
delete out.learning.current_buckets;
}
if (out.marketLearning && typeof out.marketLearning === "object") {
out.marketLearning = { ...out.marketLearning };
delete out.marketLearning.buckets;
}
return out;
}
function compactMakerOutcome(row) {
if (!row || typeof row !== "object") return row;
return {
quote_id: row.quote_id, market_id: row.market_id, event_key: row.event_key, category: row.category,
spread: row.spread, spread_band: row.spread_band, reward_yield_band: row.reward_yield_band,
price_balance_band: row.price_balance_band, day_move_band: row.day_move_band, margin_band: row.margin_band,
competition_band: row.competition_band, status: row.status,
pnl: row.pnl, shadow_only: row.shadow_only, deployed_capital: row.deployed_capital, reserved_capital: row.reserved_capital,
completed_at: row.completed_at, strategy_version: row.strategy_version, maker_strategy_version: row.maker_strategy_version,
};
}
export function compactRuntimeTransportSnapshot(snapshot) {
if (!snapshot || typeof snapshot !== "object" || !snapshot.items) return snapshot;
const out = { ...snapshot, items: { ...snapshot.items } };
let state;
let suggestions;
try {
state = JSON.parse(out.items[AGENTS_KEY]);
suggestions = JSON.parse(out.items[SUGGESTIONS_KEY]);
} catch {
return out;
}
for (const portfolio of Object.values(state.agents || {})) {
portfolio.history = (portfolio.history || []).slice(-TRANSPORT_HISTORY_LIMIT).map(compactHistoryRow);
portfolio.snapshots = sampleSnapshots(portfolio.snapshots, TRANSPORT_SNAPSHOT_LIMIT);
portfolio.maker_outcomes = (portfolio.maker_outcomes || []).slice(-TRANSPORT_MAKER_OUTCOME_LIMIT).map(compactMakerOutcome);
portfolio.lastDecision = compactDecision(portfolio.lastDecision);
}
suggestions.suggestions = (suggestions.suggestions || []).slice(0, 300).map(compactPublicSuggestion);
const writeItems = () => {
out.items[AGENTS_KEY] = JSON.stringify(state);
out.items[SUGGESTIONS_KEY] = JSON.stringify(suggestions);
return Buffer.byteLength(JSON.stringify(out));
};
let bytes = writeItems();
if (bytes > TRANSPORT_TARGET_BYTES) {
for (const portfolio of Object.values(state.agents || {})) {
portfolio.history = (portfolio.history || []).slice(-TRANSPORT_HISTORY_FLOOR);
portfolio.maker_outcomes = (portfolio.maker_outcomes || []).slice(-TRANSPORT_MAKER_OUTCOME_FLOOR);
}
suggestions.suggestions = suggestions.suggestions.slice(0, TRANSPORT_SUGGESTION_FLOOR);
bytes = writeItems();
}
const ledger = state.signal_ledger && typeof state.signal_ledger === "object" ? state.signal_ledger : null;
const pending = Array.isArray(ledger?.pending) ? ledger.pending : [];
const outcomes = Array.isArray(ledger?.outcomes) ? ledger.outcomes : [];
if (bytes > TRANSPORT_TARGET_BYTES && ledger) {
ledger.pending = [];
ledger.outcomes = outcomes.slice(-Math.min(TRANSPORT_SIGNAL_OUTCOME_RESERVE, outcomes.length));
bytes = writeItems();
for (const limit of TRANSPORT_SNAPSHOT_FALLBACKS) {
if (bytes <= TRANSPORT_TARGET_BYTES) break;
for (const portfolio of Object.values(state.agents || {})) {
portfolio.snapshots = sampleSnapshots(portfolio.snapshots, limit);
}
bytes = writeItems();
}
if (bytes > TRANSPORT_TARGET_BYTES && ledger.outcomes.length) {
let low = 0;
let high = ledger.outcomes.length;
while (low < high) {
const mid = Math.ceil((low + high) / 2);
ledger.outcomes = outcomes.slice(-mid);
if (writeItems() <= TRANSPORT_TARGET_BYTES) low = mid;
else high = mid - 1;
}
ledger.outcomes = low ? outcomes.slice(-low) : [];
bytes = writeItems();
}
let pendingLow = 0;
let pendingHigh = pending.length;
while (pendingLow < pendingHigh) {
const mid = Math.ceil((pendingLow + pendingHigh) / 2);
ledger.pending = pending.slice(0, mid);
if (writeItems() <= TRANSPORT_TARGET_BYTES) pendingLow = mid;
else pendingHigh = mid - 1;
}
ledger.pending = pending.slice(0, pendingLow);
bytes = writeItems();
if (pendingLow === pending.length && ledger.outcomes.length < outcomes.length) {
let outcomeLow = ledger.outcomes.length;
let outcomeHigh = outcomes.length;
while (outcomeLow < outcomeHigh) {
const mid = Math.ceil((outcomeLow + outcomeHigh) / 2);
ledger.outcomes = outcomes.slice(-mid);
if (writeItems() <= TRANSPORT_TARGET_BYTES) outcomeLow = mid;
else outcomeHigh = mid - 1;
}
ledger.outcomes = outcomeLow ? outcomes.slice(-outcomeLow) : [];
writeItems();
}
}
const syncLedgerSummary = () => {
if (!out.summary?.signal_ledger || !ledger) return;
out.summary.signal_ledger.pending_retained = ledger.pending.length;
out.summary.signal_ledger.pending_total = Math.max(Number(out.summary.signal_ledger.pending_total || 0), pending.length);
out.summary.signal_ledger.outcomes_retained = ledger.outcomes.length;
out.summary.signal_ledger.outcomes_total = Math.max(Number(out.summary.signal_ledger.outcomes_total || 0), outcomes.length);
out.summary.signal_ledger.byte_budget = TRANSPORT_TARGET_BYTES;
};
syncLedgerSummary();
bytes = writeItems();
while (bytes > TRANSPORT_TARGET_BYTES && ledger?.pending.length) {
ledger.pending.pop();
syncLedgerSummary();
bytes = writeItems();
}
while (bytes > TRANSPORT_TARGET_BYTES && ledger?.outcomes.length) {
ledger.outcomes.shift();
syncLedgerSummary();
bytes = writeItems();
}
return out;
}
function required(name, fallback = "") {
const value = process.env[name] || fallback;
if (!value) throw new Error(`${name} is required`);
return value;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
export function validateRuntimeSnapshot(snapshot, expectedBuild = 0, { allowIncomplete = false } = {}) {
if (!snapshot || typeof snapshot !== "object" || !snapshot.items || typeof snapshot.items !== "object") {
throw new Error("Autonomous export is not a state snapshot");
}
const keys = Object.keys(snapshot.items).sort();
const allowed = [...ALLOWED_RUNTIME_KEYS].sort();
if (JSON.stringify(keys) !== JSON.stringify(allowed)) {
throw new Error(`Autonomous export contains unexpected keys: ${keys.join(", ")}`);
}
for (const key of FORBIDDEN_RUNTIME_KEYS) {
if (Object.prototype.hasOwnProperty.call(snapshot.items, key)) throw new Error(`Private key ${key} cannot enter autonomous state`);
}
for (const key of ALLOWED_RUNTIME_KEYS) {
if (typeof snapshot.items[key] !== "string") throw new Error(`Runtime item ${key} must be serialized JSON`);
JSON.parse(snapshot.items[key]);
}
const agents = JSON.parse(snapshot.items[AGENTS_KEY]);
const suggestions = JSON.parse(snapshot.items[SUGGESTIONS_KEY]);
if (!agents.agents || Object.keys(agents.agents).length !== 10) throw new Error("Runtime snapshot must contain ten public agents");
if (!agents.seeded || !agents.last_cycle_hour) throw new Error("Runtime snapshot has not completed a cycle");
if (expectedBuild && Number(snapshot.build_version) !== Number(expectedBuild)) {
throw new Error(`Expected Build ${expectedBuild}, received Build ${snapshot.build_version}`);
}
const suggestionCount = (suggestions.suggestions || []).length;
if (suggestionCount > 300) throw new Error("Runtime suggestion snapshot exceeds the 300-item public limit");
if (!allowIncomplete && suggestionCount === 0) throw new Error("Runtime cycle produced no live suggestions");
if (!allowIncomplete && Object.values(agents.agents).some(agent => !agent?.lastDecision)) {
throw new Error("Runtime cycle did not produce a decision for every agent");
}
const bytes = Buffer.byteLength(JSON.stringify(snapshot));
if (bytes > MAX_STATE_BYTES) throw new Error(`Runtime snapshot is ${bytes} bytes; limit is ${MAX_STATE_BYTES}`);
return { snapshot, bytes, agents, suggestions };
}
async function githubRequest(path, options = {}) {
const token = required("GITHUB_TOKEN");
const response = await fetch(`https://api.github.com${path}`, {
...options,
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${token}`,
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "polymarket-arena-autonomous-runtime",
...(options.headers || {}),
},
});
if (response.status === 404 && options.allowNotFound) return null;
const body = await response.json().catch(() => null);
if (!response.ok) throw new Error(`GitHub API ${options.method || "GET"} ${path} failed with HTTP ${response.status}: ${body?.message || "unknown error"}`);
return body;
}
async function ensureRuntimeBranch(repository, branch) {
const encoded = encodeURIComponent(`heads/${branch}`);
const current = await githubRequest(`/repos/${repository}/git/ref/${encoded}`, { allowNotFound: true });
if (current) return;
await githubRequest(`/repos/${repository}/git/refs`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ref: `refs/heads/${branch}`, sha: required("GITHUB_SHA") }),
});
}
async function readRuntimeFile(repository, branch, pathname) {
const file = await githubRequest(`/repos/${repository}/contents/${pathname}?ref=${encodeURIComponent(branch)}`, { allowNotFound: true });
if (!file || file.type !== "file" || !file.content) return { snapshot: null, sha: null };
try {
return { snapshot: JSON.parse(Buffer.from(file.content.replace(/\s/g, ""), "base64").toString("utf8")), sha: file.sha };
} catch {
throw new Error("Existing runtime state is not valid JSON");
}
}
async function writeRuntimeFile(repository, branch, pathname, snapshot, sha) {
const body = {
message: `Update autonomous paper cycle ${snapshot.last_cycle_hour}`,
content: Buffer.from(`${JSON.stringify(snapshot, null, 2)}\n`).toString("base64"),
branch,
};
if (sha) body.sha = sha;
await githubRequest(`/repos/${repository}/contents/${pathname}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
}
async function waitForProductionBuild(page, arenaUrl, expectedBuild) {
const deadline = Date.now() + 12 * 60_000;
while (Date.now() < deadline) {
await page.goto(`${arenaUrl}?automation=1&build=${expectedBuild}&t=${Date.now()}`, { waitUntil: "domcontentloaded", timeout: 90_000 });
await page.waitForFunction(() => Boolean(window.PMA_AUTOMATION), null, { timeout: 20_000 }).catch(() => {});
const status = await page.evaluate(() => window.PMA_AUTOMATION?.status?.() || null);
if (Number(status?.build) === expectedBuild) return status;
if (Number(status?.build) > expectedBuild) {
throw new Error(`Production already advanced to Build ${status.build}; retiring stale Build ${expectedBuild} runner`);
}
await sleep(20_000);
}
throw new Error(`Production did not reach Build ${expectedBuild} before the autonomous cycle deadline`);
}
async function main() {
const repository = required("GITHUB_REPOSITORY", "theodore-song/polymarket-analyst");
const branch = process.env.RUNTIME_BRANCH || "runtime-state";
const pathname = process.env.RUNTIME_STATE_PATH || "runtime/state.json";
const arenaUrl = (process.env.ARENA_URL || "https://polymarket-site-eta.vercel.app").replace(/\/$/, "");
const expectedBuild = Number(required("EXPECTED_BUILD", "121"));
await ensureRuntimeBranch(repository, branch);
const prior = await readRuntimeFile(repository, branch, pathname);
if (prior.snapshot) validateRuntimeSnapshot(prior.snapshot, 0, { allowIncomplete: true });
const browser = await chromium.launch({ headless: true });
try {
const context = await browser.newContext({ serviceWorkers: "block" });
if (prior.snapshot) {
await context.addInitScript(({ snapshot, allowedKeys }) => {
try {
for (const key of allowedKeys) {
if (typeof snapshot.items?.[key] === "string") localStorage.setItem(key, snapshot.items[key]);
}
} catch {}
}, { snapshot: prior.snapshot, allowedKeys: ALLOWED_RUNTIME_KEYS });
}
const page = await context.newPage();
const pageErrors = [];
page.on("pageerror", error => pageErrors.push(String(error?.message || error)));
await waitForProductionBuild(page, arenaUrl, expectedBuild);
await page.waitForFunction(() => {
const status = window.PMA_AUTOMATION?.status?.();
return Boolean(status?.seeded && !status?.running);
}, null, { timeout: 12 * 60_000 });
await page.evaluate(() => window.PMA_AUTOMATION.runCycle());
await page.waitForFunction(() => !window.PMA_AUTOMATION?.status?.().running, null, { timeout: 12 * 60_000 });
const rawSnapshot = await page.evaluate(({ agentKey, suggestionsKey }) => {
const snapshot = window.PMA_AUTOMATION.exportShared();
const agents = localStorage.getItem(agentKey);
const suggestions = localStorage.getItem(suggestionsKey);
if (agents) snapshot.items[agentKey] = agents;
if (suggestions) snapshot.items[suggestionsKey] = suggestions;
return snapshot;
}, { agentKey: AGENTS_KEY, suggestionsKey: SUGGESTIONS_KEY });
const snapshot = compactRuntimeTransportSnapshot(rawSnapshot);
const validated = validateRuntimeSnapshot(snapshot, expectedBuild);
await writeRuntimeFile(repository, branch, pathname, snapshot, prior.sha);
const status = await page.evaluate(() => window.PMA_AUTOMATION.status());
console.log(JSON.stringify({
ok: true,
build: status.build,
cycle: snapshot.last_cycle_hour,
suggestions: validated.suggestions.suggestions?.length || 0,
bytes: validated.bytes,
agent_summary: snapshot.summary?.agents || [],
page_errors: pageErrors.slice(0, 3),
}));
} finally {
await browser.close();
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch(error => {
console.error(error?.stack || error);
process.exitCode = 1;
});
}
+335
View File
@@ -0,0 +1,335 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import { ALLOWED_RUNTIME_KEYS, compactRuntimeTransportSnapshot, validateRuntimeSnapshot } from "./run-autonomous-cycle.mjs";
const index = fs.readFileSync(new URL("../index.html", import.meta.url), "utf8");
const api = fs.readFileSync(new URL("../api/state.js", import.meta.url), "utf8");
const workflow = fs.readFileSync(new URL("../.github/workflows/autonomous-cycle.yml", import.meta.url), "utf8");
const routingReleaseWorkflow = fs.readFileSync(new URL("../.github/workflows/release-protected-routing.yml", import.meta.url), "utf8");
const vercelConfig = JSON.parse(fs.readFileSync(new URL("../vercel.json", import.meta.url), "utf8"));
const runner = fs.readFileSync(new URL("./run-autonomous-cycle.mjs", import.meta.url), "utf8");
const resolutionAudit = JSON.parse(fs.readFileSync(new URL("../research/resolution-week-no-audit.json", import.meta.url), "utf8"));
const sportsContestAudit = JSON.parse(fs.readFileSync(new URL("../research/sports-contest-no-exploration-audit.json", import.meta.url), "utf8"));
const sportsDisjointAudit = JSON.parse(fs.readFileSync(new URL("../research/sports-contest-no-disjoint-10000-audit.json", import.meta.url), "utf8"));
const adaptiveAudit = JSON.parse(fs.readFileSync(new URL("../research/adaptive-strategy-64-audit.json", import.meta.url), "utf8"));
const settlementCalibrationAudit = JSON.parse(fs.readFileSync(new URL("../research/settlement-calibration-exact-fee-5000-audit.json", import.meta.url), "utf8"));
const sportsEvaluator = fs.readFileSync(new URL("./evaluate-sports-favorites.mjs", import.meta.url), "utf8");
const build = Number(index.match(/const BUILD_VERSION = (\d+);/)?.[1]);
assert.equal(build, 128);
assert.equal(vercelConfig.git?.deploymentEnabled?.["runtime-state"], false);
assert.equal(settlementCalibrationAudit.requested_markets, 5000);
assert.equal(settlementCalibrationAudit.train_passed, 0);
assert.equal(settlementCalibrationAudit.validation_selected, 0);
assert.equal(settlementCalibrationAudit.holdout_passed, 0);
assert.match(index, /Adaptive strategy 65 · feature-aware maker research · build 128/);
assert.deepEqual([...ALLOWED_RUNTIME_KEYS].sort(), ["pma_agents_v2", "pma_suggestions_v5"]);
assert.match(index, /function collectPublicRuntimeItems\(\)/);
assert.match(index, /const PUBLIC_RUNTIME_KEYS=Object\.freeze\(\[AGENTS_KEY,SUG_KEY\]\)/);
assert.match(index, /suggestions:300/);
assert.match(index, /function compactPublicSuggestionsForSync\(payload\)/);
assert.match(index, /drivers\.slice\(0,1\)/);
assert.match(index, /if\(!out\.trade_ready\)\{delete out\.clob_yes;delete out\.clob_no;\}/);
assert.match(index, /function compactPublicSignalLedger\(ledger\)/);
assert.match(index, /st\.signal_ledger=compactPublicSignalLedger\(st\.signal_ledger\)/);
assert.match(index, /const PUBLIC_RUNTIME_SNAPSHOT_BUDGET_BYTES = 875000;/);
assert.match(index, /const PUBLIC_SIGNAL_OUTCOME_RESERVE = 144;/);
assert.match(index, /const PUBLIC_SIGNAL_LEDGER_RESERVE_BYTES = 75000;/);
assert.match(index, /history:48,snapshots:96,suggestions:300/);
assert.match(index, /function compactHistoryForPublic\(row\)/);
assert.match(index, /function compactSnapshotsForPublic\(rows,limit=PUBLIC_RUNTIME_LIMITS\.snapshots\)/);
assert.match(index, /sourceLimits=Object\.assign\(\{\},PUBLIC_RUNTIME_LIMITS,\{snapshots:SYNC_LIMITS\.snapshots\}\)/);
assert.match(index, /PUBLIC_RUNTIME_MAKER_OUTCOME_LIMIT/);
assert.match(index, /PUBLIC_RUNTIME_HISTORY_FLOOR/);
assert.match(index, /PUBLIC_RUNTIME_SUGGESTION_FLOOR/);
assert.match(index, /function compactMakerOutcomeForPublic\(row\)/);
assert.match(index, /publicMakerCompactionPreservesCalibration:/);
assert.match(index, /function fitPublicRuntimeSnapshot\(snapshot\)/);
assert.match(index, /reservedCount=Math\.min\(PUBLIC_SIGNAL_OUTCOME_RESERVE,outcomes\.length\)/);
assert.match(index, /return fitPublicRuntimeSnapshot\(snapshot\)/);
assert.match(index, /publicCompactionPreservesForwardGrading:/);
assert.match(index, /publicCompactionPreservesAgentCalibration:/);
assert.match(index, /const DIRECTIONAL_PROBATION_HORIZON_HOURS = 6;/);
assert.match(index, /const DIRECTIONAL_PROBATION_MIN_EVENTS = 12;/);
assert.match(index, /sixHourEvidenceStartsBoundedProbation:/);
assert.match(index, /probationMetadataSurvivesOfflineCompaction:/);
assert.match(index, /probationExitsAtExecutableSixHourPrice:/);
assert.match(index, /updateSignalLedger\(st,\[\.\.\.analysisMarkets,\.\.\.supplemental\],sugs,\{liveEvidence:runMode==="live"\}\)/);
assert.match(index, /offlineEvidenceCapabilityIsReadOnly:/);
assert.match(index, /claimedProbationEvents/);
assert.match(api, /adaptive_probation: s\.adaptive_probation, probation_exit_hours: s\.probation_exit_hours/);
assert.match(index, /function buildRuntimeFallbackMarketCache\(stored,state\)/);
assert.match(index, /buildRuntimeFallbackMarketCache\(stored,st\)/);
assert.match(index, /window\.PMA_AUTOMATION=Object\.freeze/);
assert.match(index, /if\(CLOUD_STATE_HEALTH\.read_only\)return false/);
assert.match(index, /function autonomousRuntimeControlsCycle/);
assert.match(index, /if\(CYCLE_RUNNING\|\|autonomousRuntimeControlsCycle\(\)\)return/);
assert.match(index, /btn\.textContent="Auto cycle"/);
assert.match(api, /runtime-state\/runtime\/state\.json/);
assert.match(api, /sanitizeGithubRuntimeState/);
assert.match(api, /api\.github\.com\/repos\/theodore-song\/polymarket-analyst\/contents\/runtime\/state\.json/);
assert.match(api, /Buffer\.from\(file\.content/);
assert.match(api, /searchParams\.set\("runtime", `\$\{Date\.now\(\)\}/);
assert.match(workflow, /cron: "2,7,12,17,22,27,32,37,42,47,52,57 \* \* \* \*"/);
assert.match(workflow, /contents: write/);
assert.match(workflow, /EXPECTED_BUILD: "128"/);
assert.equal((workflow.match(/if: always\(\)/g) || []).length, 2);
assert.match(routingReleaseWorkflow, /name: Release expanded executable search after Vercel quota reset/);
assert.match(routingReleaseWorkflow, /BUNDLE_DEPTH_CANDIDATE_LIMIT=200/);
assert.match(routingReleaseWorkflow, /cron: "25 1-23\/2 23 8 \*"/);
assert.match(workflow, /actions: write/);
assert.match(workflow, /cancel-in-progress: false/);
assert.match(workflow, /next=\$\(\( \(now \/ 300 \+ 1\) \* 300 \+ 15 \)\)/);
assert.match(workflow, /actions\/workflows\/autonomous-cycle\.yml\/dispatches/);
assert.match(workflow, /--data '\{"ref":"main"\}'/);
assert.match(index, /cloudRestartReconstructsOfflineCache:true/);
assert.match(index, /saveSuggestions\(sugs,markets\.length,analysisMarkets\.length,bundleAudit\)/);
assert.match(index, /const NEG_RISK_EVENT_SCAN_LIMIT=1000;/);
assert.match(index, /bundleOpportunityTelemetry:true/);
assert.match(index, /function completeBundleSecuredProfit\(p\)/);
assert.match(index, /label:"Secured bundle profit"/);
assert.match(index, /additional settlement profit is secured by intact verified bundles/);
assert.match(index, /const BUNDLE_DEPTH_CANDIDATE_LIMIT=200;/);
assert.match(index, /const BINARY_COMPLEMENT_DEPTH_RESERVE=40;/);
assert.match(index, /const NEG_RISK_CONVERSION_DEPTH_RESERVE=40;/);
assert.match(index, /bundleRequiresDepthVerification:true/);
assert.match(index, /bundleRequiresClobFeeVerification:true/);
assert.match(index, /function bundleExecutableLeg\(book,units,feeSchedule\)/);
assert.match(index, /function generalAdaptiveLearningExclusion\(trade\)/);
assert.match(index, /specializedUnsafeTradesCannotBiasDirectionalLearning:/);
assert.match(index, /function fetchBundleFeeSchedules\(conditionIds\)/);
assert.match(index, /function fetchNegativeRiskConversionTerms\(marketIds\)/);
assert.match(index, /const NEG_RISK_ADAPTER_ADDRESS="0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296";/);
assert.match(index, /const NEG_RISK_FEE_BIPS_SELECTOR="0x2582cb5e";/);
assert.match(index, /const NEG_RISK_QUESTION_COUNT_SELECTOR="0xb7f75d2c";/);
assert.match(index, /const NEG_RISK_RPC_BATCH_CALL_LIMIT=10;/);
assert.match(index, /function applyNegativeRiskConversionTerms\(candidate,terms\)/);
assert.match(index, /function bundleFeeSchedulesMatch\(expected,reported\)/);
assert.match(index, /rejectsMismatchedClobFeeCurve:/);
assert.doesNotMatch(index, /\/fee-rate\?token_id=/);
assert.match(index, /function maximizeBundleExecution\(candidate,books,minimumUnits\)/);
assert.match(index, /const BUNDLE_MAX_VERIFIED_NOTIONAL=1000;/);
assert.match(index, /const BUNDLE_EVENT_CAP_PCT=0\.12;/);
assert.match(index, /const NEG_RISK_MIN_NET_PROFIT=0\.001;/);
assert.match(index, /const BUNDLE_MIN_EXECUTION_PROFIT=0\.50;/);
assert.match(index, /const BUNDLE_IMMEDIATE_MIN_DAILY_RETURN=0\.0001;/);
assert.match(index, /const BUNDLE_MIN_DAILY_RETURN=0\.0003;/);
assert.match(index, /const BUNDLE_MEDIUM_MIN_DAILY_RETURN=0\.0004;/);
assert.match(index, /const BUNDLE_LONG_MIN_DAILY_RETURN=0\.0005;/);
assert.match(index, /const BUNDLE_EXTENDED_MIN_DAILY_RETURN=0\.00075;/);
assert.match(index, /function bundleMinimumDailyReturn\(item=\{\}\)/);
assert.match(index, /function bundleClearsExecutionProfit\(item=\{\}\)/);
assert.match(index, /const BUNDLE_BASE_POSITION_CAP_PCT=0\.06;/);
assert.match(index, /const BUNDLE_HIGH_EFFICIENCY_POSITION_CAP_PCT=0\.08;/);
assert.match(index, /const BUNDLE_IMMEDIATE_POSITION_CAP_PCT=0\.10;/);
assert.match(index, /const BUNDLE_HIGH_EFFICIENCY_DAILY_RETURN=0\.002;/);
assert.match(index, /function bundlePositionCapPct\(item=\{\}\)/);
assert.match(index, /fee_check_eligible_structures:executableAudit\.filter\(candidate=>candidate\.fee_check_attempted\)\.length/);
assert.match(index, /"profit-below-floor"/);
assert.match(index, /"total-profit-below-floor"/);
assert.match(index, /"return-below-floor"/);
assert.match(index, /function compareBundleOpportunities\(a,b\)/);
assert.match(index, /function binaryComplementBundleSuggestions\(event,\{includeUnprofitable=false\}=\{\}\)/);
assert.match(index, /discoversExecutableBinaryComplements:/);
assert.match(index, /immediatelyMergesBinaryCompleteSet:/);
assert.match(index, /rejectsCrossMarketBinaryMerge:/);
assert.match(index, /immediateMergeMetadataSurvivesSync:/);
assert.match(api, /bundle_immediate_merge: s\.bundle_immediate_merge/);
assert.match(index, /function convertCompleteNegativeRiskNoBundle\(p,bundleId\)/);
assert.match(index, /function negativeRiskBundleSuggestions\(event,\{includeUnprofitable=false,conversionTerms=null\}=\{\}\)/);
assert.match(index, /function negativeRiskExclusivePairSuggestion\(event,\{includeUnprofitable=false\}=\{\}\)/);
assert.match(index, /bundle_logic:"neg-risk-exclusive-no-pair"/);
assert.match(index, /const exclusivePair=negativeRiskExclusivePairSuggestion\(event,\{includeUnprofitable:true\}\)/);
assert.match(index, /discoversExclusiveNoPair:/);
assert.match(index, /rejectsUnprofitableExclusiveNoPair:/);
assert.match(index, /rejectsMismatchedNegativeRiskPair:/);
assert.match(index, /exclusiveNoPairSettlesAtGuaranteedFloor:/);
assert.match(index, /exclusive_pair_structures:exclusivePairs\.length/);
assert.match(index, /Exclusive NO pairs:/);
assert.match(index, /preservesBothCompleteSidesThroughAudit:/);
assert.match(index, /discoversExactCompleteNoConversion:/);
assert.match(index, /deductsAdapterFeeFromConversionPayout:/);
assert.match(index, /atomicallyConvertsCompleteNoSet:/);
assert.match(index, /blocksImmediateConversionOnMismatchedMetadata:/);
assert.match(index, /blocksImmediateConversionOnQuestionCountMismatch:/);
assert.match(index, /blocksImmediateConversionOnMetadataFeeMismatch:/);
assert.match(index, /doesNotInventConversionWithoutAdapterMetadata:/);
assert.match(index, /conversion_structures:conversions\.length/);
assert.match(index, /conversion_candidates_scanned:audited\.filter\(candidate=>candidate\.bundle_conversion_candidate\)\.length/);
assert.match(index, /actionable_conversions:unique\.filter\(candidate=>candidate\.bundle_immediate_convert\)\.length/);
assert.match(index, /Exact complete-NO conversion:/);
assert.match(api, /bundle_immediate_convert: s\.bundle_immediate_convert/);
assert.match(api, /bundle_min_daily_return: s\.bundle_min_daily_return/);
assert.match(api, /execution_total_profit: s\.execution_total_profit/);
assert.match(api, /neg_risk_market_id: s\.neg_risk_market_id, neg_risk_fee_bips: s\.neg_risk_fee_bips/);
assert.match(index, /reservesBinaryComplementDepthChecks:/);
assert.match(index, /reservesExactConversionDepthChecks:/);
assert.match(index, /function bundleAgentOwnerId\(s\)/);
assert.match(index, /routesSettlementCompleteSetsToValue:/);
assert.match(index, /routesExactConversionsToMomentum:/);
assert.match(index, /routesBinaryMergesToBreakout:/);
assert.match(index, /routesExclusiveNoPairToTailAlpha:/);
assert.match(index, /ownedProtectedBundlesBypassDirectionalRankFilter:/);
assert.match(index, /routesDominanceSpreadsToDiversifier:/);
assert.match(index, /bundleResearchOwnershipMatchesCapital:/);
assert.match(index, /function currentBundleEventKeys\(st\)/);
assert.match(index, /tracksGlobalBundleEventOwnership:/);
assert.match(index, /diversifierExecutesVerifiedDominanceSpread:/);
assert.match(index, /const claimedBundleEvents=currentBundleEventKeys\(st\);/);
assert.match(index, /function bundleVerificationShortlist\(candidates\)/);
assert.match(index, /function prioritizeIndependentBundles\(candidates,limit=Infinity\)/);
assert.match(index, /prioritizesIndependentEventsBeforeAlternates:/);
assert.match(index, /function bundleEventExposure\(portfolio,eventKey\)/);
assert.match(index, /function recycleVerifiedBundleCapital\(st,priceMap,\{execute=true\}=\{\}\)/);
assert.match(index, /bundleProfitRecyclingRequiresLive:/);
assert.match(index, /const BUNDLE_EXIT_PROFIT_CAPTURE=0\.85;/);
assert.match(index, /atomicallyRecyclesEightySixPercentProfit:/);
assert.match(index, /prioritizesReturnPerLockedDay:/);
assert.match(index, /capsUnderlyingEventExposure:/);
assert.match(index, /blocksLowReturnPerLockedDay:/);
assert.match(index, /shortLocksUseProductiveHurdle:/);
assert.match(index, /longLocksRequireMoreReturn:/);
assert.match(index, /scalesSlimMarginsToMeaningfulProfit:/);
assert.match(index, /rejectsDustProfitDespitePositiveUnitMargin:/);
assert.match(index, /actualOpenedSizeMustPreserveProfitFloor:/);
assert.match(index, /durationAwareSizingScalesProtectedCapital:/);
assert.match(index, /sameCycleEventBurstStaysWithinCap:/);
assert.match(index, /sizesToLargestVerifiedProfitableFill:/);
assert.match(index, /stopsSizingAtShallowestBundleLeg:/);
assert.match(index, /!s\.depth_verified\|\|!s\.fees_verified\|\|s\.verification_status!=="executable"/);
assert.match(runner, /MAX_STATE_BYTES = 900_000/);
assert.match(runner, /compactRuntimeTransportSnapshot\(rawSnapshot\)/);
assert.match(runner, /localStorage\.getItem\(agentKey\)/);
assert.match(runner, /localStorage\.getItem\(suggestionsKey\)/);
assert.match(runner, /TRANSPORT_TARGET_BYTES = 850_000/);
assert.match(runner, /TRANSPORT_SUGGESTION_FLOOR = 240/);
assert.match(runner, /Production already advanced to Build \$\{status\.build\}; retiring stale Build \$\{expectedBuild\} runner/);
assert.equal(resolutionAudit.strategy, "resolution-window-no-50-55-forward-shadow-v3");
assert.deepEqual(resolutionAudit.selection.horizon_days_enabled, []);
assert.deepEqual(resolutionAudit.selection.horizon_days_observed, [4]);
assert.equal(resolutionAudit.disjoint_holdout.passed_strict_gate, false);
assert.ok(resolutionAudit.disjoint_holdout.holdout_event_lower_90 < 0);
assert.equal(resolutionAudit.production_constraints.capital_enabled, false);
assert.equal(resolutionAudit.production_constraints.promotion_events, 40);
assert.equal(sportsContestAudit.production_strategy, 62);
assert.equal(sportsContestAudit.status, "bounded-paper-exploration-not-proven");
assert.equal(sportsContestAudit.corrected_5000_market_result.independent_contests, 76);
assert.equal(sportsContestAudit.corrected_5000_market_result.passed_strict_gate, false);
assert.ok(sportsContestAudit.corrected_5000_market_result.validation.lower_95 < 0);
assert.equal(sportsContestAudit.production_constraints.initial_position_pct, 0.5);
assert.equal(sportsContestAudit.production_constraints.total_lane_cap_pct, 3);
assert.equal(sportsContestAudit.production_constraints.exact_entry_fee_required, true);
assert.equal(sportsDisjointAudit.status, "zero-capital-forward-shadow");
assert.equal(sportsDisjointAudit.recent_5000.holdout_rules_passing, 0);
assert.equal(sportsDisjointAudit.older_disjoint_5000.validation_rules_passing, 0);
assert.ok(sportsDisjointAudit.recent_5000.closest_rule.holdout_lower_90 < 0);
assert.ok(sportsDisjointAudit.older_disjoint_5000.closest_rule.holdout_lower_90 < 0);
assert.equal(sportsDisjointAudit.decision.capital_enabled, false);
assert.equal(sportsDisjointAudit.decision.initial_position_pct, 0);
assert.match(sportsEvaluator, /const MARKET_SKIP =/);
assert.match(sportsEvaluator, /fetchMarkets\(MARKET_LIMIT, MARKET_SKIP\)/);
assert.equal(adaptiveAudit.strategy, 64);
assert.equal(adaptiveAudit.fetched_markets, 500);
assert.deepEqual(adaptiveAudit.probation_rule_ids, []);
assert.deepEqual(adaptiveAudit.durable_rule_ids, []);
assert.ok(adaptiveAudit.horizons.every((row) => row.validation_selected === 0 && row.holdout_passed === 0));
assert.match(index, /function sportsContestKey\(m\)/);
assert.match(index, /function sportsContestNoSuggestions\(markets\)/);
assert.match(index, /const SPORTS_FAVORITE_MAX_NEW_PER_CYCLE=1;/);
assert.match(index, /const SPORTS_FAVORITE_PILOT_POSITION_PCT=0;/);
assert.match(index, /capitalRequiresForwardPromotion:true,boundedExploration:false/);
assert.match(index, /initialStateIsZeroCapitalShadow:/);
assert.match(index, /function compactDecisionForPublic\(decision\)/);
assert.match(index, /delete out\.learning\.buckets/);
const agentIds = ["value", "momentum", "favorite", "longshot", "diversifier", "catalyst", "reversal", "breakout", "tailalpha", "conviction"];
const agents = Object.fromEntries(agentIds.map(id => [id, { cash: 10000, positions: [], lastDecision: { mode: "test" } }]));
const snapshot = {
schema_version: 1,
build_version: build,
generated_at: new Date().toISOString(),
last_cycle_hour: "2026-08-21T20|v59",
items: {
pma_agents_v2: JSON.stringify({ seeded: true, last_cycle_hour: "2026-08-21T20|v59", agents }),
pma_suggestions_v5: JSON.stringify({ suggestions: [{ market_id: "test" }] }),
},
};
assert.equal(validateRuntimeSnapshot(snapshot, build).agents.seeded, true);
assert.throws(() => validateRuntimeSnapshot({ ...snapshot, items: { ...snapshot.items, pma_paper_accounts_v1: "{}" } }, build), /unexpected keys/);
assert.throws(() => validateRuntimeSnapshot({ ...snapshot, build_version: build - 1 }, build), /Expected Build/);
assert.throws(() => validateRuntimeSnapshot({ ...snapshot, items: { ...snapshot.items, pma_suggestions_v5: JSON.stringify({ suggestions: [] }) } }, build), /no live suggestions/);
const transportState = JSON.parse(snapshot.items.pma_agents_v2);
for (const [index, portfolio] of Object.values(transportState.agents).entries()) {
portfolio.cash = 9000 + index;
portfolio.positions = [{ market_id: `position-${index}`, value: 1000 }];
portfolio.history = Array.from({ length: 80 }, (_, row) => ({ date: "2026-08-22", action: "WATCH", question: "q".repeat(180), detail: "d".repeat(500 + row) }));
portfolio.snapshots = Array.from({ length: 240 }, (_, row) => ({ timestamp: new Date(1_700_000_000_000 + row * 300_000).toISOString(), equity: 10000 + row }));
portfolio.maker_outcomes = Array.from({ length: 45 }, (_, row) => ({ quote_id: `${index}-${row}`, pnl: row / 100,
price_balance_band: "balanced", day_move_band: "calm", margin_band: "thick", competition_band: "high" }));
}
transportState.signal_ledger = {
pending: Array.from({ length: 50 }, (_, index) => ({ key: `pending-${index}`, event_key: `event-${index}` })),
outcomes: Array.from({ length: 144 }, (_, index) => ({ key: `outcome-${index}`, event_key: `event-${index}`, return: 0.01 })),
expired_ungraded: 0,
};
const transportSuggestions = {
suggestions: Array.from({ length: 300 }, (_, index) => ({ market_id: `${index}`, trade_ready: false, signal_type: "trend", rationale: "r".repeat(500), drivers: ["d".repeat(200)] })),
};
const transportInput = { ...snapshot, summary: { signal_ledger: { pending_retained: 0, pending_total: 0, outcomes_retained: 0, outcomes_total: 0 } }, items: { pma_agents_v2: JSON.stringify(transportState), pma_suggestions_v5: JSON.stringify(transportSuggestions) } };
const transportOutput = compactRuntimeTransportSnapshot(transportInput);
const transportedState = JSON.parse(transportOutput.items.pma_agents_v2);
assert.equal(transportedState.signal_ledger.pending.length, 50);
assert.equal(transportedState.signal_ledger.outcomes.length, 144);
assert.equal(transportedState.agents.value.cash, 9000);
assert.equal(transportedState.agents.value.positions[0].market_id, "position-0");
assert.equal(transportedState.agents.value.history.length, 24);
assert.equal(transportedState.agents.value.snapshots.length, 96);
assert.equal(transportedState.agents.value.snapshots[0].timestamp, transportState.agents.value.snapshots[0].timestamp);
assert.equal(transportedState.agents.value.snapshots.at(-1).timestamp, transportState.agents.value.snapshots.at(-1).timestamp);
assert.equal(transportedState.agents.value.maker_outcomes.length, 30);
assert.equal(transportedState.agents.value.maker_outcomes[0].price_balance_band, "balanced");
assert.equal(transportedState.agents.value.maker_outcomes[0].day_move_band, "calm");
assert.equal(transportedState.agents.value.maker_outcomes[0].margin_band, "thick");
assert.equal(transportedState.agents.value.maker_outcomes[0].competition_band, "high");
assert.equal(JSON.parse(transportOutput.items.pma_suggestions_v5).suggestions.length, 300);
assert.equal(transportOutput.summary.signal_ledger.pending_retained, 50);
assert.equal(transportOutput.summary.signal_ledger.outcomes_retained, 144);
assert.ok(Buffer.byteLength(JSON.stringify(transportOutput)) < Buffer.byteLength(JSON.stringify(transportInput)));
const stressState = structuredClone(transportState);
stressState.signal_ledger = {
pending: Array.from({ length: 600 }, (_, index) => ({
key: `stress-pending-${index}`,
market_id: `pending-market-${index}`,
observed_at: new Date(1_700_000_000_000 + index * 1_000).toISOString(),
side: "YES",
learning_payload: "p".repeat(1_200),
})),
outcomes: Array.from({ length: 1_000 }, (_, index) => ({
key: `stress-outcome-${index}`,
market_id: `outcome-market-${index}`,
evaluated_at: new Date(1_700_000_000_000 + index * 1_000).toISOString(),
return: index / 10_000,
learning_payload: "o".repeat(1_200),
})),
};
const stressInput = {
...transportInput,
summary: { signal_ledger: { pending_retained: 600, pending_total: 600, outcomes_retained: 1_000, outcomes_total: 1_000 } },
items: { ...transportInput.items, pma_agents_v2: JSON.stringify(stressState) },
};
const stressOutput = compactRuntimeTransportSnapshot(stressInput);
const stressedState = JSON.parse(stressOutput.items.pma_agents_v2);
assert.ok(Buffer.byteLength(JSON.stringify(stressOutput)) <= 850_000);
assert.equal(stressedState.agents.value.positions[0].market_id, "position-0");
assert.ok(stressedState.signal_ledger.pending.length > 0);
assert.ok(stressedState.signal_ledger.outcomes.length > 0);
assert.equal(stressedState.signal_ledger.pending[0].key, "stress-pending-0");
assert.equal(stressedState.signal_ledger.outcomes.at(-1).key, "stress-outcome-999");
assert.equal(stressOutput.summary.signal_ledger.pending_retained, stressedState.signal_ledger.pending.length);
assert.equal(stressOutput.summary.signal_ledger.pending_total, 600);
assert.equal(stressOutput.summary.signal_ledger.outcomes_retained, stressedState.signal_ledger.outcomes.length);
assert.equal(stressOutput.summary.signal_ledger.outcomes_total, 1_000);
console.log(`autonomous runtime verified for Build ${build}`);
+24
View File
@@ -0,0 +1,24 @@
import assert from "node:assert/strict";
import fs from "node:fs";
const index = fs.readFileSync(new URL("../index.html", import.meta.url), "utf8");
const worker = fs.readFileSync(new URL("../sw.js", import.meta.url), "utf8");
const cycleWorker = fs.readFileSync(new URL("../cycle-worker.js", import.meta.url), "utf8");
const build = Number(index.match(/const BUILD_VERSION = (\d+);/)?.[1]);
const cacheBuild = Number(worker.match(/polymarket-arena-build-(\d+)/)?.[1]);
assert.ok(Number.isInteger(build));
assert.equal(cacheBuild, build, "Service-worker cache must advance with the deployed build");
assert.match(worker, /keys\.filter\(key => key !== CACHE_NAME\)/);
assert.match(worker, /fetch\(event\.request\)\.then\(response =>/);
assert.match(worker, /\.catch\(\(\) => caches\.match\(event\.request\)\)/);
assert.doesNotMatch(worker, /caches\.match\(event\.request\)\.then\(cached => cached \|\| fetch/);
assert.match(worker, /"\/cycle-worker\.js"/);
assert.match(cycleWorker, /const INTERVAL_MS = 60000;/);
assert.match(index, /requires_live:true/);
assert.match(index, /runMode==="live"\|\|!next\.requires_live/);
assert.match(index, /adaptive_probation:s\.adaptive_probation/);
assert.match(index, /probation_exit_hours:s\.probation_exit_hours/);
assert.match(index, /awaiting-probation-executable-exit/);
console.log(`offline runtime verified for Build ${build}`);
+150
View File
@@ -0,0 +1,150 @@
import assert from "node:assert/strict";
import { databaseConnectionDiagnostics, databaseUrl, databaseUrls, normalizeDatabaseUrl } from "../lib/db.js";
import { compactAgentState, compactSuggestion, cycleVersion, providerErrorCode, sanitizeGithubRuntimeState } from "../api/state.js";
assert.equal(normalizeDatabaseUrl("psql 'postgresql://user:pass@example.test/db?sslmode=require'"),
"postgresql://user:pass@example.test/db?sslmode=require");
assert.equal(normalizeDatabaseUrl('"postgres://user:pass@example.test/db"'), "postgres://user:pass@example.test/db");
assert.equal(normalizeDatabaseUrl("DATABASE_URL=postgresql://user:pass@example.test/db"),
"postgresql://user:pass@example.test/db");
assert.equal(cycleVersion("2026-08-21T20|v59"), 59);
assert.equal(cycleVersion("2026-08-21T20:05|s62"), 62);
assert.equal(cycleVersion("2026-08-21T20:05|s63"), 63);
assert.equal(cycleVersion("2026-08-21T20:05"), 0);
const originalDatabaseUrl = process.env.DATABASE_URL;
const originalNeonUrl = process.env.NEON_DATABASE_URL;
process.env.DATABASE_URL = "not-a-postgres-url";
process.env.NEON_DATABASE_URL = "postgresql://alias:pass@example.test/db";
assert.equal(databaseUrl(), process.env.NEON_DATABASE_URL);
assert.deepEqual(databaseUrls(), [process.env.NEON_DATABASE_URL]);
process.env.DATABASE_URL = "postgresql://stale:pass@example.test/db";
assert.deepEqual(databaseUrls(), [process.env.DATABASE_URL, process.env.NEON_DATABASE_URL]);
const diagnostics = databaseConnectionDiagnostics();
assert.equal(diagnostics.candidates, 2);
assert.equal(diagnostics.urls[0].valid_postgres_url, true);
assert.equal(diagnostics.urls[0].has_username, true);
assert.equal(diagnostics.urls[0].has_password, true);
assert.equal(diagnostics.urls[0].has_database_name, true);
if (originalDatabaseUrl === undefined) delete process.env.DATABASE_URL;
else process.env.DATABASE_URL = originalDatabaseUrl;
if (originalNeonUrl === undefined) delete process.env.NEON_DATABASE_URL;
else process.env.NEON_DATABASE_URL = originalNeonUrl;
const compacted = compactSuggestion({
market_id: "bundle:1:yes", side: "YES", signal_type: "bundle-arb", signal_confidence: 1,
entry_candidate: true, adaptive_promotion: false, requires_live: true, bundle_id: "bundle:1:yes", bundle_event_id: "1",
bundle_side: "NO", bundle_logic: "neg-risk-complete-no", bundle_conversion_candidate: true,
bundle_conversion_terms_verified: true, bundle_immediate_convert: true,
neg_risk_market_id: "0xmarket", neg_risk_fee_bips: 25, neg_risk_metadata_fee_bips: 25,
neg_risk_question_count: 3, conversion_verification_status: "verified-onchain", bundle_cost_per_unit: 1.95,
bundle_payout_per_unit: 1.995, bundle_settlement_payout_per_unit: 2,
bundle_net_profit_per_unit: 0.05, bundle_capital_efficiency: 0.0025, bundle_min_daily_return: 0.0001,
bundle_legs: [{ market_id: "1", side: "YES" }],
fees_enabled: false, fee_schedule: { rate: 0, exponent: 1, takerOnly: true },
depth_verified: true, fees_verified: true, fee_model: "verified-market-specific", execution_model: "live-order-book-vwap",
execution_units: 250, execution_notional: 237.5, execution_total_profit: 12.5,
verification_status: "executable", promoted_for_agents: ["value"],
});
assert.equal(compacted.signal_type, "bundle-arb");
assert.equal(compacted.entry_candidate, true);
assert.equal(compacted.requires_live, true);
assert.equal(compacted.bundle_side, "NO");
assert.equal(compacted.bundle_event_id, "1");
assert.equal(compacted.bundle_logic, "neg-risk-complete-no");
assert.equal(compacted.bundle_conversion_candidate, true);
assert.equal(compacted.bundle_conversion_terms_verified, true);
assert.equal(compacted.bundle_immediate_convert, true);
assert.equal(compacted.neg_risk_market_id, "0xmarket");
assert.equal(compacted.neg_risk_fee_bips, 25);
assert.equal(compacted.neg_risk_metadata_fee_bips, 25);
assert.equal(compacted.neg_risk_question_count, 3);
assert.equal(compacted.conversion_verification_status, "verified-onchain");
assert.equal(compacted.bundle_settlement_payout_per_unit, 2);
assert.equal(compacted.bundle_capital_efficiency, 0.0025);
assert.equal(compacted.bundle_min_daily_return, 0.0001);
assert.equal(compacted.bundle_legs.length, 1);
assert.equal(compacted.fee_schedule.rate, 0);
assert.equal(compacted.execution_units, 250);
assert.equal(compacted.execution_total_profit, 12.5);
assert.equal(compacted.verification_status, "executable");
assert.deepEqual(compacted.promoted_for_agents, ["value"]);
const compactedShock = compactSuggestion({
market_id: "shock:1", event_key: "event:1", side: "NO", signal_type: "shock-fade-shadow",
market_price: 0.18, entry_price: 0.1845, shock_move_1h: 0.06, shock_prior_move_1h: 0.03,
shock_move_3h: 0.12, shock_observed_at: "2026-08-21T20:00:00.000Z", shock_strategy_version: 3,
pilot_prior: { modeled_cost_cents: 2 }, requires_live: true,
});
assert.equal(compactedShock.event_key, "event:1");
assert.equal(compactedShock.market_price, 0.18);
assert.equal(compactedShock.shock_move_3h, 0.12);
assert.equal(compactedShock.shock_strategy_version, 3);
assert.equal(compactedShock.pilot_prior.modeled_cost_cents, 2);
const compactedProbation = compactSuggestion({
market_id: "probation:1", event_key: "event:probation", side: "YES", signal_type: "trend",
trade_ready: true, entry_candidate: true, adaptive_probation: true, probation_exit_hours: 6,
promoted_for_agents: ["momentum"], fee_schedule: { rate: 0.04, exponent: 1, takerOnly: true },
});
assert.equal(compactedProbation.adaptive_probation, true);
assert.equal(compactedProbation.probation_exit_hours, 6);
assert.deepEqual(compactedProbation.promoted_for_agents, ["momentum"]);
const compactedState = compactAgentState({
agents: { reversal: {
positions: [], closed: [], history: [], snapshots: [],
shock_fade_shadows: [{ event_key: "event:1", shock_strategy_version: 3 }],
shock_fade_outcomes: [{ event_key: "event:0", shock_strategy_version: 3, net_return: 0.08 }],
} },
signal_ledger: { pending: [], outcomes: [], expired_ungraded: 7 },
});
assert.equal(compactedState.signal_ledger.expired_ungraded, 7);
assert.equal(compactedState.agents.reversal.shock_fade_shadows[0].shock_strategy_version, 3);
assert.equal(compactedState.agents.reversal.shock_fade_outcomes[0].net_return, 0.08);
const ledgerOrder = compactAgentState({
agents: {},
signal_ledger: {
pending: Array.from({ length: 650 }, (_, i) => ({ key: `pending-${i}` })),
outcomes: Array.from({ length: 1050 }, (_, i) => ({ key: `outcome-${i}` })),
},
}).signal_ledger;
assert.equal(ledgerOrder.pending.length, 600);
assert.equal(ledgerOrder.pending[0].key, "pending-0");
assert.equal(ledgerOrder.pending.at(-1).key, "pending-599");
assert.equal(ledgerOrder.outcomes.length, 1000);
assert.equal(ledgerOrder.outcomes[0].key, "outcome-50");
assert.equal(ledgerOrder.outcomes.at(-1).key, "outcome-1049");
const publicAgents = Object.fromEntries([
"value", "momentum", "favorite", "longshot", "diversifier", "catalyst", "reversal", "breakout", "tailalpha", "conviction",
].map(id => [id, { cash: 10000, positions: [], closed: [], history: [], snapshots: [] }]));
const sanitizedRuntime = sanitizeGithubRuntimeState({
schema_version: 1,
build_version: 88,
generated_at: "2026-08-21T20:00:00.000Z",
items: {
pma_agents_v2: JSON.stringify({ seeded: true, last_cycle_hour: "2026-08-21T20|v59", agents: publicAgents }),
pma_suggestions_v5: JSON.stringify({ suggestions: [] }),
pma_paper_accounts_v1: JSON.stringify({ password: "must-not-survive" }),
pma_trade_email_alerts_v1: JSON.stringify({ email: "private@example.com" }),
pma_live_readiness_v1: JSON.stringify({ wallet: "private" }),
},
});
assert.deepEqual(Object.keys(sanitizedRuntime.items).sort(), ["pma_agents_v2", "pma_suggestions_v5"]);
assert.equal(sanitizedRuntime.read_only, true);
assert.equal(sanitizedRuntime.source, "github-actions");
assert.equal(sanitizedRuntime.build_version, 88);
assert.throws(() => sanitizeGithubRuntimeState({ items: { pma_suggestions_v5: "{}" } }), /missing agent portfolios/);
assert.equal(providerErrorCode(new Error("403 Forbidden")), "authorization_failed");
assert.equal(providerErrorCode(new Error("Error connecting to database: HTTP status 402")), "provider_payment_required");
assert.equal(providerErrorCode(new Error("invalid connection string")), "invalid_connection_string");
assert.equal(providerErrorCode(Object.assign(new Error("database rejected login"), { code: "28P01" })), "authorization_failed");
const nestedFetchError = Object.assign(new Error("Error connecting to database"), {
name: "NeonDbError",
sourceError: Object.assign(new TypeError("fetch failed"), { cause: { code: "ENOTFOUND" } }),
});
assert.equal(providerErrorCode(nestedFetchError), "dns_failed");
console.log("server state tests passed");
+74
View File
@@ -0,0 +1,74 @@
import assert from "node:assert/strict";
import fs from "node:fs";
const report = JSON.parse(fs.readFileSync(new URL("../research/shock-strategy-3-audit.json", import.meta.url), "utf8"));
const independent = JSON.parse(fs.readFileSync(new URL("../research/shock-strategy-3-independent-audit.json", import.meta.url), "utf8"));
const strategy4 = JSON.parse(fs.readFileSync(new URL("../research/shock-strategy-4-audit.json", import.meta.url), "utf8"));
const strategy4Independent = JSON.parse(fs.readFileSync(new URL("../research/shock-strategy-4-independent-audit.json", import.meta.url), "utf8"));
const index = fs.readFileSync(new URL("../index.html", import.meta.url), "utf8");
const candidate = report.candidates.find(row => row.rule.id === "fade_h12_w3_m0.08_accelerating_v1_All_All");
assert.equal(report.markets, 2000);
assert.equal(report.observations, 534894);
assert.equal(report.methodology.costCents, 2);
assert.deepEqual(report.methodology.entryPriceRange, [0.08, 0.92]);
assert.deepEqual(report.methodology.returnWinsorization, [-1, 2]);
assert.ok(candidate, "The deployed Strategy 3 rule is missing from the audit artifact");
assert.equal(candidate.rule.horizon, 12);
assert.equal(candidate.rule.window, 3);
assert.equal(candidate.rule.minMove, 0.08);
assert.equal(candidate.rule.confirmation, "accelerating");
assert.equal(candidate.passesHoldout, true);
assert.equal(candidate.passesEventHoldout, true);
assert.equal(candidate.passesArchive, false);
for (const partition of ["train", "validation", "holdout", "eventHoldout"]) {
assert.ok(candidate[partition].events >= 80, `${partition} lacks independent event support`);
assert.ok(candidate[partition].lower > 0, `${partition} lower confidence bound is not positive`);
}
assert.equal(independent.activeMarketsSkipped, 2000);
assert.equal(independent.markets, 1000);
assert.equal(independent.observations, 246580);
assert.equal(independent.methodology.costCents, 2);
assert.deepEqual(independent.methodology.returnWinsorization, [-1, 2]);
assert.equal(independent.exactStrategy3.rule.id, candidate.rule.id);
for (const partition of ["train", "validation", "holdout", "eventHoldout"]) {
assert.ok(independent.exactStrategy3[partition].mean > 0, `${partition} independent mean is not positive`);
}
assert.ok(independent.exactStrategy3.eventHoldout.events >= 60);
assert.ok(independent.exactStrategy3.eventHoldout.lower > 0);
assert.ok(independent.exactStrategy3.train.lower < 0, "Independent train limitation must remain visible");
assert.ok(independent.exactStrategy3.holdout.lower < 0, "Independent chronological limitation must remain visible");
assert.match(strategy4.methodology.strategy4ContractGate, /Reject path barriers and exact numeric ranges/);
assert.equal(strategy4.exactStrategy4.rule.contractGate, "strategy4");
assert.equal(strategy4Independent.activeMarketsSkipped, 2000);
for (const partition of ["train", "validation", "holdout", "eventHoldout"]) {
assert.ok(strategy4.exactStrategy4[partition].events >= 60, `${partition} Strategy 4 primary support is too small`);
assert.ok(strategy4.exactStrategy4[partition].lower > 0, `${partition} Strategy 4 primary lower bound must remain positive`);
}
assert.ok(strategy4.exactStrategy4.holdout.mean < 0, "Primary trade-level holdout limitation must remain visible");
assert.ok(strategy4Independent.exactStrategy4.holdout.eventMean < 0, "Independent chronological event mean must remain negative");
assert.ok(strategy4Independent.exactStrategy4.holdout.lower < 0, "Independent chronological failure must remain visible");
assert.ok(strategy4Independent.exactStrategy4.eventHoldout.lower < 0, "Independent event-holdout uncertainty must remain visible");
const support = { train: 15, validation: 8, holdout: 8, eventHoldout: 8 };
const partitions = Object.keys(support);
const independentRefinements = new Map(strategy4Independent.strategy4Refinements.map(row => [row.rule.id, row]));
const primarySurvivors = strategy4.strategy4Refinements.filter(row => partitions.every(partition =>
row[partition].events >= support[partition] && row[partition].eventMean > 0 && row[partition].lower > 0));
assert.equal(primarySurvivors.length, 1, "Unexpected Strategy 4 primary refinement count");
assert.equal(primarySurvivors[0].rule.category, "All");
assert.equal(primarySurvivors[0].rule.band, "All");
assert.equal(primarySurvivors.filter(row => {
const untouched = independentRefinements.get(row.rule.id);
return untouched && partitions.every(partition => untouched[partition].events >= support[partition]
&& untouched[partition].eventMean > 0 && untouched[partition].lower > 0);
}).length, 0, "A Strategy 4 refinement unexpectedly passed every independent partition");
assert.match(index, /const SHOCK_FADE_BACKTEST_APPROVED=false/);
assert.match(index, /const SHOCK_FADE_QUALIFICATION_EVENTS=40/);
assert.match(index, /const SHOCK_FADE_PROMOTION_EVENTS=80/);
console.log("shock audit artifact verified");
+36
View File
@@ -0,0 +1,36 @@
const CACHE_NAME = "polymarket-arena-build-128";
const APP_SHELL = ["/", "/index.html", "/personal.html", "/cycle-worker.js"];
self.addEventListener("install", event => {
event.waitUntil(caches.open(CACHE_NAME).then(cache => cache.addAll(APP_SHELL)).then(() => self.skipWaiting()));
});
self.addEventListener("activate", event => {
event.waitUntil(caches.keys()
.then(keys => Promise.all(keys.filter(key => key !== CACHE_NAME).map(key => caches.delete(key))))
.then(() => self.clients.claim()));
});
self.addEventListener("fetch", event => {
if(event.request.method !== "GET") return;
const url = new URL(event.request.url);
if(event.request.mode === "navigate") {
event.respondWith(fetch(event.request)
.then(response => {
const copy = response.clone();
caches.open(CACHE_NAME).then(cache => cache.put(event.request, copy));
return response;
})
.catch(async () => (await caches.match(event.request)) || (await caches.match("/index.html"))));
return;
}
if(url.origin === self.location.origin && !url.pathname.startsWith("/api/")) {
event.respondWith(fetch(event.request).then(response => {
const copy = response.clone();
caches.open(CACHE_NAME).then(cache => cache.put(event.request, copy));
return response;
}).catch(() => caches.match(event.request)));
}
});
+30
View File
@@ -1,4 +1,16 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"git": {
"deploymentEnabled": {
"runtime-state": false
}
},
"rewrites": [
{
"source": "/api/liquidity",
"destination": "/api/live?action=liquidity"
}
],
"headers": [
{
"source": "/",
@@ -50,6 +62,24 @@
"value": "0"
}
]
},
{
"source": "/sw.js",
"headers": [
{
"key": "Cache-Control",
"value": "no-cache, max-age=0, must-revalidate"
}
]
},
{
"source": "/cycle-worker.js",
"headers": [
{
"key": "Cache-Control",
"value": "no-cache, max-age=0, must-revalidate"
}
]
}
]
}