# DNS1 Canonical Strategy Explainer

Generated: 2026-05-16, America/New_York

This document explains DNS1 from first principles through the latest local implementation state in this repository. It is written as the single reference document for:

- the premise of the problem
- the proposed solution
- the algorithm
- the latest version of the strategy and runtime
- the improvement path that should come next

The key source materials are:

- `src/dn_research/methodology_paper.html`
- `artifacts/live_strategy_explainer.html`
- `artifacts/live_strategy_mathematical_notes.html`
- `README.md`
- `LIVE_REALTIME_README.md`
- `src/dn_research/live_scoring.py`
- `src/dn_research/live_runtime.py`
- `src/dn_research/structural_signal.py`
- `src/dn_research/portfolio_milp.py`
- `scripts/run_trading_dashboard.py`
- `scripts/run_live_realtime_dashboard.py`
- `scripts/run_minute_signal_backtest.py`
- `src/dn_research/backtest_verifier.py`
- `artifacts/minute_signal_backtest_report.md`
- `artifacts/live_trade_realtime_classification_may9/summary.md`

Important framing: this repository is a research and paper-execution system. It is not currently a real-money order router. The live runtime discovers markets, scores them, estimates execution, allocates paper capital, simulates paper fills, records marks and PnL, and serves a dashboard. It does not place authenticated Polymarket orders.

## Executive Summary

DNS1 is a strategy for binary event markets, especially Polymarket-style YES/NO contracts. A binary contract pays `1` if the selected side resolves true and `0` otherwise. The market price is interpretable as an implied probability, but the traded price is not automatically fair value because of market inefficiency, category-specific behavior, time-to-expiry effects, fees, spread, depth, slippage, and adverse selection.

The strategy's central claim is:

> Some live binary contracts are mispriced relative to historical resolution behavior. The strategy should buy the side whose model-implied fair probability exceeds the executable live price after fees, visible slippage, and portfolio risk constraints.

The current live solution is not a raw empirical lookup. The live signal is `structural_interpolated`: it uses structural equations fitted from historical 15-minute resolution surfaces, evaluates those equations at the current market category, current price, and current days to expiry, then interpolates across neighboring price buckets.

The current live deployment profile, as represented by the local Railway entrypoint, is:

- signal model: `structural_interpolated`
- base discovery horizon: `<= 1 day`
- base selected-side price band: `0.95` to `0.999`
- minimum visible depth inside 30 bps: `$100`
- maximum selected-side spread: `0.06`
- paper execution mode: `taker`
- paper order cap: `$5,000`
- paper capital cap: `$100,000`
- maximum open paper orders: `20`
- allocator: `milp`
- paper min edge after measured slippage: `0.01`
- live state schema on Railway: `live_trading`
- fallback profile: allowed unless explicitly disabled

The latest local evidence materially changes the story from the original April HTMLs:

1. The original strict near-expiry profile was too narrow and could produce zero deployable paper allocation.
2. A bounded fallback profile was added to widen the universe when the strict profile is dead.
3. A May 9 live-trade postmortem found that real-time game/sports/esports markets dominated losses.
4. A May 13 minute-level replay added production-style exclusions and passed an independent verifier, but only over the available forward-tape window and with low absolute capital deployment.

The latest verified local version is best described as:

> A structural DNS1 minute-signal replay using live Railway forward-tape data, production exclusions, enhanced momentum-market filtering, depth-edge gating, conservative fill participation, and a locked independent verifier.

The improvement path should not be "make it trade more" by loosening filters. The next improvement should be to increase trust in the live edge by improving data quality, excluding toxic market types, aligning live/backtest mechanics, validating out of sample, collecting authenticated execution data, and upgrading allocation from raw edge maximization toward drawdown-aware, category-aware, capacity-aware portfolio construction.

## 1. Premise Of The Problem

### 1.1 Binary Event Contracts

Polymarket-style contracts are binary. For a given event:

- A YES share pays `1` if the event resolves YES and `0` otherwise.
- A NO share pays `1` if the event resolves NO and `0` otherwise.

If `Y` is the terminal YES outcome, then:

```text
Y in {0, 1}
payoff_yes = Y
payoff_no  = 1 - Y
```

A YES price of `0.96` means the market is offering YES exposure for 96 cents. If the true probability of YES is higher than 96% after all costs, the YES side may have positive expected value. If the true probability of YES is lower than 96%, then the YES side is overpriced and the NO side may be better.

The simple fair-value idea is:

```text
fair_value_yes = true_probability_yes
fair_value_no  = 1 - true_probability_yes
```

But "true probability" is not observable. DNS1 estimates it from historical resolution behavior and structural fits.

### 1.2 The Core Market Inefficiency Claim

The strategy assumes that event-market prices are sometimes inefficient, especially when grouped by:

- current price bucket
- days to expiry
- category
- liquidity regime
- market structure
- live execution quality

For example, a 95 cent side may not resolve 95% of the time in every category, at every time horizon, or under every market type. Some high-confidence sides may be genuinely underpriced; others may be traps caused by real-time game dynamics, exact-score markets, thin books, stale quotes, or adverse selection.

DNS1 tries to separate:

- markets where high price reflects genuine high probability
- markets where high price hides unresolved tail risk
- markets where apparent edge disappears after fees and slippage
- markets where the order book is too thin to trade meaningfully
- markets where historical sample support is too weak to trust

### 1.3 The Practical Problem

The practical problem is not just "find edge." It is:

> Build a live system that can discover currently open binary markets, estimate whether either side is underpriced, verify that the edge is executable at live depth, allocate capital without overconcentrating risk, and measure results without lookahead bias.

This breaks into five subproblems.

#### Subproblem A: Probability Estimation

The system needs a probability estimate for the contract side being considered. A raw market price is not enough because the entire strategy is based on price being imperfect.

The model must answer:

```text
Given category, current price, and time to expiry,
what is the expected resolution probability?
```

#### Subproblem B: Side Selection

The system must decide whether YES or NO is better. It cannot assume that the named favorite side is always the trade.

The model must compare:

```text
edge_yes = predicted_yes_probability - executable_yes_price - yes_fee
edge_no  = predicted_no_probability  - executable_no_price  - no_fee
```

Then choose the larger positive edge.

#### Subproblem C: Execution Reality

Top-of-book edge can be false. A market can show one cheap share at a good price, but a `$5,000` paper order may need to walk multiple book levels. The actual fill price is VWAP, not necessarily best ask.

