Datum: 2026-05-09 (UTC 12:05)
Status: Scope-Phase, kein Code, kein Apply, kein Worker. Read-only Planung.
G10-4.2 lifted die dry_run=false-Reject-Bedingung im Bot-Handler _handle_apply_profile_testnet und verdrahtet erfolgreiche Validation mit RuntimeConfigWriter.apply(). Der laufende Bot übernimmt den Override beim nächsten Cycle-Snapshot via bestehender G10-4.1-Etappe-2-Infrastruktur. Keine weiteren Bot-Code-Pfade ändern sich.
| Etappe | Inhalt | Live-Action | Backup-Pflicht |
|---|---|---|---|
| G10-4.2a | Bot-Handler-Branch + Idempotency-Helper im Writer + neue Tests; isolierter Test-Run; Commit | nein | nein (rein lokal) |
| G10-4.2b | Backup-Checkpoint + Container-Sync command_worker.py (+ ggf. Writer) + Worker --once mit echtem dry_run=false-Command + Post-Verify (1–2 Cycles bis Bot-Heartbeat den Override widerspiegelt) |
ja | ja, Pflicht |
Begründung: Dieselbe Aufteilung wie G10-3b/c. G10-4.2a kann lokal getestet, gereviewt, gecommittet werden ohne irgendeinen Live-Effekt. G10-4.2b ist ein eigener kontrollierter Aktivierungs-Schritt.
| Datei | Änderung | Umfang |
|---|---|---|
trading/command_worker.py |
_handle_apply_profile_testnet: dry_run=false-Pfad nach Validation-Cascade ergänzen. Aufruf RuntimeConfigWriter.apply(...), Idempotenz-Check, Result-Composition. |
~120 LoC + ~30 LoC für 2 neue Helper |
trading/runtime_config_writer.py |
Optional: neue Method apply_idempotent(...) oder Idempotency-Pre-Check als Helper-Function im Writer-Modul. Gibt ApplyResult(ok=True, no_effect=True) zurück, wenn Inhalt bereits identisch. |
~40 LoC + ~5 Tests |
trading/tests/test_g10_4_2_apply_runtime_config.py |
NEU — 30+ Tests | ~700 LoC |
trading/tests/test_g10_3b_apply_profile_testnet.py |
Update — bestehender Test test_dry_run_false_rejected muss angepasst werden. Plus neue Tests, dass dry_run=true unverändert funktioniert. |
~50 LoC |
trading/tests/run_g10_4_2_tests.sh |
NEU — Sibling-Script analog G10-3b-Pattern | ~80 LoC |
Bot-Side main.py/risk_manager.py/etc.: NICHT angefasst. PHP-Side: NICHT angefasst (G10-3.5 hat createApplyCommand bereits gebaut).
_handle_apply_profile_testnet(claim, payload):
audit_emit("apply_started") # bestehender state-transition-event
# 1. Validation cascade (UNCHANGED from G10-3b/c):
ctx = {...}
_g10_3b_validate_payload_basic(payload, ctx)
profile, risk_rows = _g10_3b_load_profile(...)
_g10_3b_validate_profile_state(profile, payload, ctx)
_g10_3b_validate_schema(payload, ctx)
# 2. Branch on dry_run:
if payload["dry_run"] is True:
return _g10_3b_dry_run_succeed(...) # G10-3b path UNCHANGED
# 3. NEW: dry_run=false path
# Idempotency pre-check via canonical sha256:
new_sha = sha256(canonical_json(summary, risk))
existing_sha = read_existing_target_sha256()
if existing_sha == new_sha:
audit_emit("apply_succeeded", {**ctx, "no_effect": True})
return {"ok": True, "no_effect": True, ...}
# 4. Mainnet-Guard: Writer enforces L1 (BINANCE_TESTNET=True);
# handler does not duplicate, but catches MainnetBlockedError.
# 5. Call writer:
apply_result = writer.apply(summary, risk, meta)
# 6. ApplyResult handling:
if not apply_result.ok:
audit_emit("apply_failed")
raise ValueError(f"apply_writer_failed: {apply_result.error}")
# 7. Optional post-write verify (re-read sha256):
if sha256(target) != apply_result.sha256:
restore_from_backup(...)
audit_emit("apply_rolled_back")
raise ValueError("apply_post_write_sha_mismatch")
# 8. Success — split apply_effective vs stored_only:
audit_emit("apply_succeeded", {
...ctx,
"applied_keys": all_13_keys,
"apply_effective_keys": [9 keys per ActiveConfigProvider.APPLY_EFFECTIVE],
"stored_only_keys": [4 keys = log_level, decision_log_verbosity, daily/weekly_loss_limit_pct],
"previous_values": old_runtime_config_or_empty,
"new_values": {summary, risk},
"runtime_config_path": apply_result.target_path,
"backup_path": apply_result.backup_path,
"sha256": apply_result.sha256,
"runtime_mutation": True,
"would_apply": True,
"handler_phase": "G10-4.2"
})
return {
"ok": True, "dry_run": False, "validated": True,
"would_apply": True, "runtime_mutation": True,
"applied_keys": ..., "runtime_config_path": ...,
"backup_path": ..., "sha256": ...,
"handler_phase": "G10-4.2", "elapsed_ms": ...
}
| Layer | Check | Implementiert in |
|---|---|---|
| L1 | Settings.BINANCE_TESTNET is True |
RuntimeConfigWriter._assert_testnet() (G10-4.1 Etappe 1) — bereits aktiv |
| L2 | BOT_ENVIRONMENT == "testnet" |
command_worker.ALLOWED_ENVIRONMENTS["apply_profile_testnet"]={"testnet"} (G10-3b) |
| L3 | command.payload.environment == "testnet" |
_g10_3b_validate_payload_basic (G10-3b) |
| L4 | profile.environment == "paper" |
_g10_3b_validate_profile_state (G10-3b) |
| L5 | exchange_connection_id is None |
_g10_3b_validate_payload_basic (G10-3b) |
LIVE_TRADING_ENABLED ist NICHT Teil der Mainnet-Sperre — durable rule per LIVE_TRADING_ENABLED-Audit. Optional als Layer L6 für Bot-Run-State-Sanity-Check.
Handler ruft KEINEN expliziten BINANCE_TESTNET-Check auf — der Writer (Layer L1) raised MainnetBlockedError, Handler surfaced den Reject-Token.
| Aspekt | Verhalten |
|---|---|
| Welche Keys werden geschrieben? | alle 13 (2 Summary + 11 Risk) — wenn im Payload präsent |
| Apply-Effective (9) | max_open_positions, max_risk_per_trade_pct, max_total_exposure_pct, min_position_value_usdt, cash_reserve_pct, fee_buffer_pct, tier_low/mid/high_allocation_pct |
| Stored-only / Phase-1.1 (4) | log_level, decision_log_verbosity, daily_loss_limit_pct, weekly_loss_limit_pct |
| Source-of-Truth für Split | ActiveConfigProvider.APPLY_EFFECTIVE Map (G10-4.1 Etappe 1) |
| Result + Audit | applied_keys, apply_effective_keys, stored_only_keys separat ausgewiesen |
| Validierung | _g10_3b_validate_schema durchläuft alle 13 Keys gleich |
Wenn Payload nur Teilmenge enthält → Writer schreibt nur diese; nicht-präsente Keys bleiben in runtime_config.json ungetouched.
| Event | Trigger | context_json wesentliche Felder |
|---|---|---|
apply_started |
Handler-Entry (gleicher Event-Type für dry-run + apply, dry_run-Flag in metadata) |
profile_id, version, dry_run, environment, handler_phase |
apply_succeeded |
nach erfolgreichem Writer-Call ODER no-op-Idempotency | 15 Felder inkl. previous_values, new_values, runtime_config_path, backup_path, sha256, apply_effective_keys, stored_only_keys, runtime_mutation, no_effect (falls idempotent) |
apply_failed |
Validation-Error / Writer-Error / Mainnet-Block | profile_id, version, dry_run=false, reason, error_detail |
apply_rolled_back |
post-write sha-mismatch ODER Restore-Versuch | profile_id, version, backup_path, restore_status, original_error |
Plus existierende state-machine-Events (claimed, started, result_written) aus G6.5.
| Fehler-Stadium | Verhalten |
|---|---|
| Validation-Fail vor Writer-Call | kein Writer-Call, kein Backup, kein Write. command_status=failed, audit apply_failed mit reason=validation_*. runtime_config.json bleibt unverändert. |
| MainnetBlockedError (Writer L1) | wie oben, plus reason=mainnet_blocked. |
Writer-IO-Fehler vor os.replace |
Writer cleant tmp-File, Backup bleibt erhalten falls erstellt, Target unverändert (POSIX-atomic). ApplyResult(ok=False). Handler: command_status=failed. |
| Writer-IO-Fehler nach Backup | wie oben — Target unverändert, Backup als Restore-Punkt verfügbar. |
| Post-Write-Verify-Mismatch (extrem unwahrscheinlich) | Handler ruft restore_from_backup, audit apply_rolled_back, command_status=failed. |
| Backup-Copy-Fail | ApplyResult(ok=False, error="backup_copy_failed"). Handler: failed, kein Write. |
Writer auto-restored NICHT (G10-4.1 Etappe 1 dokumentiert). Restore ist Handler-Verantwortung im post-Write-Verify-Pfad.
Vorschlag: Pre-Write SHA256-Compare.
new_sha = sha256(json.dumps({"summary": payload_summary,
"risk": payload_risk},
indent=2, sort_keys=True))
existing_sha = sha256(target.read_bytes()) if target.is_file() else None
if existing_sha == new_sha:
return ApplyResult(ok=True, no_effect=True,
target_path=target, backup_path=None,
sha256=existing_sha, error=None)
Effekt:
- Identischer Re-Apply → ok=True, no_effect=True, kein Backup, kein Write.
- Audit-Event apply_succeeded mit no_effect=true — auditierbar, kein Failure.
- Backup-Files füllen sich nicht.
Caveat: _meta aus Idempotency-Compare ausschließen (timestamp/command_id würden sonst jedes Apply als „verschieden" markieren). Compare nur über summary + risk.
Implementation-Ort: im Writer (neue Method apply_idempotent ODER Pre-Check in bestehender apply). Cleaner: bestehende apply mit pre-check ergänzen.
test_dry_run_true_path_unchanged (idempotent re-asserted)test_dry_run_false_was_rejected_now_accepted — UPDATE existing testtest_apply_writes_runtime_config_jsontest_apply_result_shape_correct (alle 11 result_json keys)test_apply_audit_started_then_succeededtest_apply_effective_keys_split_correct (9 vs 4)test_apply_meta_includes_command_id_profile_version_checksumtest_apply_creates_backup_when_target_existstest_apply_no_backup_on_first_writetest_apply_sha256_present_and_matches_disktest_apply_runtime_config_path_is_state_dirtest_apply_blocked_when_BINANCE_TESTNET_falsetest_apply_blocked_when_BINANCE_TESTNET_missingtest_apply_blocked_when_BINANCE_TESTNET_string_true_not_booltest_apply_NOT_blocked_when_LIVE_TRADING_ENABLED_false ← positiv-Beweistest_apply_blocked_when_BOT_ENVIRONMENT_papertest_apply_blocked_when_payload_environment_not_testnettest_apply_blocked_when_profile_environment_not_papertest_apply_blocked_when_exchange_connection_id_not_nulltest_writer_io_error_command_failedtest_writer_io_error_target_unchangedtest_writer_io_error_no_tmp_left_behindtest_writer_validation_error_before_writetest_post_write_verify_mismatch_triggers_rollbacktest_apply_rolled_back_audit_emittedtest_idempotent_apply_returns_no_effecttest_idempotent_apply_no_new_backuptest_idempotent_apply_audit_succeeded_with_no_effect_flagtest_meta_timestamp_does_NOT_break_idempotencytest_different_summary_creates_new_apply_not_idempotenttest_no_live_portfolio_writes_in_handler (AST)test_no_env_writes_in_handler (AST)test_no_orders_or_ccxt_in_handler (AST)test_no_worker_daemon_startedtest_no_gui_apply_button_enabledtest_full_apply_flow_with_g10_3a_test_profiletest_apply_then_dry_run_with_same_profile_independenttest_concurrent_apply_command_lock_worksTotal: ~33 Tests, plus AST-Boundary-Tests.
| Aspekt | G10-4.2a | G10-4.2b |
|---|---|---|
| Code-Änderung | ja | nein |
| Tests laufen | im throwaway Container | nein (Live-E2E) |
| Container-Sync | nein | ja, command_worker.py (+ ggf. Writer) |
| Backup vorab | nein | ja, Pflicht |
PHP-Side createApplyCommand |
nein | ja (G10-3.5 wird live verwendet) |
Worker --once |
nein | ja, einmalig mit echtem dry_run=false-Command |
runtime_config.json wird geschrieben |
nein | ja — erstmals real |
| Bot-Override greift | nein | ja, innerhalb 1–2 Cycles via ActiveConfigSnapshot |
| Bot-Restart | nein | nein (Override greift cycle-snapshot) |
| Test-Profile | nein | ja, gleiches Profile wie G10-3c |
| ID | Risiko | Mitigation |
|---|---|---|
| R1 | dry_run=true Regression | volle G10-3b-Suite muss grün bleiben; Test test_dry_run_true_path_unchanged |
| R2 | runtime_config.json falscher Inhalt | post-write-sha256-verify; canonical-json (sort_keys); test |
| R3 | Writer schreibt Keys außerhalb Validation | Writer-Whitelist (Etappe 1); M2 AST-Test (Worker↔Writer-sync) |
| R4 | stored_only Keys fälschlich apply_effective | source-of-truth APPLY_EFFECTIVE; Test |
| R5 | Mainnet-Guard falsch | 5-Layer Defense-in-Depth + LIVE_TRADING_ENABLED-Test |
| R6 | Idempotenter Apply unnötige Backups | Pre-Write-SHA256-Compare; Test |
| R7 | Backup-Restore-Pfad unklar | post-write-verify-mismatch path explizit; Test |
| R8 | Worker mehr als einen Command | --once-Mode in G10-4.2b; FOR-UPDATE-SKIP-LOCKED |
| R9 | Bot liest halbfertige Datei | os.replace POSIX-atomar; Reader fällt auf {} (G10-1) |
| R10 | Audit unvollständig | strikte Tests für Event-Shape |
| Trigger | Aktion |
|---|---|
| Bot-PID wechselt unerwartet | STOP, Restart-Forensik |
runtime_config.json falscher Inhalt |
STOP, Restore from backup_path |
.env mtime ändert sich |
STOP, Forensik |
live_portfolio.json manuell verändert |
STOP (Boundary verletzt) |
| Worker als daemon statt --once | STOP, kill Worker |
| Orders / ccxt-Calls sichtbar | STOP, Boundary verletzt |
BINANCE_TESTNET=false |
STOP, Mainnet-Guard verletzt |
| Mainnet-Pfad sichtbar | STOP |
| Command failed unerwarteter reason | STOP, untersuche; eventuell rollback |
| Audit fehlt/unvollständig | STOP, fix audit-emit |
| ID | Frage | Default-Vorschlag |
|---|---|---|
| Q-1 | Idempotent (no_effect): previous_values/new_values gleich oder leer? |
gleich + zusätzlich no_effect: true Flag |
| Q-2 | apply_idempotent-Logik im Writer ODER Handler? |
Writer — Single-Point-of-Truth |
| Q-3 | Bei Idempotency Backup-Schritt überspringen? | ja — kein Backup wenn no_effect |
| Q-4 | apply_effective_keys in Result/Audit-Liste — alle Phasen? |
alle Phasen — durable Audit-Feld |
| Q-5 | Nur Phase-1.1-Keys (stored_only) im Payload — Apply trotzdem schreiben? | ja — runtime_mutation=true aber apply_effective_keys=[], stored_only_keys=[…] |
| Q-6 | Test-DB für G10-4.2: gleicher G10-3b-Pattern oder separat? | separat tradingbot_gui_g10_4_2_test (Isolation) |
| Q-7 | Writer Restore-Helper exportieren oder Handler ruft direkt? | Writer-Helper restore_from_backup(backup_path, target_path) für Test-Coverage |
| Check | Status |
|---|---|
| Read-only Scope | ja |
| Code-Änderung | nein |
| Tests geschrieben | nein |
| Worker | nein |
| Apply | nein |
runtime_config.json Write |
nein (nicht existent) |
| Bot-Restart | nein, PID 4246 stable |
| Orders | nein |
| Mainnet | nein, BINANCE_TESTNET=true |
| Push | nein |
| Backup-Status | für Scope nicht nötig (kein Live-Action). G10-4.2b braucht Pre-Live-Backup per durable Rule. |
Scope-Bestätigung: Vorschlag oben akzeptieren mit den 7 offenen Fragen Q-1 bis Q-7.
Sequenz:
--once → Live-E2E Apply mit Test-Profile → Verify Bot-Override greift in 1–2 Cycles → Lieferbericht.Geschätzter Umfang G10-4.2a: ~300 LoC Bot-Code + ~700 LoC Tests, 1 Commit im G10-3b-Stil.
Geschätzter Umfang G10-4.2b: ~6–8 Schritte analog G10-3c.