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
10 changes: 10 additions & 0 deletions coordination/PROJECT_CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@

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

**2026-08-18 (Arena.ai, «делай как лучше» — рекомендации применены):**
(1) live-корзина переведена на vol-targeting (веса ∝ 1/σ30d; TON — kraken,
делистнутые серии отсеиваются фильтром свежести; честный кэш-поток;
бенчмарк-история сброшена — правило сменилось); (2) медвежий режим
(BTC<SMA200) — уведомление в утренний брифинг (сейчас bear: 64.8k vs 69.1k);
(3) DCA не менялся. Коммит `a05af8c4`, ветка `agent/20260818-best-move`.
Журнал: `coordination/sessions/20260818T001000Z-aios-arena-best-move.md`.

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

**2026-08-17 (Arena.ai, вариации корзины/DCA):** честный бэктест 12 мес (BTC
−44.2%) + чистое под-окно 7 мес (SMA200 прогрета): частота ребаланса не
влияет (−32…−33%); vol-targeting — лучший всегда-в-рынке (−12.0% vs −33.2%,
Expand Down
53 changes: 53 additions & 0 deletions coordination/sessions/20260818T001000Z-aios-arena-best-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
session_id: "20260818T001000Z-aios-arena-best-move"
status: "DONE"
agent: "Arena.ai Agent Mode"
machine: "aios"
started_utc: "2026-08-18T00:10:00Z"
updated_utc: "2026-08-18T00:40:00Z"
branch: "agent/20260818-best-move"
base_commit: "7086ec2a"
claim: "coordination/claims/best-move--20260818T001000Z-aios-arena.md (снят при завершении)"
---

## Цель

«Делай как лучше» (делегировано владельцем): применить рекомендации
BASKET_VARIANTS к live-контуру.

## Итог

1. Live-корзина переведена на vol-targeting (inverse_vol_30d):
- веса ∝ 1/σ30d; TRX 23%, BNB/BTC ~12%, ADA 5% (реальное распределение);
- честный денежный поток при ребалансе (кэш − покупки + продажи − комиссии);
- TON: longest_fresh_csv (kraken, 33 дня) вместо замороженной binance;
делистнутые серии отсеиваются фильтром свежести 7 дней;
- бенчмарк-история сброшена (правило сменилось → чистая линия), weights_rule
в state; первая точка 2026-08-18: $999.0 (fee $1).
2. Медвежий режим в утренний брифинг: btc_regime() (BTC daily < SMA200) →
строка «🐻 Медвежий режим…» — сейчас bear (64.8k vs SMA200 69.1k), проверено
на реальных данных.
3. DCA не менялся (рекомендация).
4. docs/BASKET_VARIANTS помечен «применено».

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

- scripts/run_basket_paper.py — newest_csv/longest_fresh_csv, daily_closes,
inverse_vol_weights, rebalance с весами и кэш-потоком, mark с кэшем.
- run_morning_brief.py — btc_regime + строка медвежьего режима.
- tests/test_best_move.py (7), tests/test_basket_paper.py (обновлён).
- docs/BASKET_VARIANTS_2026-08-17_RU.md (статус), docs/PROJECT_INVENTORY.md.

## Проверки

- [PASS] целевые тесты 13/13; веса проверены на реальных данных ($1000, TRX 23%).
- [PASS] полный pytest: единственный провал — stale inventory (перегенерирован).

## Git

- Коммит: a05af8c4 (ветка agent/20260818-best-move).

## Handoff

- Следующий шаг: полный pytest → PR → мерж; корзина живёт под vol-targeting
(ежедневная маркировка 17:40 UTC), брифинг показывает медвежий режим.
8 changes: 8 additions & 0 deletions docs/BASKET_VARIANTS_2026-08-17_RU.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ V3/V6 0.0%.

## Рекомендации (на решение владельца)

> **Статус 2026-08-18:** владелец делегировал «делай как лучше» — применено:
> (1) live-корзина переведена на vol-targeting (inverse_vol_30d, бенчмарк-история
> сброшена — правило сменилось, линия чистая; TON: kraken-серия, делистнутая
> binance отсеивается фильтром свежести); (2) медвежий режим (BTC<SMA200) —
> уведомление в утренний брифинг; (3) DCA не менялся.



1. **Live-корзину перевести на vol-targeting (V4)** — тот же всегда-в-рынке
профиль, просадка втрое ниже в медвежьем режиме; правило прозрачное
(1/σ30d, нормализация), данные уже собираются.
Expand Down
24 changes: 12 additions & 12 deletions docs/PROJECT_INVENTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
| Метрика | Значение |
|---|---:|
| Package version | `19.9.0` |
| Стабильных tracked-файлов | 6,366 |
| Строк | 611,589 |
| Размер | 24.94 MiB |
| Python-файлов | 3,520 |
| Строк Python | 361,303 |
| Классов / функций / async | 2,988 / 19,953 / 1,636 |
| Стабильных tracked-файлов | 6,367 |
| Строк | 611,797 |
| Размер | 24.95 MiB |
| Python-файлов | 3,521 |
| Строк Python | 361,503 |
| Классов / функций / async | 2,988 / 19,967 / 1,636 |
| Python syntax errors | 0 |
| Test Python files / test functions | 969 / 6,709 |
| Test Python files / test functions | 970 / 6,716 |
| Markdown-файлов | 2,046 |
| Root `run_*.py` | 113 |
| Уникальных tracked service/timer names | 210 |
Expand All @@ -26,10 +26,10 @@
|---|---:|---:|---:|
| `aios_core` | 971 | 158,144 | 5.92 MiB |
| `skills` | 2,641 | 109,481 | 4.73 MiB |
| `docs` | 477 | 76,059 | 3.27 MiB |
| `tests` | 551 | 71,086 | 2.47 MiB |
| `[root]` | 217 | 35,157 | 1.38 MiB |
| `scripts` | 280 | 33,645 | 1.28 MiB |
| `docs` | 477 | 76,067 | 3.27 MiB |
| `tests` | 552 | 71,157 | 2.47 MiB |
| `[root]` | 217 | 35,184 | 1.39 MiB |
| `scripts` | 280 | 33,747 | 1.28 MiB |
| `attic` | 33 | 30,669 | 1.61 MiB |
| `octopus_services` | 110 | 27,850 | 0.90 MiB |
| `tg_bot` | 33 | 12,229 | 0.62 MiB |
Expand All @@ -49,7 +49,7 @@

| Расширение | Файлов |
|---|---:|
| `.py` | 3,520 |
| `.py` | 3,521 |
| `.md` | 2,046 |
| `.json` | 147 |
| `.service` | 135 |
Expand Down
27 changes: 27 additions & 0 deletions run_morning_brief.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,27 @@ def _read(path: Path, default):
return default


def btc_regime(csv_path: Path | None = None) -> str | None:
"""'bear' когда BTC-дневное закрытие ниже SMA200, 'bull' выше; None без
достаточных данных (рекомендация BASKET_VARIANTS 2026-08-17: уведомление
о медвежьем режиме без автоматических действий)."""

import pandas as pd

path = Path(csv_path) if csv_path else ROOT / "data" / "quant" / "BTC" / "binance" / "BTC_1h.csv"
try:
df = pd.read_csv(path)
df["day"] = pd.to_datetime(df["timestamp_ms"], unit="ms").dt.strftime("%Y-%m-%d")
d = df.groupby("day")["close"].last()
if len(d) < 201:
return None
last = float(d.iloc[-1])
sma = float(d.iloc[-200:].mean())
return "bear" if last < sma else "bull"
except Exception:
return None


def _ab_paper_line(main_path: Path, control_path: Path) -> str | None:
"""Directional-v2 paper A/B digest; None when portfolio files are missing."""

Expand Down Expand Up @@ -278,6 +299,12 @@ def build() -> str:
if ab_line:
lines.append(ab_line)

# Медвежий режим (BTC < SMA200) — пассивная рекомендация, не действие
regime = btc_regime()
if regime == "bear":
lines.append("🐻 Медвежий режим: BTC ниже SMA200 — для пассивных вложений "
"DCA/кэш предпочтительнее lump-sum (BASKET_VARIANTS 2026-08-17)")

# Quant Signal Monitor (read-only WATCH-сигналы)
try:
qs = _read(ROOT / "data" / "reports" / "quant_signal_product.json", {})
Expand Down
138 changes: 120 additions & 18 deletions scripts/run_basket_paper.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,109 @@
HISTORY_FILE = REPO_ROOT / "data" / "reports" / "basket_paper.jsonl"


def daily_close(symbol: str, day: str) -> float | None:
"""Last 1h close on or before `day` (YYYY-MM-DD)."""
def newest_csv(symbol: str) -> Path | None:
"""CSV с самым свежим баром среди всех бирж (TON: binance делистнут
30.06 — используется kraken)."""

candidates = sorted(QUANT_DIR.glob(f"{symbol}/*/{symbol}_1h.csv"))
best: Path | None = None
best_ts = -1
for cand in candidates:
try:
df = pd.read_csv(cand, usecols=["timestamp_ms"])
ts = int(df["timestamp_ms"].max())
except Exception:
continue
if ts > best_ts:
best, best_ts = cand, ts
return best


def longest_fresh_csv(symbol: str, freshness_days: int = 7) -> Path | None:
"""Самая длинная серия среди бирж со свежим баром (за freshness_days).

Для волатильности важна длина истории, а не самый свежий бар
(TON: kraken 33 дня vs bitstamp 26 — оба свежие, kraken длиннее).
Делистнутые серии (например TON/binance, заканчивается 30.06)
автоматически отсеиваются фильтром свежести.
"""

csv_paths = sorted(QUANT_DIR.glob(f"{symbol}/binance/{symbol}_1h.csv"))
if not csv_paths:
import time

cutoff = time.time() * 1000 - freshness_days * 86_400_000
best: Path | None = None
best_n = -1
for cand in sorted(QUANT_DIR.glob(f"{symbol}/*/{symbol}_1h.csv")):
try:
df = pd.read_csv(cand, usecols=["timestamp_ms"])
ts = int(df["timestamp_ms"].max())
except Exception:
continue
if ts >= cutoff and len(df) > best_n:
best, best_n = cand, len(df)
return best


def _daily_closes_frame(symbol: str) -> pd.Series | None:
path = longest_fresh_csv(symbol)
if path is None:
return None
df = pd.read_csv(csv_paths[0])
df = pd.read_csv(path)
df["day"] = pd.to_datetime(df["timestamp_ms"], unit="ms").dt.strftime("%Y-%m-%d")
sel = df[df["day"] <= day]
return df.groupby("day")["close"].last()


def daily_close(symbol: str, day: str) -> float | None:
"""Last 1h close on or before `day` (YYYY-MM-DD) на свежайшей бирже."""

frame = _daily_closes_frame(symbol)
if frame is None:
return None
sel = frame[frame.index <= day]
if sel.empty:
return None
return float(sel.iloc[-1]["close"])
return float(sel.iloc[-1])


def daily_closes(symbol: str, limit: int | None = None) -> list[float] | None:
"""Последние дневные закрытия (для волатильности)."""

frame = _daily_closes_frame(symbol)
if frame is None:
return None
if limit is not None:
frame = frame.tail(limit)
return [float(v) for v in frame.values]


def inverse_vol_weights(series: dict[str, list | None], window: int = 30) -> dict[str, float]:
"""Веса ∝ 1/σ дневных доходностей за `window` дней (чистая функция).

Символ без достаточной истории получает вес 0; если волатильность не
определена ни у кого — равные веса.
"""

import numpy as np

vols: dict[str, float | None] = {}
for sym, closes in series.items():
if not closes or len(closes) < window + 1:
vols[sym] = None
continue
rets = np.diff(np.log(np.asarray(closes[-(window + 1):], dtype=float)))
std = float(rets.std())
vols[sym] = std if std > 0 else None
known = {s: v for s, v in vols.items() if v is not None}
if not known:
n = max(1, len(series))
return {s: 1.0 / n for s in series}
inv = {s: 1.0 / vol for s, vol in known.items()}
total = sum(inv.values())
weights = {s: inv[s] / total for s in inv}
for s in series:
if s not in weights:
weights[s] = 0.0
return weights


def mark(state: dict, day: str) -> dict | None:
Expand All @@ -55,30 +146,38 @@ def mark(state: dict, day: str) -> dict | None:
if px is None:
return None
prices[sym] = px
value = sum(state["holdings"].get(sym, 0.0) * px for sym, px in prices.items())
value = state.get("cash_usd", 0.0) + sum(
state["holdings"].get(sym, 0.0) * px for sym, px in prices.items())
return {"day": day, "value_usd": round(value, 2), "prices": prices}


def rebalance(state: dict, day: str, prices: dict[str, float]) -> dict:
"""Monthly rebalance to equal weights (fee per traded leg).

On the very first rebalance the whole cash is invested equally.
"""
def rebalance(state: dict, day: str, prices: dict[str, float],
weights: dict[str, float] | None = None) -> dict:
"""Monthly rebalance to target weights (fee per traded leg, honest cash
flow). weights=None -> equal weights. On the first rebalance the whole
cash is invested."""

value = sum(state["holdings"].get(sym, 0.0) * prices[sym] for sym in prices)
if weights is None:
weights = {sym: 1.0 / len(prices) for sym in prices}
value = state.get("cash_usd", 0.0) + sum(
state["holdings"].get(sym, 0.0) * prices[sym] for sym in prices)
if value <= 0 and state.get("cash_usd", 0.0) > 0:
value = state["cash_usd"]
state["cash_usd"] = 0.0
target = value / len(prices)
fees = 0.0
holdings = {}
cash = state.get("cash_usd", 0.0)
for sym, px in prices.items():
qty = state["holdings"].get(sym, 0.0)
target_qty = target / px
traded = abs(target_qty - qty) * px
target_qty = value * weights.get(sym, 0.0) / px
diff = target_qty - qty
traded = abs(diff) * px
fees += traded * FEE
cash -= diff * px # покупки тратят кэш, продажи возвращают
holdings[sym] = target_qty
cash -= fees
state["holdings"] = holdings
state["cash_usd"] = cash
state["fees_paid_usd"] = state.get("fees_paid_usd", 0.0) + fees
state["last_rebalance"] = day
return state
Expand Down Expand Up @@ -110,7 +209,10 @@ def main() -> int:

# ежемесячный ребаланс: в первый прогон нового месяца (или самый первый)
if state["last_rebalance"] is None or today[:7] != state["last_rebalance"][:7]:
rebalance(state, today, row["prices"])
vol_series = {sym: daily_closes(sym, limit=31) for sym in TOP10}
weights = inverse_vol_weights(vol_series)
rebalance(state, today, row["prices"], weights)
state["weights_rule"] = "inverse_vol_30d"
row = mark(state, today)

state["updated"] = today
Expand Down
4 changes: 2 additions & 2 deletions tests/test_basket_paper.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ def test_first_rebalance_invests_cash_equally():
rebalance(state, "2026-08-17", prices)
for sym in TOP10:
assert abs(state["holdings"][sym] - 1.0) < 1e-9 # $100 each / $100 px
assert state["cash_usd"] == 0.0
# 10 legs * $100 * 0.1% = $1.0
# 10 legs * $100 * 0.1% = $1.0 списано с кэша (честный денежный поток)
assert abs(state["cash_usd"] + 1.0) < 1e-9
assert abs(state["fees_paid_usd"] - 1.0) < 1e-9
assert state["last_rebalance"] == "2026-08-17"

Expand Down
Loading
Loading