The system must measure:

- best bid
- best ask
- midpoint
- spread
- visible depth
- 30 bps capacity
- target-size VWAP
- slippage versus touch
- post-slippage edge

#### Subproblem D: Portfolio Construction

If many candidates survive, the system must allocate capital under constraints. It should not simply buy every positive-edge contract at maximum size.

The allocator must handle:

- total capital
- maximum single-position size
- minimum position size
- candidate capacity
- category concentration
- theme concentration
- DTE bucket exposure
- tail-loss budget
- volatility budget
- existing exposure

#### Subproblem E: Verification Without Lookahead

The system must avoid accidentally using future information. This is especially important because the strategy uses historical resolution outcomes to learn probability surfaces and live forward-tape data to test replay behavior.

The latest verifier enforces temporal checks:

```text
signal_ts <= entry_ts
source_quote_ts <= signal_ts
feature_cutoff_ts <= signal_ts
train_end_ts <= signal_ts
entry_ts < exit_ts
```

It also requires:

- annualized return `>= 8%`
- annualized Sharpe `>= 2.0`
- max drawdown `<= 10%`
- observed granularity `<= 1 minute`
- at least `30` periods

### 1.4 Why This Is Hard

The problem is hard for reasons that are easy to miss:

- Historical surfaces can overfit sparse cells.
- A category label like `Overall` may hide very different market types.
- Near-expiry markets can move discontinuously when news arrives.
- Real-time sports and esports markets can have extreme hidden risk despite high prices.
- Public trade prints do not reveal queue position.
- Visible book depth is not guaranteed executable depth.
- Paper fills are not authenticated exchange fills.
- A high win rate can still lose money if rare losses are large.
- A short backtest can pass thresholds by chance.
- A strategy can allocate zero because filters interact too tightly.
- A strategy can allocate too much if filters are loosened without understanding why losses happened.

The current repository reflects that evolution. Early reports showed attractive structural edges. Later live data showed that some market classes, especially real-time sports/esports, were toxic enough to dominate losses.

## 2. Solution

### 2.1 What The Solution Is

The solution is a live research and paper-execution pipeline:

1. Build historical 15-minute resolution surfaces from resolved markets.
2. Fit structural equations to make the empirical edge surface continuous in days to expiry.
3. Discover currently open markets.
4. Fetch live YES/NO CLOB books.
5. Estimate model probability with `structural_interpolated`.
6. Compute fee-adjusted edge for YES and NO.
7. Select the better side.
8. Apply live filters.
9. Measure slippage by walking the visible order book.
10. Keep only candidates whose edge survives measured execution.
11. Allocate paper capital with MILP.
12. Simulate taker paper fills at measured VWAP.
13. Mark positions with live tape and settlement marks.
14. Export forward tape and run minute-level independent verification.

In short:

```text
historical resolved markets
  -> empirical 15m resolution surface
  -> structural edge equations
  -> live market scoring
  -> live book execution test
  -> MILP allocation
  -> paper taker fills
  -> forward-tape verification
```

### 2.2 What The Solution Is Not

The solution is not:

- a real-money Polymarket order router
- a guarantee of profitability
- a full historical L2 replay engine
- authenticated maker/taker fill attribution
- proof that all high-price contracts are good trades
- proof that the latest filter set will remain valid across regimes

The live runtime is a scoring, allocation, observability, and paper-trading system.

### 2.3 Why Structural Interpolation Exists

The original empirical surface is a bucket table. Buckets are useful for research but awkward for live trading because a market can jump from one bucket to the next because of a tiny price move.

For example, a contract at `0.949` and a contract at `0.951` might fall into different buckets even though economically they are almost the same.

Structural interpolation addresses this by:

1. fitting equations for edge as a function of DTE inside category x price-bucket families
2. evaluating neighboring bucket equations at the current DTE
3. interpolating between the neighboring bucket predictions based on current price

That makes the live signal smoother and less brittle.

### 2.4 Why The Live System Uses Execution Filters

The strategy does not trust model edge alone. It rejects candidates for practical reasons:

- missing probability
- missing YES midpoint
- missing DTE
- DTE outside configured range
- selected-side price outside configured band
- edge below threshold
- insufficient depth inside 30 bps
- spread too wide

This is the difference between theoretical edge and executable edge.

### 2.5 Why The Live System Uses MILP

MILP is used for portfolio construction, not for signal discovery.

The signal engine answers:

```text
Is this candidate attractive after model, fee, and slippage?
```

The allocator answers:

```text
Given all attractive candidates, how much capital should go to each one under constraints?
```

The current MILP objective is approximately:

```text
maximize sum_i(edge_i * allocated_usdc_i)
```

subject to capital, concentration, risk, and capacity constraints.

### 2.6 Why The Latest Verification Uses Minute Replay

The latest local validation discovered that the local research DB did not contain usable minute history in `public.market_quote_1m`. Therefore, the replay uses Railway forward-tape tables:

- `live_trading.signal_snapshots`
- `live_trading.price_snapshots`

This is important because the replay is based on what the live system actually saw and recorded, not an idealized historical dataset.

The latest passing run uses:

- live signal snapshots
- live price snapshots
- one entry per market
- depth-edge gating
- production exclusions
- enhanced momentum exclusions
- conservative depth participation
- hold-to-expiry style behavior by disabling the 1c reversion stop with `--drawdown-floor 1.0`
- independent verification from exported artifacts only

## 3. Algorithm

The algorithm has two layers:

- an offline research/model-building layer
- an online live scoring/allocation layer

It also has a verification layer that replays exported live signals from forward tape.

## 3.1 Offline Research Layer

### Step 1: Collect Resolved Markets

The system starts from resolved Polymarket markets. For each market it needs:

- market ID
- condition ID
- title
- category
- token IDs
- end date
- final resolution
- historical price path

### Step 2: Build 15-Minute Historical Observations

For each resolved market, the system loads historical YES marks from CLOB price history.

For each quote:

```text
quote_ts = timestamp from CLOB history
obs_ts = quote_ts floored to 15-minute bin
yes_mark = historical YES midpoint or mark
days_to_expiry = (market_end_ts - obs_ts) / 86400
yes_resolution = 1 if the market resolved YES else 0
no_resolution = 1 - yes_resolution
yes_edge = yes_resolution - yes_mark
no_edge = yes_mark - yes_resolution
```

This creates one row per market x 15-minute observation, not just one row per market.

The mathematical notes report:

- requested markets: `121,694`
- usable markets: `117,501`
- 15-minute observations: about `41.96M`
- source endpoint: `/prices-history`

### Step 3: Assign Buckets

Each observation is assigned to:

- a probability bucket
- a DTE bucket
- a grouping family

Probability buckets include:

```text
0-1%
1-3%
3-5%
5-10%
10-20%
20-35%
35-50%
50-65%
65-80%
80-90%
90-95%
95-97%
97-99%
99-100%
```

DTE buckets include:

```text
0-1d
1-3d
3-7d
7-14d
14-30d
```

Grouping families include:

- Overall
- Category
- Liquidity

### Step 4: Build Empirical Surface Cells

For each cell:

```text
cell = (group_type, group, probability_bucket, dte_bucket)
```

The system computes:

```text
n_cell = number of observations
u_cell = number of unique markets
yes_resolution_probability = mean(yes_resolution)
no_resolution_probability = mean(no_resolution)
avg_yes_price = mean(yes_mark)
yes_edge_vs_price = mean(yes_resolution - yes_mark)
no_edge_vs_price = mean(no_resolution - (1 - yes_mark))
standard_error = sqrt(p * (1 - p)) / sqrt(n)
```

This surface is useful but not sufficient for live trading because it is bucketed, sparse in some cells, and not an execution model.

### Step 5: Fit Structural Equations

The structural fitting script compares simple functional forms for edge as a function of DTE.

Candidate forms include:

```text
constant:    e(d) = c
linear:      e(d) = a + b*d
exp_only:    e(d) = a * exp(-d / tau)
exp_const:   e(d) = a * exp(-d / tau) + c
double_exp:  e(d) = a1 * exp(-d / tau1) + a2 * exp(-d / tau2) + c
log_linear:  e(d) = a + b * log(1 + d)
```

For each category x price bucket family:

```text
x_j = DTE midpoint for bucket j
y_j = yes_edge_vs_price in cell j
w_j = observation count in cell j

theta_hat = argmin_theta sum_j w_j * (f_theta(x_j) - y_j)^2
```

The model selection uses weighted AIC/AICc style scoring. The winning model and parameters are written to:

```text
artifacts/intraday_15m_resolution/structural_fits/intraday_structural_fit_winners.csv
```

The live runtime reads that winner table.

## 3.2 Online Live Runtime Algorithm

### Step 1: Discover Markets

The live runtime fetches open Gamma markets.

It keeps markets with:

- usable token IDs
- valid end dates
- nonnegative time to expiry
- DTE inside discovery horizon
- Gamma-side price close enough to the configured live price band

The Railway wrapper defaults include:

```text
MAX_MARKETS=250
MAX_WS_MARKETS=80
MAX_GAMMA_PAGES=20
DISCOVERY_MAX_DTE_DAYS=1
DISCOVERY_SIDE_PRICE_BUFFER=0.05
POLL_SECONDS=10
BOOK_WORKERS=16
```

The discovery-side price buffer means that if the live selected-side floor is `0.95`, discovery can still pull markets around `0.90` for CLOB scoring so that it does not miss candidates that are close to the band.

### Step 2: Apply Market Exclusions

The latest code includes title/category exclusions.

Built-in momentum-market exclusion catches patterns such as:

- "up or down"
- "close above"
- "close below"
- "finish week above"
- crypto/equity/commodity price-threshold markets with a date

Environment-level exclusions can also remove configured categories and titles:

```text
EXCLUDED_MARKET_CATEGORIES
EXCLUDED_MARKET_TITLE_REGEX
```

This became critical after the May 9 postmortem showed that real-time game/sports/esports markets dominated losses.

### Step 3: Fetch Live Books

For each candidate market, the runtime fetches YES and NO token books from the Polymarket CLOB.

It also uses the upstream websocket when enabled:

```text
wss://ws-subscriptions-clob.polymarket.com/ws/market
```

REST polling is still used for reconciliation and slippage walking.

The runtime records:

- best bid
- best ask
- midpoint
- spread
- top levels
- L2 snapshots
- price snapshots
- trade ticks when available

### Step 4: Infer Category

If Gamma provides a useful category, the runtime uses it. If Gamma reports a generic category like `Overall`, the scorer falls back to title heuristics.

Examples:

- Bitcoin, BTC, ETH -> `Crypto-Markets`
- Fed, GDP, CPI, jobs -> `Economics-Markets`
- OpenAI, Anthropic, Nvidia, AI -> `Technology-Companies`
- Russia, Ukraine, Iran, ceasefire -> `Geopolitics-Conflict`
- election, senate, governor -> `Politics-Elections`
- oil, gas, Brent, WTI -> `Energy-Oil-Gas`
- weather, hurricane, temperature -> `Climate-Environment`
- sports-style title patterns -> `Other-Miscellaneous`

This matters because the structural fit table is category-aware.

### Step 5: Compute YES And NO Midpoints

From live books:

```text
yes_bid = top YES bid
yes_ask = top YES ask
yes_mid = (yes_bid + yes_ask) / 2
```

For NO:

```text
no_mid = direct NO midpoint if available
no_mid = 1 - yes_mid if direct NO book is absent
```

### Step 6: Evaluate Structural Signal

Given:

```text
category
yes_mid
days_to_expiry
```

The live structural model:

1. loads the winner rows for the normalized category
2. finds the neighboring price-bucket centers around `yes_mid`
3. evaluates the lower and upper bucket equations at current DTE
4. interpolates between those two edge estimates
5. converts edge to probability

In formula form:

```text
edge_lower = f_lower(days_to_expiry)
edge_upper = f_upper(days_to_expiry)

alpha = (yes_mid - center_lower) / (center_upper - center_lower)
edge = (1 - alpha) * edge_lower + alpha * edge_upper

q_yes = clip(yes_mid + edge)
q_no = 1 - q_yes
```

If the structural category or bucket is missing, the model can fall back to empirical surface probability when available.

### Step 7: Estimate Fees

Fees are estimated as one-way taker fee per share.

The conceptual fee curve is:

```text
fee_per_share = rate * [p * (1 - p)]^gamma
```

The runtime uses market fee schedule data when available and category fallback logic when needed.

Fees matter because a 1 cent apparent edge is not real if it costs more than 1 cent per share to enter.

### Step 8: Select Side

The scorer computes:

```text
yes_edge = q_yes - yes_mid - fee_yes
no_edge  = q_no  - no_mid  - fee_no
```

Then:

```text
selected_side = YES if yes_edge >= no_edge else NO
selected_side_probability = q_yes or q_no
side_mid = yes_mid or no_mid
edge_after_fee = max(yes_edge, no_edge)
```

MILP is not used here. Side selection is direct edge comparison.

### Step 9: Measure 30 Bps Depth

The runtime estimates how much notional can be bought before VWAP moves more than 30 bps from touch.

Conceptually:

```text
max_slippage = 0.003
Q_30bps = max Q such that VWAP(Q) - best_price <= 0.003
depth_edge = selected_side_probability - VWAP(Q_30bps) - fee
```

This creates:

- `depth_capacity_usdc_30bps`
- `depth_vwap_30bps`
- `depth_slippage_30bps`
- `depth_edge_after_fee_30bps`

### Step 10: Apply Live Eligibility Filters

The base live scoring config is:

```text
signal_model = structural_interpolated
min_edge = 0.00
min_depth_usdc_30bps = 100.0
max_spread = 0.06
min_dte_days = 0.0
max_dte_days = 1.0
min_side_price = 0.95
max_side_price = 0.999
max_candidates = 50
```

A market is rejected if:

```text
missing_surface_cell
missing_yes_mid
missing_dte
dte_outside_dns1_cap
side_price_outside_band
edge_below_threshold
insufficient_30bps_depth
spread_too_wide
```

If `signal_model` is not structural, there is also a Brownian-bridge exclusion for:

```text
35-50% / 0-1d
50-65% / 0-1d
```

In the current structural mode, those old empirical-cell Brownian exclusions are not the primary control.

### Step 11: Sort Top Candidates

Eligible candidates are sorted primarily by:

```text
depth_edge_after_fee_30bps
```

falling back to:

```text
edge_after_fee
```

and then by 30 bps depth.

### Step 12: Measure Target-Size Slippage

For each candidate and each target size, the runtime walks the visible book.

Railway wrapper target sizes:

```text
100
250
500
1000
2500
5000
25000
100000
250000
1000000
```

For each target:

```text
remaining = target_usdc
spent = 0
shares = 0

for each ask level ordered by price:
    consume as much as needed from this level
    spent += consumed_notional
    shares += consumed_shares
    remaining -= consumed_notional

vwap_price = spent / shares
slippage_vs_touch = vwap_price - best_price
edge_after_slippage = edge_after_fee - slippage_vs_touch
```

This produces `slippage_quotes`.

### Step 13: Build Paper-Sized Candidates

The paper selector keeps a target-size quote only if:

- filled notional is positive
- quote is fillable
- target size does not exceed paper order cap
- slippage is below the paper slippage cap
- edge after slippage is above the paper edge floor

Railway wrapper defaults:

```text
PAPER_ORDER_USDC=5000
PAPER_MAX_SLIPPAGE=0.03
PAPER_MIN_EDGE_AFTER_SLIPPAGE=0.01
```

The result is a list of candidates that are theoretically paper-fillable at measured live VWAP.

### Step 14: Allocate Paper Capital With MILP

The MILP prefilter rejects candidates when:

```text
capacity < min_position_usdc
post_slip_edge <= 0
structural_edge < min_structural_edge
missing DTE
DTE above theme-specific cap
```

Railway wrapper defaults include:

```text
ALLOCATOR_MODE=milp
MILP_MAX_POSITION_FRACTION=0.10
MILP_MIN_POSITIONS=15
MILP_MIN_POSITION_USDC=10
MILP_TAIL_BUDGET_FRACTION=0.08
MILP_VOL_BUDGET_MULTIPLIER=1.8
MILP_MIN_STRUCTURAL_EDGE=0.015
MILP_DTE_BUCKET_CAP_0_1D=1.0
MILP_DTE_BUCKET_CAP_1_3D=1.0
MILP_CATEGORY_CAP_OVERRIDES=Geopolitics=0.60
```

The MILP variables are:

```text
w_i = allocated dollars to candidate i
x_i = binary selected flag for candidate i
```

Objective:

```text
maximize sum_i(edge_i * w_i)
```

Core constraints:

```text
0 <= w_i <= capacity_i * x_i
w_i >= min_position * x_i
sum_i w_i <= remaining_capital
sum_i x_i >= min_positions when feasible
sum_i loss_probability_i * w_i <= tail_budget
sum_i volatility_multiplier_i * w_i <= volatility_budget
category caps
correlated category tail budgets
DTE bucket caps
```

If the solver fails or times out, the allocator falls back to greedy highest-edge allocation.

### Step 15: Paper Execution

The current Railway trading wrapper defaults to:

```text
PAPER_EXECUTION_MODE=taker
```

Taker paper execution means:

```text
paper_fill_price = measured live book VWAP
paper_fill = immediate simulated fill
```

This is still paper execution. It does not place real exchange orders.

### Step 16: Position Marking And Exits

The runtime records paper positions and marks them from the forward price tape.

Exit mechanisms in the codebase include:

- hard sentinel drawdown exit
- panel Sentinel exit model
- ML-gated reversion exit
- resolution settlement

Railway wrapper behavior:

- `SENTINEL_EXIT_ENABLED` defaults to enabled in the wrapper unless overridden.
- `PANEL_SENTINEL_EXIT_ENABLED` defaults to disabled unless explicitly enabled.
- `REVERSION_EXIT_ENABLED` defaults to disabled unless explicitly enabled.

The methodology HTML presents Sentinel plus ML-gated reversion as the intended exit stack. The latest minute replay, however, achieved the passing verifier by using `--drawdown-floor 1.0`, effectively disabling the 1 cent reversion stop and holding to end marks. That is an important mismatch between the full methodology and the latest verified replay.

## 3.3 Minute Signal Backtest Algorithm

The latest verifier path is in:

```text
scripts/run_minute_signal_backtest.py
src/dn_research/backtest_verifier.py
```

### Step 1: Load Replay Bounds

The script reads the min and max timestamps from:

```text
live_trading.signal_snapshots
```

The full available tested period in the report is:

```text
2026-04-23 20:06:00+00:00 to 2026-05-13 14:40:00+00:00
```

### Step 2: Load Eligible Minute Signals

Signals are loaded from `signal_snapshots`, deduplicated by minute and market:

```text
partition by date_trunc('minute', created_at), market_id
order by edge_after_fee desc nulls last, created_at desc
```

