mirror of
https://github.com/saymrwulf/BraiinsRatchet.git
synced 2026-05-14 20:37:52 +00:00
Add public Braiins market collector
This commit is contained in:
parent
274b89e7f3
commit
e6f001b980
11 changed files with 468 additions and 7 deletions
23
README.md
23
README.md
|
|
@ -17,6 +17,7 @@ The first implementation is deliberately conservative:
|
|||
python3 -m venv .venv
|
||||
./.venv/bin/python -m braiins_ratchet.cli init-db
|
||||
./.venv/bin/python -m braiins_ratchet.cli collect-ocean
|
||||
./.venv/bin/python -m braiins_ratchet.cli collect-braiins-public
|
||||
./.venv/bin/python -m braiins_ratchet.cli evaluate
|
||||
```
|
||||
|
||||
|
|
@ -44,6 +45,28 @@ The JSON shape is:
|
|||
}
|
||||
```
|
||||
|
||||
## Public Braiins Market Data
|
||||
|
||||
The collector first uses unauthenticated public web endpoints from `hashpower.braiins.com`; no token is needed for live price action. See `docs/BRAIINS_PUBLIC_MARKET.md`.
|
||||
|
||||
Watcher-only tokens are only relevant if we later need account-specific read-only data such as your private balance, historical fills, or order status. Owner tokens remain out of scope.
|
||||
|
||||
## Documentation
|
||||
|
||||
- `PROGRAM.md`: research charter and ratchet rules.
|
||||
- `SECURITY.md`: token, computer, and trading safety guardrails.
|
||||
- `docs/BRAIINS_PUBLIC_MARKET.md`: public market collector behavior.
|
||||
- `docs/RATCHET_OPERATIONS.md`: day-to-day monitor cycle.
|
||||
- `docs/CLI_REFERENCE.md`: command reference and test command.
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src ./.venv/bin/python -m unittest discover -s tests
|
||||
```
|
||||
|
||||
The tests are network-free and use fixtures for public Braiins parsing. Live collectors are intentionally separate operational checks.
|
||||
|
||||
## Guardrail Model
|
||||
|
||||
`braiins_ratchet.strategy` can propose a manual bid, but `braiins_ratchet.guardrails` decides whether that proposal is admissible. The executor layer currently has no write-capable Braiins methods. If live execution is ever added, it must be a separate reviewed change and remain disabled by default.
|
||||
|
|
|
|||
50
docs/BRAIINS_PUBLIC_MARKET.md
Normal file
50
docs/BRAIINS_PUBLIC_MARKET.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# Braiins Public Market Collector
|
||||
|
||||
The public Braiins Hashpower page loads market data from unauthenticated browser endpoints under:
|
||||
|
||||
```text
|
||||
https://hashpower.braiins.com/webapi
|
||||
```
|
||||
|
||||
The monitor-only collector currently reads:
|
||||
|
||||
- `/spot/stats`: market status, last average price, total available hashrate, matched hashrate.
|
||||
- `/orderbook`: public bids and asks.
|
||||
|
||||
No token is required for these calls. The collector does not send an `apikey` header and only performs HTTP `GET`.
|
||||
|
||||
## Price Selection
|
||||
|
||||
For a buyer, the conservative live reference price is:
|
||||
|
||||
1. Best ask, if available.
|
||||
2. Last average price, if no ask exists.
|
||||
3. Best bid, if neither ask nor last price exists.
|
||||
|
||||
The chosen value is stored as `best_price_btc_per_eh_day` for strategy evaluation. The raw fields are also stored:
|
||||
|
||||
- `best_bid_btc_per_eh_day`
|
||||
- `best_ask_btc_per_eh_day`
|
||||
- `last_price_btc_per_eh_day`
|
||||
- `total_hashrate_eh_s`
|
||||
- `available_hashrate_eh_s`
|
||||
- `status`
|
||||
|
||||
## Commands
|
||||
|
||||
Collect one public Braiins snapshot:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli collect-braiins-public
|
||||
```
|
||||
|
||||
Then evaluate the current strategy:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli evaluate
|
||||
```
|
||||
|
||||
## Failure Mode
|
||||
|
||||
These endpoints are discovered from the public web app, not from a stable public API contract. If Braiins changes field names or requires authentication later, tests and collection should fail visibly. Do not add an owner token to recover functionality.
|
||||
|
||||
88
docs/CLI_REFERENCE.md
Normal file
88
docs/CLI_REFERENCE.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# CLI Reference
|
||||
|
||||
All commands should be run from the repository root.
|
||||
|
||||
Use the local virtual environment:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli <command>
|
||||
```
|
||||
|
||||
## `init-db`
|
||||
|
||||
Creates `data/ratchet.sqlite` if it does not exist.
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli init-db
|
||||
```
|
||||
|
||||
## `collect-ocean`
|
||||
|
||||
Fetches one OCEAN dashboard snapshot and stores:
|
||||
|
||||
- pool hashrate
|
||||
- network difficulty
|
||||
- share-log window
|
||||
- estimated OCEAN block time
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli collect-ocean
|
||||
```
|
||||
|
||||
## `collect-braiins-public`
|
||||
|
||||
Fetches one token-free Braiins public market snapshot and stores:
|
||||
|
||||
- best bid
|
||||
- best ask
|
||||
- last average price
|
||||
- market status
|
||||
- total hashrate
|
||||
- available hashrate
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli collect-braiins-public
|
||||
```
|
||||
|
||||
Override the public base URL only for testing:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli collect-braiins-public --base-url https://hashpower.braiins.com/webapi
|
||||
```
|
||||
|
||||
## `import-market`
|
||||
|
||||
Imports a manual market JSON snapshot. Use this when public Braiins endpoints are unavailable.
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli import-market examples/market_snapshot.example.json
|
||||
```
|
||||
|
||||
## `evaluate`
|
||||
|
||||
Evaluates the latest stored OCEAN and Braiins snapshots against `config.example.toml`.
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli evaluate
|
||||
```
|
||||
|
||||
The command returns `observe` or `manual_bid`. It never places an order.
|
||||
|
||||
## `guardrails`
|
||||
|
||||
Prints the active guardrails.
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli guardrails
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
Run all tests:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src ./.venv/bin/python -m unittest discover -s tests
|
||||
```
|
||||
|
||||
The test suite is network-free. Live collectors are validated separately by explicitly running their commands.
|
||||
|
||||
36
docs/RATCHET_OPERATIONS.md
Normal file
36
docs/RATCHET_OPERATIONS.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Ratchet Operations
|
||||
|
||||
## Normal Monitor Cycle
|
||||
|
||||
Run these commands from the repository root:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli collect-ocean
|
||||
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli collect-braiins-public
|
||||
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli evaluate
|
||||
```
|
||||
|
||||
The result is a recommendation only. `manual_bid` means the strategy thinks a manually placed bid clears the configured guardrails. `observe` means no action is recommended.
|
||||
|
||||
## Manual Market Snapshot
|
||||
|
||||
If Braiins public endpoints are unavailable, use:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli import-market examples/market_snapshot.example.json
|
||||
```
|
||||
|
||||
Edit a copy of the example JSON with values read from the Braiins UI. Do not put secrets in that file.
|
||||
|
||||
## Interpreting The Proposal
|
||||
|
||||
- `breakeven_btc_per_eh_day`: estimated expected mining value at current OCEAN/network inputs.
|
||||
- `expected_reward_btc`: expected OCEAN reward before subtracting Braiins spend.
|
||||
- `expected_net_btc`: expected reward minus Braiins spend.
|
||||
- `score_btc`: risk-adjusted score after applying the configured penalty.
|
||||
- `maturity_note`: how long to wait before treating a canary result as mature under the TIDES window.
|
||||
|
||||
## Safety
|
||||
|
||||
The program never places orders. Manual execution remains outside the repo and should use the Braiins UI, not this code.
|
||||
|
||||
|
|
@ -1,6 +1,11 @@
|
|||
{
|
||||
"timestamp_utc": "2026-04-25T12:00:00+00:00",
|
||||
"best_price_btc_per_eh_day": "0.30",
|
||||
"best_bid_btc_per_eh_day": "0.29",
|
||||
"best_ask_btc_per_eh_day": "0.30",
|
||||
"last_price_btc_per_eh_day": "0.31",
|
||||
"total_hashrate_eh_s": "0.25",
|
||||
"available_hashrate_eh_s": "0.10",
|
||||
"status": "SPOT_INSTRUMENT_STATUS_ACTIVE",
|
||||
"source": "manual-example"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from datetime import UTC, datetime
|
|||
from decimal import Decimal
|
||||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
from .guardrails import token_looks_unsafe
|
||||
|
|
@ -15,6 +16,36 @@ class BraiinsSafetyError(RuntimeError):
|
|||
pass
|
||||
|
||||
|
||||
SATOSHIS_PER_BTC = Decimal("100000000")
|
||||
PH_PER_EH = Decimal("1000")
|
||||
DEFAULT_PUBLIC_BASE = "https://hashpower.braiins.com/webapi"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BraiinsPublicClient:
|
||||
api_base: str = DEFAULT_PUBLIC_BASE
|
||||
|
||||
def get_json(self, path: str) -> object:
|
||||
if not path.startswith("/"):
|
||||
raise BraiinsSafetyError("API path must start with /")
|
||||
request = Request(
|
||||
f"{self.api_base.rstrip('/')}{path}",
|
||||
headers={"User-Agent": "BraiinsRatchet/0.1 public-market"},
|
||||
method="GET",
|
||||
)
|
||||
with urlopen(request, timeout=15) as response:
|
||||
return json.loads(response.read().decode("utf-8"))
|
||||
|
||||
def fetch_market_snapshot(self) -> 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)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BraiinsWatcherClient:
|
||||
api_base: str
|
||||
|
|
@ -48,7 +79,8 @@ class BraiinsWatcherClient:
|
|||
|
||||
|
||||
def market_snapshot_from_json_file(path: str) -> MarketSnapshot:
|
||||
raw = json.loads(open(path, "r", encoding="utf-8").read())
|
||||
with open(path, "r", encoding="utf-8") as handle:
|
||||
raw = json.loads(handle.read())
|
||||
return MarketSnapshot(
|
||||
timestamp_utc=str(raw.get("timestamp_utc") or datetime.now(UTC).isoformat(timespec="seconds")),
|
||||
best_price_btc_per_eh_day=(
|
||||
|
|
@ -56,10 +88,82 @@ def market_snapshot_from_json_file(path: str) -> MarketSnapshot:
|
|||
if raw.get("best_price_btc_per_eh_day") is not None
|
||||
else None
|
||||
),
|
||||
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")),
|
||||
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=(
|
||||
Decimal(str(raw["available_hashrate_eh_s"]))
|
||||
if raw.get("available_hashrate_eh_s") is not None
|
||||
else None
|
||||
),
|
||||
status=str(raw["status"]) if raw.get("status") is not None else None,
|
||||
source=str(raw.get("source") or path),
|
||||
)
|
||||
|
||||
|
||||
def market_snapshot_from_public_api(
|
||||
stats: dict[str, Any],
|
||||
orderbook: dict[str, Any],
|
||||
timestamp_utc: str | None = None,
|
||||
) -> MarketSnapshot:
|
||||
best_bid = _best_price_from_orders(orderbook.get("bids"), prefer="max")
|
||||
best_ask = _best_price_from_orders(orderbook.get("asks"), prefer="min")
|
||||
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"))
|
||||
|
||||
available_hashrate = None
|
||||
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
|
||||
|
||||
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,
|
||||
last_price_btc_per_eh_day=last_price,
|
||||
total_hashrate_eh_s=total_hashrate,
|
||||
available_hashrate_eh_s=available_hashrate,
|
||||
status=str(stats["status"]) if stats.get("status") is not None else None,
|
||||
source="braiins-public",
|
||||
)
|
||||
|
||||
|
||||
def _best_price_from_orders(orders: object, prefer: str) -> Decimal | None:
|
||||
if not isinstance(orders, list):
|
||||
return None
|
||||
prices = [
|
||||
price
|
||||
for order in orders
|
||||
if isinstance(order, dict)
|
||||
for price in [_sat_to_btc(order.get("price_sat"))]
|
||||
if price is not None
|
||||
]
|
||||
if not prices:
|
||||
return None
|
||||
if prefer == "min":
|
||||
return min(prices)
|
||||
return max(prices)
|
||||
|
||||
|
||||
def _sat_to_btc(value: object) -> Decimal | None:
|
||||
decimal = _optional_decimal(value)
|
||||
if decimal is None:
|
||||
return None
|
||||
return decimal / SATOSHIS_PER_BTC
|
||||
|
||||
|
||||
def _ph_to_eh(value: object) -> Decimal | None:
|
||||
decimal = _optional_decimal(value)
|
||||
if decimal is None:
|
||||
return None
|
||||
return decimal / PH_PER_EH
|
||||
|
||||
|
||||
def _optional_decimal(value: object) -> Decimal | None:
|
||||
if value is None:
|
||||
return None
|
||||
return Decimal(str(value))
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import json
|
|||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from .braiins import market_snapshot_from_json_file
|
||||
from .braiins import BraiinsPublicClient, market_snapshot_from_json_file
|
||||
from .config import load_config
|
||||
from .ocean import fetch_snapshot
|
||||
from .storage import (
|
||||
|
|
@ -46,6 +46,16 @@ def cmd_import_market(args: argparse.Namespace) -> int:
|
|||
return 0
|
||||
|
||||
|
||||
def cmd_collect_braiins_public(args: argparse.Namespace) -> int:
|
||||
client = BraiinsPublicClient(api_base=args.base_url.rstrip("/"))
|
||||
snapshot = client.fetch_market_snapshot()
|
||||
with connect() as conn:
|
||||
init_db(conn)
|
||||
save_market_snapshot(conn, snapshot)
|
||||
print(json.dumps(snapshot.__dict__, default=str, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_evaluate(args: argparse.Namespace) -> int:
|
||||
config = load_config(Path(args.config) if args.config else None)
|
||||
with connect() as conn:
|
||||
|
|
@ -86,6 +96,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
market.add_argument("path")
|
||||
market.set_defaults(func=cmd_import_market)
|
||||
|
||||
braiins = sub.add_parser(
|
||||
"collect-braiins-public",
|
||||
help="collect one unauthenticated Braiins public market snapshot",
|
||||
)
|
||||
braiins.add_argument("--base-url", default="https://hashpower.braiins.com/webapi")
|
||||
braiins.set_defaults(func=cmd_collect_braiins_public)
|
||||
|
||||
evaluate = sub.add_parser("evaluate", help="emit monitor-only strategy recommendation")
|
||||
evaluate.add_argument("--config")
|
||||
evaluate.set_defaults(func=cmd_evaluate)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,12 @@ class OceanSnapshot:
|
|||
class MarketSnapshot:
|
||||
timestamp_utc: str
|
||||
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
|
||||
last_price_btc_per_eh_day: Decimal | None = None
|
||||
total_hashrate_eh_s: Decimal | None = None
|
||||
available_hashrate_eh_s: Decimal | None = None
|
||||
status: str | None = None
|
||||
source: str = "manual"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,12 @@ def init_db(conn: sqlite3.Connection) -> None:
|
|||
id INTEGER PRIMARY KEY,
|
||||
timestamp_utc TEXT NOT NULL,
|
||||
best_price_btc_per_eh_day TEXT,
|
||||
best_bid_btc_per_eh_day TEXT,
|
||||
best_ask_btc_per_eh_day TEXT,
|
||||
last_price_btc_per_eh_day TEXT,
|
||||
total_hashrate_eh_s TEXT,
|
||||
available_hashrate_eh_s TEXT,
|
||||
status TEXT,
|
||||
source TEXT NOT NULL
|
||||
);
|
||||
|
||||
|
|
@ -55,9 +60,27 @@ def init_db(conn: sqlite3.Connection) -> None:
|
|||
);
|
||||
"""
|
||||
)
|
||||
_ensure_market_columns(conn)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def _ensure_market_columns(conn: sqlite3.Connection) -> None:
|
||||
existing = {
|
||||
row[1]
|
||||
for row in conn.execute("PRAGMA table_info(market_snapshots)").fetchall()
|
||||
}
|
||||
desired = {
|
||||
"best_bid_btc_per_eh_day": "TEXT",
|
||||
"best_ask_btc_per_eh_day": "TEXT",
|
||||
"last_price_btc_per_eh_day": "TEXT",
|
||||
"total_hashrate_eh_s": "TEXT",
|
||||
"status": "TEXT",
|
||||
}
|
||||
for column, column_type in desired.items():
|
||||
if column not in existing:
|
||||
conn.execute(f"ALTER TABLE market_snapshots ADD COLUMN {column} {column_type}")
|
||||
|
||||
|
||||
def save_ocean_snapshot(conn: sqlite3.Connection, snapshot: OceanSnapshot) -> None:
|
||||
conn.execute(
|
||||
"""
|
||||
|
|
@ -83,18 +106,33 @@ def save_market_snapshot(conn: sqlite3.Connection, snapshot: MarketSnapshot) ->
|
|||
conn.execute(
|
||||
"""
|
||||
INSERT INTO market_snapshots (
|
||||
timestamp_utc, best_price_btc_per_eh_day, available_hashrate_eh_s, source
|
||||
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,
|
||||
total_hashrate_eh_s, available_hashrate_eh_s, status, source
|
||||
)
|
||||
VALUES (?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
snapshot.timestamp_utc,
|
||||
str(snapshot.best_price_btc_per_eh_day)
|
||||
if snapshot.best_price_btc_per_eh_day is not None
|
||||
else None,
|
||||
str(snapshot.best_bid_btc_per_eh_day)
|
||||
if snapshot.best_bid_btc_per_eh_day is not None
|
||||
else None,
|
||||
str(snapshot.best_ask_btc_per_eh_day)
|
||||
if snapshot.best_ask_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,
|
||||
str(snapshot.total_hashrate_eh_s)
|
||||
if snapshot.total_hashrate_eh_s is not None
|
||||
else None,
|
||||
str(snapshot.available_hashrate_eh_s)
|
||||
if snapshot.available_hashrate_eh_s is not None
|
||||
else None,
|
||||
snapshot.status,
|
||||
snapshot.source,
|
||||
),
|
||||
)
|
||||
|
|
@ -128,7 +166,9 @@ def latest_ocean_snapshot(conn: sqlite3.Connection) -> OceanSnapshot | None:
|
|||
def latest_market_snapshot(conn: sqlite3.Connection) -> MarketSnapshot | None:
|
||||
row = conn.execute(
|
||||
"""
|
||||
SELECT timestamp_utc, best_price_btc_per_eh_day, available_hashrate_eh_s, source
|
||||
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,
|
||||
total_hashrate_eh_s, available_hashrate_eh_s, status, source
|
||||
FROM market_snapshots
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
|
|
@ -141,8 +181,13 @@ def latest_market_snapshot(conn: sqlite3.Connection) -> MarketSnapshot | None:
|
|||
return MarketSnapshot(
|
||||
timestamp_utc=row[0],
|
||||
best_price_btc_per_eh_day=Decimal(row[1]) if row[1] else None,
|
||||
available_hashrate_eh_s=Decimal(row[2]) if row[2] else None,
|
||||
source=row[3],
|
||||
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],
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
47
tests/test_braiins_public.py
Normal file
47
tests/test_braiins_public.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
from decimal import Decimal
|
||||
import unittest
|
||||
|
||||
from braiins_ratchet.braiins import market_snapshot_from_public_api
|
||||
|
||||
|
||||
class BraiinsPublicParserTests(unittest.TestCase):
|
||||
def test_public_snapshot_uses_best_ask_as_buy_reference(self) -> None:
|
||||
stats = {
|
||||
"status": "SPOT_INSTRUMENT_STATUS_ACTIVE",
|
||||
"last_avg_price_sat": 31000000,
|
||||
"hash_rate_available_10m_ph": 250,
|
||||
"hash_rate_matched_10m_ph": 40,
|
||||
}
|
||||
orderbook = {
|
||||
"bids": [{"price_sat": 29000000}, {"price_sat": 28000000}],
|
||||
"asks": [{"price_sat": 30000000}, {"price_sat": 32000000}],
|
||||
}
|
||||
|
||||
snapshot = market_snapshot_from_public_api(
|
||||
stats,
|
||||
orderbook,
|
||||
timestamp_utc="2026-04-25T12:00:00+00:00",
|
||||
)
|
||||
|
||||
self.assertEqual(snapshot.best_price_btc_per_eh_day, Decimal("0.3"))
|
||||
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.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"))
|
||||
self.assertEqual(snapshot.status, "SPOT_INSTRUMENT_STATUS_ACTIVE")
|
||||
|
||||
def test_public_snapshot_falls_back_to_last_price_without_asks(self) -> None:
|
||||
snapshot = market_snapshot_from_public_api(
|
||||
{"last_avg_price_sat": 31000000},
|
||||
{"bids": [{"price_sat": 29000000}], "asks": []},
|
||||
timestamp_utc="2026-04-25T12:00:00+00:00",
|
||||
)
|
||||
|
||||
self.assertEqual(snapshot.best_price_btc_per_eh_day, Decimal("0.31"))
|
||||
self.assertEqual(snapshot.best_bid_btc_per_eh_day, Decimal("0.29"))
|
||||
self.assertIsNone(snapshot.best_ask_btc_per_eh_day)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
41
tests/test_storage.py
Normal file
41
tests/test_storage.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
from decimal import Decimal
|
||||
import sqlite3
|
||||
import unittest
|
||||
|
||||
from braiins_ratchet.models import MarketSnapshot
|
||||
from braiins_ratchet.storage import init_db, latest_market_snapshot, save_market_snapshot
|
||||
|
||||
|
||||
class StorageTests(unittest.TestCase):
|
||||
def test_market_snapshot_round_trip_extended_fields(self) -> None:
|
||||
conn = sqlite3.connect(":memory:")
|
||||
init_db(conn)
|
||||
save_market_snapshot(
|
||||
conn,
|
||||
MarketSnapshot(
|
||||
timestamp_utc="2026-04-25T12:00:00+00:00",
|
||||
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"),
|
||||
last_price_btc_per_eh_day=Decimal("0.31"),
|
||||
total_hashrate_eh_s=Decimal("0.25"),
|
||||
available_hashrate_eh_s=Decimal("0.21"),
|
||||
status="SPOT_INSTRUMENT_STATUS_ACTIVE",
|
||||
source="test",
|
||||
),
|
||||
)
|
||||
|
||||
snapshot = latest_market_snapshot(conn)
|
||||
self.assertIsNotNone(snapshot)
|
||||
assert snapshot is not None
|
||||
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.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"))
|
||||
self.assertEqual(snapshot.status, "SPOT_INSTRUMENT_STATUS_ACTIVE")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Reference in a new issue