Add depth-aware shadow bid pricing

This commit is contained in:
saymrwulf 2026-04-25 19:54:24 +02:00
parent 6096434ef4
commit 5691bbb299
15 changed files with 227 additions and 19 deletions

View file

@ -61,6 +61,12 @@ Watcher-only tokens are only relevant if we later need account-specific read-onl
- `manual_canary`: a tiny manually executed research experiment is within the configured loss budget.
- `manual_bid`: a manually executed bid clears profit-seeking discount and risk guardrails.
The Braiins market report distinguishes visible top-of-book from executable depth:
- `best_ask_btc_per_eh_day`: cheapest visible ask.
- `fillable_price_btc_per_eh_day`: cheapest ask level with enough unmatched supply for the configured target PH/s.
- `suggested_bid_btc_per_eh_day`: fillable price plus the configured overpay cushion.
## Documentation
- `PROGRAM.md`: research charter and ratchet rules.

View file

@ -22,3 +22,5 @@ recommend_only = true
target_duration_minutes = 180
target_spend_btc = "0.00010"
risk_lambda = "0.35"
shadow_target_ph = "10"
shadow_overpay_btc_per_eh_day = "0.01"

View file

@ -77,6 +77,8 @@ The market is not a money-printer setup, but the expected loss is inside the con
Important fields:
- `best_ask_btc_per_eh_day`: current buy reference from Braiins.
- `fillable_price_btc_per_eh_day`: depth-aware price where enough unmatched ask supply exists for the configured target PH/s.
- `suggested_bid_btc_per_eh_day`: fillable price plus the configured overpay cushion; this is the shadow-autopilot would-bid price.
- `breakeven_btc_per_eh_day`: estimated mining EV at current OCEAN/network inputs.
- `score_btc`: risk-adjusted expected value. Negative means no action.
- `proposed_spend_btc`: canary spend, currently tiny by design.

View file

@ -12,6 +12,8 @@ The result is a recommendation only. `observe` means no action is recommended. `
The report's sampled price min/avg/max uses public Braiins snapshots only. Manual imports are still stored and can drive evaluation, but they do not pollute live market summary statistics.
The strategy price is depth-aware when orderbook depth is available: it prefers `suggested_bid_btc_per_eh_day` over raw best ask. This prevents top-of-book slivers or already-matched asks from looking cheaper than actually executable depth.
## Repeated Sampling
For short monitor sessions:

View file

@ -36,14 +36,24 @@ class BraiinsPublicClient:
with urlopen(request, timeout=15) as response:
return json.loads(response.read().decode("utf-8"))
def fetch_market_snapshot(self) -> MarketSnapshot:
def fetch_market_snapshot(
self,
*,
target_ph: Decimal = Decimal("10"),
overpay_btc_per_eh_day: Decimal = Decimal("0.01"),
) -> MarketSnapshot:
stats = self.get_json("/spot/stats")
orderbook = self.get_json("/orderbook")
if not isinstance(stats, dict):
raise BraiinsSafetyError("/spot/stats did not return an object")
if not isinstance(orderbook, dict):
raise BraiinsSafetyError("/orderbook did not return an object")
return market_snapshot_from_public_api(stats, orderbook)
return market_snapshot_from_public_api(
stats,
orderbook,
target_ph=target_ph,
overpay_btc_per_eh_day=overpay_btc_per_eh_day,
)
@dataclass(frozen=True)
@ -90,6 +100,13 @@ def market_snapshot_from_json_file(path: str) -> MarketSnapshot:
),
best_bid_btc_per_eh_day=_optional_decimal(raw.get("best_bid_btc_per_eh_day")),
best_ask_btc_per_eh_day=_optional_decimal(raw.get("best_ask_btc_per_eh_day")),
fillable_price_btc_per_eh_day=_optional_decimal(raw.get("fillable_price_btc_per_eh_day")),
fillable_target_ph=_optional_decimal(raw.get("fillable_target_ph")),
fillable_available_ph=_optional_decimal(raw.get("fillable_available_ph")),
suggested_bid_btc_per_eh_day=_optional_decimal(raw.get("suggested_bid_btc_per_eh_day")),
suggested_overpay_btc_per_eh_day=_optional_decimal(
raw.get("suggested_overpay_btc_per_eh_day")
),
last_price_btc_per_eh_day=_optional_decimal(raw.get("last_price_btc_per_eh_day")),
total_hashrate_eh_s=_optional_decimal(raw.get("total_hashrate_eh_s")),
available_hashrate_eh_s=(
@ -106,9 +123,12 @@ def market_snapshot_from_public_api(
stats: dict[str, Any],
orderbook: dict[str, Any],
timestamp_utc: str | None = None,
target_ph: Decimal = Decimal("10"),
overpay_btc_per_eh_day: Decimal = Decimal("0.01"),
) -> MarketSnapshot:
best_bid = _best_price_from_orders(orderbook.get("bids"), prefer="max")
best_ask = _best_price_from_orders(orderbook.get("asks"), prefer="min")
depth = fillable_ask_for_target(orderbook.get("asks"), target_ph)
last_price = _sat_to_btc(stats.get("last_avg_price_sat"))
total_hashrate = _ph_to_eh(stats.get("hash_rate_available_10m_ph"))
matched_hashrate = _ph_to_eh(stats.get("hash_rate_matched_10m_ph"))
@ -117,13 +137,23 @@ def market_snapshot_from_public_api(
if total_hashrate is not None and matched_hashrate is not None:
available_hashrate = max(Decimal("0"), total_hashrate - matched_hashrate)
best_price = best_ask or last_price or best_bid
suggested_bid = (
depth.price_btc_per_eh_day + overpay_btc_per_eh_day
if depth.price_btc_per_eh_day is not None
else None
)
best_price = suggested_bid or best_ask or last_price or best_bid
return MarketSnapshot(
timestamp_utc=timestamp_utc or datetime.now(UTC).isoformat(timespec="seconds"),
best_price_btc_per_eh_day=best_price,
best_bid_btc_per_eh_day=best_bid,
best_ask_btc_per_eh_day=best_ask,
fillable_price_btc_per_eh_day=depth.price_btc_per_eh_day,
fillable_target_ph=target_ph,
fillable_available_ph=depth.available_ph,
suggested_bid_btc_per_eh_day=suggested_bid,
suggested_overpay_btc_per_eh_day=overpay_btc_per_eh_day,
last_price_btc_per_eh_day=last_price,
total_hashrate_eh_s=total_hashrate,
available_hashrate_eh_s=available_hashrate,
@ -132,6 +162,59 @@ def market_snapshot_from_public_api(
)
@dataclass(frozen=True)
class FillableDepth:
price_btc_per_eh_day: Decimal | None
available_ph: Decimal
def fillable_ask_for_target(asks: object, target_ph: Decimal) -> FillableDepth:
if not isinstance(asks, list) or target_ph <= 0:
return FillableDepth(price_btc_per_eh_day=None, available_ph=Decimal("0"))
levels: list[tuple[Decimal, Decimal]] = []
for ask in asks:
if not isinstance(ask, dict):
continue
price = _sat_to_btc(ask.get("price_sat"))
available = _available_ask_ph(ask)
if price is not None and available > 0:
levels.append((price, available))
levels.sort(key=lambda item: item[0])
cumulative = Decimal("0")
for price, available in levels:
cumulative += available
if cumulative >= target_ph:
return FillableDepth(price_btc_per_eh_day=price, available_ph=cumulative)
return FillableDepth(price_btc_per_eh_day=None, available_ph=cumulative)
def _available_ask_ph(ask: dict[str, Any]) -> Decimal:
for key in ("hash_rate_available_ph", "available_hashrate_ph", "available_ph"):
value = _optional_decimal(ask.get(key))
if value is not None:
return max(Decimal("0"), value)
limit = _optional_decimal(
ask.get("hash_rate_limit_ph")
or ask.get("limit_ph")
or ask.get("hashRateAvailable")
or ask.get("amount")
)
used = _optional_decimal(
ask.get("hash_rate_matched_ph")
or ask.get("used_ph")
or ask.get("hashRateMatched")
or ask.get("total")
)
if limit is not None:
return max(Decimal("0"), limit - (used or Decimal("0")))
return Decimal("0")
def _best_price_from_orders(orders: object, prefer: str) -> Decimal | None:
if not isinstance(orders, list):
return None

View file

@ -50,8 +50,12 @@ def cmd_import_market(args: argparse.Namespace) -> int:
def cmd_collect_braiins_public(args: argparse.Namespace) -> int:
config = load_config(Path(args.config) if args.config else None)
client = BraiinsPublicClient(api_base=args.base_url.rstrip("/"))
snapshot = client.fetch_market_snapshot()
snapshot = client.fetch_market_snapshot(
target_ph=config.strategy.shadow_target_ph,
overpay_btc_per_eh_day=config.strategy.shadow_overpay_btc_per_eh_day,
)
with connect() as conn:
init_db(conn)
save_market_snapshot(conn, snapshot)
@ -139,6 +143,7 @@ def build_parser() -> argparse.ArgumentParser:
"collect-braiins-public",
help="collect one unauthenticated Braiins public market snapshot",
)
braiins.add_argument("--config")
braiins.add_argument("--base-url", default="https://hashpower.braiins.com/webapi")
braiins.set_defaults(func=cmd_collect_braiins_public)

View file

@ -51,6 +51,8 @@ class StrategyConfig:
target_duration_minutes: int
target_spend_btc: Decimal
risk_lambda: Decimal
shadow_target_ph: Decimal
shadow_overpay_btc_per_eh_day: Decimal
@dataclass(frozen=True)
@ -98,5 +100,9 @@ def load_config(path: Path | None = None) -> AppConfig:
target_duration_minutes=int(strategy.get("target_duration_minutes", 180)),
target_spend_btc=_decimal(strategy.get("target_spend_btc"), "0.0001"),
risk_lambda=_decimal(strategy.get("risk_lambda"), "0.25"),
shadow_target_ph=_decimal(strategy.get("shadow_target_ph"), "10"),
shadow_overpay_btc_per_eh_day=_decimal(
strategy.get("shadow_overpay_btc_per_eh_day"), "0.01"
),
),
)

View file

@ -24,6 +24,11 @@ class MarketSnapshot:
best_price_btc_per_eh_day: Decimal | None
best_bid_btc_per_eh_day: Decimal | None = None
best_ask_btc_per_eh_day: Decimal | None = None
fillable_price_btc_per_eh_day: Decimal | None = None
fillable_target_ph: Decimal | None = None
fillable_available_ph: Decimal | None = None
suggested_bid_btc_per_eh_day: Decimal | None = None
suggested_overpay_btc_per_eh_day: Decimal | None = None
last_price_btc_per_eh_day: Decimal | None = None
total_hashrate_eh_s: Decimal | None = None
available_hashrate_eh_s: Decimal | None = None

View file

@ -48,7 +48,12 @@ def run_cycle(
save_ocean_snapshot(conn, ocean)
if collect_braiins:
fetcher = market_fetcher or BraiinsPublicClient().fetch_market_snapshot
fetcher = market_fetcher or (
lambda: BraiinsPublicClient().fetch_market_snapshot(
target_ph=config.strategy.shadow_target_ph,
overpay_btc_per_eh_day=config.strategy.shadow_overpay_btc_per_eh_day,
)
)
market = fetcher()
save_market_snapshot(conn, market)

View file

@ -39,10 +39,13 @@ def _market_lines(market: MarketSnapshot | None, stats: PriceStats) -> list[str]
f" status: {market.status or 'unknown'}",
f" best_bid_btc_per_eh_day: {_fmt(market.best_bid_btc_per_eh_day)}",
f" best_ask_btc_per_eh_day: {_fmt(market.best_ask_btc_per_eh_day)}",
f" fillable_target_ph: {_fmt(market.fillable_target_ph)}",
f" fillable_price_btc_per_eh_day: {_fmt(market.fillable_price_btc_per_eh_day)}",
f" suggested_bid_btc_per_eh_day: {_fmt(market.suggested_bid_btc_per_eh_day)}",
f" last_price_btc_per_eh_day: {_fmt(market.last_price_btc_per_eh_day)}",
f" available_hashrate_eh_s: {_fmt(market.available_hashrate_eh_s)}",
f" sampled_price_count: {stats.count}",
f" sampled_price_min_avg_max: {_fmt(stats.min_price)} / {_fmt(stats.avg_price)} / {_fmt(stats.max_price)}",
f" sampled_strategy_price_count: {stats.count}",
f" sampled_strategy_price_min_avg_max: {_fmt(stats.min_price)} / {_fmt(stats.avg_price)} / {_fmt(stats.max_price)}",
]

View file

@ -37,6 +37,11 @@ def init_db(conn: sqlite3.Connection) -> None:
best_price_btc_per_eh_day TEXT,
best_bid_btc_per_eh_day TEXT,
best_ask_btc_per_eh_day TEXT,
fillable_price_btc_per_eh_day TEXT,
fillable_target_ph TEXT,
fillable_available_ph TEXT,
suggested_bid_btc_per_eh_day TEXT,
suggested_overpay_btc_per_eh_day TEXT,
last_price_btc_per_eh_day TEXT,
total_hashrate_eh_s TEXT,
available_hashrate_eh_s TEXT,
@ -72,6 +77,11 @@ def _ensure_market_columns(conn: sqlite3.Connection) -> None:
desired = {
"best_bid_btc_per_eh_day": "TEXT",
"best_ask_btc_per_eh_day": "TEXT",
"fillable_price_btc_per_eh_day": "TEXT",
"fillable_target_ph": "TEXT",
"fillable_available_ph": "TEXT",
"suggested_bid_btc_per_eh_day": "TEXT",
"suggested_overpay_btc_per_eh_day": "TEXT",
"last_price_btc_per_eh_day": "TEXT",
"total_hashrate_eh_s": "TEXT",
"status": "TEXT",
@ -107,10 +117,12 @@ def save_market_snapshot(conn: sqlite3.Connection, snapshot: MarketSnapshot) ->
"""
INSERT INTO market_snapshots (
timestamp_utc, best_price_btc_per_eh_day, best_bid_btc_per_eh_day,
best_ask_btc_per_eh_day, last_price_btc_per_eh_day,
best_ask_btc_per_eh_day, fillable_price_btc_per_eh_day,
fillable_target_ph, fillable_available_ph, suggested_bid_btc_per_eh_day,
suggested_overpay_btc_per_eh_day, last_price_btc_per_eh_day,
total_hashrate_eh_s, available_hashrate_eh_s, status, source
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
snapshot.timestamp_utc,
@ -123,6 +135,21 @@ def save_market_snapshot(conn: sqlite3.Connection, snapshot: MarketSnapshot) ->
str(snapshot.best_ask_btc_per_eh_day)
if snapshot.best_ask_btc_per_eh_day is not None
else None,
str(snapshot.fillable_price_btc_per_eh_day)
if snapshot.fillable_price_btc_per_eh_day is not None
else None,
str(snapshot.fillable_target_ph)
if snapshot.fillable_target_ph is not None
else None,
str(snapshot.fillable_available_ph)
if snapshot.fillable_available_ph is not None
else None,
str(snapshot.suggested_bid_btc_per_eh_day)
if snapshot.suggested_bid_btc_per_eh_day is not None
else None,
str(snapshot.suggested_overpay_btc_per_eh_day)
if snapshot.suggested_overpay_btc_per_eh_day is not None
else None,
str(snapshot.last_price_btc_per_eh_day)
if snapshot.last_price_btc_per_eh_day is not None
else None,
@ -167,7 +194,9 @@ def latest_market_snapshot(conn: sqlite3.Connection) -> MarketSnapshot | None:
row = conn.execute(
"""
SELECT timestamp_utc, best_price_btc_per_eh_day, best_bid_btc_per_eh_day,
best_ask_btc_per_eh_day, last_price_btc_per_eh_day,
best_ask_btc_per_eh_day, fillable_price_btc_per_eh_day,
fillable_target_ph, fillable_available_ph, suggested_bid_btc_per_eh_day,
suggested_overpay_btc_per_eh_day, last_price_btc_per_eh_day,
total_hashrate_eh_s, available_hashrate_eh_s, status, source
FROM market_snapshots
ORDER BY id DESC
@ -183,11 +212,16 @@ def latest_market_snapshot(conn: sqlite3.Connection) -> MarketSnapshot | None:
best_price_btc_per_eh_day=Decimal(row[1]) if row[1] else None,
best_bid_btc_per_eh_day=Decimal(row[2]) if row[2] else None,
best_ask_btc_per_eh_day=Decimal(row[3]) if row[3] else None,
last_price_btc_per_eh_day=Decimal(row[4]) if row[4] else None,
total_hashrate_eh_s=Decimal(row[5]) if row[5] else None,
available_hashrate_eh_s=Decimal(row[6]) if row[6] else None,
status=row[7],
source=row[8],
fillable_price_btc_per_eh_day=Decimal(row[4]) if row[4] else None,
fillable_target_ph=Decimal(row[5]) if row[5] else None,
fillable_available_ph=Decimal(row[6]) if row[6] else None,
suggested_bid_btc_per_eh_day=Decimal(row[7]) if row[7] else None,
suggested_overpay_btc_per_eh_day=Decimal(row[8]) if row[8] else None,
last_price_btc_per_eh_day=Decimal(row[9]) if row[9] else None,
total_hashrate_eh_s=Decimal(row[10]) if row[10] else None,
available_hashrate_eh_s=Decimal(row[11]) if row[11] else None,
status=row[12],
source=row[13],
)

View file

@ -1,11 +1,11 @@
from decimal import Decimal
import unittest
from braiins_ratchet.braiins import market_snapshot_from_public_api
from braiins_ratchet.braiins import fillable_ask_for_target, market_snapshot_from_public_api
class BraiinsPublicParserTests(unittest.TestCase):
def test_public_snapshot_uses_best_ask_as_buy_reference(self) -> None:
def test_public_snapshot_uses_depth_aware_suggested_bid_as_buy_reference(self) -> None:
stats = {
"status": "SPOT_INSTRUMENT_STATUS_ACTIVE",
"last_avg_price_sat": 31000000,
@ -14,18 +14,27 @@ class BraiinsPublicParserTests(unittest.TestCase):
}
orderbook = {
"bids": [{"price_sat": 29000000}, {"price_sat": 28000000}],
"asks": [{"price_sat": 30000000}, {"price_sat": 32000000}],
"asks": [
{"price_sat": 30000000, "hash_rate_available_ph": 4},
{"price_sat": 32000000, "hash_rate_available_ph": 10},
],
}
snapshot = market_snapshot_from_public_api(
stats,
orderbook,
timestamp_utc="2026-04-25T12:00:00+00:00",
target_ph=Decimal("10"),
overpay_btc_per_eh_day=Decimal("0.01"),
)
self.assertEqual(snapshot.best_price_btc_per_eh_day, Decimal("0.3"))
self.assertEqual(snapshot.best_price_btc_per_eh_day, Decimal("0.33"))
self.assertEqual(snapshot.best_bid_btc_per_eh_day, Decimal("0.29"))
self.assertEqual(snapshot.best_ask_btc_per_eh_day, Decimal("0.3"))
self.assertEqual(snapshot.fillable_price_btc_per_eh_day, Decimal("0.32"))
self.assertEqual(snapshot.fillable_target_ph, Decimal("10"))
self.assertEqual(snapshot.fillable_available_ph, Decimal("14"))
self.assertEqual(snapshot.suggested_bid_btc_per_eh_day, Decimal("0.33"))
self.assertEqual(snapshot.last_price_btc_per_eh_day, Decimal("0.31"))
self.assertEqual(snapshot.total_hashrate_eh_s, Decimal("0.25"))
self.assertEqual(snapshot.available_hashrate_eh_s, Decimal("0.21"))
@ -42,6 +51,38 @@ class BraiinsPublicParserTests(unittest.TestCase):
self.assertEqual(snapshot.best_bid_btc_per_eh_day, Decimal("0.29"))
self.assertIsNone(snapshot.best_ask_btc_per_eh_day)
def test_fillable_ask_uses_limit_minus_used_when_available_absent(self) -> None:
depth = fillable_ask_for_target(
[
{"price_sat": 30000000, "hash_rate_limit_ph": 5, "hash_rate_matched_ph": 3},
{"price_sat": 31000000, "hash_rate_limit_ph": 8, "hash_rate_matched_ph": 1},
],
Decimal("6"),
)
self.assertEqual(depth.price_btc_per_eh_day, Decimal("0.31"))
self.assertEqual(depth.available_ph, Decimal("9"))
def test_fillable_ask_supports_public_camel_case_orderbook(self) -> None:
depth = fillable_ask_for_target(
[
{
"price_sat": 46090000,
"hashRateAvailable": 106,
"hashRateMatched": 106,
},
{
"price_sat": 47486000,
"hashRateAvailable": 576,
"hashRateMatched": 182,
},
],
Decimal("10"),
)
self.assertEqual(depth.price_btc_per_eh_day, Decimal("0.47486"))
self.assertEqual(depth.available_ph, Decimal("394"))
if __name__ == "__main__":
unittest.main()

View file

@ -57,6 +57,8 @@ def _config() -> AppConfig:
target_duration_minutes=180,
target_spend_btc=Decimal("0.00010"),
risk_lambda=Decimal("0.35"),
shadow_target_ph=Decimal("10"),
shadow_overpay_btc_per_eh_day=Decimal("0.01"),
),
)

View file

@ -17,6 +17,11 @@ class StorageTests(unittest.TestCase):
best_price_btc_per_eh_day=Decimal("0.30"),
best_bid_btc_per_eh_day=Decimal("0.29"),
best_ask_btc_per_eh_day=Decimal("0.30"),
fillable_price_btc_per_eh_day=Decimal("0.32"),
fillable_target_ph=Decimal("10"),
fillable_available_ph=Decimal("14"),
suggested_bid_btc_per_eh_day=Decimal("0.33"),
suggested_overpay_btc_per_eh_day=Decimal("0.01"),
last_price_btc_per_eh_day=Decimal("0.31"),
total_hashrate_eh_s=Decimal("0.25"),
available_hashrate_eh_s=Decimal("0.21"),
@ -31,6 +36,11 @@ class StorageTests(unittest.TestCase):
self.assertEqual(snapshot.best_price_btc_per_eh_day, Decimal("0.30"))
self.assertEqual(snapshot.best_bid_btc_per_eh_day, Decimal("0.29"))
self.assertEqual(snapshot.best_ask_btc_per_eh_day, Decimal("0.30"))
self.assertEqual(snapshot.fillable_price_btc_per_eh_day, Decimal("0.32"))
self.assertEqual(snapshot.fillable_target_ph, Decimal("10"))
self.assertEqual(snapshot.fillable_available_ph, Decimal("14"))
self.assertEqual(snapshot.suggested_bid_btc_per_eh_day, Decimal("0.33"))
self.assertEqual(snapshot.suggested_overpay_btc_per_eh_day, Decimal("0.01"))
self.assertEqual(snapshot.last_price_btc_per_eh_day, Decimal("0.31"))
self.assertEqual(snapshot.total_hashrate_eh_s, Decimal("0.25"))
self.assertEqual(snapshot.available_hashrate_eh_s, Decimal("0.21"))

View file

@ -70,6 +70,8 @@ def _config() -> AppConfig:
target_duration_minutes=180,
target_spend_btc=Decimal("0.00010"),
risk_lambda=Decimal("0.35"),
shadow_target_ph=Decimal("10"),
shadow_overpay_btc_per_eh_day=Decimal("0.01"),
),
)