Filters include:

```text
eligible is true
side_mid between min_side_price and max_side_price
days_to_expiry between 0 and max_dte_days
coalesce(depth_edge_after_fee_30bps, edge_after_fee) >= min_edge_after_fee
coalesce(depth_capacity_usdc_30bps, 0) >= min_depth_usdc_30bps
```

Then the script applies:

- momentum-market exclusions
- configured category exclusions
- configured title regex exclusions

### Step 3: Load Minute Marks

Marks are loaded from:

```text
live_trading.price_snapshots
```

The script keeps the latest mark per minute, token, and side:

```text
mark_price = coalesce(mid, best_bid, best_ask)
```

### Step 4: Replay Minute By Minute

For each minute:

1. update latest marks
2. exit positions if end date has arrived
3. optionally exit on drawdown stop if enabled
4. enter new signals if capacity allows
5. mark open positions
6. write equity row

Important mechanics:

- only one entry per market
- max open positions applies
- total deployed notional cap applies
- each entry uses conservative capacity:

```text
capacity = depth_capacity_usdc_30bps * fill_participation
notional = min(order_usdc, capacity, remaining_budget)
```

Entry price:

```text
entry_price = side_mid + half_spread + entry_slippage_bps
```

Exit price:

```text
exit_price = mark_price * (1 - exit_slippage_bps)
```

### Step 5: Export Artifacts

The replay writes:

- `equity.csv`
- `trades.csv`
- `signals.csv`
- `verifier_manifest.json`
- `verification.json`

### Step 6: Independent Verification

The verifier only reads exported artifacts:

- `equity.csv`
- `trades.csv`
- `verifier_manifest.json`

It computes:

- total return
- annualized return
- annualized Sharpe
- max drawdown
- observed granularity
- period count
- temporal integrity failures

It does not trust the replay script's printed metrics.

## 4. Latest Version

This section reflects the latest local files found in the repository as of 2026-05-16. The newest local strategy artifacts are from 2026-05-13.

## 4.1 Latest Runtime Version

The live runtime is packaged under:

```text
artifacts/dns1_reproducible_project/
```

The Railway entrypoint is:

```text
scripts/run_trading_dashboard.py
```

That wrapper launches:

```text
scripts/run_live_realtime_dashboard.py
```

which initializes:

- live storage
- market discovery
- REST book polling
- optional websocket ingestion
- structural scoring
- candidate filtering
- slippage measurement
- MILP allocation
- paper fills
- dashboard endpoints

The live state schema is expected to be:

```text
live_trading
```

on Railway.

### Current Railway-Style Defaults

From `scripts/run_trading_dashboard.py`, the current Railway-style defaults are:

```text
MAX_MARKETS=250
MAX_GAMMA_PAGES=20
MAX_WS_MARKETS=80
POLL_SECONDS=10
BOOK_WORKERS=16

SIGNAL_MODEL=structural_interpolated
MIN_EDGE=0.0
MIN_DEPTH_USDC_30BPS=100
MAX_SPREAD=0.06
MIN_DTE_DAYS=0
MAX_DTE_DAYS=1
MIN_SIDE_PRICE=0.95
MAX_SIDE_PRICE=0.999

FALLBACK_DISCOVERY_MAX_DTE_DAYS=3
FALLBACK_MAX_DTE_DAYS=3
FALLBACK_MIN_SIDE_PRICE=0.65
FALLBACK_MAX_SPREAD=0.06

PAPER_EXECUTION_MODE=taker
PAPER_ORDER_USDC=5000
PAPER_MAX_TOTAL_USDC=100000
PAPER_MAX_OPEN_ORDERS=20
PAPER_FILL_PARTICIPATION=0.25
PAPER_MAX_SLIPPAGE=0.03
PAPER_MIN_EDGE_AFTER_SLIPPAGE=0.01

ALLOCATOR_MODE=milp
MILP_MAX_POSITION_FRACTION=0.10
MILP_MIN_POSITIONS=15
MILP_MIN_POSITION_USDC=10
MILP_TAIL_BUDGET_FRACTION=0.08
MILP_VOL_BUDGET_MULTIPLIER=1.8
MILP_MIN_STRUCTURAL_EDGE=0.015
MILP_DTE_BUCKET_CAP_0_1D=1.0
MILP_DTE_BUCKET_CAP_1_3D=1.0
MILP_CATEGORY_CAP_OVERRIDES=Geopolitics=0.60

ORDERBOOK_SNAPSHOT_LEVELS=25
SLIPPAGE_TARGETS_USDC=100,250,500,1000,2500,5000,25000,100000,250000,1000000
```

### Base Profile And Fallback Profile

The base profile remains strict:

```text
DTE <= 1 day
selected-side price >= 0.95
selected-side price <= 0.999
```

But if the base profile produces zero deployable paper capital, the runtime can evaluate a fallback profile:

```text
DTE <= 3 days
selected-side price >= 0.65
spread cap remains 0.06
```

The fallback is bounded. It does not permanently replace the strict profile; it activates when the strict profile is dead.

This was added because the strict profile could discover and score markets yet allocate `0` dollars because no candidate survived the combined filter, slippage, and MILP prefilter stack.

## 4.2 Latest Validation Version

The latest local validation report is:

```text
artifacts/minute_signal_backtest_report.md
```

The latest passing run is:

```text
artifacts/minute_signal_backtest_full_available_prod_filters_enhanced_momentum/
```

Verifier:

```text
independent-backtest-verifier-v1
```

Data source:

```text
Railway DATABASE_URL live_trading.signal_snapshots
Railway DATABASE_URL live_trading.price_snapshots
```

Full available tested period:

```text
2026-04-23 20:06:00+00:00 to 2026-05-13 14:40:00+00:00
```

Passing run row counts:

```text
eligible minute signals: 9,690
minute marks: 96,659
equity rows: 28,475
closed trades: 63
```

Passing verifier metrics:

```text
annualized_return: 8.173%
annualized_sharpe: 2.144
max_drawdown_fraction: 0.287%
ending_equity_usdc: $100,442.79
total_return: 0.443%
observed_granularity_minutes: 1.0
period_count: 28,475
```

The passing run command was:

```bash
railway run .venv/bin/python scripts/run_minute_signal_backtest.py \
  --output-dir artifacts/minute_signal_backtest_full_available_prod_filters_enhanced_momentum \
  --drawdown-floor 1.0
```

The important interpretation is:

- it passed the independent verifier
- it passed on available forward tape, not on a multi-year dataset
- it used production exclusions
- it used enhanced momentum exclusions
- it used hold-to-expiry style behavior
- it had low absolute total return over the short sample
- it does not by itself prove production profitability

## 4.3 Latest Loss Postmortem

The May 9 live classification report is:

```text
artifacts/live_trade_realtime_classification_may9/summary.md
```

It analyzed a live verification endpoint generated:

```text
2026-05-09T17:20:43.370884+00:00
```

Trade window:

```text
2026-05-05 09:55:07.495838+00:00
to
2026-05-09 17:19:05.812149+00:00
```

Headline:

```text
total trades: 466
closed realized: 375
open MTM: 91
total PnL: -$140,385.10
closed realized PnL: -$137,186.63
open MTM: -$3,198.47
```

Real-time game/sports trades:

```text
303 trades
65.0% of total trades
PnL: -$134,681.09
```

Non-game/sports trades:

```text
163 trades
PnL: -$5,704.01
```

Loss attribution:

```text
loss trades: 192
real-time game/sports losses: 163
real-time game/sports losses were 84.9% of loss trades
real-time game/sports losses contributed -$137,229.37 of -$145,583.65 gross losses
```

The worst subtypes included:

- `esports_prop`
- `sports_exact_score`
- `sports_spread`
- `sports_total`

This is the most important postmortem finding in the latest local artifacts:

> The strategy cannot treat all high-priced near-expiry markets as comparable. Real-time game/sports/esports markets have different risk dynamics and caused the overwhelming majority of large losses in that live sample.

## 4.4 Latest State In Plain English

The latest local state is:

1. The structural live scorer still exists and remains the signal engine.
2. The live runtime now has fallback logic because the strict profile can be too narrow.
3. The runtime has stronger title/category exclusions for momentum-style markets.
4. A minute-level replay using the available Railway forward tape passed the locked verifier after enhanced momentum exclusion.
5. The May 9 live postmortem shows that sports/esports real-time market types are toxic enough to require explicit exclusion or separate modeling.
6. The strategy is still paper/research, not real-money execution.
7. The strongest next step is robust validation and data improvement, not immediate real-money deployment.

## 5. What The Improvement Should Be

The improvement path should be conservative and evidence-driven. The main lesson from the latest artifacts is that the strategy's biggest risk is not a missing formula. It is false confidence from broad historical edge when live market types, execution, and tail events are not controlled tightly enough.

## 5.1 Improvement Priority 1: Exclude Or Separately Model Real-Time Sports/Esports

The May 9 postmortem makes this the highest-priority improvement.

Real-time game/sports/esports markets should not be allowed into the main DNS1 strategy unless they pass a separate, sport-specific model and validation gate.

The current broad exclusion should be formalized into a maintained classifier:

- exact score
- spread
- totals
- esports props
- live match markets
- in-game props
- player event props
- real-time contest dynamics

The classifier should output:

```text
allowed_main_dns1 = true/false
market_subtype
exclusion_reason
confidence
```

Every excluded row should be logged so future reviews can distinguish:

- true toxic exclusion
- false positive exclusion
- market that needs its own model

Success condition:

- the May 9 loss concentration should disappear under replay
- excluded market PnL should be reported separately
- the main DNS1 strategy should not silently re-include sports/esports because of generic categories like `Overall`

## 5.2 Improvement Priority 2: Align Live Runtime And Verified Replay

The latest passing verifier used a minute replay with specific assumptions:

- one entry per market
- depth-edge gating
- production exclusions
- enhanced momentum exclusion
- 25% fill participation
- hold-to-expiry behavior via `--drawdown-floor 1.0`

The live runtime uses:

- live slippage ladder
- MILP allocation
- paper taker fills
- optional sentinel/reversion exits
- fallback profile

These are close but not identical. The next improvement should make the live runtime and verified replay match exactly, or explicitly document every intentional difference.

Specific alignment tasks:

1. Export the live runtime config into every replay manifest.
2. Replay exactly the same fallback behavior used live.
3. Replay exactly the same paper-sized candidate selector used live.
4. Replay exactly the same MILP settings used live.
5. Replay the same exclusion classifier used live.
6. Replay the same exit settings used live.
7. Treat any mismatch as a named experiment, not as the canonical validation.

Success condition:

```text
same input tape + same config + same algorithm = same selected trades
```

## 5.3 Improvement Priority 3: Extend The Forward Tape

The passing minute replay used data from:

```text
2026-04-23 to 2026-05-13
```

That is useful, but it is not enough to trust the strategy in production.

The system needs:

- several months of continuous forward tape
- full signal snapshots
- full price snapshots
- L2 snapshots
- slippage quotes
- paper orders
- paper fills
- paper positions
- settlement outcomes
- classifier decisions
- exclusion decisions
- config snapshots

The tape should be immutable and partitioned by date.

Success condition:

- every day can be replayed independently
- every live decision can be reconstructed
- no replay depends on a mutable current config unless explicitly requested

## 5.4 Improvement Priority 4: Add Rolling Out-Of-Sample Validation

The May 13 passing run is one sample. The next version should not rely on one pass/fail over one forward-tape period.

The verifier should run rolling windows:

- daily
- 3-day
- 7-day
- 14-day
- full-to-date

Each window should report:

- total return
- annualized return
- Sharpe
- max drawdown
- trade count
- win rate
- average PnL
- median PnL
- worst trade
- category PnL
- subtype PnL
- exclusion impact
- capital utilization

Promotion should require stability across multiple windows, not only one aggregate pass.

Success condition:

```text
No single lucky short window can promote a strategy.
No single hidden toxic subtype can dominate losses without being visible.
```

## 5.5 Improvement Priority 5: Upgrade Execution Truth

Current paper execution is useful but incomplete. It estimates taker fills from visible book VWAP and uses public tape for marks. It does not provide authenticated exchange fill truth.

The next execution upgrade should collect:

- authenticated order submissions if/when real routing exists
- order acknowledgements
- cancellations
- partial fills
- maker/taker attribution
- queue position proxy
- fill timestamp
- actual fill price
- post-fill markout
- settlement
- realized PnL

This should be logged separately from paper simulation.

Success condition:

- simulated fill PnL can be compared against actual fill PnL
- maker adverse selection can be measured instead of assumed
- taker slippage can be measured against actual fills

## 5.6 Improvement Priority 6: Improve Probability Calibration

