mirror of
https://github.com/saymrwulf/BraiinsRatchet.git
synced 2026-05-14 20:37:52 +00:00
Prevent repeated watch loops
This commit is contained in:
parent
a4a19f30d8
commit
70beecd615
7 changed files with 214 additions and 37 deletions
|
|
@ -39,6 +39,15 @@ You do not need to babysit it. It will:
|
|||
|
||||
If you want the technical report, run `./scripts/ratchet report`. The normal workflow intentionally shows the cockpit first.
|
||||
|
||||
After a watch finishes, the cockpit enters a post-watch cooldown. That is deliberate.
|
||||
|
||||
Post-watch cooldown means:
|
||||
|
||||
1. The current experimental stage is complete.
|
||||
2. Starting another identical watch immediately is not useful ratcheting.
|
||||
3. The run report is the evidence artifact.
|
||||
4. The next planned touch is a later fresh sample, usually `./scripts/ratchet once`.
|
||||
|
||||
## Research Pathway
|
||||
|
||||
The cockpit has two different time horizons:
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ At the end of a normal watch, inspect the ratchet record:
|
|||
|
||||
The ledger is the main artifact. It says what was tested, how long it ran, what actions the strategy would have considered, and what adaptation should be considered next.
|
||||
|
||||
After a completed watch, the cockpit should not immediately recommend another identical watch. It enters post-watch cooldown, which means the current stage is complete and the next useful operator touch is a later fresh sample.
|
||||
|
||||
If a run already happened before automatic bookkeeping was available, reconstruct it from the stored SQLite snapshots:
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -25,3 +25,25 @@ Karpathy-style ratchet rule: every run states a hypothesis, collects data, score
|
|||
- action_counts: manual_canary=24
|
||||
- report: reports/retro-2026-04-25T19-08-00-00-00.md
|
||||
- adaptation: The run repeatedly found bounded canary conditions, but modeled net was negative on average. Next ratchet: do not escalate spend; test a smaller depth target or a lower overpay cushion.
|
||||
|
||||
## run-20260427T135327Z-222826
|
||||
|
||||
- status: running
|
||||
- started_utc: 2026-04-27T13:53:27+00:00
|
||||
- planned_cycles: 24
|
||||
- interval_seconds: 300
|
||||
- planned_duration_minutes: 120.0
|
||||
- hypothesis: Depth-aware fillable price plus a small overpay cushion is a better canary trigger than raw best ask.
|
||||
- plan: collect public Braiins depth, collect OCEAN state, compute shadow canary, store every proposal.
|
||||
- operator_action: none by default; manual action only if report later says manual_canary or manual_bid and operator agrees.
|
||||
|
||||
- status_update: completed
|
||||
- ended_utc: 2026-04-27T15:48:52+00:00
|
||||
- collected_samples: 24
|
||||
- action_counts: manual_canary=24
|
||||
- strategy_price_min_avg_max: 0.47763 / 0.4798921698412225 / 0.48492
|
||||
- expected_net_min_avg_max_btc: -0.00000383101148933686517479144602 / -0.000002821503981988926325106899014 / -0.00000236319764547711127977695701
|
||||
- latest_action: manual_canary
|
||||
- latest_reason: bounded research canary: profit guardrails not cleared, but expected_net=-0.00000238363529903760002629072482 is within loss budget 0.000025
|
||||
- report: reports/run-20260427T135327Z-222826.md
|
||||
- adaptation: The run repeatedly found bounded canary conditions, but modeled net was negative on average. Next ratchet: do not escalate spend; test a smaller depth target or a lower overpay cushion.
|
||||
|
|
|
|||
73
reports/run-20260427T135327Z-222826.md
Normal file
73
reports/run-20260427T135327Z-222826.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# run-20260427T135327Z-222826
|
||||
|
||||
## Ratchet Question
|
||||
|
||||
Depth-aware fillable price plus a small overpay cushion is a better canary trigger than raw best ask.
|
||||
|
||||
## Run Summary
|
||||
|
||||
- started_utc: 2026-04-27T13:53:27+00:00
|
||||
- ended_utc: 2026-04-27T15:48:52+00:00
|
||||
- planned_cycles: 24
|
||||
- interval_seconds: 300
|
||||
- collected_samples: 24
|
||||
- first_sample_utc: 2026-04-27T13:53:28+00:00
|
||||
- last_sample_utc: 2026-04-27T15:48:52+00:00
|
||||
- action_counts: manual_canary=24
|
||||
- strategy_price_min_avg_max: 0.47763 / 0.4798921698412225 / 0.48492
|
||||
- expected_net_min_avg_max_btc: -0.00000383101148933686517479144602 / -0.000002821503981988926325106899014 / -0.00000236319764547711127977695701
|
||||
|
||||
## Interpretation
|
||||
|
||||
The run repeatedly found bounded canary conditions, but modeled net was negative on average. Next ratchet: do not escalate spend; test a smaller depth target or a lower overpay cushion.
|
||||
|
||||
## Operator Reading
|
||||
|
||||
This run collected 24 public Braiins market samples. The strategy outcomes were: manual_canary=24. Expected net ranged from -0.00000383101148933686517479144602 to -0.00000236319764547711127977695701 BTC. For ratcheting, do not ask whether one line was green or red. Ask whether this run changed one control knob: depth target, overpay cushion, canary spend, duration, or timing window.
|
||||
|
||||
## Current Full Report Context
|
||||
|
||||
The block below is the latest complete human report available when this markdown was written. For retroactive reports, the authoritative reconstructed run data is the summary above.
|
||||
|
||||
```text
|
||||
Braiins Ratchet Report
|
||||
|
||||
OCEAN snapshot: 2026-04-27T15:48:52+00:00
|
||||
pool_hashrate_eh_s: 15.51
|
||||
network_difficulty_t: 135.59
|
||||
share_log_window_t: 1084.76
|
||||
avg_block_time_hours: 9
|
||||
|
||||
Braiins market snapshot: 2026-04-27T15:48:52+00:00
|
||||
status: SPOT_INSTRUMENT_STATUS_ACTIVE
|
||||
best_bid_btc_per_eh_day: 0.50343
|
||||
best_ask_btc_per_eh_day: 0.46075
|
||||
fillable_target_ph: 2
|
||||
fillable_price_btc_per_eh_day: 0.46773
|
||||
suggested_bid_btc_per_eh_day: 0.47773
|
||||
last_price_btc_per_eh_day: 0.47933997316187
|
||||
available_hashrate_eh_s: 1.91368833114474
|
||||
sampled_strategy_price_count: 50
|
||||
sampled_strategy_price_min_avg_max: 0.46103 / 0.4792744415237868 / 0.48492
|
||||
|
||||
Strategy action: manual_canary
|
||||
reason: bounded research canary: profit guardrails not cleared, but expected_net=-0.00000238363529903760002629072482 is within loss budget 0.000025
|
||||
breakeven_btc_per_eh_day: 0.4663426590859076733944013201
|
||||
expected_reward_btc: 0.00009761636470096239997370927518
|
||||
expected_net_btc: -0.00000238363529903760002629072482
|
||||
score_btc: -0.00003654936294437444001708897113
|
||||
maturity: treat canary as immature for about 72 hours after spend
|
||||
proposed_price_btc_per_eh_day: 0.47773
|
||||
proposed_spend_btc: 0.00010
|
||||
proposed_duration_minutes: 180
|
||||
implied_hashrate_eh_s: 0.001674586063257488539551629582
|
||||
|
||||
Plain English
|
||||
Decision: a tiny manual research canary is allowed by the loss budget, but this is not a profit signal.
|
||||
Market depth: the visible best ask is 0.46075, but enough depth for 2 PH/s starts at 0.46773 (gap 0.00698).
|
||||
Price check: proposed price is 0.47773; estimated breakeven is 0.4663426590859076733944013201; edge is -0.0113873409140923266055986799 BTC/EH/day.
|
||||
Manual canary card: spend 0.00010 BTC (~10000 sats), duration 180 minutes, estimated speed 1.674586063257488539551629582 PH/s.
|
||||
Expected result: -0.00000238363529903760002629072482 BTC (~-238 sats) before luck; this is a model estimate, not a promise.
|
||||
Wait time: treat canary as immature for about 72 hours after spend.
|
||||
Rule: manual_canary means buying information with bounded downside; manual_bid means the stricter profit-seeking guardrails cleared.
|
||||
```
|
||||
|
|
@ -97,7 +97,7 @@ cmd_watch() {
|
|||
echo
|
||||
echo "Watch finished. Reading the cockpit now."
|
||||
echo
|
||||
run_python -m braiins_ratchet.cli next
|
||||
BRAIINS_RATCHET_IGNORE_PROCESS_WATCH=1 run_python -m braiins_ratchet.cli next
|
||||
return "$status"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import subprocess
|
|||
from .experiments import ACTIVE_WATCH, EXPERIMENT_LOG, REPORTS_DIR
|
||||
from .storage import latest_market_snapshot, latest_ocean_snapshot, latest_proposal
|
||||
|
||||
POST_WATCH_COOLDOWN_MINUTES = 360
|
||||
|
||||
|
||||
def build_operator_cockpit(conn) -> str:
|
||||
ocean = latest_ocean_snapshot(conn)
|
||||
|
|
@ -17,6 +19,7 @@ def build_operator_cockpit(conn) -> str:
|
|||
latest_report = _latest_report()
|
||||
running_runs = _running_runs()
|
||||
active_watch = _active_watch()
|
||||
completed_watch = _recent_completed_watch(latest_report, market.timestamp_utc if market else None)
|
||||
freshness = _freshness_minutes(market.timestamp_utc if market else None)
|
||||
is_fresh = freshness is not None and freshness <= 30
|
||||
|
||||
|
|
@ -32,6 +35,7 @@ def build_operator_cockpit(conn) -> str:
|
|||
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'}",
|
||||
f" Active watch: {active_watch or 'none detected'}",
|
||||
f" Research stage: {_research_stage(active_watch, completed_watch)}",
|
||||
]
|
||||
|
||||
if running_runs:
|
||||
|
|
@ -41,6 +45,7 @@ def build_operator_cockpit(conn) -> str:
|
|||
lines.extend(
|
||||
_do_this_now(
|
||||
active_watch=active_watch,
|
||||
completed_watch=completed_watch,
|
||||
has_ocean=ocean is not None,
|
||||
has_market=market is not None,
|
||||
is_fresh=is_fresh,
|
||||
|
|
@ -51,6 +56,7 @@ def build_operator_cockpit(conn) -> str:
|
|||
lines.extend(
|
||||
_pathway_forecast(
|
||||
active_watch=active_watch,
|
||||
completed_watch=completed_watch,
|
||||
has_ocean=ocean is not None,
|
||||
has_market=market is not None,
|
||||
is_fresh=is_fresh,
|
||||
|
|
@ -83,6 +89,7 @@ def build_operator_cockpit(conn) -> str:
|
|||
|
||||
def _do_this_now(
|
||||
active_watch: str | None,
|
||||
completed_watch: tuple[str, int] | None,
|
||||
has_ocean: bool,
|
||||
has_market: bool,
|
||||
is_fresh: bool,
|
||||
|
|
@ -97,6 +104,19 @@ def _do_this_now(
|
|||
" ./scripts/ratchet",
|
||||
]
|
||||
|
||||
if completed_watch and action == "manual_canary":
|
||||
report_path, age_minutes = completed_watch
|
||||
remaining = max(0, POST_WATCH_COOLDOWN_MINUTES - age_minutes)
|
||||
return [
|
||||
" STOP.",
|
||||
f" The latest 2-hour watch already finished {age_minutes} minutes ago.",
|
||||
f" Report written: {report_path}",
|
||||
" Do not start another identical watch now.",
|
||||
f" Next planned operator touch: after about {remaining} minutes, run exactly:",
|
||||
" ./scripts/ratchet once",
|
||||
" Reason: this ratchet stage is complete; repeating it immediately would be loop-chasing, not research.",
|
||||
]
|
||||
|
||||
if not has_ocean or not has_market:
|
||||
return [
|
||||
" Run exactly:",
|
||||
|
|
@ -140,6 +160,7 @@ def _do_this_now(
|
|||
|
||||
def _pathway_forecast(
|
||||
active_watch: str | None,
|
||||
completed_watch: tuple[str, int] | None,
|
||||
has_ocean: bool,
|
||||
has_market: bool,
|
||||
is_fresh: bool,
|
||||
|
|
@ -153,6 +174,15 @@ def _pathway_forecast(
|
|||
" Longterm, possible: adjust one strategy knob if the report says the run taught us something.",
|
||||
]
|
||||
|
||||
if completed_watch and action == "manual_canary":
|
||||
report_path, _age_minutes = completed_watch
|
||||
return [
|
||||
" Planning probabilities are workflow estimates, not profit probabilities.",
|
||||
f" Immediate, certain: stop this stage and keep {report_path} as the evidence artifact.",
|
||||
" Midterm, likely: after cooldown, refresh with one once command and compare against this report.",
|
||||
" Longterm, possible: adjust exactly one knob only if repeated reports show the same pattern.",
|
||||
]
|
||||
|
||||
if not has_ocean or not has_market:
|
||||
return [
|
||||
" Planning probabilities are workflow estimates, not profit probabilities.",
|
||||
|
|
@ -228,6 +258,32 @@ def _latest_report() -> str | None:
|
|||
return str(reports[0].relative_to(REPORTS_DIR.parent))
|
||||
|
||||
|
||||
def _recent_completed_watch(latest_report: str | None, latest_market_timestamp: str | None) -> tuple[str, int] | None:
|
||||
if latest_report is None:
|
||||
return None
|
||||
report_path = REPORTS_DIR.parent / latest_report
|
||||
if not report_path.name.startswith("run-") or not report_path.exists():
|
||||
return None
|
||||
market_dt = _parse_utc(latest_market_timestamp)
|
||||
if market_dt is not None and report_path.stat().st_mtime < market_dt.timestamp():
|
||||
return None
|
||||
age_seconds = datetime.now(UTC).timestamp() - report_path.stat().st_mtime
|
||||
age_minutes = max(0, int(age_seconds // 60))
|
||||
if age_minutes > POST_WATCH_COOLDOWN_MINUTES:
|
||||
return None
|
||||
return latest_report, age_minutes
|
||||
|
||||
|
||||
def _research_stage(active_watch: str | None, completed_watch: tuple[str, int] | None) -> str:
|
||||
if active_watch:
|
||||
return "watch running"
|
||||
if completed_watch:
|
||||
report_path, age_minutes = completed_watch
|
||||
remaining = max(0, POST_WATCH_COOLDOWN_MINUTES - age_minutes)
|
||||
return f"post-watch cooldown ({remaining} minutes left, report {report_path})"
|
||||
return "ready"
|
||||
|
||||
|
||||
def _running_runs() -> list[str]:
|
||||
if not EXPERIMENT_LOG.exists():
|
||||
return []
|
||||
|
|
@ -266,6 +322,8 @@ def _active_watch_from_state_file() -> str | None:
|
|||
|
||||
|
||||
def _active_watch_from_process_table() -> str | None:
|
||||
if os.environ.get("BRAIINS_RATCHET_IGNORE_PROCESS_WATCH") == "1":
|
||||
return None
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
["ps", "-axo", "pid=,command="],
|
||||
|
|
@ -302,6 +360,14 @@ def _pid_exists(pid: int) -> bool:
|
|||
|
||||
|
||||
def _freshness_minutes(timestamp_utc: str | None) -> int | None:
|
||||
parsed = _parse_utc(timestamp_utc)
|
||||
if parsed is None:
|
||||
return None
|
||||
age = datetime.now(UTC) - parsed
|
||||
return max(0, int(age.total_seconds() // 60))
|
||||
|
||||
|
||||
def _parse_utc(timestamp_utc: str | None) -> datetime | None:
|
||||
if not timestamp_utc:
|
||||
return None
|
||||
try:
|
||||
|
|
@ -310,8 +376,7 @@ def _freshness_minutes(timestamp_utc: str | None) -> int | None:
|
|||
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))
|
||||
return parsed.astimezone(UTC)
|
||||
|
||||
|
||||
def _freshness_text(freshness: int | None) -> str:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from datetime import UTC, datetime
|
|||
import sqlite3
|
||||
import unittest
|
||||
|
||||
from braiins_ratchet.guidance import build_operator_cockpit
|
||||
from braiins_ratchet.guidance import _do_this_now, _pathway_forecast, 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
|
||||
|
||||
|
|
@ -66,44 +66,50 @@ class GuidanceTests(unittest.TestCase):
|
|||
self.assertIn("Longterm, possible", 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",
|
||||
),
|
||||
lines = _do_this_now(
|
||||
active_watch=None,
|
||||
completed_watch=None,
|
||||
has_ocean=True,
|
||||
has_market=True,
|
||||
is_fresh=False,
|
||||
action="manual_canary",
|
||||
)
|
||||
|
||||
text = build_operator_cockpit(conn)
|
||||
text = "\n".join(lines)
|
||||
|
||||
self.assertIn("Braiins sample freshness: stale", text)
|
||||
self.assertIn("./scripts/ratchet once", text)
|
||||
self.assertIn("latest Braiins sample is stale", text)
|
||||
self.assertIn("DO THIS NOW", text)
|
||||
self.assertIn("if the fresh state still says manual_canary", text)
|
||||
|
||||
def test_recent_completed_watch_stops_identical_watch_loop(self) -> None:
|
||||
lines = _do_this_now(
|
||||
active_watch=None,
|
||||
completed_watch=("reports/run-example.md", 4),
|
||||
has_ocean=True,
|
||||
has_market=True,
|
||||
is_fresh=True,
|
||||
action="manual_canary",
|
||||
)
|
||||
|
||||
text = "\n".join(lines)
|
||||
|
||||
self.assertIn("STOP.", text)
|
||||
self.assertIn("Do not start another identical watch now.", text)
|
||||
self.assertIn("./scripts/ratchet once", text)
|
||||
|
||||
def test_recent_completed_watch_forecast_enters_cooldown(self) -> None:
|
||||
lines = _pathway_forecast(
|
||||
active_watch=None,
|
||||
completed_watch=("reports/run-example.md", 4),
|
||||
has_ocean=True,
|
||||
has_market=True,
|
||||
is_fresh=True,
|
||||
action="manual_canary",
|
||||
)
|
||||
|
||||
text = "\n".join(lines)
|
||||
|
||||
self.assertIn("Immediate, certain: stop this stage", text)
|
||||
self.assertIn("Midterm, likely: after cooldown", text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
Loading…
Reference in a new issue