mirror of
https://github.com/theodore-song/polymarket-analyst.git
synced 2026-08-23 12:38:11 +00:00
Compare commits
168
Commits
86fa65f8f1
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d210ffb93 | ||
|
|
53141d9071 | ||
|
|
974a957f58 | ||
|
|
2de28533d8 | ||
|
|
c5f59aa992 | ||
|
|
f1799a2384 | ||
|
|
e087d824ca | ||
|
|
8e04e1bff7 | ||
|
|
1ea235b0cf | ||
|
|
3183612d13 | ||
|
|
05ebefaee0 | ||
|
|
c261a1ae73 | ||
|
|
dd7b376b44 | ||
|
|
008801f179 | ||
|
|
6853821dc9 | ||
|
|
c15dc12323 | ||
|
|
1e0921fc49 | ||
|
|
d64810fc18 | ||
|
|
d32ce0de42 | ||
|
|
605a5f2ae4 | ||
|
|
9e87d1f9f2 | ||
|
|
ee2d3edd72 | ||
|
|
4738767514 | ||
|
|
7ae2d6bdfc | ||
|
|
a6c9fc1a14 | ||
|
|
0496bb1d9f | ||
|
|
d9e1e29a8d | ||
|
|
933290290b | ||
|
|
ca302b7168 | ||
|
|
36d0dae5bd | ||
|
|
4d4a4b6d4d | ||
|
|
ffd3acb7d9 | ||
|
|
dcede02676 | ||
|
|
e3086ed5fb | ||
|
|
0084654f15 | ||
|
|
c52368dc9e | ||
|
|
5e4355b850 | ||
|
|
8ee554ea16 | ||
|
|
2181d62104 | ||
|
|
26387e059f | ||
|
|
aa5885b295 | ||
|
|
546f174276 | ||
|
|
3152096057 | ||
|
|
d375ed7fc3 | ||
|
|
7f39f6b6bb | ||
|
|
066cad13da | ||
|
|
c065ab5496 | ||
|
|
5c7c0440a7 | ||
|
|
4539456ac2 | ||
|
|
98744e07ba | ||
|
|
efd53a5f88 | ||
|
|
ab165b0617 | ||
|
|
95947c4df6 | ||
|
|
1da2ab3e29 | ||
|
|
a01571d5ee | ||
|
|
c0ce373833 | ||
|
|
b956f50cbe | ||
|
|
3efc5484c3 | ||
|
|
4513f266c5 | ||
|
|
50db7ed7b3 | ||
|
|
22b5ddcee1 | ||
|
|
84e0ef703b | ||
|
|
6e6477a4e3 | ||
|
|
83e4ea8203 | ||
|
|
a10c9a304c | ||
|
|
6f77c2c3bd | ||
|
|
d59f7ace03 | ||
|
|
ed140548d2 | ||
|
|
55ab1f02ea | ||
|
|
067b5f7ab8 | ||
|
|
5a892e6b0d | ||
|
|
f97b0d9e90 | ||
|
|
6f3982a4da | ||
|
|
03dbed706a | ||
|
|
32f5d78374 | ||
|
|
b932c9fc59 | ||
|
|
f49138e1ed | ||
|
|
9fca4e303d | ||
|
|
39e893a553 | ||
|
|
11ea832daf | ||
|
|
7e75d98d8c | ||
|
|
5f14d012a5 | ||
|
|
007692f4e4 | ||
|
|
b73057cc3d | ||
|
|
d7c6b8aa5e | ||
|
|
846b3fd5ba | ||
|
|
b8415dc750 | ||
|
|
ff337588be | ||
|
|
6b1ab20439 | ||
|
|
b7b66d5c48 | ||
|
|
bd326a2123 | ||
|
|
e5efc85f65 | ||
|
|
c983c1e99f | ||
|
|
b9d8d66264 | ||
|
|
bf25c62871 | ||
|
|
720aa75d78 | ||
|
|
f75e6b0877 | ||
|
|
45421c985a | ||
|
|
78b832f926 | ||
|
|
0dbc6bf650 | ||
|
|
a5441e8659 | ||
|
|
c7abdf8d4d | ||
|
|
97186482df | ||
|
|
a1fc52bc20 | ||
|
|
c20ac0e78f | ||
|
|
7001366334 | ||
|
|
c78a13bbb7 | ||
|
|
25622d2ee5 | ||
|
|
846d1f1482 | ||
|
|
b768d92f36 | ||
|
|
ad11de39ad | ||
|
|
7b81dbfa4b | ||
|
|
0db53c46f8 | ||
|
|
aea8f5281d | ||
|
|
8f89847cd6 | ||
|
|
bc5de3c013 | ||
|
|
807eb1b506 | ||
|
|
c6c7dc30b4 | ||
|
|
36345e7cad | ||
|
|
38945b2045 | ||
|
|
f341136d80 | ||
|
|
901ad8a959 | ||
|
|
8cdbcd00ba | ||
|
|
4b35b89f6d | ||
|
|
283a009e00 | ||
|
|
9fea16988c | ||
|
|
dc9199a433 | ||
|
|
b906e9654d | ||
|
|
e4e27c885a | ||
|
|
444fd10e79 | ||
|
|
449cbdfd94 | ||
|
|
ab333dd045 | ||
|
|
0ef05ea734 | ||
|
|
cfecd2e78b | ||
|
|
6233efc098 | ||
|
|
ecbdbf4cbf | ||
|
|
a4402134ff | ||
|
|
e57d84af1d | ||
|
|
c7c8359f09 | ||
|
|
8dc7c9506c | ||
|
|
313c4f9326 | ||
|
|
1effdd8373 | ||
|
|
4630bd482b | ||
|
|
91a6c5678c | ||
|
|
8a897b9a6b | ||
|
|
2a2f8af765 | ||
|
|
a352e86a2b | ||
|
|
21f89a9fea | ||
|
|
f8e3799f21 | ||
|
|
5f57608ee7 | ||
|
|
697830a782 | ||
|
|
cb4a539256 | ||
|
|
09b4eac736 | ||
|
|
c7ce683db9 | ||
|
|
d04ed59088 | ||
|
|
82a1e0c831 | ||
|
|
67beab0144 | ||
|
|
3c39cfe9f3 | ||
|
|
744387199e | ||
|
|
49888c7b56 | ||
|
|
e36ef12e6a | ||
|
|
29f9dcf29b | ||
|
|
1f1e0c39b8 | ||
|
|
b5fed7980f | ||
|
|
4b3e03a353 | ||
|
|
734b989072 | ||
|
|
8d69619dad | ||
|
|
018d1d9294 |
+7
-1
@@ -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=
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
Protected routing deployment retry from run 32609010526 at 2026-08-23T00:53:41Z
|
||||
@@ -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 / 8–25% 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
@@ -1,4 +1,4 @@
|
||||
import { recordAuditEvent, recordUserConsent } from "./_db.js";
|
||||
import { recordAuditEvent, recordUserConsent } from "../lib/db.js";
|
||||
|
||||
function activeVersions() {
|
||||
return {
|
||||
|
||||
+1
-1
@@ -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
@@ -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(/&/g, "&")
|
||||
.replace(/"/g, "\"")
|
||||
.replace(/'/g, "'")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/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,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
@@ -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
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+106
-5
@@ -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() {
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
Generated
+64
-3
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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."
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
@@ -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);
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
@@ -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}`);
|
||||
@@ -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}`);
|
||||
@@ -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");
|
||||
@@ -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");
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user