From 8e9d1cfc6139da75aa30bf4d7eecbb5e783634bc Mon Sep 17 00:00:00 2001 From: Elmehdi Aitbrahim Date: Sat, 8 Aug 2026 19:29:37 -0400 Subject: [PATCH 1/2] chore: bump to 0.5.6 for the discovery liquidity probe `assets discover --probe-liquidity` and `screen.median_daily_quote_volume` are on main but not in any release, so the deployment (0.5.5) cannot run them. The scout skill now makes the probe the first step of every sweep, which fails against 0.5.5 -- this closes that gap. Co-Authored-By: Claude Opus 5 (1M context) --- packages/keel-broker-api/pyproject.toml | 2 +- packages/keel-broker-coinbase/pyproject.toml | 2 +- packages/keel-broker-fake/pyproject.toml | 2 +- packages/keel-core/pyproject.toml | 2 +- pyproject.toml | 2 +- uv.lock | 10 +++++----- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/keel-broker-api/pyproject.toml b/packages/keel-broker-api/pyproject.toml index c9d9dd61..86475468 100644 --- a/packages/keel-broker-api/pyproject.toml +++ b/packages/keel-broker-api/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "keel-broker-api" -version = "0.5.5" +version = "0.5.6" description = "Broker port, domain types, capability model, and conformance suite for keel" requires-python = ">=3.14.4" dependencies = ["keel-core"] diff --git a/packages/keel-broker-coinbase/pyproject.toml b/packages/keel-broker-coinbase/pyproject.toml index 507f12b7..eef11095 100644 --- a/packages/keel-broker-coinbase/pyproject.toml +++ b/packages/keel-broker-coinbase/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "keel-broker-coinbase" -version = "0.5.5" +version = "0.5.6" description = "Coinbase Advanced Trade adapter for keel" requires-python = ">=3.14.4" dependencies = ["keel-core", "keel-broker-api", "coinbase-advanced-py>=1.8.4"] diff --git a/packages/keel-broker-fake/pyproject.toml b/packages/keel-broker-fake/pyproject.toml index 6744446e..0e96e194 100644 --- a/packages/keel-broker-fake/pyproject.toml +++ b/packages/keel-broker-fake/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "keel-broker-fake" -version = "0.5.5" +version = "0.5.6" description = "A deliberately divergent second broker adapter, to keep the port honest" requires-python = ">=3.14.4" dependencies = ["keel-core", "keel-broker-api"] diff --git a/packages/keel-core/pyproject.toml b/packages/keel-core/pyproject.toml index f56d2bb7..8f1c66ff 100644 --- a/packages/keel-core/pyproject.toml +++ b/packages/keel-core/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "keel-core" -version = "0.5.5" +version = "0.5.6" description = "Shared domain types, configuration, and logging for keel" requires-python = ">=3.14.4" dependencies = ["pyyaml>=6.0.3", "python-dotenv>=1.2.2"] diff --git a/pyproject.toml b/pyproject.toml index fdc98673..99830c1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "keel-trader" -version = "0.5.5" +version = "0.5.6" description = "Offline-first, broker-agnostic, rule-based spot-trading agent (halal policy by default)" readme = "README.md" authors = [ diff --git a/uv.lock b/uv.lock index b9e8396c..9f181bda 100644 --- a/uv.lock +++ b/uv.lock @@ -275,7 +275,7 @@ wheels = [ [[package]] name = "keel-broker-api" -version = "0.5.5" +version = "0.5.6" source = { editable = "packages/keel-broker-api" } dependencies = [ { name = "keel-core" }, @@ -295,7 +295,7 @@ provides-extras = ["conformance"] [[package]] name = "keel-broker-coinbase" -version = "0.5.5" +version = "0.5.6" source = { editable = "packages/keel-broker-coinbase" } dependencies = [ { name = "coinbase-advanced-py" }, @@ -312,7 +312,7 @@ requires-dist = [ [[package]] name = "keel-broker-fake" -version = "0.5.5" +version = "0.5.6" source = { editable = "packages/keel-broker-fake" } dependencies = [ { name = "keel-broker-api" }, @@ -327,7 +327,7 @@ requires-dist = [ [[package]] name = "keel-core" -version = "0.5.5" +version = "0.5.6" source = { editable = "packages/keel-core" } dependencies = [ { name = "python-dotenv" }, @@ -342,7 +342,7 @@ requires-dist = [ [[package]] name = "keel-trader" -version = "0.5.5" +version = "0.5.6" source = { editable = "." } dependencies = [ { name = "click" }, From fc4dfee0b8d889f4ccfec645daa23cc06ab20c01 Mon Sep 17 00:00:00 2001 From: Elmehdi Aitbrahim Date: Sat, 8 Aug 2026 19:58:05 -0400 Subject: [PATCH 2/2] fix(assets): lower the discovery floor to 1M and pin it to the admission floor The sweep's 24h-volume pre-filter was 5,000,000 -- five times the liquidity floor the admission gate actually applies. Discovery was therefore STRICTER than the criterion it screens for, and was hiding assets the gate would have accepted. Measured on a live sweep 2026-08-08: at the 5M floor, 920 venue products -> 9 candidates, of which exactly one was an unsettled survivor. Lower the floor and seven more clear BOTH mechanical gates (4-year history AND the real liquidity criterion): FET, AVAX, ICP, ALGO, UNI, CRV, DOT. FET sits at $2.94M/24h -- invisible to the sweep -- while measuring 4.8x the admission floor. DOGE, at $3.95M, never appeared in a default sweep at all despite being ~30x the admission floor. The floor, not the market, was the binding constraint on the candidate pipeline. At 1M the same sweep returns 38. 1,000,000 is not a fresh guess: it is `ScreenPolicy.min_median_daily_volume`. Setting them equal makes the pre-filter exactly as permissive as the gate, which is the most it can be without proposing assets that cannot pass. The two remain DIFFERENT STATISTICS -- a 24h snapshot versus a median over history -- which is what `--probe-liquidity` exists to reconcile; this pins the numbers, not the methods. Three modules carried this default (`cli.assets_discover`'s option, admission's `DEFAULT_MIN_QUOTE_24H_VOLUME`, `screen.DiscoveryPolicy`). Only the first two were pinned to each other, so a caller constructing `DiscoveryPolicy` directly -- which `assets_discover` does -- could have drifted from `build_discover_report` silently. Now all three are pinned, plus a fourth pin to the admission floor, so they move together or the suite fails. Behaviour of the gate is unchanged: same criterion, same floor, same verdicts. Only how many candidates a sweep surfaces for a human to consider. Co-Authored-By: Claude Opus 5 (1M context) --- keel/cli.py | 7 +++++-- keel/commands/admission.py | 13 +++++++++++- keel/compliance/screen.py | 2 +- tests/commands/test_admission.py | 36 +++++++++++++++++++++++++++++--- 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/keel/cli.py b/keel/cli.py index fa263e62..b5ab1cdc 100644 --- a/keel/cli.py +++ b/keel/cli.py @@ -699,8 +699,11 @@ def assets_holdings(ctx: click.Context, min_balance: str, run_screen: bool) -> N @assets_group.command("discover") @click.option("--quote", default=None, help="Settlement currency (default: config.quote_currency).") @click.option( - "--min-volume-24h", default="5000000", show_default=True, - help="Cheap pre-filter on the venue's reported 24h quote volume.", + "--min-volume-24h", default="1000000", show_default=True, + help="Cheap pre-filter on the venue's reported 24h quote volume. Bounds the request count; it " + "is NOT a liquidity criterion -- a 24h snapshot is a different statistic from the median the " + "gate applies, so use --probe-liquidity for that. Set equal to the admission floor so " + "discovery cannot be stricter than the criterion it screens for.", ) @click.option("--limit", default=25, show_default=True, help="Show at most this many candidates.") @click.option( diff --git a/keel/commands/admission.py b/keel/commands/admission.py index 98871b33..950a7b76 100644 --- a/keel/commands/admission.py +++ b/keel/commands/admission.py @@ -66,7 +66,18 @@ #: create the cycle `cli -> tui -> admission -> cli`. `test_build_discover_report_applies_ #: default_volume_floor_matching_assets_discover` reads the CLI option's own default and asserts #: it equals this constant, so the two cannot silently drift apart. -DEFAULT_MIN_QUOTE_24H_VOLUME = Decimal("5000000") +#: Discovery's 24h-volume pre-filter, pinned EQUAL to `ScreenPolicy.min_median_daily_volume` so a +#: sweep can never be stricter than the gate it feeds. It bounds how many products get probed; it +#: is not a liquidity verdict (that is `--probe-liquidity`, which computes the gate's own median). +#: +#: Was 5,000,000 until 2026-08-08. At that floor the sweep returned 9 candidates and exactly one +#: unsettled survivor; at a lower floor, seven more cleared BOTH mechanical gates -- FET among them +#: at $2.94M/24h, i.e. invisible to the sweep while measuring 4.8x the admission floor. The floor, +#: not the market, was the binding constraint on the candidate pipeline. +#: +#: `tests/commands/test_admission.py` pins this to the CLI option, to `DiscoveryPolicy`'s default +#: and to the admission floor; all four move together or the suite fails. +DEFAULT_MIN_QUOTE_24H_VOLUME = Decimal("1000000") # -- 2a. shortlist location (offline) ------------------------------------------------------------ diff --git a/keel/compliance/screen.py b/keel/compliance/screen.py index 4b7f20e7..2e5620f8 100644 --- a/keel/compliance/screen.py +++ b/keel/compliance/screen.py @@ -362,7 +362,7 @@ class DiscoveryPolicy: """ quote_currency: str = "USD" - min_quote_24h_volume: Decimal = Decimal("5000000") + min_quote_24h_volume: Decimal = Decimal("1000000") def median_daily_quote_volume(candles: Sequence[Any]) -> Decimal: diff --git a/tests/commands/test_admission.py b/tests/commands/test_admission.py index e87f2834..a2183588 100644 --- a/tests/commands/test_admission.py +++ b/tests/commands/test_admission.py @@ -26,6 +26,7 @@ import keel.cli as cli_module from keel.commands.admission import ( + DEFAULT_MIN_QUOTE_24H_VOLUME, DEFAULT_PROPOSALS_DIR, DiscoverReport, ProposeView, @@ -651,11 +652,11 @@ def test_build_discover_report_applies_default_volume_floor_matching_assets_disc p for p in cli_module.assets_discover.params if p.name == "min_volume_24h" ) default_floor = Decimal(cli_option.default) - assert default_floor == Decimal("5000000") + assert default_floor == Decimal("1000000") config = _config(allowlist=[]) - below = _venue_product("DOGE", volume="4999999") - above = _venue_product("SHIB", volume="5000001") + below = _venue_product("DOGE", volume="999999") + above = _venue_product("SHIB", volume="1000001") report = build_discover_report([below, above], config) @@ -712,3 +713,32 @@ def test_render_discover_report_never_includes_probe_history_marker(): text = "\n".join(lines).lower() assert "4yr?" not in text + + +def test_every_discovery_floor_default_agrees(): + """Three modules carry this default; a drift between them is silent and changes the sweep. + + `cli.assets_discover`'s option, `admission.DEFAULT_MIN_QUOTE_24H_VOLUME` and + `screen.DiscoveryPolicy` each name a floor. The first two were already pinned to each other; + `DiscoveryPolicy` was not, so a caller constructing one directly (as `cli.assets_discover` + does) could silently use a different floor from `build_discover_report`. + """ + from keel.compliance.screen import DiscoveryPolicy + + cli_option = next(p for p in cli_module.assets_discover.params if p.name == "min_volume_24h") + + assert Decimal(cli_option.default) == DEFAULT_MIN_QUOTE_24H_VOLUME + assert DiscoveryPolicy().min_quote_24h_volume == DEFAULT_MIN_QUOTE_24H_VOLUME + + +def test_the_discovery_floor_matches_the_admission_liquidity_floor(): + """Discovery should not hide assets the gate would admit. + + The sweep's floor is a 24h snapshot and the gate's is a median over history -- different + statistics (which is why `--probe-liquidity` exists). Pinning the two NUMBERS equal keeps the + pre-filter from being stricter than the criterion it screens for: at the old 5x-higher floor, + FET sat at $2.94M/24h and never appeared, while measuring 4.8x the admission floor. + """ + from keel.compliance.screen import ScreenPolicy + + assert DEFAULT_MIN_QUOTE_24H_VOLUME == ScreenPolicy().min_median_daily_volume