Add operator cockpit guidance

This commit is contained in:
saymrwulf 2026-04-25 23:41:02 +02:00
parent fbd6311800
commit b9fda29c9a
9 changed files with 417 additions and 6 deletions

View file

@ -16,9 +16,11 @@ The first implementation is deliberately conservative:
```bash
./scripts/ratchet setup
./scripts/ratchet once
./scripts/ratchet
```
`./scripts/ratchet` is the cockpit. It tells you exactly what to do next.
For a 6-hour monitoring session:
```bash
@ -84,6 +86,7 @@ The Braiins market report distinguishes visible top-of-book from executable dept
## Documentation
- `PROGRAM.md`: research charter and ratchet rules.
- `START_HERE.md`: no-prior-knowledge operating instructions.
- `SECURITY.md`: token, computer, and trading safety guardrails.
- `docs/BRAIINS_PUBLIC_MARKET.md`: public market collector behavior.
- `docs/RATCHET_OPERATIONS.md`: day-to-day monitor cycle.

69
START_HERE.md Normal file
View file

@ -0,0 +1,69 @@
# Start Here
This project now has one operator entry point:
```bash
./scripts/ratchet
```
That is the same as:
```bash
./scripts/ratchet next
```
It prints the cockpit: current state, exact next action, interpretation, safe commands, and ratchet rule.
## Your Job
Your job is not to understand every metric.
Your job is:
1. Run `./scripts/ratchet`.
2. Do the first item under `What You Do Now`.
3. If a watch is running, leave it alone until it finishes.
4. After a watch finishes, run `./scripts/ratchet` again.
5. If you manually place a Braiins canary, write down the order details outside this repo and wait through the maturity window before judging it.
## What The Actions Mean
`observe` means do not bid.
`manual_canary` means a tiny research experiment is inside the configured loss budget. It is not a profit signal.
`manual_bid` means the stricter profit-seeking guardrails cleared. The code still does not place the order. You decide manually in Braiins.
## Where The Reports Are
The master ledger is:
```text
reports/EXPERIMENT_LOG.md
```
Each completed watch creates one run report:
```text
reports/run-*.md
```
Older sessions can be embedded with:
```bash
./scripts/ratchet retro START_UTC END_UTC
```
## The Ratchet Rule
One run is not a verdict. One run is a measurement.
Only change one knob at a time:
1. Depth target.
2. Overpay cushion.
3. Canary spend.
4. Duration.
5. Timing window.
Do not increase spend until multiple mature runs point in the same direction.

View file

@ -4,11 +4,11 @@ Most users should use the wrapper:
```bash
./scripts/ratchet setup
./scripts/ratchet once
./scripts/ratchet watch 6
./scripts/ratchet report
./scripts/ratchet
```
`./scripts/ratchet` defaults to `./scripts/ratchet next`, the operator cockpit. It tells you exactly what to do next.
Use `./scripts/ratchet raw-cycle` only when debugging the machine-readable cycle output.
The raw Python CLI is documented below for debugging and development.
@ -21,6 +21,14 @@ Use the local virtual environment:
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli <command>
```
## `next`
Prints the cockpit: current state, exact next operator action, interpretation, and safe commands.
```bash
PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli next
```
## `init-db`
Creates `data/ratchet.sqlite` if it does not exist.

View file

@ -25,6 +25,14 @@ What this does:
- Does not install packages.
- Does not touch anything outside the repo.
After setup, use the cockpit:
```bash
./scripts/ratchet
```
The cockpit tells you exactly what to do next. If you are unsure, run it again.
## First Live Check
Run:
@ -103,6 +111,8 @@ During the watch:
Stop early with `Ctrl-C`.
If you stop early, the tool writes a partial experiment report and then prints the cockpit.
When the watch completes normally, it writes:
- `reports/EXPERIMENT_LOG.md`: the master ratchet ledger.

View file

@ -4,6 +4,14 @@
Run these commands from the repository root:
```bash
./scripts/ratchet
```
This prints the cockpit and tells you exactly what to do next.
If the cockpit tells you to collect one fresh sample, run:
```bash
./scripts/ratchet once
```

View file

