mirror of
https://github.com/saymrwulf/BraiinsRatchet.git
synced 2026-05-14 20:37:52 +00:00
Add operator cockpit guidance
This commit is contained in:
parent
fbd6311800
commit
b9fda29c9a
9 changed files with 417 additions and 6 deletions
|
|
@ -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
69
START_HERE.md
Normal 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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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 "$@" ;;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
179
src/braiins_ratchet/guidance.py
Normal file
179
src/braiins_ratchet/guidance.py
Normal 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
104
tests/test_guidance.py
Normal 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()
|
||||
Loading…
Reference in a new issue