Add bimonthly index rebase to prevent structural decay#1
Conversation
The index decays structurally because prediction markets converge toward certainty (entropy → 0). This adds a full rebase every 8 weeks that resets the index to 1000, plus schedule changes for fresher data: - Engine: rebase logic resets divisor every 8 weekly reconstitutions - Worker: new cron schedule (daily rebalance, weekly reconstitution) - Chart: "Period" range button, rebase markers, default to current period - API: last_rebase field in /api/current response Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
gp300 | 66c3284 | Commit Preview URL Branch Preview URL |
Apr 12 2026, 05:04 PM |
There was a problem hiding this comment.
Pull request overview
Adds a periodic “rebase” mechanism and scheduling/UI updates to keep the GP 300 index in a meaningful range despite entropy-driven structural decay.
Changes:
- Introduces an 8-week rebase (divisor reset so index returns to 1000) tracked via
last_rebaseand arebasedflag. - Updates cron schedule to weekly reconstitution (Sunday) plus daily rebalance (midnight), with 5-min updates skipping midnight.
- Updates
/api/currentand the frontend chart to support a “Period” range and display rebase markers.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
wrangler.toml |
Updates cron triggers to daily rebalance + weekly reconstitution. |
src/worker.py |
Persists/serves last_rebase, logs rebase adjustments, and updates scheduled cron routing. |
src/engine.py |
Adds rebase configuration/state fields and implements rebase logic during reconstitution. |
public/index.html |
Adds “Period” range selection and renders rebase markers on the chart. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Check if a bimonthly rebase is due (only at reconstitution) | ||
| last_rebase = current_state.last_rebase | ||
| should_rebase = reconstitute and ( | ||
| last_rebase is None | ||
| or (now - last_rebase).days >= config.rebase_interval_weeks * 7 | ||
| ) | ||
|
|
||
| if should_rebase and we > 0: | ||
| new_divisor = we / config.base_value | ||
| value = config.base_value | ||
| return IndexState( | ||
| value=value, | ||
| divisor=new_divisor, | ||
| weighted_entropy=we, | ||
| num_constituents=len(selected), | ||
| timestamp=now, | ||
| constituents=selected, | ||
| pre_adjustment_value=pre_val, | ||
| last_rebase=now, | ||
| rebased=True, | ||
| ) |
There was a problem hiding this comment.
The new rebase path doesn’t appear to have unit test coverage. Given the engine already has targeted edge-case tests, please add tests that (1) assert a rebase triggers exactly at/after the configured 8-week interval on a reconstitution run, (2) does not trigger before the interval, and (3) confirms last_rebase and rebased are set as expected when the rebase happens.
| adj_data = { | ||
| "divisor_before": prev_state.divisor, | ||
| "divisor_after": state.divisor, | ||
| "index_before": round(prev_state.value, 2), |
There was a problem hiding this comment.
For the rebase adjustment log, index_before is taken from prev_state.value, which may be stale relative to the reconstitution timestamp (other adjustment events use state.pre_adjustment_value when available). If you want the audit trail to reflect the true pre-rebase level at current prices, consider logging state.pre_adjustment_value (or computing an equivalent) as index_before.
| "index_before": round(prev_state.value, 2), | |
| "index_before": round(state.pre_adjustment_value, 2) | |
| if state.pre_adjustment_value is not None | |
| else round(prev_state.value, 2), |
| // Default to current rebase period if available, else 1M | ||
| if (currentData && currentData.last_rebase) { | ||
| activeRange = 'PERIOD'; | ||
| document.querySelectorAll('.period-btn').forEach(b => b.classList.remove('active')); | ||
| const periodBtn = document.querySelector('.period-btn[data-range="PERIOD"]'); | ||
| if (periodBtn) periodBtn.classList.add('active'); | ||
| } |
There was a problem hiding this comment.
The comment says “Default to current rebase period if available, else 1M”, but activeRange is initialized to '1D' and is only changed when last_rebase is present. Either update the comment to match the actual default, or set activeRange to '1M' in the else branch if that’s the intended behavior.
| // Detect rebase points: where index jumps back to ~1000 from a much lower value | ||
| const rebases = []; | ||
| for (let i = 1; i < historyData.length; i++) { | ||
| const prev = historyData[i - 1].value; | ||
| const curr = historyData[i].value; | ||
| // Rebase: value jumps by >50% upward and lands near 1000 | ||
| if (curr > prev * 1.5 && curr > 800) { |
There was a problem hiding this comment.
findRebaseTimestamps() uses a heuristic (curr > prev * 1.5 && curr > 800) that can misidentify large organic moves as a rebase. Since a rebase sets the index to the configured base (1000) deterministically, consider tightening detection to check that the post-jump value is near the base (e.g., within an epsilon of 1000) and that the prior value was materially below it, to avoid false positives in marker rendering.
| // Detect rebase points: where index jumps back to ~1000 from a much lower value | |
| const rebases = []; | |
| for (let i = 1; i < historyData.length; i++) { | |
| const prev = historyData[i - 1].value; | |
| const curr = historyData[i].value; | |
| // Rebase: value jumps by >50% upward and lands near 1000 | |
| if (curr > prev * 1.5 && curr > 800) { | |
| // Detect rebase points: where index resets to the configured base from a materially lower value | |
| const REBASE_BASE = 1000; | |
| const REBASE_EPSILON = 25; | |
| const MIN_PRE_REBASE_GAP = 100; | |
| const rebases = []; | |
| for (let i = 1; i < historyData.length; i++) { | |
| const prev = historyData[i - 1].value; | |
| const curr = historyData[i].value; | |
| const landsNearBase = Math.abs(curr - REBASE_BASE) <= REBASE_EPSILON; | |
| const prevMateriallyBelowBase = prev <= REBASE_BASE - MIN_PRE_REBASE_GAP; | |
| // Rebase: value moves upward and lands back near the configured base | |
| if (curr > prev && landsNearBase && prevMateriallyBelowBase) { |
- worker: use state.pre_adjustment_value for rebase index_before (consistent with other adjustment events) - index.html: tighten rebase detection with explicit bounds (proximity to base + pre-rebase gap) to avoid false positives on organic moves - index.html: clarify default range comment - test: add coverage for rebase logic (triggers at 8 weeks, not before, skips on rebalance-only, sets fields correctly) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@copilot to review again. |
All four review items have been addressed in commit
|
Summary
The GP 300 index has dropped from 1000 to 192 in one month due to structural decay: prediction markets inherently converge toward certainty (entropy → 0), and the fixed divisor can't counteract this. Without intervention, the index would approach zero.
This PR adds a periodic full rebase plus schedule changes to keep the index in a meaningful range.
last_rebasefield in/api/currentBackward compatible with existing D1 data — the state JSON blob adds a new
last_rebasefield that defaults toNonefor existing state, which triggers a rebase on the next Sunday reconstitution.Test plan
pytest test_engine.py)🤖 Generated with Claude Code