mirror of
https://github.com/saymrwulf/BraiinsRatchet.git
synced 2026-05-14 20:37:52 +00:00
Add monitor cycle and reporting commands
This commit is contained in:
parent
e6f001b980
commit
b51a8b137e
11 changed files with 478 additions and 10 deletions
11
README.md
11
README.md
|
|
@ -16,9 +16,8 @@ The first implementation is deliberately conservative:
|
||||||
```bash
|
```bash
|
||||||
python3 -m venv .venv
|
python3 -m venv .venv
|
||||||
./.venv/bin/python -m braiins_ratchet.cli init-db
|
./.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 cycle
|
||||||
./.venv/bin/python -m braiins_ratchet.cli collect-braiins-public
|
./.venv/bin/python -m braiins_ratchet.cli report
|
||||||
./.venv/bin/python -m braiins_ratchet.cli evaluate
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Set `PYTHONPATH=src` if running without installation:
|
Set `PYTHONPATH=src` if running without installation:
|
||||||
|
|
@ -27,6 +26,12 @@ Set `PYTHONPATH=src` if running without installation:
|
||||||
PYTHONPATH=src python3 -m braiins_ratchet.cli evaluate
|
PYTHONPATH=src python3 -m braiins_ratchet.cli evaluate
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Run a bounded monitor loop:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli watch --cycles 12 --interval-seconds 300
|
||||||
|
```
|
||||||
|
|
||||||
Import a manual Braiins market snapshot:
|
Import a manual Braiins market snapshot:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,34 @@ Override the public base URL only for testing:
|
||||||
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli collect-braiins-public --base-url https://hashpower.braiins.com/webapi
|
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli collect-braiins-public --base-url https://hashpower.braiins.com/webapi
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `cycle`
|
||||||
|
|
||||||
|
Runs one complete monitor pass:
|
||||||
|
|
||||||
|
1. collect OCEAN
|
||||||
|
2. collect public Braiins market data
|
||||||
|
3. evaluate the strategy
|
||||||
|
4. store the resulting proposal
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli cycle
|
||||||
|
```
|
||||||
|
|
||||||
|
Use existing stored data for one side if needed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli cycle --skip-ocean
|
||||||
|
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli cycle --skip-braiins
|
||||||
|
```
|
||||||
|
|
||||||
|
## `watch`
|
||||||
|
|
||||||
|
Runs a bounded monitor loop. The interval floor is 30 seconds to avoid hammering public endpoints.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli watch --cycles 12 --interval-seconds 300
|
||||||
|
```
|
||||||
|
|
||||||
## `import-market`
|
## `import-market`
|
||||||
|
|
||||||
Imports a manual market JSON snapshot. Use this when public Braiins endpoints are unavailable.
|
Imports a manual market JSON snapshot. Use this when public Braiins endpoints are unavailable.
|
||||||
|
|
@ -68,6 +96,21 @@ PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli evaluate
|
||||||
|
|
||||||
The command returns `observe` or `manual_bid`. It never places an order.
|
The command returns `observe` or `manual_bid`. It never places an order.
|
||||||
|
|
||||||
|
## `report`
|
||||||
|
|
||||||
|
Prints the latest OCEAN snapshot, latest Braiins snapshot, sampled price range, and latest proposal.
|
||||||
|
The sampled price range uses public Braiins snapshots only, so manually imported examples do not skew operational statistics.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli report
|
||||||
|
```
|
||||||
|
|
||||||
|
Control the number of recent market samples used for min/avg/max:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli report --samples 200
|
||||||
|
```
|
||||||
|
|
||||||
## `guardrails`
|
## `guardrails`
|
||||||
|
|
||||||
Prints the active guardrails.
|
Prints the active guardrails.
|
||||||
|
|
@ -85,4 +128,3 @@ 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.
|
The test suite is network-free. Live collectors are validated separately by explicitly running their commands.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,24 @@
|
||||||
Run these commands from the repository root:
|
Run these commands from the repository root:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli collect-ocean
|
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli cycle
|
||||||
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli collect-braiins-public
|
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli report
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
The report's sampled price min/avg/max uses public Braiins snapshots only. Manual imports are still stored and can drive evaluation, but they do not pollute live market summary statistics.
|
||||||
|
|
||||||
|
## Repeated Sampling
|
||||||
|
|
||||||
|
For short monitor sessions:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli watch --cycles 12 --interval-seconds 300
|
||||||
|
```
|
||||||
|
|
||||||
|
This runs for about one hour at 5-minute intervals. It is bounded by design; use your shell or system scheduler if you want recurring sessions.
|
||||||
|
|
||||||
## Manual Market Snapshot
|
## Manual Market Snapshot
|
||||||
|
|
||||||
If Braiins public endpoints are unavailable, use:
|
If Braiins public endpoints are unavailable, use:
|
||||||
|
|
@ -33,4 +44,3 @@ Edit a copy of the example JSON with values read from the Braiins UI. Do not put
|
||||||
## Safety
|
## Safety
|
||||||
|
|
||||||
The program never places orders. Manual execution remains outside the repo and should use the Braiins UI, not this code.
|
The program never places orders. Manual execution remains outside the repo and should use the Braiins UI, not this code.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,13 @@ import argparse
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
from .braiins import BraiinsPublicClient, market_snapshot_from_json_file
|
from .braiins import BraiinsPublicClient, market_snapshot_from_json_file
|
||||||
from .config import load_config
|
from .config import load_config
|
||||||
|
from .monitor import run_cycle
|
||||||
from .ocean import fetch_snapshot
|
from .ocean import fetch_snapshot
|
||||||
|
from .report import build_text_report
|
||||||
from .storage import (
|
from .storage import (
|
||||||
connect,
|
connect,
|
||||||
init_db,
|
init_db,
|
||||||
|
|
@ -56,6 +59,35 @@ def cmd_collect_braiins_public(args: argparse.Namespace) -> int:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_cycle(args: argparse.Namespace) -> int:
|
||||||
|
config = load_config(Path(args.config) if args.config else None)
|
||||||
|
with connect() as conn:
|
||||||
|
result = run_cycle(
|
||||||
|
conn,
|
||||||
|
config,
|
||||||
|
collect_ocean=not args.skip_ocean,
|
||||||
|
collect_braiins=not args.skip_braiins,
|
||||||
|
)
|
||||||
|
print(_proposal_json(result))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_watch(args: argparse.Namespace) -> int:
|
||||||
|
config = load_config(Path(args.config) if args.config else None)
|
||||||
|
if args.interval_seconds < 30:
|
||||||
|
raise SystemExit("interval must be at least 30 seconds")
|
||||||
|
if args.cycles < 1:
|
||||||
|
raise SystemExit("cycles must be at least 1")
|
||||||
|
|
||||||
|
with connect() as conn:
|
||||||
|
for index in range(args.cycles):
|
||||||
|
result = run_cycle(conn, config)
|
||||||
|
print(f"cycle {index + 1}/{args.cycles}: {result.proposal.action} - {result.proposal.reason}")
|
||||||
|
if index + 1 < args.cycles:
|
||||||
|
time.sleep(args.interval_seconds)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def cmd_evaluate(args: argparse.Namespace) -> int:
|
def cmd_evaluate(args: argparse.Namespace) -> int:
|
||||||
config = load_config(Path(args.config) if args.config else None)
|
config = load_config(Path(args.config) if args.config else None)
|
||||||
with connect() as conn:
|
with connect() as conn:
|
||||||
|
|
@ -66,6 +98,13 @@ def cmd_evaluate(args: argparse.Namespace) -> int:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_report(args: argparse.Namespace) -> int:
|
||||||
|
with connect() as conn:
|
||||||
|
init_db(conn)
|
||||||
|
print(build_text_report(conn, sample_limit=args.samples))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def cmd_guardrails(args: argparse.Namespace) -> int:
|
def cmd_guardrails(args: argparse.Namespace) -> int:
|
||||||
config = load_config(Path(args.config) if args.config else None)
|
config = load_config(Path(args.config) if args.config else None)
|
||||||
print(json.dumps(config.guardrails.__dict__, default=str, indent=2))
|
print(json.dumps(config.guardrails.__dict__, default=str, indent=2))
|
||||||
|
|
@ -103,10 +142,26 @@ def build_parser() -> argparse.ArgumentParser:
|
||||||
braiins.add_argument("--base-url", default="https://hashpower.braiins.com/webapi")
|
braiins.add_argument("--base-url", default="https://hashpower.braiins.com/webapi")
|
||||||
braiins.set_defaults(func=cmd_collect_braiins_public)
|
braiins.set_defaults(func=cmd_collect_braiins_public)
|
||||||
|
|
||||||
|
cycle = sub.add_parser("cycle", help="collect OCEAN, collect public Braiins, then evaluate")
|
||||||
|
cycle.add_argument("--config")
|
||||||
|
cycle.add_argument("--skip-ocean", action="store_true")
|
||||||
|
cycle.add_argument("--skip-braiins", action="store_true")
|
||||||
|
cycle.set_defaults(func=cmd_cycle)
|
||||||
|
|
||||||
|
watch = sub.add_parser("watch", help="run bounded repeated monitor cycles")
|
||||||
|
watch.add_argument("--config")
|
||||||
|
watch.add_argument("--cycles", type=int, default=3)
|
||||||
|
watch.add_argument("--interval-seconds", type=int, default=300)
|
||||||
|
watch.set_defaults(func=cmd_watch)
|
||||||
|
|
||||||
evaluate = sub.add_parser("evaluate", help="emit monitor-only strategy recommendation")
|
evaluate = sub.add_parser("evaluate", help="emit monitor-only strategy recommendation")
|
||||||
evaluate.add_argument("--config")
|
evaluate.add_argument("--config")
|
||||||
evaluate.set_defaults(func=cmd_evaluate)
|
evaluate.set_defaults(func=cmd_evaluate)
|
||||||
|
|
||||||
|
report = sub.add_parser("report", help="print latest state and proposal")
|
||||||
|
report.add_argument("--samples", type=int, default=50)
|
||||||
|
report.set_defaults(func=cmd_report)
|
||||||
|
|
||||||
guardrails = sub.add_parser("guardrails", help="print active guardrails")
|
guardrails = sub.add_parser("guardrails", help="print active guardrails")
|
||||||
guardrails.add_argument("--config")
|
guardrails.add_argument("--config")
|
||||||
guardrails.set_defaults(func=cmd_guardrails)
|
guardrails.set_defaults(func=cmd_guardrails)
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,14 @@ class MarketSnapshot:
|
||||||
source: str = "manual"
|
source: str = "manual"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PriceStats:
|
||||||
|
count: int
|
||||||
|
min_price: Decimal | None
|
||||||
|
avg_price: Decimal | None
|
||||||
|
max_price: Decimal | None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class CandidateOrder:
|
class CandidateOrder:
|
||||||
price_btc_per_eh_day: Decimal
|
price_btc_per_eh_day: Decimal
|
||||||
|
|
|
||||||
58
src/braiins_ratchet/monitor.py
Normal file
58
src/braiins_ratchet/monitor.py
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from .braiins import BraiinsPublicClient
|
||||||
|
from .config import AppConfig
|
||||||
|
from .models import MarketSnapshot, OceanSnapshot, StrategyProposal
|
||||||
|
from .ocean import fetch_snapshot as fetch_ocean_snapshot
|
||||||
|
from .storage import (
|
||||||
|
init_db,
|
||||||
|
latest_market_snapshot,
|
||||||
|
latest_ocean_snapshot,
|
||||||
|
save_market_snapshot,
|
||||||
|
save_ocean_snapshot,
|
||||||
|
save_proposal,
|
||||||
|
)
|
||||||
|
from .strategy import propose
|
||||||
|
|
||||||
|
|
||||||
|
OceanFetcher = Callable[[str], OceanSnapshot]
|
||||||
|
MarketFetcher = Callable[[], MarketSnapshot]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CycleResult:
|
||||||
|
ocean: OceanSnapshot | None
|
||||||
|
market: MarketSnapshot | None
|
||||||
|
proposal: StrategyProposal
|
||||||
|
|
||||||
|
|
||||||
|
def run_cycle(
|
||||||
|
conn,
|
||||||
|
config: AppConfig,
|
||||||
|
*,
|
||||||
|
collect_ocean: bool = True,
|
||||||
|
collect_braiins: bool = True,
|
||||||
|
ocean_fetcher: OceanFetcher = fetch_ocean_snapshot,
|
||||||
|
market_fetcher: MarketFetcher | None = None,
|
||||||
|
) -> CycleResult:
|
||||||
|
init_db(conn)
|
||||||
|
|
||||||
|
ocean = latest_ocean_snapshot(conn)
|
||||||
|
market = latest_market_snapshot(conn)
|
||||||
|
|
||||||
|
if collect_ocean:
|
||||||
|
ocean = ocean_fetcher(config.ocean.dashboard_url)
|
||||||
|
save_ocean_snapshot(conn, ocean)
|
||||||
|
|
||||||
|
if collect_braiins:
|
||||||
|
fetcher = market_fetcher or BraiinsPublicClient().fetch_market_snapshot
|
||||||
|
market = fetcher()
|
||||||
|
save_market_snapshot(conn, market)
|
||||||
|
|
||||||
|
proposal = propose(config, ocean, market)
|
||||||
|
save_proposal(conn, proposal)
|
||||||
|
|
||||||
|
return CycleResult(ocean=ocean, market=market, proposal=proposal)
|
||||||
76
src/braiins_ratchet/report.py
Normal file
76
src/braiins_ratchet/report.py
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .models import MarketSnapshot, OceanSnapshot, PriceStats, StrategyProposal
|
||||||
|
from .storage import latest_market_snapshot, latest_ocean_snapshot, latest_proposal, market_price_stats
|
||||||
|
|
||||||
|
|
||||||
|
def build_text_report(conn, *, sample_limit: int = 50) -> str:
|
||||||
|
ocean = latest_ocean_snapshot(conn)
|
||||||
|
market = latest_market_snapshot(conn)
|
||||||
|
proposal = latest_proposal(conn)
|
||||||
|
stats = market_price_stats(conn, sample_limit, source="braiins-public")
|
||||||
|
|
||||||
|
lines = ["Braiins Ratchet Report", ""]
|
||||||
|
lines.extend(_ocean_lines(ocean))
|
||||||
|
lines.append("")
|
||||||
|
lines.extend(_market_lines(market, stats))
|
||||||
|
lines.append("")
|
||||||
|
lines.extend(_proposal_lines(proposal))
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _ocean_lines(ocean: OceanSnapshot | None) -> list[str]:
|
||||||
|
if ocean is None:
|
||||||
|
return ["OCEAN: no snapshot stored"]
|
||||||
|
return [
|
||||||
|
f"OCEAN snapshot: {ocean.timestamp_utc}",
|
||||||
|
f" pool_hashrate_eh_s: {_fmt(ocean.pool_hashrate_eh_s)}",
|
||||||
|
f" network_difficulty_t: {_fmt(ocean.network_difficulty_t)}",
|
||||||
|
f" share_log_window_t: {_fmt(ocean.share_log_window_t)}",
|
||||||
|
f" avg_block_time_hours: {_fmt(ocean.avg_block_time_hours)}",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _market_lines(market: MarketSnapshot | None, stats: PriceStats) -> list[str]:
|
||||||
|
if market is None:
|
||||||
|
return ["Braiins market: no snapshot stored"]
|
||||||
|
return [
|
||||||
|
f"Braiins market snapshot: {market.timestamp_utc}",
|
||||||
|
f" status: {market.status or 'unknown'}",
|
||||||
|
f" best_bid_btc_per_eh_day: {_fmt(market.best_bid_btc_per_eh_day)}",
|
||||||
|
f" best_ask_btc_per_eh_day: {_fmt(market.best_ask_btc_per_eh_day)}",
|
||||||
|
f" last_price_btc_per_eh_day: {_fmt(market.last_price_btc_per_eh_day)}",
|
||||||
|
f" available_hashrate_eh_s: {_fmt(market.available_hashrate_eh_s)}",
|
||||||
|
f" sampled_price_count: {stats.count}",
|
||||||
|
f" sampled_price_min_avg_max: {_fmt(stats.min_price)} / {_fmt(stats.avg_price)} / {_fmt(stats.max_price)}",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _proposal_lines(proposal: StrategyProposal | None) -> list[str]:
|
||||||
|
if proposal is None:
|
||||||
|
return ["Strategy: no proposal stored"]
|
||||||
|
lines = [
|
||||||
|
f"Strategy action: {proposal.action}",
|
||||||
|
f" reason: {proposal.reason}",
|
||||||
|
f" breakeven_btc_per_eh_day: {_fmt(proposal.breakeven_btc_per_eh_day)}",
|
||||||
|
f" expected_reward_btc: {_fmt(proposal.expected_reward_btc)}",
|
||||||
|
f" expected_net_btc: {_fmt(proposal.expected_net_btc)}",
|
||||||
|
f" score_btc: {_fmt(proposal.score_btc)}",
|
||||||
|
f" maturity: {proposal.maturity_note}",
|
||||||
|
]
|
||||||
|
if proposal.order is not None:
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
f" proposed_price_btc_per_eh_day: {_fmt(proposal.order.price_btc_per_eh_day)}",
|
||||||
|
f" proposed_spend_btc: {_fmt(proposal.order.spend_btc)}",
|
||||||
|
f" proposed_duration_minutes: {proposal.order.duration_minutes}",
|
||||||
|
f" implied_hashrate_eh_s: {_fmt(proposal.order.implied_hashrate_eh_s)}",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt(value: object) -> str:
|
||||||
|
if value is None:
|
||||||
|
return "n/a"
|
||||||
|
return str(value)
|
||||||
|
|
@ -4,7 +4,7 @@ from pathlib import Path
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
from .config import REPO_ROOT
|
from .config import REPO_ROOT
|
||||||
from .models import MarketSnapshot, OceanSnapshot, StrategyProposal
|
from .models import CandidateOrder, MarketSnapshot, OceanSnapshot, PriceStats, StrategyProposal
|
||||||
|
|
||||||
|
|
||||||
DATA_DIR = REPO_ROOT / "data"
|
DATA_DIR = REPO_ROOT / "data"
|
||||||
|
|
@ -191,6 +191,68 @@ def latest_market_snapshot(conn: sqlite3.Connection) -> MarketSnapshot | None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def latest_proposal(conn: sqlite3.Connection) -> StrategyProposal | None:
|
||||||
|
row = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT 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
|
||||||
|
FROM proposals
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
).fetchone()
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
order = None
|
||||||
|
if row[2] is not None and row[3] is not None and row[4] is not None:
|
||||||
|
order = CandidateOrder(
|
||||||
|
price_btc_per_eh_day=Decimal(row[2]),
|
||||||
|
spend_btc=Decimal(row[3]),
|
||||||
|
duration_minutes=int(row[4]),
|
||||||
|
)
|
||||||
|
|
||||||
|
return StrategyProposal(
|
||||||
|
action=row[0],
|
||||||
|
reason=row[1],
|
||||||
|
order=order,
|
||||||
|
breakeven_btc_per_eh_day=Decimal(row[5]) if row[5] else None,
|
||||||
|
expected_reward_btc=Decimal(row[6]),
|
||||||
|
expected_net_btc=Decimal(row[7]),
|
||||||
|
score_btc=Decimal(row[8]),
|
||||||
|
maturity_note=row[9],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def market_price_stats(conn: sqlite3.Connection, limit: int, source: str | None = None):
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
source_filter = "AND source = ?" if source else ""
|
||||||
|
params: tuple[object, ...] = (source, limit) if source else (limit,)
|
||||||
|
rows = conn.execute(
|
||||||
|
f"""
|
||||||
|
SELECT best_price_btc_per_eh_day
|
||||||
|
FROM market_snapshots
|
||||||
|
WHERE best_price_btc_per_eh_day IS NOT NULL
|
||||||
|
{source_filter}
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT ?
|
||||||
|
""",
|
||||||
|
params,
|
||||||
|
).fetchall()
|
||||||
|
prices = [Decimal(row[0]) for row in rows]
|
||||||
|
if not prices:
|
||||||
|
return PriceStats(count=0, min_price=None, avg_price=None, max_price=None)
|
||||||
|
return PriceStats(
|
||||||
|
count=len(prices),
|
||||||
|
min_price=min(prices),
|
||||||
|
avg_price=sum(prices) / Decimal(len(prices)),
|
||||||
|
max_price=max(prices),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def save_proposal(conn: sqlite3.Connection, proposal: StrategyProposal) -> None:
|
def save_proposal(conn: sqlite3.Connection, proposal: StrategyProposal) -> None:
|
||||||
order = proposal.order
|
order = proposal.order
|
||||||
conn.execute(
|
conn.execute(
|
||||||
|
|
|
||||||
63
tests/test_monitor.py
Normal file
63
tests/test_monitor.py
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
from decimal import Decimal
|
||||||
|
import sqlite3
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from braiins_ratchet.config import AppConfig, CapitalConfig, GuardrailsConfig, OceanConfig, StrategyConfig
|
||||||
|
from braiins_ratchet.models import MarketSnapshot, OceanSnapshot
|
||||||
|
from braiins_ratchet.monitor import run_cycle
|
||||||
|
from braiins_ratchet.storage import latest_market_snapshot, latest_ocean_snapshot, latest_proposal
|
||||||
|
|
||||||
|
|
||||||
|
class MonitorTests(unittest.TestCase):
|
||||||
|
def test_run_cycle_collects_and_evaluates(self) -> None:
|
||||||
|
conn = sqlite3.connect(":memory:")
|
||||||
|
result = run_cycle(
|
||||||
|
conn,
|
||||||
|
_config(),
|
||||||
|
ocean_fetcher=lambda _: OceanSnapshot(
|
||||||
|
timestamp_utc="2026-04-25T12:00:00+00:00",
|
||||||
|
network_difficulty_t=Decimal("135.59"),
|
||||||
|
avg_block_time_hours=Decimal("9"),
|
||||||
|
),
|
||||||
|
market_fetcher=lambda: MarketSnapshot(
|
||||||
|
timestamp_utc="2026-04-25T12:00:01+00:00",
|
||||||
|
best_price_btc_per_eh_day=Decimal("0.30"),
|
||||||
|
best_ask_btc_per_eh_day=Decimal("0.30"),
|
||||||
|
source="test",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result.proposal.action, "manual_bid")
|
||||||
|
self.assertIsNotNone(latest_ocean_snapshot(conn))
|
||||||
|
self.assertIsNotNone(latest_market_snapshot(conn))
|
||||||
|
self.assertIsNotNone(latest_proposal(conn))
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
62
tests/test_report.py
Normal file
62
tests/test_report.py
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
from decimal import Decimal
|
||||||
|
import sqlite3
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from braiins_ratchet.models import CandidateOrder, MarketSnapshot, OceanSnapshot, StrategyProposal
|
||||||
|
from braiins_ratchet.report import build_text_report
|
||||||
|
from braiins_ratchet.storage import init_db, save_market_snapshot, save_ocean_snapshot, save_proposal
|
||||||
|
|
||||||
|
|
||||||
|
class ReportTests(unittest.TestCase):
|
||||||
|
def test_report_includes_latest_state_and_proposal(self) -> None:
|
||||||
|
conn = sqlite3.connect(":memory:")
|
||||||
|
init_db(conn)
|
||||||
|
save_ocean_snapshot(
|
||||||
|
conn,
|
||||||
|
OceanSnapshot(
|
||||||
|
timestamp_utc="2026-04-25T12:00:00+00:00",
|
||||||
|
pool_hashrate_eh_s=Decimal("19.04"),
|
||||||
|
network_difficulty_t=Decimal("135.59"),
|
||||||
|
share_log_window_t=Decimal("1084.76"),
|
||||||
|
avg_block_time_hours=Decimal("9"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
save_market_snapshot(
|
||||||
|
conn,
|
||||||
|
MarketSnapshot(
|
||||||
|
timestamp_utc="2026-04-25T12:00:01+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"),
|
||||||
|
available_hashrate_eh_s=Decimal("0.21"),
|
||||||
|
status="SPOT_INSTRUMENT_STATUS_ACTIVE",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
save_proposal(
|
||||||
|
conn,
|
||||||
|
StrategyProposal(
|
||||||
|
action="manual_bid",
|
||||||
|
reason="test",
|
||||||
|
order=CandidateOrder(
|
||||||
|
price_btc_per_eh_day=Decimal("0.30"),
|
||||||
|
spend_btc=Decimal("0.00010"),
|
||||||
|
duration_minutes=180,
|
||||||
|
),
|
||||||
|
breakeven_btc_per_eh_day=Decimal("0.46"),
|
||||||
|
expected_reward_btc=Decimal("0.00015"),
|
||||||
|
expected_net_btc=Decimal("0.00005"),
|
||||||
|
score_btc=Decimal("0.00001"),
|
||||||
|
maturity_note="wait",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
report = build_text_report(conn)
|
||||||
|
|
||||||
|
self.assertIn("Strategy action: manual_bid", report)
|
||||||
|
self.assertIn("best_ask_btc_per_eh_day: 0.30", report)
|
||||||
|
self.assertIn("network_difficulty_t: 135.59", report)
|
||||||
|
self.assertIn("implied_hashrate_eh_s", report)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
|
|
@ -3,7 +3,7 @@ import sqlite3
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from braiins_ratchet.models import MarketSnapshot
|
from braiins_ratchet.models import MarketSnapshot
|
||||||
from braiins_ratchet.storage import init_db, latest_market_snapshot, save_market_snapshot
|
from braiins_ratchet.storage import init_db, latest_market_snapshot, market_price_stats, save_market_snapshot
|
||||||
|
|
||||||
|
|
||||||
class StorageTests(unittest.TestCase):
|
class StorageTests(unittest.TestCase):
|
||||||
|
|
@ -36,6 +36,33 @@ class StorageTests(unittest.TestCase):
|
||||||
self.assertEqual(snapshot.available_hashrate_eh_s, Decimal("0.21"))
|
self.assertEqual(snapshot.available_hashrate_eh_s, Decimal("0.21"))
|
||||||
self.assertEqual(snapshot.status, "SPOT_INSTRUMENT_STATUS_ACTIVE")
|
self.assertEqual(snapshot.status, "SPOT_INSTRUMENT_STATUS_ACTIVE")
|
||||||
|
|
||||||
|
def test_market_price_stats_can_filter_source(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"),
|
||||||
|
source="manual-example",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
save_market_snapshot(
|
||||||
|
conn,
|
||||||
|
MarketSnapshot(
|
||||||
|
timestamp_utc="2026-04-25T12:00:01+00:00",
|
||||||
|
best_price_btc_per_eh_day=Decimal("0.46"),
|
||||||
|
source="braiins-public",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
stats = market_price_stats(conn, 50, source="braiins-public")
|
||||||
|
|
||||||
|
self.assertEqual(stats.count, 1)
|
||||||
|
self.assertEqual(stats.min_price, Decimal("0.46"))
|
||||||
|
self.assertEqual(stats.avg_price, Decimal("0.46"))
|
||||||
|
self.assertEqual(stats.max_price, Decimal("0.46"))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue