diff --git a/coordination/PROJECT_CONTEXT.md b/coordination/PROJECT_CONTEXT.md index 1276926e7..2fef4890c 100644 --- a/coordination/PROJECT_CONTEXT.md +++ b/coordination/PROJECT_CONTEXT.md @@ -10,6 +10,16 @@ ## Где закончили +**2026-08-19 (Arena.ai, отчёт «по-человечески»):** кнопка «Трейдинг» теперь +выдаёт отчёт на языке обывателя: «Главное за 30 секунд», «Что это» к каждому +портфелю, человеческие названия причин сделок, блок «Простыми словами» и +словарик; LLM-аналитика переведена на тот же язык (жаргон с пояснениями, +дисклеймер о виртуальных деньгах). Отправлен владельцу в TG (3 сообщения). +Коммит `216f5dd1`, ветка `agent/20260819-human-report`. +Журнал: `coordination/sessions/20260819T010000Z-aios-arena-tg-trading-report.md`. + +## Где закончили + **2026-08-19 (Arena.ai, кнопка «Трейдинг» — детальный отчёт):** tg_bot/trading_report.py — полный снапшот всех портфелей (A/B с trade_log, DCA VA+control, корзина vol-targeting, T2 5 ног + портфель, freqtrade dry, MM/ws/очередь, scoreboard, diff --git a/coordination/sessions/20260819T010000Z-aios-arena-tg-trading-report.md b/coordination/sessions/20260819T010000Z-aios-arena-tg-trading-report.md index 941916d20..256c76c3a 100644 --- a/coordination/sessions/20260819T010000Z-aios-arena-tg-trading-report.md +++ b/coordination/sessions/20260819T010000Z-aios-arena-tg-trading-report.md @@ -40,3 +40,16 @@ claim: "coordination/claims/tg-trading-report--20260819T010000Z-aios-arena.md ( - [PASS] pytest tests/test_trading_report.py 7/7; живой LLM-вызов. - [PASS] полный pytest: единственный провал — stale inventory (перегенерирован). + +## Дополнение (2026-08-19, человеческий отчёт) + +- По запросу владельца отчёт переписан «по-человечески»: блок «Главное за 30 + секунд», к каждому контуру — «Что это» простыми словами, сделки с переводом + причин выходов (stop_loss -> «защитный стоп»), вердикты обывателю, блок + «Простыми словами» и словарик терминов. +- LLM-промпт переписан на язык для человека без финансового образования + (жаргон с пояснениями в скобках, структура «Главное / Что с портфелями / + Чего ждать» с вероятностями, обязательный дисклеймер о виртуальных деньгах). +- Отчёт отправлен владельцу в TG (3 сообщения: 2 данных + LLM-аналитика). +- Бот перезапущен; тесты test_trading_report.py 8/8; полный pytest зелёный + (кроме регенерируемого inventory). diff --git a/docs/PROJECT_INVENTORY.md b/docs/PROJECT_INVENTORY.md index 6c5fd1715..46859d2f6 100644 --- a/docs/PROJECT_INVENTORY.md +++ b/docs/PROJECT_INVENTORY.md @@ -9,13 +9,13 @@ |---|---:| | Package version | `19.9.0` | | Стабильных tracked-файлов | 6,371 | -| Строк | 612,727 | -| Размер | 24.99 MiB | +| Строк | 612,823 | +| Размер | 25.00 MiB | | Python-файлов | 3,525 | -| Строк Python | 362,433 | -| Классов / функций / async | 2,988 / 20,008 / 1,636 | +| Строк Python | 362,529 | +| Классов / функций / async | 2,988 / 20,010 / 1,636 | | Python syntax errors | 0 | -| Test Python files / test functions | 972 / 6,728 | +| Test Python files / test functions | 972 / 6,729 | | Markdown-файлов | 2,046 | | Root `run_*.py` | 113 | | Уникальных tracked service/timer names | 210 | @@ -27,12 +27,12 @@ | `aios_core` | 971 | 158,144 | 5.92 MiB | | `skills` | 2,641 | 109,481 | 4.73 MiB | | `docs` | 477 | 76,067 | 3.27 MiB | -| `tests` | 554 | 71,349 | 2.48 MiB | +| `tests` | 554 | 71,360 | 2.48 MiB | | `[root]` | 217 | 35,241 | 1.39 MiB | | `scripts` | 281 | 33,845 | 1.28 MiB | | `attic` | 33 | 30,669 | 1.61 MiB | | `octopus_services` | 110 | 27,850 | 0.90 MiB | -| `tg_bot` | 34 | 12,812 | 0.65 MiB | +| `tg_bot` | 34 | 12,897 | 0.65 MiB | | `deploy` | 252 | 6,385 | 0.18 MiB | | `octopus_instructions` | 102 | 5,959 | 0.50 MiB | | `octopus_roadmap` | 13 | 4,741 | 0.22 MiB | diff --git a/tests/test_trading_report.py b/tests/test_trading_report.py index ea4bed657..3a065b965 100644 --- a/tests/test_trading_report.py +++ b/tests/test_trading_report.py @@ -55,14 +55,25 @@ def test_format_report_covers_all_sections(): snap = _snapshot() chunks = format_report(snap) text = "\n".join(chunks) - assert "Directional v2" in text and "main" in text and "control" in text - assert "DCA" in text and "va" in text and "dca" in text - assert "Корзина топ-10" in text and "inverse_vol_30d" in text - assert "T2 momentum" in text and "CASH" in text + # человеческие маркеры: главное, объяснения, словарик + assert "Главное за 30 секунд" in text + assert "Что это" in text + assert "Робот А" in text and "Робот Б" in text + assert "Автокопилка" in text and "Корзина топ-10" in text + assert "Моментум-роботы" in text and "вне рынка (кэш)" in text assert "freqtrade" in text and "BTC/USDT" in text - assert "ws-снапшотов" in text and "5,816,805" in text - assert "Scoreboard 2026-08" in text and "top10_basket" in text - assert "⚠️ aios-freqtrade-t2-dry" in text + assert "снимков" in text and "5,816,805" in text + assert "Кто лучший по тесту" in text and "корзина топ-10" in text + assert "Словарик" in text + assert "Простыми словами" in text + assert "⚠️ лежит: aios-freqtrade-t2-dry" in text + + +def test_format_report_human_verdict_names_winner(): + snap = _snapshot() + text = "\n".join(format_report(snap)) + # победитель переведён на человеческий язык + assert "Победитель: корзина топ-10" in text def test_format_report_no_crash_on_empty(): diff --git a/tg_bot/trading_report.py b/tg_bot/trading_report.py index 99fd99d24..66e86ed4c 100644 --- a/tg_bot/trading_report.py +++ b/tg_bot/trading_report.py @@ -227,8 +227,14 @@ def snap_services() -> dict[str, str]: def build_snapshot() -> dict: + try: + from run_morning_brief import btc_regime + regime = btc_regime() + except Exception: + regime = None return { "generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC"), + "btc_regime": regime, "directional": snap_directional(), "dca": snap_dca(), "basket": snap_basket(), @@ -241,121 +247,198 @@ def build_snapshot() -> dict: # -------------------------------------------------------------- formatting -- +REASON_HUMAN = { + "stop_loss": "защитный стоп (продали, чтобы не потерять больше)", + "trailing_stop": "трейлинг-стоп (зафиксировали прибыль по пути)", + "take_profit": "цель прибыли достигнута", + "confirmed_bearish_exit": "сигнал разворота (робот увидел слабость)", +} + + +def _short_pct(v: float, base: float) -> str: + if not base: + return "—" + return f"{v / base * 100:+.2f}%" + + def format_report(snap: dict) -> list[str]: - """HTML-отчёт кусками (Telegram-безопасными).""" + """Отчёт «по-человечески»: простые слова, «что это» к каждому контуру, + вердикты обывателю и словарик. Telegram-безопасные куски.""" lines = [ - "📊 Детальный трейдинг-отчёт", + "📊 Трейдинг-отчёт (по-человечески)", f"🕐 {snap['generated_at']}", "", + "── Главное за 30 секунд ──", ] - # Directional v2 A/B d = snap["directional"] - lines.append("⚖️ Directional v2 paper (A/B)") + main_a = d["arms"].get("main") + ctrl_a = d["arms"].get("control") + if d["ok"] and main_a and ctrl_a: + lines.append( + f"🤖 Робот-трейдер (ученик): {_short_pct(main_a['realized'], 10000)} и " + f"{_short_pct(ctrl_a['realized'], 10000)} от счёта — тренируется на виртуальных " + f"деньгах, убытки копеечные.") + b = snap["basket"] + if b.get("value") is not None: + lines.append(f"🧺 Корзина топ-10: {b['pnl_pct']:+.2f}% — наш «крипто-ETF».") + dca = snap["dca"] + va = dca.get("VA main", {}) + if va.get("value") is not None: + pnl = va["value"] - va["deposited"] + lines.append(f"🐷 Автокопилка: {_short_pct(pnl, va['deposited'])} — копит по чуть-чуть каждую неделю.") + t2 = snap["t2"] + if t2.get("portfolio"): + pct = (t2["portfolio"]["portfolio"] / 10000 - 1) * 100 + lines.append(f"📈 Моментум-роботы: +{pct:.0f}% за всё время теста.") + if snap.get("btc_regime") == "bear": + lines.append("🐻 Рынок в медвежьей фазе: BTC ниже своего долгосрочного среднего.") + sb = snap["scoreboard"] + if sb: + v = sb.get("verdict") or {} + lines.append(f"🏆 По тесту за месяц лучшая стратегия — корзина топ-10.") + lines.append("") + + # ---------- Directional ---------- + lines.append("🤖 Робот-трейдер (сам ищет моменты, на виртуальные деньги)") + lines.append("Что это: программа с машинным обучением сама решает, когда купить и когда " + "продать. Два робота-близнеца «А» и «Б» соревнуются — отличаются лишь тем, " + "как выходят из сделки.") if d["ok"]: - for tag in ("main", "control"): + for tag, icon, name in (("main", "🅰️", "Робот А"), ("control", "🅱️", "Робот Б")): a = d["arms"].get(tag) if not a: continue - icon = "🅰️" if tag == "main" else "🅱️" - wr = f" (win {a['win_rate']}%)" if a["win_rate"] is not None else "" + wr = f"{a['win_rate']}%" if a["win_rate"] is not None else "—" lines.append( - f"{icon} {tag} (trail {'1.0' if tag == 'main' else '0.988'}): " - f"{a['closed']} сд.{wr} | PnL {_fmt_usd(a['realized'])} " - f"| gross {_fmt_usd(a['gross'])} | fees {_fmt_usd(a['fees'])}") - lines.append(f" equity {_fmt_usd(a['equity'])} | DD {a['dd_pct']:.3f}% " - f"| открытых: {a['open_positions']} | режим {a['entry_mode']}") - for t in a["recent_trades"]: - lines.append( - f" • {t.get('exchange')}:{t.get('symbol')} {t.get('reason')} " - f"PnL {_fmt_usd(float(t.get('net_pnl_usd', 0.0) or 0.0))}") + f"{icon} {name}: {a['closed']} сделок, удачных {wr}, " + f"итог {_short_pct(a['realized'], 10000)} от счёта " + f"(в деньгах {_fmt_usd(a['realized'])})") + lines.append(f" на счету {_fmt_usd(a['equity'])} из 10 000$ " + f"| просадка {a['dd_pct']:.3f}%") + if a["recent_trades"]: + lines.append(" последние сделки:") + for t in a["recent_trades"]: + reason = REASON_HUMAN.get(t.get("reason"), t.get("reason") or "—") + lines.append(f" • {t.get('symbol')} ({t.get('exchange')}): {reason} — " + f"итог {_fmt_usd(float(t.get('net_pnl_usd', 0.0) or 0.0))}") + if main_a and ctrl_a: + better = "Робот А" if main_a["realized"] >= ctrl_a["realized"] else "Робот Б" + lines.append(f"Кто впереди: {better} — но сделок мало " + f"(по {main_a['closed']}), выводы рано.") + lines.append("Защита: если счёт за день просядет на 0.25% — торговля остановится сама.") else: lines.append("нет данных") lines.append("") - # DCA - lines.append("📈 DCA (еженедельный)") - for tag in ("VA main", "control"): - a = snap["dca"].get(tag, {}) + # ---------- DCA ---------- + lines.append("🐷 Автокопилка (DCA)") + lines.append("Что это: каждую неделю докупается крипта на фиксированную сумму. Цены упали — " + "купили дешевле, выросли — купили меньше. Классический способ копить в долгую.") + for tag, name in (("VA main", "основная копилка"), ("control", "контрольная")): + a = dca.get(tag, {}) if a.get("value") is None: continue - pnl = a["value"] - (a["deposited"] or 0) - pct = pnl / a["deposited"] * 100 if a["deposited"] else 0 - lines.append( - f"• {tag} [{a['mode']}, ${a['weekly']}/нед]: " - f"внесено {_fmt_usd(a['deposited'])} → {_fmt_usd(a['value'])} " - f"({_fmt_usd(pnl)} / {pct:+.1f}%) | {a['date']}") + pnl = a["value"] - a["deposited"] + lines.append(f"• {name}: вложено {_fmt_usd(a['deposited'])} → сейчас {_fmt_usd(a['value'])} " + f"({_short_pct(pnl, a['deposited'])})") lines.append("") - # Basket - b = snap["basket"] - lines.append("🧺 Корзина топ-10 (vol-targeting)") + # ---------- Basket ---------- + lines.append("🧺 Корзина топ-10 (наш «крипто-ETF»)") + lines.append("Что это: одна покупка = сразу 10 крупнейших криптовалют. Правило тихое: чем " + "спокойнее монета (меньше скачет цена), тем больше её доля — чтобы корзину " + "меньше трясло.") if b.get("value") is not None: - lines.append( - f"• {_fmt_usd(b['value'])} ({b['pnl_pct']:+.2f}%) | внесено {_fmt_usd(b['invested'])} " - f"| fees {_fmt_usd(b['fees'])} | кэш {_fmt_usd(b['cash'])} | {b['date']}") + lines.append(f"• вложено {_fmt_usd(b['invested'])} → сейчас {_fmt_usd(b['value'])} " + f"({b['pnl_pct']:+.2f}%) | комиссии {_fmt_usd(b['fees'])}") lines.append(f"• правило весов: {b.get('weights_rule', '?')}") else: lines.append("нет данных") lines.append("") - # T2 momentum - t2 = snap["t2"] - lines.append("📉 T2 momentum paper (SMA50-гистерезис, ежедневно)") + # ---------- T2 ---------- + lines.append("📈 Моментум-роботы (T2) — держим монету, пока растёт") + lines.append("Что это: робот заходит, когда монета в устойчивом тренде, и выходит, когда " + "тренд ломается. Проценты ниже — за всю историю теста (год), не за месяц.") for tag, leg in t2["legs"].items(): pct = (leg["equity"] / 10000 - 1) * 100 - lines.append( - f"• {tag}: {leg['position']} | equity {_fmt_usd(leg['equity'])} ({pct:+.1f}%) " - f"| BH-эквив {_fmt_usd(leg['cash_equiv'])} | {leg['trades']} сд.") + state = {"LONG": "в позиции", "CASH": "вне рынка (кэш)"}.get(leg["position"], str(leg["position"])) + lines.append(f"• {tag}: {state} | счёт {_fmt_usd(leg['equity'])} (+{pct:.0f}%) | сделок {leg['trades']}") if t2["portfolio"]: p = t2["portfolio"] pct = (p["portfolio"] / 10000 - 1) * 100 bh_pct = (p["bh"] / 10000 - 1) * 100 - lines.append(f"🧺 портфель T2: {_fmt_usd(p['portfolio'])} ({pct:+.1f}%) " - f"| BH {bh_pct:+.1f}% | {p['date']}") + lines.append(f"🧺 все вместе: {_fmt_usd(p['portfolio'])} (+{pct:.0f}%) против простого " + f"«купи и держи» (+{bh_pct:.0f}%)") lines.append("") - # Freqtrade + # ---------- freqtrade ---------- ft = snap["freqtrade"] - lines.append(f"🤖 freqtrade T2 dry (закрытых: {ft['closed']})") + lines.append("🤖 Второй робот (freqtrade) — торгует по тренду на 5 монетах") + lines.append(f"Закрытых сделок: {ft['closed']}. Сейчас держит:") for row in ft["open"]: - lines.append(f"• {row[0]}: открыта {row[3][:16]} @ {row[1]}, SL {row[4]}") + lines.append(f"• {row[0]}: куплено по {row[1]}, защитный стоп на {row[4]} " + f"(если цена упадёт — продаст сам)") if not ft["open"]: - lines.append("открытых сделок нет") + lines.append("сейчас ничего не держит") lines.append("") - # MM + # ---------- MM ---------- m = snap["mm"] - lines.append("🌊 MM / микроструктура") + lines.append("🌊 Данные для обучения") if m: - lines.append(f"• ws-снапшотов: {m.get('snapshots', 0):,} ({m.get('span_h', 0)} ч), " - f"trade-flow: {m.get('trade_flow', 0):,}") + lines.append(f"• собрано {m.get('snapshots', 0):,} «снимков» биржевого стакана за " + f"{m.get('span_h', 0)} часов — на них учатся алгоритмы предсказаний") if m.get("btc_touch_life_s"): - lines.append(f"• BTC тач-жизнь {m['btc_touch_life_s']}с | " - f"fill(60с, $2000) {m.get('btc_fill60_q2000')}") + lines.append(f"• BTC: лучшая цена живёт в среднем {m['btc_touch_life_s']} секунд") else: lines.append("нет данных") lines.append("") - # Scoreboard - sb = snap["scoreboard"] + # ---------- Scoreboard ---------- if sb: v = sb.get("verdict") or {} w = v.get("winner", "—") + winner_name = {"top10_basket": "корзина топ-10", + "directional_v2": "робот-трейдер"}.get(w, w) if v.get("unstable"): - w += " ⚠️" + winner_name += " ⚠️" basket = sb.get("top10_basket_pct") - basket_s = f"{basket:+.2f}%" if basket is not None else "—" - lines.append(f"🏆 Scoreboard {sb['date']}: DV2 {sb['dv2']['pnl_pct']:+.2f}% | " - f"корзина {basket_s} | рынок {sb['market_mean_pct']:+.2f}% | 🥇 {w}") + lines.append(f"🏆 Кто лучший по тесту за {sb['date']}") + lines.append("Протестировали стратегии «как будто начали месяц назад»:") + lines.append(f"• робот-трейдер: {sb['dv2']['pnl_pct']:+.2f}%") + if basket is not None: + lines.append(f"• корзина топ-10: {basket:+.2f}%") + lines.append(f"• рынок в среднем: {sb['market_mean_pct']:+.2f}%") + lines.append(f"🥇 Победитель: {winner_name}") lines.append("") - # Services + # ---------- services ---------- svc = snap["services"] bad = [k for k, v in svc.items() if v != "active"] - lines.append(f"🖥 Сервисы: {len(svc) - len(bad)}/{len(svc)} active" - + (f" | ⚠️ {', '.join(bad)}" if bad else "")) + total = len(svc) + lines.append(f"🖥 Техника: {total - len(bad)} из {total} сервисов работают" + + (f" | ⚠️ лежит: {', '.join(bad)}" if bad else " — всё в порядке")) + lines.append("") + + # ---------- простой вывод ---------- + lines.append("🧸 Простыми словами") + if snap.get("btc_regime") == "bear": + lines.append("Рынок сейчас в медвежьей фазе (цены под давлением). Наши тесты показали, " + "что угадывать короткие движения рынка невыгодно, поэтому робот " + "осторожничает и тренируется на виртуальных деньгах. Зарабатывают тихие " + "стратегии — копилка и корзина: они и держат основную роль.") + else: + lines.append("Система работает в штатном режиме: робот тренируется на виртуальных " + "деньгах, тихие стратегии (копилка и корзина) накапливают, всё под " + "автоматической защитой от крупных потерь.") + lines.append("") + lines.append("📖 Словарик") + lines.append("стоп — автопродажа, чтобы убыток не рос; тренд — общее направление цены; " + "просадка — насколько счёт опускался от максимума; волатильность — насколько " + "сильно скачет цена.") return _chunks("\n".join(lines)) @@ -402,17 +485,19 @@ def prompt_for_llm(snap: dict) -> str: SYSTEM_PROMPT = ( - "Ты — количественный аналитик торговой системы AIOS. Все контуры paper (без реальных " - "денег). Известные результаты исследований системы: направленный edge на 1h не найден " - "(ML AUC ~0.52-0.53, все эксперименты отрицательны); market-making edge нет; " - "пассивная корзина топ-10 и DCA — единственные устойчиво положительные контуры; " - "текущий режим рынка медвежий (BTC ниже SMA200). Отвечай строго по данным, не выдумывай. " - "Формат ответа (русский, ≤900 символов): " - "1) Риски — 3-5 пунктов по текущему состоянию; " - "2) Анализ портфелей — что здорово/что слабо по цифрам; " - "3) Сценарии-прогнозы — 3 варианта (вероятности %) на 1-2 недели с учётом медвежьего " - "режима и отсутствия edge; в конце обязательная строка: 'Не является финансовым " - "советом; paper-исследование'." + "Ты — дружелюбный помощник, который объясняет состояние торговой системы AIOS " + "простым языком, понятным человеку без финансового образования. Все портфели " + "виртуальные (paper) — реальные деньги не используются. Известные факты системы: " + "попытки угадывать короткие движения рынка не дают преимущества (проверено десятками " + "честных тестов); тихие стратегии (корзина из 10 монет, еженедельная копилка) — " + "единственные устойчиво положительные; сейчас рынок в медвежьей фазе. " + "Пиши без жаргона; если без термина никак — объясни его в скобках. " + "Формат ответа (русский, до 900 символов): " + "1) «Главное за 30 секунд» — 3-4 простых предложения; " + "2) «Что с каждым портфелем» — по одной строке на портфель; " + "3) «Чего ждать» — 3 сценария на 1-2 недели с вероятностями в процентах и что каждый " + "сценарий значит для обычного человека. В конце обязательная строка: " + "«Это не финансовый совет — система работает на виртуальные деньги»." ) @@ -441,9 +526,9 @@ def llm_section(snap: dict) -> list[str]: analysis = llm_analysis(snap) if not analysis: - return ["🤖 LLM-аналитика: временно недоступна (балансер не ответил). " - "Данные отчёта выше актуальны."] - return _chunks("🤖 LLM-аналитика и сценарии\n" + analysis) + return ["🤖 Что думает наш ИИ-аналитик: временно недоступен (сервис не " + "ответил). Цифры выше актуальны, попробуйте ещё раз чуть позже."] + return _chunks("🤖 Что думает наш ИИ-аналитик\n" + analysis) def full_report() -> list[str]: