Initial monitor-only ratchet scaffold

This commit is contained in:
saymrwulf 2026-04-25 14:20:15 +02:00
commit 274b89e7f3
24 changed files with 1092 additions and 0 deletions

4
.env.example Normal file
View file

@ -0,0 +1,4 @@
# Copy to .env for local shell use if desired. Never commit .env.
BRAIINS_WATCHER_TOKEN=
BRAIINS_API_BASE=
OCEAN_DASHBOARD_URL=https://ocean.xyz/dashboard

10
.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
.venv/
__pycache__/
*.py[cod]
.DS_Store
.env
data/*.sqlite
data/*.sqlite-shm
data/*.sqlite-wal
data/raw/
*.log

44
PROGRAM.md Normal file
View file

@ -0,0 +1,44 @@
# Program Charter
## Goal
Maximize expected BTC profit, or minimize BTC loss, for manually buying BTC hashpower on Braiins and pointing it at OCEAN/DATUM.
## Operating Premises
- OCEAN uses TIDES: shares are paid only if they are inside the current share-log when OCEAN finds a block.
- OCEAN block discovery is stochastic and memoryless. A recent drought is not evidence that OCEAN is "due".
- The useful edge is expected value versus Braiins market price, plus operational quality and timing around observable fee/reward conditions.
- The system should improve through repeated small experiments, not through one large theoretical bet.
## Ratchet Loop
1. Collect read-only snapshots from OCEAN, Braiins, local DATUM/Knots where available, and manual experiment results.
2. Score candidate policies against current guardrails and historical/paper-trade results.
3. Emit a manual recommendation.
4. If the recommendation is executed manually, record the exact order parameters and later realized rewards.
5. Keep changes to `strategy.py` only when they improve the measured score under comparable risk.
## Hard Guardrails
- No code path places, modifies, or cancels Braiins orders.
- No owner token may be stored, loaded, or requested by the code.
- Watcher-only Braiins token may be provided via environment variable only.
- No secrets in Git.
- No containers or VMs.
- Runtime files stay inside this repository.
- Default branch name is `master`.
- Production-like behavior starts with monitor-only and paper trading.
- First live canary, if manually executed, should use minimum viable spend only.
## Initial Scoring Metric
Use BTC-denominated expected value:
```text
expected_net_btc = expected_ocean_rewards_after_fee - braiins_cost_btc
score = expected_net_btc - risk_penalty_btc - execution_penalty_btc
```
The strategy must show break-even price, discount to break-even, spend, duration, and maturity assumptions.

53
README.md Normal file
View file

@ -0,0 +1,53 @@
# Braiins Ratchet
Monitor-only research scaffold for optimizing a manual "buy hashpower on Braiins, mine through OCEAN" strategy.
The first implementation is deliberately conservative:
- The code never places, modifies, or cancels Braiins orders.
- The default strategy emits recommendations only.
- The Braiins integration accepts a watcher-only token only.
- All mutable runtime state stays inside this repository under `data/`.
- The Git branch is `master`.
- The project uses Python standard library only.
## Quick Start
```bash
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 evaluate
```
Set `PYTHONPATH=src` if running without installation:
```bash
PYTHONPATH=src python3 -m braiins_ratchet.cli evaluate
```
Import a manual Braiins market snapshot:
```bash
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli import-market examples/market_snapshot.example.json
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli evaluate
```
The JSON shape is:
```json
{
"timestamp_utc": "2026-04-25T12:00:00+00:00",
"best_price_btc_per_eh_day": "0.30",
"available_hashrate_eh_s": "0.10",
"source": "manual"
}
```
## 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.
## Data Maturity
OCEAN's TIDES payout model means a canary experiment should not be scored immediately after spend completion. A spend should be treated as immature until its shares have had time to age through the pool's share-log window. The strategy therefore records both expected value and maturity notes.

25
SECURITY.md Normal file
View file

@ -0,0 +1,25 @@
# Security And Safety
This repository is designed to be monitor-only.
## Token Policy
- `BRAIINS_WATCHER_TOKEN` may be used for read-only API calls.
- Owner/admin Braiins tokens must never be passed to this code.
- `.env` is ignored by Git.
- The CLI refuses token values containing common owner/admin labels.
## Computer Safety
- No containers.
- No VMs.
- No writes outside this repository.
- No shelling out from package code.
- No destructive filesystem operations.
## Trading Safety
- Recommendations are not orders.
- Manual execution remains outside the program.
- Any future live executor must be reviewed as a separate change and disabled by default.

23
config.example.toml Normal file
View file

@ -0,0 +1,23 @@
[capital]
available_btc = "0.01638650"
[ocean]
fee_rate = "0.01"
block_subsidy_btc = "3.125"
default_tx_fees_btc = "0.05"
dashboard_url = "https://ocean.xyz/dashboard"
[guardrails]
max_manual_order_btc = "0.00025"
max_daily_spend_btc = "0.00050"
max_price_btc_per_eh_day = "0.42"
min_discount_to_breakeven = "0.08"
min_duration_minutes = 30
max_duration_minutes = 720
recommend_only = true
[strategy]
target_duration_minutes = 180
target_spend_btc = "0.00010"
risk_lambda = "0.35"

1
data/.gitkeep Normal file
View file

@ -0,0 +1 @@

View file

@ -0,0 +1,6 @@
{
"timestamp_utc": "2026-04-25T12:00:00+00:00",
"best_price_btc_per_eh_day": "0.30",
"available_hashrate_eh_s": "0.10",
"source": "manual-example"
}

17
pyproject.toml Normal file
View file

@ -0,0 +1,17 @@
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "braiins-ratchet"
version = "0.1.0"
description = "Monitor-only ratchet strategy tooling for Braiins Hashpower and OCEAN mining."
requires-python = ">=3.11"
dependencies = []
[project.scripts]
braiins-ratchet = "braiins_ratchet.cli:main"
[tool.setuptools.packages.find]
where = ["src"]

1
results.tsv Normal file
View file

@ -0,0 +1 @@
timestamp_utc experiment_id mode proposal action price_btc_per_eh_day spend_btc duration_minutes expected_reward_btc expected_net_btc score_btc status notes
1 timestamp_utc experiment_id mode proposal action price_btc_per_eh_day spend_btc duration_minutes expected_reward_btc expected_net_btc score_btc status notes

View file

@ -0,0 +1,5 @@
"""Monitor-only ratchet tooling for Braiins Hashpower and OCEAN mining."""
__all__ = ["__version__"]
__version__ = "0.1.0"

View file

@ -0,0 +1,65 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import UTC, datetime
from decimal import Decimal
import json
import os
from urllib.request import Request, urlopen
from .guardrails import token_looks_unsafe
from .models import MarketSnapshot
class BraiinsSafetyError(RuntimeError):
pass
@dataclass(frozen=True)
class BraiinsWatcherClient:
api_base: str
watcher_token: str
@classmethod
def from_env(cls) -> "BraiinsWatcherClient":
token = os.environ.get("BRAIINS_WATCHER_TOKEN", "").strip()
api_base = os.environ.get("BRAIINS_API_BASE", "").strip().rstrip("/")
if not token:
raise BraiinsSafetyError("BRAIINS_WATCHER_TOKEN is not set")
if not api_base:
raise BraiinsSafetyError("BRAIINS_API_BASE is not set")
if token_looks_unsafe(token):
raise BraiinsSafetyError("token label looks unsafe; watcher-only token required")
return cls(api_base=api_base, watcher_token=token)
def get_json(self, path: str) -> object:
if not path.startswith("/"):
raise BraiinsSafetyError("API path must start with /")
request = Request(
f"{self.api_base}{path}",
headers={
"Authorization": f"Bearer {self.watcher_token}",
"User-Agent": "BraiinsRatchet/0.1 watcher-only",
},
method="GET",
)
with urlopen(request, timeout=15) as response:
return json.loads(response.read().decode("utf-8"))
def market_snapshot_from_json_file(path: str) -> MarketSnapshot:
raw = json.loads(open(path, "r", encoding="utf-8").read())
return MarketSnapshot(
timestamp_utc=str(raw.get("timestamp_utc") or datetime.now(UTC).isoformat(timespec="seconds")),
best_price_btc_per_eh_day=(
Decimal(str(raw["best_price_btc_per_eh_day"]))
if raw.get("best_price_btc_per_eh_day") is not None
else None
),
available_hashrate_eh_s=(
Decimal(str(raw["available_hashrate_eh_s"]))
if raw.get("available_hashrate_eh_s") is not None
else None
),
source=str(raw.get("source") or path),
)

107
src/braiins_ratchet/cli.py Normal file
View file

@ -0,0 +1,107 @@
from __future__ import annotations
import argparse
import json
from pathlib import Path
import sys
from .braiins import market_snapshot_from_json_file
from .config import load_config
from .ocean import fetch_snapshot
from .storage import (
connect,
init_db,
latest_market_snapshot,
latest_ocean_snapshot,
save_market_snapshot,
save_ocean_snapshot,
save_proposal,
)
from .strategy import propose
def cmd_init_db(_: argparse.Namespace) -> int:
with connect() as conn:
init_db(conn)
print("initialized data/ratchet.sqlite")
return 0
def cmd_collect_ocean(args: argparse.Namespace) -> int:
config = load_config(Path(args.config) if args.config else None)
snapshot = fetch_snapshot(config.ocean.dashboard_url)
with connect() as conn:
init_db(conn)
save_ocean_snapshot(conn, snapshot)
print(json.dumps(snapshot.__dict__, default=str, indent=2))
return 0
def cmd_import_market(args: argparse.Namespace) -> int:
snapshot = market_snapshot_from_json_file(args.path)
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:
init_db(conn)
proposal = propose(config, latest_ocean_snapshot(conn), latest_market_snapshot(conn))
save_proposal(conn, proposal)
print(_proposal_json(proposal))
return 0
def cmd_guardrails(args: argparse.Namespace) -> int:
config = load_config(Path(args.config) if args.config else None)
print(json.dumps(config.guardrails.__dict__, default=str, indent=2))
return 0
def _proposal_json(proposal: object) -> str:
def default(value: object) -> object:
if hasattr(value, "__dict__"):
return value.__dict__
return str(value)
return json.dumps(proposal, default=default, indent=2)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="braiins-ratchet")
sub = parser.add_subparsers(required=True)
init = sub.add_parser("init-db", help="create local SQLite database")
init.set_defaults(func=cmd_init_db)
ocean = sub.add_parser("collect-ocean", help="collect one OCEAN dashboard snapshot")
ocean.add_argument("--config")
ocean.set_defaults(func=cmd_collect_ocean)
market = sub.add_parser("import-market", help="import manual Braiins market JSON snapshot")
market.add_argument("path")
market.set_defaults(func=cmd_import_market)
evaluate = sub.add_parser("evaluate", help="emit monitor-only strategy recommendation")
evaluate.add_argument("--config")
evaluate.set_defaults(func=cmd_evaluate)
guardrails = sub.add_parser("guardrails", help="print active guardrails")
guardrails.add_argument("--config")
guardrails.set_defaults(func=cmd_guardrails)
return parser
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
return args.func(args)
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,94 @@
from __future__ import annotations
from dataclasses import dataclass
from decimal import Decimal
from pathlib import Path
import tomllib
REPO_ROOT = Path(__file__).resolve().parents[2]
DEFAULT_CONFIG_PATH = REPO_ROOT / "config.example.toml"
def _decimal(value: object, default: str) -> Decimal:
if value is None:
return Decimal(default)
return Decimal(str(value))
@dataclass(frozen=True)
class CapitalConfig:
available_btc: Decimal
@dataclass(frozen=True)
class OceanConfig:
fee_rate: Decimal
block_subsidy_btc: Decimal
default_tx_fees_btc: Decimal
dashboard_url: str
@property
def expected_block_reward_btc(self) -> Decimal:
return self.block_subsidy_btc + self.default_tx_fees_btc
@dataclass(frozen=True)
class GuardrailsConfig:
max_manual_order_btc: Decimal
max_daily_spend_btc: Decimal
max_price_btc_per_eh_day: Decimal
min_discount_to_breakeven: Decimal
min_duration_minutes: int
max_duration_minutes: int
recommend_only: bool
@dataclass(frozen=True)
class StrategyConfig:
target_duration_minutes: int
target_spend_btc: Decimal
risk_lambda: Decimal
@dataclass(frozen=True)
class AppConfig:
capital: CapitalConfig
ocean: OceanConfig
guardrails: GuardrailsConfig
strategy: StrategyConfig
def load_config(path: Path | None = None) -> AppConfig:
config_path = path or DEFAULT_CONFIG_PATH
raw = tomllib.loads(config_path.read_text(encoding="utf-8"))
capital = raw.get("capital", {})
ocean = raw.get("ocean", {})
guardrails = raw.get("guardrails", {})
strategy = raw.get("strategy", {})
return AppConfig(
capital=CapitalConfig(
available_btc=_decimal(capital.get("available_btc"), "0"),
),
ocean=OceanConfig(
fee_rate=_decimal(ocean.get("fee_rate"), "0.01"),
block_subsidy_btc=_decimal(ocean.get("block_subsidy_btc"), "3.125"),
default_tx_fees_btc=_decimal(ocean.get("default_tx_fees_btc"), "0"),
dashboard_url=str(ocean.get("dashboard_url", "https://ocean.xyz/dashboard")),
),
guardrails=GuardrailsConfig(
max_manual_order_btc=_decimal(guardrails.get("max_manual_order_btc"), "0.0001"),
max_daily_spend_btc=_decimal(guardrails.get("max_daily_spend_btc"), "0.0002"),
max_price_btc_per_eh_day=_decimal(guardrails.get("max_price_btc_per_eh_day"), "0"),
min_discount_to_breakeven=_decimal(guardrails.get("min_discount_to_breakeven"), "0.05"),
min_duration_minutes=int(guardrails.get("min_duration_minutes", 30)),
max_duration_minutes=int(guardrails.get("max_duration_minutes", 720)),
recommend_only=bool(guardrails.get("recommend_only", True)),
),
strategy=StrategyConfig(
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"),
),
)

41
src/braiins_ratchet/ev.py Normal file
View file

@ -0,0 +1,41 @@
from __future__ import annotations
from decimal import Decimal, getcontext
from .models import CandidateOrder
getcontext().prec = 28
HASHES_PER_EH = Decimal("1000000000000000000")
SECONDS_PER_DAY = Decimal("86400")
TWO_POW_32 = Decimal(2) ** Decimal(32)
def breakeven_btc_per_eh_day(
network_difficulty_t: Decimal,
expected_block_reward_btc: Decimal,
pool_fee_rate: Decimal,
) -> Decimal:
difficulty = network_difficulty_t * Decimal("1000000000000")
expected_blocks_per_eh_day = HASHES_PER_EH * SECONDS_PER_DAY / (difficulty * TWO_POW_32)
return expected_blocks_per_eh_day * expected_block_reward_btc * (Decimal("1") - pool_fee_rate)
def expected_reward_for_order(
order: CandidateOrder,
network_difficulty_t: Decimal,
expected_block_reward_btc: Decimal,
pool_fee_rate: Decimal,
) -> Decimal:
price = breakeven_btc_per_eh_day(
network_difficulty_t=network_difficulty_t,
expected_block_reward_btc=expected_block_reward_btc,
pool_fee_rate=pool_fee_rate,
)
days = Decimal(order.duration_minutes) / Decimal(1440)
return order.implied_hashrate_eh_s * days * price
def downside_penalty(expected_reward_btc: Decimal, risk_lambda: Decimal) -> Decimal:
# Mining rewards are lumpy. Penalize exposure until enough observations justify relaxing it.
return expected_reward_btc * risk_lambda

View file

@ -0,0 +1,51 @@
from __future__ import annotations
from decimal import Decimal
from .config import GuardrailsConfig
from .models import CandidateOrder
def validate_order(
order: CandidateOrder,
guardrails: GuardrailsConfig,
breakeven_btc_per_eh_day: Decimal | None,
) -> list[str]:
violations: list[str] = []
if not guardrails.recommend_only:
violations.append("recommend_only must remain true in the PoC")
if order.spend_btc <= 0:
violations.append("spend must be positive")
if order.spend_btc > guardrails.max_manual_order_btc:
violations.append(f"spend exceeds max_manual_order_btc={guardrails.max_manual_order_btc}")
if order.price_btc_per_eh_day <= 0:
violations.append("price must be positive")
if (
guardrails.max_price_btc_per_eh_day > 0
and order.price_btc_per_eh_day > guardrails.max_price_btc_per_eh_day
):
violations.append(
f"price exceeds max_price_btc_per_eh_day={guardrails.max_price_btc_per_eh_day}"
)
if order.duration_minutes < guardrails.min_duration_minutes:
violations.append(f"duration below min_duration_minutes={guardrails.min_duration_minutes}")
if order.duration_minutes > guardrails.max_duration_minutes:
violations.append(f"duration exceeds max_duration_minutes={guardrails.max_duration_minutes}")
if breakeven_btc_per_eh_day and breakeven_btc_per_eh_day > 0:
required = breakeven_btc_per_eh_day * (Decimal("1") - guardrails.min_discount_to_breakeven)
if order.price_btc_per_eh_day > required:
violations.append(
"price does not clear required discount to breakeven "
f"({order.price_btc_per_eh_day} > {required})"
)
else:
violations.append("cannot validate discount without breakeven estimate")
return violations
def token_looks_unsafe(token: str) -> bool:
lowered = token.lower()
unsafe_markers = ("owner", "admin", "trade", "write", "order", "secret")
return any(marker in lowered for marker in unsafe_markers)

View file

@ -0,0 +1,53 @@
from __future__ import annotations
from dataclasses import dataclass
from decimal import Decimal
from typing import Literal
BTC = Decimal
@dataclass(frozen=True)
class OceanSnapshot:
timestamp_utc: str
pool_hashrate_eh_s: Decimal | None = None
network_difficulty_t: Decimal | None = None
share_log_window_t: Decimal | None = None
avg_block_time_hours: Decimal | None = None
source: str = "ocean"
@dataclass(frozen=True)
class MarketSnapshot:
timestamp_utc: str
best_price_btc_per_eh_day: Decimal | None
available_hashrate_eh_s: Decimal | None = None
source: str = "manual"
@dataclass(frozen=True)
class CandidateOrder:
price_btc_per_eh_day: Decimal
spend_btc: Decimal
duration_minutes: int
objective: str = "manual-canary"
@property
def implied_hashrate_eh_s(self) -> Decimal:
days = Decimal(self.duration_minutes) / Decimal(1440)
if days <= 0 or self.price_btc_per_eh_day <= 0:
return Decimal("0")
return self.spend_btc / (self.price_btc_per_eh_day * days)
@dataclass(frozen=True)
class StrategyProposal:
action: Literal["observe", "manual_bid"]
reason: str
order: CandidateOrder | None
breakeven_btc_per_eh_day: Decimal | None
expected_reward_btc: Decimal
expected_net_btc: Decimal
score_btc: Decimal
maturity_note: str

View file

@ -0,0 +1,69 @@
from __future__ import annotations
from datetime import UTC, datetime
from decimal import Decimal
import html
import re
from urllib.request import Request, urlopen
from .models import OceanSnapshot
NUMBER = r"([0-9]+(?:\.[0-9]+)?)"
def fetch_dashboard_html(url: str, timeout_seconds: int = 15) -> str:
request = Request(url, headers={"User-Agent": "BraiinsRatchet/0.1 monitor-only"})
with urlopen(request, timeout=timeout_seconds) as response:
return response.read().decode("utf-8", errors="replace")
def _find_decimal(patterns: tuple[str, ...], text: str) -> Decimal | None:
without_tags = re.sub(r"<[^>]+>", " ", text)
normalized = html.unescape(re.sub(r"\s+", " ", without_tags))
for pattern in patterns:
match = re.search(pattern, normalized, flags=re.IGNORECASE)
if match:
return Decimal(match.group(1))
return None
def parse_dashboard(html_text: str, source: str = "ocean-dashboard") -> OceanSnapshot:
return OceanSnapshot(
timestamp_utc=datetime.now(UTC).isoformat(timespec="seconds"),
pool_hashrate_eh_s=_find_decimal(
(
rf"OCEAN Hashrate:\s*{NUMBER}\s*Eh/s",
rf"{NUMBER}\s*EH/s\s*Pool Hashrate",
rf"Pool Hashrate\s*{NUMBER}\s*EH/s",
),
html_text,
),
network_difficulty_t=_find_decimal(
(
rf"{NUMBER}\s*T\s*Network Difficulty",
rf"Network Difficulty\s*{NUMBER}\s*T",
),
html_text,
),
share_log_window_t=_find_decimal(
(
rf"{NUMBER}\s*T\s*Share Log",
rf"Share Log.*?{NUMBER}\s*T",
),
html_text,
),
avg_block_time_hours=_find_decimal(
(
rf"{NUMBER}\s*h\s*Avg Block Time",
rf"Avg Block Time\s*{NUMBER}\s*h",
rf"Average Time to Block.*?{NUMBER}\s*hours",
),
html_text,
),
source=source,
)
def fetch_snapshot(url: str) -> OceanSnapshot:
return parse_dashboard(fetch_dashboard_html(url), source=url)

View file

@ -0,0 +1,175 @@
from __future__ import annotations
from pathlib import Path
import sqlite3
from .config import REPO_ROOT
from .models import MarketSnapshot, OceanSnapshot, StrategyProposal
DATA_DIR = REPO_ROOT / "data"
DB_PATH = DATA_DIR / "ratchet.sqlite"
def connect(path: Path = DB_PATH) -> sqlite3.Connection:
path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(path, timeout=30)
conn.execute("PRAGMA journal_mode=WAL")
return conn
def init_db(conn: sqlite3.Connection) -> None:
conn.executescript(
"""
CREATE TABLE IF NOT EXISTS ocean_snapshots (
id INTEGER PRIMARY KEY,
timestamp_utc TEXT NOT NULL,
pool_hashrate_eh_s TEXT,
network_difficulty_t TEXT,
share_log_window_t TEXT,
avg_block_time_hours TEXT,
source TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS market_snapshots (
id INTEGER PRIMARY KEY,
timestamp_utc TEXT NOT NULL,
best_price_btc_per_eh_day TEXT,
available_hashrate_eh_s TEXT,
source TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS proposals (
id INTEGER PRIMARY KEY,
timestamp_utc TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
action TEXT NOT NULL,
reason TEXT NOT NULL,
price_btc_per_eh_day TEXT,
spend_btc TEXT,
duration_minutes INTEGER,
breakeven_btc_per_eh_day TEXT,
expected_reward_btc TEXT NOT NULL,
expected_net_btc TEXT NOT NULL,
score_btc TEXT NOT NULL,
maturity_note TEXT NOT NULL
);
"""
)
conn.commit()
def save_ocean_snapshot(conn: sqlite3.Connection, snapshot: OceanSnapshot) -> None:
conn.execute(
"""
INSERT INTO ocean_snapshots (
timestamp_utc, pool_hashrate_eh_s, network_difficulty_t,
share_log_window_t, avg_block_time_hours, source
)
VALUES (?, ?, ?, ?, ?, ?)
""",
(
snapshot.timestamp_utc,
str(snapshot.pool_hashrate_eh_s) if snapshot.pool_hashrate_eh_s is not None else None,
str(snapshot.network_difficulty_t) if snapshot.network_difficulty_t is not None else None,
str(snapshot.share_log_window_t) if snapshot.share_log_window_t is not None else None,
str(snapshot.avg_block_time_hours) if snapshot.avg_block_time_hours is not None else None,
snapshot.source,
),
)
conn.commit()
def save_market_snapshot(conn: sqlite3.Connection, snapshot: MarketSnapshot) -> None:
conn.execute(
"""
INSERT INTO market_snapshots (
timestamp_utc, best_price_btc_per_eh_day, available_hashrate_eh_s, source
)
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.available_hashrate_eh_s)
if snapshot.available_hashrate_eh_s is not None
else None,
snapshot.source,
),
)
conn.commit()
def latest_ocean_snapshot(conn: sqlite3.Connection) -> OceanSnapshot | None:
row = conn.execute(
"""
SELECT timestamp_utc, pool_hashrate_eh_s, network_difficulty_t,
share_log_window_t, avg_block_time_hours, source
FROM ocean_snapshots
ORDER BY id DESC
LIMIT 1
"""
).fetchone()
if not row:
return None
from decimal import Decimal
return OceanSnapshot(
timestamp_utc=row[0],
pool_hashrate_eh_s=Decimal(row[1]) if row[1] else None,
network_difficulty_t=Decimal(row[2]) if row[2] else None,
share_log_window_t=Decimal(row[3]) if row[3] else None,
avg_block_time_hours=Decimal(row[4]) if row[4] else None,
source=row[5],
)
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
FROM market_snapshots
ORDER BY id DESC
LIMIT 1
"""
).fetchone()
if not row:
return None
from decimal import Decimal
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],
)
def save_proposal(conn: sqlite3.Connection, proposal: StrategyProposal) -> None:
order = proposal.order
conn.execute(
"""
INSERT INTO proposals (
action, reason, price_btc_per_eh_day, spend_btc, duration_minutes,
breakeven_btc_per_eh_day, expected_reward_btc, expected_net_btc,
score_btc, maturity_note
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
proposal.action,
proposal.reason,
str(order.price_btc_per_eh_day) if order else None,
str(order.spend_btc) if order else None,
order.duration_minutes if order else None,
str(proposal.breakeven_btc_per_eh_day)
if proposal.breakeven_btc_per_eh_day is not None
else None,
str(proposal.expected_reward_btc),
str(proposal.expected_net_btc),
str(proposal.score_btc),
proposal.maturity_note,
),
)
conn.commit()

View file

@ -0,0 +1,109 @@
from __future__ import annotations
from decimal import Decimal
from .config import AppConfig
from .ev import breakeven_btc_per_eh_day, downside_penalty, expected_reward_for_order
from .guardrails import validate_order
from .models import CandidateOrder, MarketSnapshot, OceanSnapshot, StrategyProposal
def propose(
config: AppConfig,
ocean: OceanSnapshot | None,
market: MarketSnapshot | None,
) -> StrategyProposal:
if ocean is None or ocean.network_difficulty_t is None:
return _observe("missing OCEAN difficulty snapshot")
if market is None or market.best_price_btc_per_eh_day is None:
breakeven = breakeven_btc_per_eh_day(
ocean.network_difficulty_t,
config.ocean.expected_block_reward_btc,
config.ocean.fee_rate,
)
return StrategyProposal(
action="observe",
reason="missing Braiins market price snapshot",
order=None,
breakeven_btc_per_eh_day=breakeven,
expected_reward_btc=Decimal("0"),
expected_net_btc=Decimal("0"),
score_btc=Decimal("0"),
maturity_note=_maturity_note(ocean),
)
breakeven = breakeven_btc_per_eh_day(
ocean.network_difficulty_t,
config.ocean.expected_block_reward_btc,
config.ocean.fee_rate,
)
spend = min(config.strategy.target_spend_btc, config.guardrails.max_manual_order_btc)
order = CandidateOrder(
price_btc_per_eh_day=market.best_price_btc_per_eh_day,
spend_btc=spend,
duration_minutes=config.strategy.target_duration_minutes,
)
expected_reward = expected_reward_for_order(
order,
ocean.network_difficulty_t,
config.ocean.expected_block_reward_btc,
config.ocean.fee_rate,
)
expected_net = expected_reward - order.spend_btc
score = expected_net - downside_penalty(expected_reward, config.strategy.risk_lambda)
violations = validate_order(order, config.guardrails, breakeven)
if violations:
return StrategyProposal(
action="observe",
reason="guardrail blocked: " + "; ".join(violations),
order=order,
breakeven_btc_per_eh_day=breakeven,
expected_reward_btc=expected_reward,
expected_net_btc=expected_net,
score_btc=score,
maturity_note=_maturity_note(ocean),
)
if score <= 0:
return StrategyProposal(
action="observe",
reason=f"risk-adjusted score is not positive ({score})",
order=order,
breakeven_btc_per_eh_day=breakeven,
expected_reward_btc=expected_reward,
expected_net_btc=expected_net,
score_btc=score,
maturity_note=_maturity_note(ocean),
)
return StrategyProposal(
action="manual_bid",
reason="market price clears configured guardrails and positive risk-adjusted score",
order=order,
breakeven_btc_per_eh_day=breakeven,
expected_reward_btc=expected_reward,
expected_net_btc=expected_net,
score_btc=score,
maturity_note=_maturity_note(ocean),
)
def _observe(reason: str) -> StrategyProposal:
return StrategyProposal(
action="observe",
reason=reason,
order=None,
breakeven_btc_per_eh_day=None,
expected_reward_btc=Decimal("0"),
expected_net_btc=Decimal("0"),
score_btc=Decimal("0"),
maturity_note="no maturity estimate without OCEAN snapshot",
)
def _maturity_note(ocean: OceanSnapshot) -> str:
if ocean.avg_block_time_hours is None:
return "treat canary as immature until the OCEAN share-log window has elapsed"
window_hours = ocean.avg_block_time_hours * Decimal("8")
return f"treat canary as immature for about {window_hours} hours after spend"

19
tests/test_ev.py Normal file
View file

@ -0,0 +1,19 @@
from decimal import Decimal
import unittest
from braiins_ratchet.ev import breakeven_btc_per_eh_day
class EvTests(unittest.TestCase):
def test_breakeven_near_expected_current_scale(self) -> None:
value = breakeven_btc_per_eh_day(
network_difficulty_t=Decimal("135.59"),
expected_block_reward_btc=Decimal("3.175"),
pool_fee_rate=Decimal("0.01"),
)
self.assertGreater(value, Decimal("0.45"))
self.assertLess(value, Decimal("0.47"))
if __name__ == "__main__":
unittest.main()

34
tests/test_guardrails.py Normal file
View file

@ -0,0 +1,34 @@
from decimal import Decimal
import unittest
from braiins_ratchet.config import GuardrailsConfig
from braiins_ratchet.guardrails import token_looks_unsafe, validate_order
from braiins_ratchet.models import CandidateOrder
class GuardrailTests(unittest.TestCase):
def test_guardrails_block_high_price(self) -> None:
guardrails = GuardrailsConfig(
max_manual_order_btc=Decimal("0.00025"),
max_daily_spend_btc=Decimal("0.00050"),
max_price_btc_per_eh_day=Decimal("0.42"),
min_discount_to_breakeven=Decimal("0.08"),
min_duration_minutes=30,
max_duration_minutes=720,
recommend_only=True,
)
order = CandidateOrder(
price_btc_per_eh_day=Decimal("0.50"),
spend_btc=Decimal("0.00010"),
duration_minutes=180,
)
violations = validate_order(order, guardrails, Decimal("0.46"))
self.assertTrue(violations)
def test_token_label_screening(self) -> None:
self.assertTrue(token_looks_unsafe("owner-secret-token"))
self.assertFalse(token_looks_unsafe("watcher-readonly-token"))
if __name__ == "__main__":
unittest.main()

24
tests/test_ocean.py Normal file
View file

@ -0,0 +1,24 @@
from decimal import Decimal
import unittest
from braiins_ratchet.ocean import parse_dashboard
class OceanParserTests(unittest.TestCase):
def test_parse_dashboard_values_across_tags(self) -> None:
snapshot = parse_dashboard(
"""
<div>OCEAN Hashrate: <span>19.04 Eh/s</span></div>
<section>Network Difficulty</section><strong>135.59T</strong>
<section>Average Time to Block</section><small>Based on 24-hour average</small><b>9 hours</b>
<section>Share Log Window</section><span>1084.76T</span>
"""
)
self.assertEqual(snapshot.pool_hashrate_eh_s, Decimal("19.04"))
self.assertEqual(snapshot.network_difficulty_t, Decimal("135.59"))
self.assertEqual(snapshot.avg_block_time_hours, Decimal("9"))
self.assertEqual(snapshot.share_log_window_t, Decimal("1084.76"))
if __name__ == "__main__":
unittest.main()

62
tests/test_strategy.py Normal file
View file

@ -0,0 +1,62 @@
from decimal import Decimal
import unittest
from braiins_ratchet.config import AppConfig, CapitalConfig, GuardrailsConfig, OceanConfig, StrategyConfig
from braiins_ratchet.models import MarketSnapshot, OceanSnapshot
from braiins_ratchet.strategy import propose
class StrategyTests(unittest.TestCase):
def test_strategy_recommends_observe_without_market(self) -> None:
config = _config()
ocean = OceanSnapshot(
timestamp_utc="2026-04-25T00:00:00+00:00",
network_difficulty_t=Decimal("135.59"),
avg_block_time_hours=Decimal("9"),
)
proposal = propose(config, ocean, None)
self.assertEqual(proposal.action, "observe")
def test_strategy_can_emit_manual_bid_for_deep_discount(self) -> None:
config = _config()
ocean = OceanSnapshot(
timestamp_utc="2026-04-25T00:00:00+00:00",
network_difficulty_t=Decimal("135.59"),
avg_block_time_hours=Decimal("9"),
)
market = MarketSnapshot(
timestamp_utc="2026-04-25T00:00:00+00:00",
best_price_btc_per_eh_day=Decimal("0.30"),
)
proposal = propose(config, ocean, market)
self.assertEqual(proposal.action, "manual_bid")
def _config() -> AppConfig:
return AppConfig(
capital=CapitalConfig(available_btc=Decimal("0.01638650")),
ocean=OceanConfig(
fee_rate=Decimal("0.01"),
block_subsidy_btc=Decimal("3.125"),
default_tx_fees_btc=Decimal("0.05"),
dashboard_url="https://ocean.xyz/dashboard",
),
guardrails=GuardrailsConfig(
max_manual_order_btc=Decimal("0.00025"),
max_daily_spend_btc=Decimal("0.00050"),
max_price_btc_per_eh_day=Decimal("0.42"),
min_discount_to_breakeven=Decimal("0.08"),
min_duration_minutes=30,
max_duration_minutes=720,
recommend_only=True,
),
strategy=StrategyConfig(
target_duration_minutes=180,
target_spend_btc=Decimal("0.00010"),
risk_lambda=Decimal("0.35"),
),
)
if __name__ == "__main__":
unittest.main()