mirror of
https://github.com/saymrwulf/BraiinsRatchet.git
synced 2026-05-14 20:37:52 +00:00
Initial monitor-only ratchet scaffold
This commit is contained in:
commit
274b89e7f3
24 changed files with 1092 additions and 0 deletions
4
.env.example
Normal file
4
.env.example
Normal 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
10
.gitignore
vendored
Normal 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
44
PROGRAM.md
Normal 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
53
README.md
Normal 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
25
SECURITY.md
Normal 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
23
config.example.toml
Normal 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
1
data/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
6
examples/market_snapshot.example.json
Normal file
6
examples/market_snapshot.example.json
Normal 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
17
pyproject.toml
Normal 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
1
results.tsv
Normal 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
|
||||
|
5
src/braiins_ratchet/__init__.py
Normal file
5
src/braiins_ratchet/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
"""Monitor-only ratchet tooling for Braiins Hashpower and OCEAN mining."""
|
||||
|
||||
__all__ = ["__version__"]
|
||||
|
||||
__version__ = "0.1.0"
|
||||
65
src/braiins_ratchet/braiins.py
Normal file
65
src/braiins_ratchet/braiins.py
Normal 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
107
src/braiins_ratchet/cli.py
Normal 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())
|
||||
94
src/braiins_ratchet/config.py
Normal file
94
src/braiins_ratchet/config.py
Normal 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
41
src/braiins_ratchet/ev.py
Normal 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
|
||||
51
src/braiins_ratchet/guardrails.py
Normal file
51
src/braiins_ratchet/guardrails.py
Normal 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)
|
||||
53
src/braiins_ratchet/models.py
Normal file
53
src/braiins_ratchet/models.py
Normal 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
|
||||
69
src/braiins_ratchet/ocean.py
Normal file
69
src/braiins_ratchet/ocean.py
Normal 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)
|
||||
175
src/braiins_ratchet/storage.py
Normal file
175
src/braiins_ratchet/storage.py
Normal 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()
|
||||
109
src/braiins_ratchet/strategy.py
Normal file
109
src/braiins_ratchet/strategy.py
Normal 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
19
tests/test_ev.py
Normal 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
34
tests/test_guardrails.py
Normal 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
24
tests/test_ocean.py
Normal 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
62
tests/test_strategy.py
Normal 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()
|
||||
Loading…
Reference in a new issue