@ -9,6 +9,7 @@ usage() {
Braiins Ratchet
Commands:
next Show exactly what to do next. Default command.
setup Create the local .venv and initialize the local database.
once Run one full monitor cycle, then print the human report.
watch [hours] Run repeated monitor cycles for N hours. Default: 6.
@ -20,6 +21,8 @@ Commands:
explain Print the operating procedure and interpretation guide.
Examples:
./scripts/ratchet
./scripts/ratchet next
./scripts/ratchet setup
./scripts/ratchet once
./scripts/ratchet watch 6
@ -45,13 +48,19 @@ cmd_setup() {
ensure_venv
run_python -m braiins_ratchet.cli init-db
echo
echo "Setup complete. Next: ./scripts/ratchet once"
echo "Setup complete. Next: ./scripts/ratchet next"
}
cmd_next() {
run_python -m braiins_ratchet.cli next
}
cmd_once() {
run_python -m braiins_ratchet.cli cycle >/dev/null
echo
run_python -m braiins_ratchet.cli report
echo
run_python -m braiins_ratchet.cli next
}
cmd_raw_cycle() {
@ -74,9 +83,18 @@ cmd_watch() {
echo "At completion it writes reports/EXPERIMENT_LOG.md and reports/run-*.md."
echo
set +e
run_python -m braiins_ratchet.cli watch --cycles "$cycles" --interval-seconds "$interval_seconds"
local status=$?
set -e
if [[ "$status" -ne 0 && "$status" -ne 130 ]]; then
exit "$status"
fi
echo
run_python -m braiins_ratchet.cli report
echo
run_python -m braiins_ratchet.cli next
return "$status"
}
cmd_report() {
@ -115,10 +133,11 @@ cmd_explain() {
main() {
cd "$ROOT_DIR"
local command="${1:-help}"
local command="${1:-next}"
shift || true
case "$command" in
next) cmd_next "$@" ;;
setup) cmd_setup "$@" ;;
once) cmd_once "$@" ;;
watch) cmd_watch "$@" ;;

View file

@ -15,6 +15,7 @@ from .experiments import (
summarize_since,
write_retro_report,
)
from .guidance import build_operator_cockpit
from .monitor import run_cycle
from .ocean import fetch_snapshot
from .report import build_text_report
@ -139,6 +140,13 @@ def cmd_report(args: argparse.Namespace) -> int:
return 0
def cmd_next(_: argparse.Namespace) -> int:
with connect() as conn:
init_db(conn)
print(build_operator_cockpit(conn))
return 0
def cmd_experiments(_: argparse.Namespace) -> int:
if not EXPERIMENT_LOG.exists():
print("No experiment log yet. Run ./scripts/ratchet watch 2.")
@ -244,6 +252,9 @@ def build_parser() -> argparse.ArgumentParser:
report.add_argument("--samples", type=int, default=50)
report.set_defaults(func=cmd_report)
next_step = sub.add_parser("next", help="print exactly what the operator should do next")
next_step.set_defaults(func=cmd_next)
experiments = sub.add_parser("experiments", help="print the Karpathy-style experiment log")
experiments.set_defaults(func=cmd_experiments)

View file

@ -0,0 +1,179 @@
from __future__ import annotations
from datetime import UTC, datetime
from pathlib import Path
from .experiments import EXPERIMENT_LOG, REPORTS_DIR
from .storage import latest_market_snapshot, latest_ocean_snapshot, latest_proposal
def build_operator_cockpit(conn) -> str:
ocean = latest_ocean_snapshot(conn)
market = latest_market_snapshot(conn)
proposal = latest_proposal(conn)
latest_report = _latest_report()
running_runs = _running_runs()
freshness = _freshness_minutes(market.timestamp_utc if market else None)
is_fresh = freshness is not None and freshness <= 30
lines = [
"Braiins Ratchet Cockpit",
"",
"Situation",
f" Database: {'ready' if ocean or market or proposal else 'empty'}",
f" Latest OCEAN sample: {ocean.timestamp_utc if ocean else 'none'}",
f" Latest Braiins sample: {market.timestamp_utc if market else 'none'}",
f" Braiins sample freshness: {_freshness_text(freshness)}",
f" Latest strategy action: {proposal.action if proposal else 'none'}",
f" Latest run report: {latest_report or 'none yet'}",
f" Experiment ledger: {EXPERIMENT_LOG.relative_to(REPORTS_DIR.parent) if EXPERIMENT_LOG.exists() else 'none yet'}",
]
if running_runs:
lines.append(f" Ledger has unfinished run markers: {', '.join(running_runs)}")
lines.extend(["", "What You Do Now"])
lines.extend(
_next_steps(
has_ocean=ocean is not None,
has_market=market is not None,
is_fresh=is_fresh,
action=proposal.action if proposal else None,
)
)
lines.extend(["", "How To Interpret The Current Action"])
lines.extend(_action_explanation(proposal.action if proposal else None))
lines.extend(["", "Ratchet Rule"])
lines.extend(
[
" One run is not a verdict. One run is a measurement.",
" Change only one knob at a time: depth target, overpay cushion, canary spend, duration, or timing window.",
" Do not increase spend until multiple mature runs point in the same direction.",
]
)
lines.extend(["", "Safe Commands"])
lines.extend(
[
" ./scripts/ratchet next # read this cockpit",
" ./scripts/ratchet once # fetch one fresh sample and report",
" ./scripts/ratchet watch 2 # run a bounded 2-hour experiment",
" ./scripts/ratchet experiments # read the experiment ledger",
" ./scripts/ratchet report # read the latest raw human report",
]
)
return "\n".join(lines)
def _next_steps(has_ocean: bool, has_market: bool, is_fresh: bool, action: str | None) -> list[str]:
if not has_ocean or not has_market:
return [
" 1. Run: ./scripts/ratchet setup",
" 2. Run: ./scripts/ratchet once",
" 3. Then run: ./scripts/ratchet next",
" Reason: the cockpit needs at least one OCEAN sample and one Braiins sample.",
]
if not is_fresh:
return [
" 1. If a watch is currently running in another terminal, do nothing until it finishes.",
" 2. If no watch is running, run: ./scripts/ratchet once",
" 3. Then run: ./scripts/ratchet next",
" Reason: the latest Braiins sample is stale; do not interpret old price action as a current signal.",
]
if action == "manual_bid":
return [
" 1. Run: ./scripts/ratchet report",
" 2. Read the Plain English section.",
" 3. If you manually bid, keep spend tiny and write down the Braiins order parameters.",
" 4. After the order ends, wait through the maturity window before judging it.",
" Reason: manual_bid is the only profit-seeking signal, but execution is still manual.",
]
if action == "manual_canary":
return [
" 1. If a watch is currently running in another terminal, do nothing until it finishes.",
" 2. If no watch is running, run: ./scripts/ratchet watch 2",
" 3. After the watch finishes, run: ./scripts/ratchet next",
" 4. Read: ./scripts/ratchet experiments",
" Reason: manual_canary means the model sees a bounded learning opportunity, not proven profit.",
]
return [
" 1. If a watch is currently running in another terminal, do nothing until it finishes.",
" 2. If no watch is running and you want more data, run: ./scripts/ratchet watch 2",
" 3. If you are done for now, stop. No action is expected from you.",
" Reason: observe means the strategy did not find a useful action window.",
]
def _action_explanation(action: str | None) -> list[str]:
if action == "manual_bid":
return [
" manual_bid: stricter profit-seeking guardrails cleared.",
" This does not place an order. You still decide manually in Braiins.",
]
if action == "manual_canary":
return [
" manual_canary: a tiny research canary is inside the configured loss budget.",
" Treat it as buying information. It can lose money and still be scientifically useful.",
]
if action == "observe":
return [
" observe: do not bid.",
" The correct action is either wait, collect more samples, or change one research knob later.",
]
return [
" none: no strategy proposal exists yet.",
" Run ./scripts/ratchet once to create the first proposal.",
]
def _latest_report() -> str | None:
if not REPORTS_DIR.exists():
return None
reports = sorted(
(path for path in REPORTS_DIR.glob("*.md") if path.name != EXPERIMENT_LOG.name),
key=lambda path: path.stat().st_mtime,
reverse=True,
)
if not reports:
return None
return str(reports[0].relative_to(REPORTS_DIR.parent))
def _running_runs() -> list[str]:
if not EXPERIMENT_LOG.exists():
return []
current_run: str | None = None
running: list[str] = []
completed: set[str] = set()
for line in EXPERIMENT_LOG.read_text(encoding="utf-8").splitlines():
if line.startswith("## "):
current_run = line.removeprefix("## ").strip()
elif current_run and line.strip() == "- status: running":
running.append(current_run)
elif current_run and line.startswith("- status_update:"):
completed.add(current_run)
return [run for run in running if run not in completed]
def _freshness_minutes(timestamp_utc: str | None) -> int | None:
if not timestamp_utc:
return None
try:
parsed = datetime.fromisoformat(timestamp_utc.replace("Z", "+00:00"))
except ValueError:
return None
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=UTC)
age = datetime.now(UTC) - parsed.astimezone(UTC)
return max(0, int(age.total_seconds() // 60))
def _freshness_text(freshness: int | None) -> str:
if freshness is None:
return "unknown"
if freshness <= 30:
return f"fresh ({freshness} minutes old)"
return f"stale ({freshness} minutes old)"

104
tests/test_guidance.py Normal file
View file

@ -0,0 +1,104 @@
from decimal import Decimal
from datetime import UTC, datetime
import sqlite3
import unittest
from braiins_ratchet.guidance import build_operator_cockpit
from braiins_ratchet.models import CandidateOrder, MarketSnapshot, OceanSnapshot, StrategyProposal
from braiins_ratchet.storage import init_db, save_market_snapshot, save_ocean_snapshot, save_proposal
class GuidanceTests(unittest.TestCase):
def test_empty_database_tells_operator_to_setup_and_sample(self) -> None:
conn = sqlite3.connect(":memory:")
init_db(conn)
text = build_operator_cockpit(conn)
self.assertIn("Braiins Ratchet Cockpit", text)
self.assertIn("./scripts/ratchet setup", text)
self.assertIn("./scripts/ratchet once", text)
def test_manual_canary_tells_operator_to_watch_not_escalate(self) -> None:
conn = sqlite3.connect(":memory:")
init_db(conn)
save_ocean_snapshot(
conn,
OceanSnapshot(
timestamp_utc=datetime.now(UTC).isoformat(timespec="seconds"),
pool_hashrate_eh_s=Decimal("16.95"),
),
)
save_market_snapshot(
conn,
MarketSnapshot(
timestamp_utc=datetime.now(UTC).isoformat(timespec="seconds"),
best_price_btc_per_eh_day=Decimal("0.48031"),
source="braiins-public",
),
)
save_proposal(
conn,
StrategyProposal(
action="manual_canary",
reason="inside research loss budget",
order=CandidateOrder(
price_btc_per_eh_day=Decimal("0.48031"),
spend_btc=Decimal("0.00010"),
duration_minutes=180,
),
breakeven_btc_per_eh_day=Decimal("0.46634"),
expected_reward_btc=Decimal("0.000097"),
expected_net_btc=Decimal("-0.000003"),
score_btc=Decimal("-0.000037"),
maturity_note="treat canary as immature",
),
)
text = build_operator_cockpit(conn)
self.assertIn("Latest strategy action: manual_canary", text)
self.assertIn("./scripts/ratchet watch 2", text)
self.assertIn("not proven profit", text)
def test_stale_market_data_routes_operator_to_once(self) -> None:
conn = sqlite3.connect(":memory:")
init_db(conn)
save_ocean_snapshot(
conn,
OceanSnapshot(
timestamp_utc="2000-01-01T00:00:00+00:00",
pool_hashrate_eh_s=Decimal("16.95"),
),
)
save_market_snapshot(
conn,
MarketSnapshot(
timestamp_utc="2000-01-01T00:00:01+00:00",
best_price_btc_per_eh_day=Decimal("0.48031"),
source="braiins-public",
),
)
save_proposal(
conn,
StrategyProposal(
action="manual_canary",
reason="inside research loss budget",
order=None,
breakeven_btc_per_eh_day=Decimal("0.46634"),
expected_reward_btc=Decimal("0.000097"),
expected_net_btc=Decimal("-0.000003"),
score_btc=Decimal("-0.000037"),
maturity_note="treat canary as immature",
),
)
text = build_operator_cockpit(conn)
self.assertIn("Braiins sample freshness: stale", text)
self.assertIn("run: ./scripts/ratchet once", text)
self.assertIn("do not interpret old price action", text)
if __name__ == "__main__":
unittest.main()