The structural model predicts probability by adding fitted edge to current price:

```text
q_yes = clip(yes_mid + structural_edge)
```

This is simple and interpretable, but the model should be calibrated by:

- category
- subtype
- DTE
- price bucket
- liquidity bucket
- source sample size
- recent regime
- exclusion status

The model should report both:

```text
predicted_probability
uncertainty_or_haircut
```

For sparse or fragile cells, the live edge should be shrunk:

```text
usable_edge = predicted_edge - uncertainty_haircut
```

Potential approaches:

- empirical Bayes shrinkage toward category/global priors
- minimum unique-market count thresholds
- bootstrap confidence intervals
- category-specific calibration curves
- isotonic or logistic calibration on forward tape
- separate sports/esports/real-time models if they are ever reintroduced

Success condition:

- predicted 98% events should resolve near 98% after excluding known toxic classes
- calibration error should be reported by bucket and category
- edge should not be trusted when uncertainty is large

## 5.7 Improvement Priority 7: Replace Raw Edge-Dollar Objective With Risk-Adjusted Growth

The current MILP objective maximizes:

```text
sum(edge_i * allocated_usdc_i)
```

That is reasonable for a first allocator, but it can overemphasize nominal expected edge while underweighting tail loss and correlation.

The next allocator should optimize a risk-adjusted objective, such as:

```text
expected_edge_dollars
- lambda_1 * expected_tail_loss
- lambda_2 * drawdown_contribution
- lambda_3 * category_concentration
- lambda_4 * liquidity_shortfall
- lambda_5 * uncertainty_penalty
```

Or use a constrained log-growth/Kelly-style objective with strong caps.

The allocator should also make category caps dynamic based on realized recent losses. If a subtype starts losing, exposure should automatically shrink.

Success condition:

- fewer catastrophic single-subtype losses
- lower drawdown at similar expected return
- clearer capital utilization versus risk tradeoff

## 5.8 Improvement Priority 8: Treat Sentinel/Reversion As Experimental Until Proven

The methodology page describes a desirable exit stack:

- Panel Sentinel direct exits
- ML-gated reversion exits

But the latest verified replay passed by disabling the normal 1 cent reversion stop. Also, the local Sentinel summary showed:

```text
baseline_total_pnl: -1138.07
sentinel_total_pnl: -1197.02
delta_total_pnl: -58.96
exit_count: 6
trade_count: 78
```

That suggests caution. Even if a Sentinel policy satisfies a false-alarm cap, it should not be treated as fully promoted unless it improves chronological validation PnL or materially reduces drawdown without unacceptable missed upside.

Recommended policy:

- keep Sentinel visible in dashboard
- log Sentinel scores
- shadow Sentinel exits
- do not auto-enable Panel Sentinel for canonical production until out-of-sample PnL/drawdown benefit is proven
- keep reversion ML-gated if reversion is used at all

Success condition:

- exit model improves risk-adjusted PnL in forward replay
- exit model reduces drawdown without creating excess false exits
- exit logic in live runtime matches exit logic in verifier

## 5.9 Improvement Priority 9: Increase Capital Utilization Without Loosening Toxic Filters

The passing minute replay ended at:

```text
ending_equity_usdc: $100,442.79
total_return: 0.443%
```

That is positive, but low in absolute dollars. The report also notes low active capital usage under the conservative 25% depth-participation assumption.

Capital utilization should improve through better candidate quality and broader safe coverage, not by reintroducing toxic market types.

Possible improvements:

- find more non-sports categories with robust edge
- use better category inference
- improve fallback profile only for trusted categories
- add time-at-level stability requirements
- require minimum unique-market support
- use smaller order sizes in thin markets
- allow more positions only when correlation is low
- increase notional only after actual fill/settlement evidence

Success condition:

- higher average deployed capital
- no collapse in Sharpe
- no new loss concentration by subtype

## 5.10 Improvement Priority 10: Build A Promotion Gate

Before any strategy version is considered promotable, it should pass a named gate.

Suggested promotion gate:

1. Verifier passes on full available tape.
2. Verifier passes on most rolling windows.
3. No excluded/toxic subtype accounts for hidden losses.
4. Trade count is large enough to be meaningful.
5. Drawdown remains below threshold.
6. Capital utilization is sufficient.
7. Live runtime and verifier configs are identical.
8. All exclusions are logged.
9. Settlement mechanics are verified.
10. If real execution is contemplated, authenticated fill logs are available.

No real-money deployment should be inferred from a single passing paper replay.

## 6. Exact Current Algorithm In Pseudocode

This is the canonical current live algorithm, including fallback and paper allocation.

```text
load structural_fit_winners.csv
load empirical surface fallback
initialize live store

loop every POLL_SECONDS:
    reconcile resolved paper markets

    base_profile = {
        discovery_max_dte_days: 1,
        max_dte_days: 1,
        min_side_price: 0.95,
        max_side_price: 0.999,
        max_spread: 0.06
    }

    base_eval = evaluate_profile(base_profile)

    if fallback_enabled and base_eval.allocated_usdc == 0:
        fallback_profile = {
            discovery_max_dte_days: 3,
            max_dte_days: 3,
            min_side_price: 0.65,
            max_side_price: 0.999,
            max_spread: 0.06
        }
        fallback_eval = evaluate_profile(fallback_profile)
        chosen_eval = fallback_eval if fallback_eval deploys more else base_eval
    else:
        chosen_eval = base_eval

    persist markets, token states, snapshots, signals, candidates
    sync selected paper candidates
    update marks and PnL
    apply enabled exits
    update dashboard state
```

The `evaluate_profile` function is:

```text
function evaluate_profile(profile):
    markets = discover Gamma open markets inside profile discovery DTE
    markets = remove excluded categories and title patterns
    books = fetch CLOB books for YES and NO tokens

    signals = []
    for market in markets:
        yes_book = books[yes_token]
        no_book = books[no_token]
        signal = score_market(market, yes_book, no_book, profile)
        signals.append(signal)

    candidates = top eligible signals
    slippage_rows = walk book for each candidate and target size
    paper_candidates = keep target-size rows that survive slippage and edge floors
    selected, allocation_summary = allocate_milp(paper_candidates)

    return evaluation(markets, books, signals, candidates, slippage_rows, paper_candidates, selected, allocation_summary)
```

The `score_market` function is:

```text
function score_market(market, yes_book, no_book):
    yes_mid = midpoint(yes_book)
    no_mid = midpoint(no_book) or 1 - yes_mid
    days_to_expiry = market.end_date - now
    category = infer_category(market)
    normalized_category = normalize_category(category)

    empirical_probability = lookup_surface(category, price_bucket(yes_mid), dte_bucket(days_to_expiry))

    if signal_model == structural_interpolated:
        q_yes = structural_model.predict(
            category=normalized_category,
            yes_mid=yes_mid,
            days_to_expiry=days_to_expiry,
            empirical_fallback=empirical_probability
        )
    else:
        q_yes = empirical_probability

    q_no = 1 - q_yes

    fee_yes = estimated_taker_fee(yes_mid, category)
    fee_no = estimated_taker_fee(no_mid, category)

    edge_yes = q_yes - yes_mid - fee_yes
    edge_no = q_no - no_mid - fee_no

    if edge_yes >= edge_no:
        side = YES
        side_mid = yes_mid
        side_probability = q_yes
        side_edge = edge_yes
        side_fee = fee_yes
        side_book = yes_book
    else:
        side = NO
        side_mid = no_mid
        side_probability = q_no
        side_edge = edge_no
        side_fee = fee_no
        side_book = no_book or complement from yes_book

    depth = estimate 30 bps capacity from side_book
    depth_edge = side_probability - depth.vwap - side_fee
    spread = side ask - side bid

    reject if missing probability
    reject if missing yes_mid
    reject if missing DTE
    reject if DTE outside profile
    reject if side_mid outside profile price band
    reject if side_edge < min_edge
    reject if depth.capacity_usdc < min_depth_usdc_30bps
    reject if spread > max_spread

    return signal row
```

The minute verifier replay is:

```text
load eligible minute signals from live_trading.signal_snapshots
apply production exclusions
apply enhanced momentum exclusions
load minute marks from live_trading.price_snapshots

for each minute in replay:
    update latest marks
    close positions at end date
    optionally close positions on drawdown stop
    enter new signals if:
        market not already entered
        open position count below cap
        deployed notional below cap
        depth participation capacity available
    mark open positions
    write equity row

write equity.csv, trades.csv, signals.csv, verifier_manifest.json
verify independently from exported artifacts
```

## 7. Known Risks And Limitations

### 7.1 Data Limitations

- Full minute history was not available in the local research DB.
- Latest replay depends on Railway forward tape from April 23 to May 13, 2026.
- Forward tape is not multi-year.
- L2 history is not yet a complete historical replay source.
- Public prints do not reveal queue position.
- Paper fills are not authenticated exchange fills.

### 7.2 Model Limitations

- Structural fits are learned from historical surfaces and can overfit.
- Sparse categories need stronger shrinkage.
- Generic category fallback can still misclassify markets.
- Sports/esports need separate modeling or exclusion.
- Momentum and threshold markets need explicit exclusion.
- Predicted edge needs uncertainty haircuts.

### 7.3 Execution Limitations

- Visible book VWAP is not guaranteed executable in real time.
- Taker paper fills do not simulate latency or market impact.
- Maker mode lacks authenticated queue/fill attribution.
- Slippage can change between scoring and execution.
- Small books can vanish.

### 7.4 Portfolio Limitations

- Edge-dollar maximization may not be robust enough.
- Category caps can be too blunt.
- Tail loss can cluster by hidden subtype.
- Fallback profile can increase trade count but also widen risk.
- Low capital utilization means headline annualized metrics can hide small absolute PnL.

### 7.5 Validation Limitations

- One passing verifier window is not enough.
- Annualized return over a short period can be unstable.
- The passing run has only 63 closed trades.
- The passing run total return is about 0.443%, not a large absolute result.
- The verifier proves artifact consistency and threshold pass, not future profitability.

## 8. Recommended Next Version

The next version should be called something like:

```text
DNS1 structural_interpolated verified-replay v2
```

Its defining changes should be:

1. main DNS1 excludes real-time sports/esports by default
2. exclusion classifier is versioned and logged
3. live and replay config are identical
4. all paper decisions write config snapshots
5. rolling verifier windows run automatically
6. forward tape is immutable and date-partitioned
7. structural edge is uncertainty-haircut before allocation
8. MILP objective is risk-adjusted, not raw edge-only
9. Sentinel and reversion stay in shadow mode until proven
10. promotion requires multiple out-of-sample verifier passes

The recommended next live settings should keep these explicit:

```text
SIGNAL_MODEL=structural_interpolated
LIVE_SCHEMA=live_trading
PAPER_EXECUTION_MODE=taker
ALLOCATOR_MODE=milp
PAPER_MAX_TOTAL_USDC=100000
PAPER_ORDER_USDC=5000
PAPER_FILL_PARTICIPATION=0.25
PAPER_MIN_EDGE_AFTER_SLIPPAGE=0.01
MILP_MIN_STRUCTURAL_EDGE=0.015
FALLBACK_DISCOVERY_MAX_DTE_DAYS=3
FALLBACK_MAX_DTE_DAYS=3
FALLBACK_MIN_SIDE_PRICE=0.65
EXCLUDE_MOMENTUM_MARKETS=1
```

And add explicit sports/esports exclusions until a separate model is validated.

## 9. Bottom Line

The premise is that binary event markets contain category/time/price-dependent mispricings, but only some of those mispricings survive fees, slippage, liquidity, and tail risk.

The solution is a structural probability model plus live execution-aware filtering plus MILP paper allocation plus independent replay verification.

The algorithm is:

```text
build empirical surface
fit structural edge equations
discover live markets
score YES and NO
select the better side
reject weak or unexecutable candidates
walk the book for slippage
allocate with MILP
paper fill at measured VWAP
record forward tape
verify minute replay from exported artifacts
```

The latest local version is not just the original HTML methodology. It is the May 13 minute-level verified replay with production exclusions and enhanced momentum filtering, built on top of the structural live runtime.

The improvement should be to make the latest pass robust:

- remove toxic real-time sports/esports from the main strategy
- extend the forward tape
- align live and replay exactly
- validate over rolling out-of-sample windows
- collect authenticated execution data
- add uncertainty haircuts
- upgrade allocation to risk-adjusted growth
- keep exit models in shadow mode until proven

That path improves the actual weakness shown by the local evidence: not the lack of an explanatory formula, but the risk that apparent edge becomes false edge when market subtype, execution, and validation are not controlled tightly enough.
