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/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/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 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" },