Skip to content

feat(research): measure what a round trip costs on a commission-free … - #697

Merged
eaitbrahim merged 2 commits into
mainfrom
feat-371-equities-cost-fidelity
Sep 3, 2026
Merged

feat(research): measure what a round trip costs on a commission-free …#697
eaitbrahim merged 2 commits into
mainfrom
feat-371-equities-cost-fidelity

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

…venue (#371)

Alpaca Phase C's cost-fidelity half. The PRD makes this a precondition -- "no strategy claim is believed before it" (§6.3, O4) -- and the ordering earned its keep: config.paper-equities.yaml ships taker_pct: 0.0, honestly labelled as awaiting this work, and priced literally that models a FREE venue.

Result: a round trip costs ~2.2bp on Alpaca equities and ~306bp on Coinbase spot, a factor of 141. keel's crypto model is validated in passing (332bp charged vs 306bp measured, conservative by 8%, from an estimator with no knowledge of Coinbase's fee schedule). Its equities pricing is 7.5x too high.

Two findings mattered more than the headline.

THE AGGREGATION CHOICE IS WORTH 22x, AND THE OBVIOUS ONE IS WRONG. Corwin-Schultz two-day estimates go negative constantly. Flooring each pair and averaging the survivors keeps the positive half of symmetric noise and discards the negative half, so the mean converges on E[max(X,0)] > 0 -- it reports a spread that is not there, and a bigger one the more volatile the series is. Worst, therefore, exactly on the asset class this exists to price. The paper's own procedure averages within a month and floors the MONTHLY mean; negatives then cancel inside the block. Same data, same estimator: MSFT 41.8bp -> 1.9bp. The first run of this measurement reported 43.6bp and a 4.1x ratio, and what exposed it was the implausibility of a mega-cap quoting 41.8bp, not a test. Both aggregations stay reachable, because the comparison between them IS the finding.

KEEL READS MSFT AS A THIN ASSET. Cached median daily quote volume is $186M -- about 2% of MSFT's real consolidated volume, which is approximately IEX's market share, and the equities profile runs on the IEX feed. Every ticker sits below the model's $500M anchor, so every one is priced as thin. The candles are fine; the statistic is being asked a question the feed cannot answer. Bounded for cost modelling, unbounded for liquidity screens and admission floors. Filed as #696.

Tests (written first, red before green):

tests/research/test_spread.py -- 28 tests. The load-bearing one is
test_flooring_each_pair_biases_a_quiet_series_up: a seeded random walk
with NO spread at all, where the naive aggregation must read >5x the
blocked one. test_the_overnight_gap_adjustment_is_applied pins §I.B --
equities gap nightly and crypto does not, so an unadjusted estimator would
inflate the equities side of exactly this comparison, in the direction that
flatters keel's existing prior.

Mutation-verified, 12 mutants. One survived -- averaging over positive blocks only rather than all blocks -- because no test had a series with both positive and negative BLOCKS; test_a_floored_block_still_counts_toward_the_average was added and kills it. One further mutant proved equivalent (sum(max(m,0)) and sum(m for m if m>0) over the same denominator) and is recorded as such rather than counted as a survivor.

keel/commands/research.py gains the index row its completeness pin demands.

Refs #371
Refs #696

Claude-Session: https://claude.ai/code/session_01NzuKAe2RVrPt9acVAWjRyL

What & why

Tests-first evidence

  • Tests written first, seen failing for the right reason

Gates (all must pass)

  • uv run ruff check clean
  • uv run mypy clean
  • uv run pytest -q green

Scope check

  • This PR touches a rail or a default classification — checked means it DOES;
    leave checked only if true, and if so: cite the source and open the discussion
    BEFORE review (CONTRIBUTING.md, "Governance: rulings vs. machinery").
  • New dependency added (needs discussion first)

eaitbrahim and others added 2 commits September 2, 2026 19:28
…venue (#371)

Alpaca Phase C's cost-fidelity half. The PRD makes this a precondition -- "no
strategy claim is believed before it" (§6.3, O4) -- and the ordering earned its
keep: `config.paper-equities.yaml` ships `taker_pct: 0.0`, honestly labelled as
awaiting this work, and priced literally that models a FREE venue.

Result: a round trip costs ~2.2bp on Alpaca equities and ~306bp on Coinbase spot,
a factor of 141. keel's crypto model is validated in passing (332bp charged vs
306bp measured, conservative by 8%, from an estimator with no knowledge of
Coinbase's fee schedule). Its equities pricing is 7.5x too high.

Two findings mattered more than the headline.

THE AGGREGATION CHOICE IS WORTH 22x, AND THE OBVIOUS ONE IS WRONG. Corwin-Schultz
two-day estimates go negative constantly. Flooring each pair and averaging the
survivors keeps the positive half of symmetric noise and discards the negative
half, so the mean converges on E[max(X,0)] > 0 -- it reports a spread that is not
there, and a bigger one the more volatile the series is. Worst, therefore, exactly
on the asset class this exists to price. The paper's own procedure averages within
a month and floors the MONTHLY mean; negatives then cancel inside the block. Same
data, same estimator: MSFT 41.8bp -> 1.9bp. The first run of this measurement
reported 43.6bp and a 4.1x ratio, and what exposed it was the implausibility of a
mega-cap quoting 41.8bp, not a test. Both aggregations stay reachable, because the
comparison between them IS the finding.

KEEL READS MSFT AS A THIN ASSET. Cached median daily quote volume is $186M --
about 2% of MSFT's real consolidated volume, which is approximately IEX's market
share, and the equities profile runs on the IEX feed. Every ticker sits below the
model's $500M anchor, so every one is priced as thin. The candles are fine; the
statistic is being asked a question the feed cannot answer. Bounded for cost
modelling, unbounded for liquidity screens and admission floors. Filed as #696.

Tests (written first, red before green):

  tests/research/test_spread.py  -- 28 tests. The load-bearing one is
      `test_flooring_each_pair_biases_a_quiet_series_up`: a seeded random walk
      with NO spread at all, where the naive aggregation must read >5x the
      blocked one. `test_the_overnight_gap_adjustment_is_applied` pins §I.B --
      equities gap nightly and crypto does not, so an unadjusted estimator would
      inflate the equities side of exactly this comparison, in the direction that
      flatters keel's existing prior.

Mutation-verified, 12 mutants. One survived -- averaging over positive blocks
only rather than all blocks -- because no test had a series with both positive
and negative BLOCKS; `test_a_floored_block_still_counts_toward_the_average` was
added and kills it. One further mutant proved equivalent (`sum(max(m,0))` and
`sum(m for m if m>0)` over the same denominator) and is recorded as such rather
than counted as a survivor.

`keel/commands/research.py` gains the index row its completeness pin demands.

Refs #371
Refs #696

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzuKAe2RVrPt9acVAWjRyL
Five findings from the review of #697, fixed on the branch rather than posted as
comments. Two are substantive; the headline (141x, 2.17bp vs 306.31bp) is
unchanged by any of them.

1. `corwin_schultz_spread` did not validate `block_size`. `0` reached
   `range(step=0)` and raised `ValueError: range() arg 3 must not be zero`;
   negatives built an empty block list and raised `ZeroDivisionError`. Now
   refused by name, the way `autonomy_on` bounds `--for-hours`. `None` keeps its
   meaning as the one non-positive-integer value that has one.

2. `negative_pair_share`'s docstring said "how much of the series floored to
   zero". Under the DEFAULT aggregation that is false -- a negative pair averages
   against the positives inside its block, and only a negative BLOCK mean floors.
   The two coincide only under `block_size=None`, which is exactly the conflation
   that makes that variant biased, so this was the worst possible place to be
   loose about it.

3. The estimator never reads `ts` and will price a pair spanning an arbitrary
   hole as a two-day pair. Benign for this measurement (MSFT's largest bar-to-bar
   gap is 4 days, TON's is 1) but unstated for a public API. The contiguity
   precondition is now written down, including that blocks are formed over usable
   PAIRS rather than over calendar time.

4. The driver derived the round trip from `2 * est.half_spread_bp`, which
   quantises to 0.01bp before doubling, so the published round-trip column
   disagreed with the spread column it comes from (GOOGL printed 1.29 and used
   1.28; AAPL 2.83 and used 2.84). Now `est.spread_bp`. Two cells move by 0.01bp:
   GOOGL 1.51 -> 1.52, AAPL 3.07 -> 3.06.

5. THE ONE THAT MATTERED. Finding 2 of the document asserted that the cached MSFT
   volume was "roughly 2% ... approximately IEX's share of US equity volume" and
   that the understatement was ~50x. Neither was measured, and the 2% was simply
   wrong -- IEX publishes ~3.8% for Q2 2026. In a document whose entire value is
   that every number is checkable, that was a recalled figure wearing a
   measurement's clothes.

   The mechanism does not need the statistic: a single-venue feed reports a
   fraction of consolidated volume BY CONSTRUCTION, however faithfully it reports
   its own executions, and that alone establishes that the liquidity statistic
   cannot answer the question being asked of it. The section now argues it
   structurally, quotes IEX's own published share as a dated and attributed
   order-of-magnitude aside, and says plainly that the size of the understatement
   is not established here and belongs to #696. #696 is corrected the same way,
   and gains an acceptance criterion requiring the factor to be measured against
   a consolidated source rather than asserted.

Test written first, red before green:

  tests/research/test_spread.py::test_a_block_size_that_cannot_form_a_block_is_refused

Mutation-verified, 3 mutants on the new guard, all killed: guard removed; guard
admitting zero; guard also rejecting `None`.

Refs #371
Refs #696

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzuKAe2RVrPt9acVAWjRyL
@eaitbrahim
eaitbrahim merged commit e9a0f5f into main Sep 3, 2026
4 checks passed
@eaitbrahim
eaitbrahim deleted the feat-371-equities-cost-fidelity branch September 3, 2026 05:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant