Separate research canaries from profit bids

This commit is contained in:
saymrwulf 2026-04-25 18:31:04 +02:00
parent d9b36082ab
commit 6096434ef4
13 changed files with 120 additions and 29 deletions

View file

@ -19,6 +19,12 @@ Maximize expected BTC profit, or minimize BTC loss, for manually buying BTC hash
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.
Recommendations have different meanings:
- `observe`: no action.
- `manual_canary`: bounded information-buying; expected loss is allowed if it is inside the canary budget.
- `manual_bid`: profit-seeking manual action; stricter discount and score guardrails apply.
## Hard Guardrails
- No code path places, modifies, or cancels Braiins orders.
@ -41,4 +47,3 @@ 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.

View file

@ -6,6 +6,7 @@ The first implementation is deliberately conservative:
- The code never places, modifies, or cancels Braiins orders.
- The default strategy emits recommendations only.
- The strategy distinguishes `manual_canary` research experiments from `manual_bid` profit-seeking opportunities.
- The Braiins integration accepts a watcher-only token only.
- All mutable runtime state stays inside this repository under `data/`.
- The Git branch is `master`.
@ -54,6 +55,12 @@ The collector first uses unauthenticated public web endpoints from `hashpower.br
Watcher-only tokens are only relevant if we later need account-specific read-only data such as your private balance, historical fills, or order status. Owner tokens remain out of scope.
## Recommendation States
- `observe`: do nothing.
- `manual_canary`: a tiny manually executed research experiment is within the configured loss budget.
- `manual_bid`: a manually executed bid clears profit-seeking discount and risk guardrails.
## Documentation
- `PROGRAM.md`: research charter and ratchet rules.

View file

@ -11,6 +11,8 @@ dashboard_url = "https://ocean.xyz/dashboard"
max_manual_order_btc = "0.00025"
max_daily_spend_btc = "0.00050"
max_price_btc_per_eh_day = "0.42"
max_canary_price_btc_per_eh_day = "0.52"
max_canary_expected_loss_btc = "0.000025"
min_discount_to_breakeven = "0.08"
min_duration_minutes = 30
max_duration_minutes = 720
@ -20,4 +22,3 @@ recommend_only = true
target_duration_minutes = 180
target_spend_btc = "0.00010"
risk_lambda = "0.35"

View file

@ -107,7 +107,7 @@ Evaluates the latest stored OCEAN and Braiins snapshots against `config.example.
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`, `manual_canary`, or `manual_bid`. It never places an order.
## `report`

View file

@ -64,7 +64,15 @@ If it says:
Strategy action: manual_bid
```
The strategy thinks a small manual canary bid clears the configured guardrails. You still decide manually in the Braiins UI.
The strategy thinks a small manual profit-seeking bid clears the configured discount guardrails. You still decide manually in the Braiins UI.
If it says:
```text
Strategy action: manual_canary
```
The market is not a money-printer setup, but the expected loss is inside the configured research budget. This is the scientific mode: a tiny canary may be useful to learn about execution, timing, OCEAN accounting, stale/reject behavior, and TIDES maturity.
Important fields:
@ -95,7 +103,7 @@ Stop early with `Ctrl-C`.
## When To Act
Only consider manual action when all of these are true:
Only consider a profit-seeking manual bid when all of these are true:
- `Strategy action: manual_bid`.
- `score_btc` is positive.
@ -105,6 +113,14 @@ Only consider manual action when all of these are true:
Do not act because OCEAN is "due". Block discovery is memoryless.
Only consider a research canary when all of these are true:
- `Strategy action: manual_canary`.
- `proposed_spend_btc` is acceptable to lose.
- `expected_net_btc` is not worse than the configured canary loss budget.
- You are explicitly buying information, not pretending the edge is proven.
- You can wait through the maturity window before judging the result.
## How Long To Wait After A Manual Canary
Use the report's `maturity` line.

View file

@ -8,7 +8,7 @@ Run these commands from the repository root:
./scripts/ratchet once
```
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. `observe` means no action is recommended. `manual_canary` means a tiny research experiment is inside the configured loss budget. `manual_bid` means a manually placed bid clears profit-seeking discount and risk guardrails.
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.

View file

@ -38,6 +38,8 @@ class GuardrailsConfig:
max_manual_order_btc: Decimal
max_daily_spend_btc: Decimal
max_price_btc_per_eh_day: Decimal
max_canary_price_btc_per_eh_day: Decimal
max_canary_expected_loss_btc: Decimal
min_discount_to_breakeven: Decimal
min_duration_minutes: int
max_duration_minutes: int
@ -81,6 +83,12 @@ def load_config(path: Path | None = None) -> AppConfig:
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"),
max_canary_price_btc_per_eh_day=_decimal(
guardrails.get("max_canary_price_btc_per_eh_day"), "0"
),
max_canary_expected_loss_btc=_decimal(
guardrails.get("max_canary_expected_loss_btc"), "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)),

View file

@ -11,16 +11,8 @@ def validate_order(
guardrails: GuardrailsConfig,
breakeven_btc_per_eh_day: Decimal | None,
) -> list[str]:
violations: list[str] = []
violations = validate_order_structure(order, guardrails)
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
@ -28,10 +20,6 @@ def validate_order(
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:
@ -45,6 +33,25 @@ def validate_order(
return violations
def validate_order_structure(order: CandidateOrder, guardrails: GuardrailsConfig) -> 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 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}")
return violations
def token_looks_unsafe(token: str) -> bool:
lowered = token.lower()
unsafe_markers = ("owner", "admin", "trade", "write", "order", "secret")

View file

@ -56,7 +56,7 @@ class CandidateOrder:
@dataclass(frozen=True)
class StrategyProposal:
action: Literal["observe", "manual_bid"]
action: Literal["observe", "manual_canary", "manual_bid"]
reason: str
order: CandidateOrder | None
breakeven_btc_per_eh_day: Decimal | None

View file

@ -4,7 +4,7 @@ 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 .guardrails import validate_order, validate_order_structure
from .models import CandidateOrder, MarketSnapshot, OceanSnapshot, StrategyProposal
@ -53,10 +53,10 @@ def propose(
score = expected_net - downside_penalty(expected_reward, config.strategy.risk_lambda)
violations = validate_order(order, config.guardrails, breakeven)
if violations:
if not violations and score > 0:
return StrategyProposal(
action="observe",
reason="guardrail blocked: " + "; ".join(violations),
action="manual_bid",
reason="profit-seeking bid clears discount guardrails and positive risk-adjusted score",
order=order,
breakeven_btc_per_eh_day=breakeven,
expected_reward_btc=expected_reward,
@ -65,10 +65,15 @@ def propose(
maturity_note=_maturity_note(ocean),
)
if score <= 0:
canary_violations = _validate_canary(order, config, expected_net)
if not canary_violations:
return StrategyProposal(
action="observe",
reason=f"risk-adjusted score is not positive ({score})",
action="manual_canary",
reason=(
"bounded research canary: profit guardrails not cleared, "
f"but expected_net={expected_net} is within loss budget "
f"{config.guardrails.max_canary_expected_loss_btc}"
),
order=order,
breakeven_btc_per_eh_day=breakeven,
expected_reward_btc=expected_reward,
@ -78,8 +83,12 @@ def propose(
)
return StrategyProposal(
action="manual_bid",
reason="market price clears configured guardrails and positive risk-adjusted score",
action="observe",
reason=(
"no experiment recommended: "
+ "; ".join(violations + canary_violations)
+ f"; risk_adjusted_score={score}"
),
order=order,
breakeven_btc_per_eh_day=breakeven,
expected_reward_btc=expected_reward,
@ -89,6 +98,24 @@ def propose(
)
def _validate_canary(order: CandidateOrder, config: AppConfig, expected_net: Decimal) -> list[str]:
violations = validate_order_structure(order, config.guardrails)
if (
config.guardrails.max_canary_price_btc_per_eh_day > 0
and order.price_btc_per_eh_day > config.guardrails.max_canary_price_btc_per_eh_day
):
violations.append(
"price exceeds max_canary_price_btc_per_eh_day="
f"{config.guardrails.max_canary_price_btc_per_eh_day}"
)
if expected_net < -config.guardrails.max_canary_expected_loss_btc:
violations.append(
f"expected loss {abs(expected_net)} exceeds canary budget "
f"{config.guardrails.max_canary_expected_loss_btc}"
)
return violations
def _observe(reason: str) -> StrategyProposal:
return StrategyProposal(
action="observe",

View file

@ -12,6 +12,8 @@ class GuardrailTests(unittest.TestCase):
max_manual_order_btc=Decimal("0.00025"),
max_daily_spend_btc=Decimal("0.00050"),
max_price_btc_per_eh_day=Decimal("0.42"),
max_canary_price_btc_per_eh_day=Decimal("0.52"),
max_canary_expected_loss_btc=Decimal("0.000025"),
min_discount_to_breakeven=Decimal("0.08"),
min_duration_minutes=30,
max_duration_minutes=720,

View file

@ -46,6 +46,8 @@ def _config() -> AppConfig:
max_manual_order_btc=Decimal("0.00025"),
max_daily_spend_btc=Decimal("0.00050"),
max_price_btc_per_eh_day=Decimal("0.42"),
max_canary_price_btc_per_eh_day=Decimal("0.52"),
max_canary_expected_loss_btc=Decimal("0.000025"),
min_discount_to_breakeven=Decimal("0.08"),
min_duration_minutes=30,
max_duration_minutes=720,

View file

@ -31,6 +31,20 @@ class StrategyTests(unittest.TestCase):
proposal = propose(config, ocean, market)
self.assertEqual(proposal.action, "manual_bid")
def test_strategy_can_emit_manual_canary_near_breakeven(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.46"),
)
proposal = propose(config, ocean, market)
self.assertEqual(proposal.action, "manual_canary")
def _config() -> AppConfig:
return AppConfig(
@ -45,6 +59,8 @@ def _config() -> AppConfig:
max_manual_order_btc=Decimal("0.00025"),
max_daily_spend_btc=Decimal("0.00050"),
max_price_btc_per_eh_day=Decimal("0.42"),
max_canary_price_btc_per_eh_day=Decimal("0.52"),
max_canary_expected_loss_btc=Decimal("0.000025"),
min_discount_to_breakeven=Decimal("0.08"),
min_duration_minutes=30,
max_duration_minutes=720,