Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions keel/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
13 changes: 12 additions & 1 deletion keel/commands/admission.py
Original file line number Diff line number Diff line change
Expand Up @@ -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) ------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion keel/compliance/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion packages/keel-broker-api/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
2 changes: 1 addition & 1 deletion packages/keel-broker-coinbase/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
2 changes: 1 addition & 1 deletion packages/keel-broker-fake/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
2 changes: 1 addition & 1 deletion packages/keel-core/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down
36 changes: 33 additions & 3 deletions tests/commands/test_admission.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
10 changes: 5 additions & 5 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading