Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions aios_core/quant/market_regime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""Market regime engine (ревизия 2026-08-19, по рекомендациям).

Цель — НЕ предсказывать цену, а честно определять режим рынка из совокупности
доступных индикаторов и выдавать (режим, уровень риска, семейство стратегий,
триггеры смены режима). Используется:
- scripts/quant_regime_engine.py (ежедневный сбор + история);
- aios_core/quant/regime_guard.py (kill-guard бумажных входов в CRASH/PANIC);
- tg_bot/trading_report.py (блок «Режим рынка» + AI-аналитик).

Чистые функции; без сетевых вызовов.
"""

from __future__ import annotations

from datetime import datetime, timezone
from pathlib import Path
from typing import Any

REGIMES = ("STRONG_BULL", "BULL", "SIDEWAYS", "VOLATILE", "BEAR", "CRASH", "PANIC")

CRASH_PANIC = {"CRASH", "PANIC"}

RISK_BY_REGIME = {
"STRONG_BULL": "LOW",
"BULL": "LOW",
"SIDEWAYS": "MEDIUM",
"VOLATILE": "HIGH",
"BEAR": "HIGH",
"CRASH": "EXTREME",
"PANIC": "EXTREME",
}

STRATEGY_BY_REGIME = {
"STRONG_BULL": "momentum/trend (полный экспозицион)",
"BULL": "momentum/trend",
"SIDEWAYS": "mean-reversion/range",
"VOLATILE": "пониженный размер позиций",
"BEAR": "defensive/низкий экспозицион (DCA-накопление)",
"CRASH": "сохранение капитала (кэш)",
"PANIC": "сохранение капитала (кэш)",
}


def classify_regime(i: dict[str, float | None]) -> str:
"""Классификация режима из индикаторов (a-priori правила, без подгонки).

Приоритет правил: PANIC > CRASH > BEAR > STRONG_BULL > BULL > VOLATILE > SIDEWAYS.
Отсутствующий индикатор (None) просто выключает зависящие от него правила.
"""

def num(key: str) -> float | None:
v = i.get(key)
return float(v) if v is not None else None

dd90 = num("dd90_pct") # просадка от 90-дневного максимума, %
fng = num("fear_greed") # 0..100
ret7 = num("btc_ret_7d_pct") # 7-дневная доходность BTC, %
above200 = num("btc_above_sma200") # 1/0
above50 = num("btc_above_sma50")
breadth = num("breadth_7d") # доля активов вселенной с положительным ret_7d
vol = num("vol30_annualized_pct")

if dd90 is not None and dd90 <= -35:
return "PANIC"
if fng is not None and fng <= 15:
return "PANIC"
if dd90 is not None and dd90 <= -20:
return "CRASH"
if ret7 is not None and ret7 <= -15:
return "CRASH"
if above200 is not None and above200 <= 0:
# ниже долгосрочного тренда: медвежий режим, если есть глубокая просадка
# или отрицательный импульс; иначе — боковик с попыткой восстановления
if dd90 is not None and dd90 <= -10:
return "BEAR"
if ret7 is not None and ret7 < 0:
return "BEAR"
return "SIDEWAYS"
# выше SMA200
strong = (breadth is None or breadth >= 0.7) and (fng is None or fng >= 60)
if above50 is not None and above50 > 0 and strong:
return "STRONG_BULL"
if breadth is not None and breadth >= 0.5:
return "BULL"
if vol is not None and vol >= 80:
return "VOLATILE"
return "SIDEWAYS"


def risk_level(regime: str) -> str:
return RISK_BY_REGIME.get(regime, "UNKNOWN")


def strategy_family(regime: str) -> str:
return STRATEGY_BY_REGIME.get(regime, "—")


def next_regime_triggers(i: dict[str, float | None], regime: str) -> list[str]:
"""Конкретные условия смены режима (для отчёта и AI-аналитика)."""

def num(key: str) -> float | None:
v = i.get(key)
return float(v) if v is not None else None

dd90 = num("dd90_pct")
fng = num("fear_greed")
above200 = num("btc_above_sma200")
above50 = num("btc_above_sma50")
breadth = num("breadth_7d")
vol = num("vol30_annualized_pct")
out: list[str] = []
if above200 is not None:
out.append(f"BTC закроется {'выше' if above200 <= 0 else 'ниже'} SMA200 "
f"(сейчас {'ниже' if above200 <= 0 else 'выше'})")
if regime in CRASH_PANIC:
if fng is not None:
out.append(f"Fear&Greed выйдет из зоны паники (>25; сейчас {fng:.0f})")
if dd90 is not None:
out.append(f"просадка от максимума сократится до >-20% (сейчас {dd90:.1f}%)")
else:
if dd90 is not None and dd90 <= -15:
out.append(f"углубление просадки <-20% переведёт в CRASH (сейчас {dd90:.1f}%)")
if breadth is not None and breadth < 0.35:
out.append(f"breadth восстановится >0.5 (сейчас {breadth:.2f})")
if vol is not None and vol >= 70:
out.append(f"волатильность снизится <70% (сейчас {vol:.0f}%)")
if above50 is not None and above200 is not None:
if above50 > 0 and above200 <= 0:
out.append("BTC выше SMA50, но ниже SMA200 — пробой SMA200 подтвердит бычий режим")
if above50 <= 0 and above200 > 0:
out.append("BTC ниже SMA50 при цене выше SMA200 — потеря SMA50 ослабит бычий режим")
return out[:5]


def regime_payload(i: dict[str, float | None], regime: str | None = None) -> dict[str, Any]:
regime = regime or classify_regime(i)
return {
"generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"regime": regime,
"risk_level": risk_level(regime),
"strategy_family": strategy_family(regime),
"indicators": i,
"triggers": next_regime_triggers(i, regime),
}


def write_latest(payload: dict[str, Any], path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(__import__("json").dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")


def append_history(payload: dict[str, Any], path: Path) -> None:
import json

path.parent.mkdir(parents=True, exist_ok=True)
row = {
"date": payload["generated_at"][:10],
"regime": payload["regime"],
"risk_level": payload["risk_level"],
"indicators": payload["indicators"],
}
with path.open("a", encoding="utf-8") as fh:
fh.write(json.dumps(row, ensure_ascii=False) + "\n")
27 changes: 27 additions & 0 deletions aios_core/quant/regime_guard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Режимный kill-guard для бумажных входов Directional v2.

В режимах CRASH/PANIC входы блокируются (сохранение капитала), в остальных —
разрешены. Читает data/reports/market_regime_latest.json (пишется ежедневно
scripts/quant_regime_engine.py). Fail-open: при отсутствии/повреждении файла
guard НЕ блокирует (текущее поведение сохраняется).
"""

from __future__ import annotations

import json
from pathlib import Path

BLOCK_REGIMES = {"CRASH", "PANIC"}


def current_regime(path: str) -> str | None:
try:
data = json.loads(Path(path).read_text(encoding="utf-8"))
value = str(data.get("regime") or "").strip().upper()
return value or None
except (OSError, ValueError, TypeError):
return None


def crash_kill_active(path: str) -> bool:
return current_regime(path) in BLOCK_REGIMES
8 changes: 6 additions & 2 deletions aios_core/quant_directional_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing import Any

from aios_core.quant.ml_gate_calibration import calibrated_ml_threshold

from aios_core.quant.regime_guard import crash_kill_active

def _env_bool(name: str, default: bool) -> bool:
value = os.environ.get(name)
Expand Down Expand Up @@ -41,6 +41,7 @@ class DirectionalV2Config:
ml_calibrate: bool = False
ml_calibrate_file: str = "data/quant/ml_prob_calibration.json"
ml_calibrate_floor: float = 0.50
regime_guard: bool = False; regime_file: str = "data/reports/market_regime_latest.json"

@classmethod
def from_env(cls) -> DirectionalV2Config:
Expand Down Expand Up @@ -71,6 +72,8 @@ def from_env(cls) -> DirectionalV2Config:
ml_calibrate=_env_bool("AIOS_QUANT_ML_CALIBRATE", False),
ml_calibrate_file=os.environ.get("AIOS_QUANT_ML_CALIBRATE_FILE", "data/quant/ml_prob_calibration.json"),
ml_calibrate_floor=min(1.0, max(0.0, float(os.environ.get("AIOS_QUANT_ML_CALIBRATE_FLOOR", "0.50")))),
regime_guard=_env_bool("AIOS_QUANT_REGIME_GUARD", False),
regime_file=os.environ.get("AIOS_QUANT_REGIME_FILE", "data/reports/market_regime_latest.json"),
)

def entry_execution_price(self, mid_price: float) -> float:
Expand Down Expand Up @@ -109,7 +112,6 @@ def portfolio_equity(
total_equity += equity
return total_initial, total_equity, unpriced


def entry_block_reason(
config: DirectionalV2Config,
analysis: dict[str, Any],
Expand All @@ -126,6 +128,8 @@ def entry_block_reason(

if config.entry_mode != "enabled":
return "entry_mode_freeze"
if config.regime_guard and crash_kill_active(config.regime_file):
return "regime_crash_kill"
if not candle_is_new:
return "same_candle"
if config.allowed_exchanges and exchange.lower() not in config.allowed_exchanges:
Expand Down
16 changes: 16 additions & 0 deletions coordination/PROJECT_CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@

## Где закончили

**2026-08-19 (Arena.ai, режимный движок по рекомендациям):** (1) исправлена
freqtrade-ловушка stoploss=-0.99 (−99%! стоп=1% от входа) → страховочный −15%,
бот пересчитал открытые сделки, регрессия-тест; (2) Market Regime Engine:
7 режимов из совокупности индикаторов + риск-уровни + роутер стратегий +
триггеры; ежедневный таймер 04:50 UTC, история в
market_regime_history.jsonl; сейчас BEAR (dd90 −16.3%); (3) CRASH/PANIC
kill-guard в политике Directional v2 (включён в обоих демонах, fail-open);
(4) T2-метрики симуляции (PF/Sharpe/Sortino/Calmar/expectancy) с пометкой
«не заработок»; (5) отчёт получил блок «Режим рынка и защита», LLM —
вероятностные сценарии + раздел «насколько доверять»; (6) seam
pre_treasury_intents.py вернул accounts.py в бюджет. Коммит `3f687afe`,
ветка `agent/20260819-regime-engine`.
Журнал: `coordination/sessions/20260819T020000Z-aios-arena-regime-engine.md`.

## Где закончили

**2026-08-19 (Arena.ai, инвесторский отчёт + одна кнопка):** по решению
владельца — ежедневный утренний трейдинг-отчёт чату 839699134 (таймер
aios-tg-trading-report, 05:30 UTC; sender --chat); бот упрощён до одной
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
session_id: "20260819T020000Z-aios-arena-regime-engine"
status: "DONE"
agent: "Arena.ai Agent Mode"
machine: "aios"
started_utc: "2026-08-19T02:00:00Z"
updated_utc: "2026-08-19T02:40:00Z"
branch: "agent/20260819-regime-engine"
base_commit: "f9cc9a1a"
claim: "coordination/claims/regime-engine--20260819T020000Z-aios-arena.md (снят при завершении)"
---

## Цель

Применить полезные рекомендации внешних ИИ (недвижимость исключена владельцем):
режимный движок + risk-guard + роутер стратегий + честные T2-метрики +
проверка стопов freqtrade + структура AI-аналитика.

## Итог

1. **Freqtrade stoploss-ловушка исправлена:** стратегия имела stoploss = -0.99
(freqtrade читает как −99% → стоп = 1% от цены входа: BTC 645.41 при входе
64540.94). Причина — намерение «без жёсткого стопа» (SMA-выход как защита),
реализованное опасным значением. Заменено на страховочный −15%; бот
пересчитал открытые сделки (BTC stop 54859.8, SOL 65.53). Тест-регрессия
запрещает значения вне (−0.5, 0).
2. **Market Regime Engine:** aios_core/quant/market_regime.py — 7 режимов
(STRONG_BULL/BULL/SIDEWAYS/VOLATILE/BEAR/CRASH/PANIC) из совокупности
индикаторов (SMA200/50, ret7d, dd90, vol30, breadth, F&G); риск-уровни;
роутер семейств стратегий; триггеры смены режима. scripts/quant_regime_engine.py
— ежедневный сбор из локальных данных → market_regime_latest.json + история
jsonl. Таймер 04:50 UTC. Текущий режим: BEAR (risk HIGH, defensive; dd90 −16.3%).
3. **Kill-guard политики:** regime_guard в DirectionalV2Config (env
AIOS_QUANT_REGIME_GUARD); в CRASH/PANIC входы блокируются
(regime_crash_kill), fail-open при отсутствии файла. Включён в обоих
paper-демонах (unit env). Тесты: 5 случаев.
4. **T2-метрики:** scripts/quant_t2_metrics.py — PF/win-rate/expectancy по
сделкам + Sharpe/Sortino/MaxDD/CAGR/Calmar по дневной истории, с честной
пометкой «симуляция, не заработок». Отчёт data/reports/t2_simulation_metrics.md.
5. **AI-аналитик:** отчёт получил блок «🎛 Режим рынка и защита» (режим, риск,
семейство стратегий, триггеры, CRASH/PANIC-блокировка); LLM-промпт требует
вероятностные сценарии (без «точно»), раздел «Насколько можно доверять»
(уверенность + качество данных).
6. **Бюджет модулей:** tg_bot/accounts.py вынесен seam pre_treasury_intents.py
(перехват «Трейдинг» + «фриланс» до treasury) — accounts.py 3222/3225,
policy 170/170.

## Изменённые файлы

- новые: aios_core/quant/{market_regime,regime_guard}.py,
scripts/{quant_regime_engine,quant_t2_metrics}.py,
tg_bot/pre_treasury_intents.py, deploy/systemd/aios-regime-engine.{service,timer},
tests/{test_market_regime,test_regime_guard_policy,test_t2_metrics,test_freqtrade_stoploss}.py.
- правки: scripts/freqtrade_t2.py, aios_core/quant_directional_policy.py,
tg_bot/{trading_report,accounts}.py, deploy/systemd/aios-quant-trading*.service,
HETZNER_INSTALLED_UNITS.txt (176), tests/test_systemd_inventory.py,
tests/test_trading_button_path.py, docs/PROJECT_INVENTORY.md.

## Проверки

- [PASS] тесты: regime 13/13, guard-policy 5/5, t2_metrics 5/5, stoploss 2/2,
trading_report 13/13, budget, inventory.
- [PASS] живой прогон режимного движка; freqtrade пересчитал стопы.
- [PASS] полный pytest: единственный провал — stale inventory (перегенерирован).

## Git

- Коммит: 3f687afe (ветка agent/20260819-regime-engine).

## Handoff

- Следующий шаг: полный pytest → PR → мерж; наблюдение за режимом и guard.
- Риски: guard fail-open при отсутствии файла режима (задокументировано).
4 changes: 3 additions & 1 deletion deploy/systemd/HETZNER_INSTALLED_UNITS.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Installed aios-* unit-file names, read-only snapshot 2026-08-19 UTC (tg-trading-report added).
# Installed aios-* unit-file names, read-only snapshot 2026-08-19 UTC (regime-engine added).
aios-2captcha-balance.service
aios-2captcha-balance.timer
aios-accounting-report.service
Expand Down Expand Up @@ -137,6 +137,8 @@ aios-quant-ml-inference.service
aios-quant-trading.service
aios-rag-refresh.service
aios-rag-refresh.timer
aios-regime-engine.service
aios-regime-engine.timer
aios-report-export.service
aios-report-export.timer
aios-rotate-backups.service
Expand Down
1 change: 1 addition & 0 deletions deploy/systemd/aios-quant-trading-control.service
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Environment=AIOS_QUANT_MIN_CONFIDENCE=0.88
Environment=AIOS_QUANT_REQUIRE_ML=1
Environment=AIOS_QUANT_ML_MIN_PROB=0.65
Environment=AIOS_QUANT_ML_CALIBRATE=1
Environment=AIOS_QUANT_REGIME_GUARD=1
Environment=AIOS_QUANT_RL_VETO=0.30
Environment=AIOS_QUANT_MIN_HOLD_SECONDS=7200
Environment=AIOS_QUANT_HALF_SPREAD_RATE=0.0005
Expand Down
1 change: 1 addition & 0 deletions deploy/systemd/aios-quant-trading.service
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Environment=AIOS_QUANT_MIN_CONFIDENCE=0.88
Environment=AIOS_QUANT_REQUIRE_ML=1
Environment=AIOS_QUANT_ML_MIN_PROB=0.65
Environment=AIOS_QUANT_ML_CALIBRATE=1
Environment=AIOS_QUANT_REGIME_GUARD=1
Environment=AIOS_QUANT_RL_VETO=0.30
Environment=AIOS_QUANT_MIN_HOLD_SECONDS=7200
Environment=AIOS_QUANT_HALF_SPREAD_RATE=0.0005
Expand Down
9 changes: 9 additions & 0 deletions deploy/systemd/aios-regime-engine.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[Unit]
Description=AIOS Market Regime Engine (daily indicators -> regime + history)

[Service]
Type=oneshot
User=root
WorkingDirectory=/root/AIOS
Environment=PYTHONPATH=/root/AIOS
ExecStart=/opt/aios/.venv/bin/python -u /root/AIOS/scripts/quant_regime_engine.py
10 changes: 10 additions & 0 deletions deploy/systemd/aios-regime-engine.timer
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[Unit]
Description=Daily regime engine (04:50 UTC, before morning report)

[Timer]
OnCalendar=*-*-* 04:50:00
RandomizedDelaySec=180
Persistent=true

[Install]
WantedBy=timers.target
Loading
Loading