From f6e72428b736f1c9394c63d89b60b970c764df58 Mon Sep 17 00:00:00 2001 From: Haozhe Wu Date: Mon, 5 Oct 2026 11:16:24 +0800 Subject: [PATCH] fix: finish research prompts and grouped validation boundaries --- CONTRIBUTING.md | 7 +- README.md | 6 +- README_ar.md | 6 +- README_es.md | 6 +- README_id.md | 6 +- README_ja.md | 6 +- README_ko.md | 6 +- README_zh.md | 6 +- agent/src/api/sessions_routes.py | 35 ++++--- agent/src/memory/persistent.py | 3 + agent/src/quantlib/crossvalidation.py | 32 ++++--- agent/src/shadow_account/extractor.py | 69 +------------- agent/src/shadow_account/indicators.py | 50 ++++++++++ agent/src/shadow_account/scanner.py | 44 +-------- agent/src/strategy_store/metrics.py | 8 +- agent/src/tools/remember_tool.py | 4 +- agent/tests/quantlib/test_crossvalidation.py | 4 +- .../tests/test_channel_authoring_contract.py | 89 +++++++++--------- agent/tests/test_interactive_message_limit.py | 48 ++++++++++ agent/tests/test_sdm_finite_metrics.py | 6 +- agent/tests/test_ux_batch_regressions.py | 91 +++++++++++++++++++ frontend/src/components/chat/Composer.tsx | 12 ++- .../chat/__tests__/Composer.test.tsx | 30 ++++++ frontend/src/i18n/locales/ar.json | 1 + frontend/src/i18n/locales/de.json | 1 + frontend/src/i18n/locales/en.json | 1 + frontend/src/i18n/locales/es.json | 1 + frontend/src/i18n/locales/id.json | 1 + frontend/src/i18n/locales/ja.json | 1 + frontend/src/i18n/locales/ko.json | 1 + frontend/src/i18n/locales/pt-BR.json | 1 + frontend/src/i18n/locales/zh-CN.json | 1 + frontend/src/lib/__tests__/api.test.ts | 9 ++ frontend/src/lib/__tests__/chatPrompt.test.ts | 13 +++ frontend/src/lib/api.ts | 5 +- frontend/src/lib/chatPrompt.ts | 20 ++++ frontend/src/pages/Agent.tsx | 17 ++-- 37 files changed, 432 insertions(+), 215 deletions(-) create mode 100644 agent/src/shadow_account/indicators.py create mode 100644 agent/tests/test_interactive_message_limit.py create mode 100644 agent/tests/test_ux_batch_regressions.py create mode 100644 frontend/src/lib/__tests__/chatPrompt.test.ts create mode 100644 frontend/src/lib/chatPrompt.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 12da580e..13a765fa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -149,9 +149,10 @@ configuration with no per-channel frontend code. from the base class and manager. 2. Field metadata lives in `agent/src/channels/config_meta.py`. Hand-written `FIELD_HINTS[name]` entries supply labels and authoritative secret flags; - anything you omit is derived from `default_config()` with type inference, and - credential-shaped keys are masked by the unconditional `SECRET_KEY_RE` - fail-safe either way. If the platform has a credential endpoint, build the + channels without hand-written hints derive them from `default_config()` with + type inference. Stored keys without a hand-written declaration use the + `SECRET_KEY_RE` fail-safe. Declared secret flags are authoritative, including + audited exceptions for benign path or timeout keys. If the platform has a credential endpoint, build the connection probe on `token_probe.py` rather than writing a new client (see `dingtalk_probe.py` for the pattern) and override `test_connection()`. 3. Run the authoring contract locally: diff --git a/README.md b/README.md index 13beef34..bf7aa05b 100644 --- a/README.md +++ b/README.md @@ -52,16 +52,18 @@ > ⚠️ **Security warning:** The X account `VibeTrading_HKU`, Virtuals project `101845`, and token contract `0x640BDBF77b6447E8b7DB7894cED84BD1c40571f4` are not official Vibe-Trading assets. We have never launched or endorsed any token or memecoin. Do not buy, connect a wallet, or sign anything. [Details](SECURITY.md#official-channels--impersonation). +- **2026-10-05** 🛠️ **Research prompts and calculation fixes**: Chat accepts longer research prompts and gives a localized recovery message when input is too large ([#1701](https://github.com/HKUDS/Vibe-Trading/pull/1701)). Backtests use full-sample Sortino downside deviation; grouped validation purges overlapping labels, covariance weights remain finite, shadow RSI uses Wilder seeds, and memory removal accepts filename stems. + - **2026-10-04** 🛠️ **Scheduled reports and research workflows**: Edit scheduled runs and choose a configured destination; Email reports support HTML or PDF attachments ([#1649](https://github.com/HKUDS/Vibe-Trading/pull/1649), [#1680](https://github.com/HKUDS/Vibe-Trading/pull/1680)). Backtests expose structured summaries and paged artifact reads ([#1646](https://github.com/HKUDS/Vibe-Trading/pull/1646), [#1647](https://github.com/HKUDS/Vibe-Trading/pull/1647)). Fixes cover memory-search snippets, export-path guidance, macro truncation, monthly risk, turnover on reversals and cash reentry, IV accuracy, VaR gaps, VCS updates and Robinhood option-order blocking. - **2026-10-03** 🛠️ **Research, reports and data reliability**: CJK session search, channel setup, broker exposure pricing and file writes now handle cases that blocked everyday use. PDF delivery embeds CJK fonts, Swarm validates preset inputs and separates task artifacts, and replayed tool results survive context compaction ([#1683](https://github.com/HKUDS/Vibe-Trading/pull/1683), [#1455](https://github.com/HKUDS/Vibe-Trading/pull/1455), [#1635](https://github.com/HKUDS/Vibe-Trading/pull/1635)). Backtests keep one adjustment basis, local caches distinguish sources, single-asset caps and weekly/monthly risk use the declared settings, audits retain loss signs, grounding checks the current engine output and exact list references, and Stooq retries after a denial cooldown ([#1684](https://github.com/HKUDS/Vibe-Trading/pull/1684), [#1650](https://github.com/HKUDS/Vibe-Trading/pull/1650), [#1685](https://github.com/HKUDS/Vibe-Trading/pull/1685), [#1640](https://github.com/HKUDS/Vibe-Trading/pull/1640)). -- **2026-10-02** 🛠️ **Backtests and report checks**: strategy-file writes retain model provenance without crashing ([#1673](https://github.com/HKUDS/Vibe-Trading/pull/1673)), and Monte Carlo drawdown and Sharpe include starting capital ([#1664](https://github.com/HKUDS/Vibe-Trading/pull/1664)). Report audits preserve accounting negatives and units ([#1663](https://github.com/HKUDS/Vibe-Trading/pull/1663)); grounding artifacts record fired declared checks ([#1661](https://github.com/HKUDS/Vibe-Trading/pull/1661)); public loader-health reports include sanitized failure reasons ([#1643](https://github.com/HKUDS/Vibe-Trading/pull/1643)). Indonesian tool documentation matches the registry ([#1671](https://github.com/HKUDS/Vibe-Trading/pull/1671)). -
Earlier news +- **2026-10-02** 🛠️ **Backtests and report checks**: strategy-file writes retain model provenance without crashing ([#1673](https://github.com/HKUDS/Vibe-Trading/pull/1673)), and Monte Carlo drawdown and Sharpe include starting capital ([#1664](https://github.com/HKUDS/Vibe-Trading/pull/1664)). Report audits preserve accounting negatives and units ([#1663](https://github.com/HKUDS/Vibe-Trading/pull/1663)); grounding artifacts record fired declared checks ([#1661](https://github.com/HKUDS/Vibe-Trading/pull/1661)); public loader-health reports include sanitized failure reasons ([#1643](https://github.com/HKUDS/Vibe-Trading/pull/1643)). Indonesian tool documentation matches the registry ([#1671](https://github.com/HKUDS/Vibe-Trading/pull/1671)). + - **2026-10-01** ✅ **Data correctness and reproducible backtests**: A-share adjustment conversion now refuses ambiguous one-bar edge cases ([#1551](https://github.com/HKUDS/Vibe-Trading/pull/1551)); Southbound Eastmoney amounts are scaled from million HKD and rejected responses no longer look empty ([#1486](https://github.com/HKUDS/Vibe-Trading/pull/1486)); Northbound fallback data distinguishes post-2024-08-19 turnover from net flow ([#1484](https://github.com/HKUDS/Vibe-Trading/pull/1484)); Binance-only unpriced positions are marked incomplete without penalising other brokers ([#1505](https://github.com/HKUDS/Vibe-Trading/pull/1505)); and backtest run cards record model provenance and warn when the training cutoff is unknown or outside the test window ([#1618](https://github.com/HKUDS/Vibe-Trading/pull/1618), closes [#1613](https://github.com/HKUDS/Vibe-Trading/issues/1613)). - **2026-09-30** 🛠️ **Feishu in the Web UI, a momentum factor that read today's price, and a profit factor for runs that never lost**: Feishu joins the guided channel setup, with a standalone connection test and a hot reload that closes the old WebSocket ([#1572](https://github.com/HKUDS/Vibe-Trading/pull/1572)). `academic_carhart_mom` subtracted the 1-month return from the 12-month one, so it moved with today's close; it is now the return from 12 months ago to 1 month ago ([#1578](https://github.com/HKUDS/Vibe-Trading/pull/1578)). A run with no losing trade reports its profit factor as undefined instead of 0.0, which ranked it last ([#1602](https://github.com/HKUDS/Vibe-Trading/pull/1602)). Stooq's anti-bot page now stops every later request in the process, not just the log line ([#1637](https://github.com/HKUDS/Vibe-Trading/pull/1637)); MCP tool results reach the agent once instead of up to four times ([#1634](https://github.com/HKUDS/Vibe-Trading/pull/1634)); and the first piece of `loop.py` moves into its own module ([#1636](https://github.com/HKUDS/Vibe-Trading/pull/1636)). diff --git a/README_ar.md b/README_ar.md index 05de280a..a54907a8 100644 --- a/README_ar.md +++ b/README_ar.md @@ -52,16 +52,18 @@ > ⚠️ **تحذير أمني:** حساب X باسم `VibeTrading_HKU`، ومشروع Virtuals رقم `101845`، وعقد التوكن `0x640BDBF77b6447E8b7DB7894cED84BD1c40571f4` كلّها غير رسمية ولا تتبع Vibe-Trading. لم نُطلق أو نؤيد مطلقًا أي توكن أو عملة ميم. لا تشترِ هذا التوكن، ولا تربط محفظتك، ولا توقّع أي شيء. [التفاصيل](SECURITY.md#official-channels--impersonation). +- **2026-10-05** 🛠️ **إدخال البحث وتصحيح الحسابات**: تدعم المحادثة نصوص بحث أطول وتوضح باللغة الحالية كيفية تقصير النص عند تجاوز الحد ([#1701](https://github.com/HKUDS/Vibe-Trading/pull/1701)). يستخدم Sortino انحراف الجانب السلبي عبر جميع الفترات، ويستبعد التحقق الجماعي التسميات المتداخلة. تحسنت أوزان التباين وRSI بمتوسط Wilder الأولي وحذف الذاكرة باسم الملف. + - **2026-10-04** 🛠️ **تحسين التقارير المجدولة وسير البحث**: يمكن تعديل المهام المجدولة واختيار وجهة مُعدّة مسبقًا، مع إرسال تقارير البريد بصيغة HTML أو كمرفقات PDF ([#1649](https://github.com/HKUDS/Vibe-Trading/pull/1649), [#1680](https://github.com/HKUDS/Vibe-Trading/pull/1680)). تتضمن الاختبارات التاريخية ملخصات منظّمة وقراءة ملفات النتائج على صفحات ([#1646](https://github.com/HKUDS/Vibe-Trading/pull/1646), [#1647](https://github.com/HKUDS/Vibe-Trading/pull/1647)). تشمل الإصلاحات مقتطفات بحث الذاكرة، وإرشادات مسار التصدير، وبيان اقتطاع البيانات الكلية، ومخاطر الشموع الشهرية، ودوران المحفظة عند عكس الاتجاه أو إعادة الاستثمار بعد التحول إلى النقد، ودقة التقلب الضمني، وفجوات VaR، وتحديث التثبيت من VCS، وحظر أوامر خيارات Robinhood غير المدعومة. - **2026-10-03** 🛠️ **تحسين موثوقية البحث والتقارير والبيانات**: أُصلحت مشكلات البحث في الجلسات بالصينية واليابانية والكورية، وإعداد القنوات، وتقييم حيازات الوسطاء، وكتابة الملفات التي كانت تعيق الاستخدام اليومي. تتضمن ملفات PDF خطوط CJK، ويتحقق Swarm من مدخلات الإعدادات المسبقة ويفصل مخرجات المهام، وتبقى نتائج الأدوات المعاد استخدامها متاحة بعد ضغط السياق ([#1683](https://github.com/HKUDS/Vibe-Trading/pull/1683), [#1455](https://github.com/HKUDS/Vibe-Trading/pull/1455), [#1635](https://github.com/HKUDS/Vibe-Trading/pull/1635)). تستخدم الاختبارات التاريخية أساسًا موحدًا لتعديل الأسعار، وتميّز الذاكرة المؤقتة المحلية بين مصادر البيانات، وتتبع حدود الأصل الواحد وحسابات المخاطر الأسبوعية والشهرية الإعدادات المعلنة. يحافظ التدقيق على إشارة الخسائر، ويعتمد التحقق الرقمي على مخرجات المحرك الحالية ومراجع القوائم الدقيقة، ويعيد Stooq المحاولة بعد انتهاء فترة الانتظار عقب الرفض ([#1684](https://github.com/HKUDS/Vibe-Trading/pull/1684), [#1650](https://github.com/HKUDS/Vibe-Trading/pull/1650), [#1685](https://github.com/HKUDS/Vibe-Trading/pull/1685), [#1640](https://github.com/HKUDS/Vibe-Trading/pull/1640)). -- **2026-10-02** 🛠️ **إصلاحات الاختبارات الخلفية وتدقيق التقارير**: تحتفظ كتابة ملفات الاستراتيجيات بمصدر النموذج دون تعطل ([#1673](https://github.com/HKUDS/Vibe-Trading/pull/1673))، وتشمل حسابات التراجع وSharpe في محاكاة مونت كارلو رأس المال الابتدائي ([#1664](https://github.com/HKUDS/Vibe-Trading/pull/1664)). يحافظ تدقيق التقارير على القيم السالبة المحاسبية بين الأقواس ووحداتها ([#1663](https://github.com/HKUDS/Vibe-Trading/pull/1663))، وتسجل مخرجات التحقق العددي الفحوص المعلنة التي تفعّلت ([#1661](https://github.com/HKUDS/Vibe-Trading/pull/1661)). تتضمن تقارير صحة مصادر البيانات العامة أسباب الفشل بعد إزالة المعلومات الحساسة ([#1643](https://github.com/HKUDS/Vibe-Trading/pull/1643))، وتطابق وثائق الأدوات الإندونيسية سجل الأدوات ([#1671](https://github.com/HKUDS/Vibe-Trading/pull/1671)). -
أخبار سابقة +- **2026-10-02** 🛠️ **إصلاحات الاختبارات الخلفية وتدقيق التقارير**: تحتفظ كتابة ملفات الاستراتيجيات بمصدر النموذج دون تعطل ([#1673](https://github.com/HKUDS/Vibe-Trading/pull/1673))، وتشمل حسابات التراجع وSharpe في محاكاة مونت كارلو رأس المال الابتدائي ([#1664](https://github.com/HKUDS/Vibe-Trading/pull/1664)). يحافظ تدقيق التقارير على القيم السالبة المحاسبية بين الأقواس ووحداتها ([#1663](https://github.com/HKUDS/Vibe-Trading/pull/1663))، وتسجل مخرجات التحقق العددي الفحوص المعلنة التي تفعّلت ([#1661](https://github.com/HKUDS/Vibe-Trading/pull/1661)). تتضمن تقارير صحة مصادر البيانات العامة أسباب الفشل بعد إزالة المعلومات الحساسة ([#1643](https://github.com/HKUDS/Vibe-Trading/pull/1643))، وتطابق وثائق الأدوات الإندونيسية سجل الأدوات ([#1671](https://github.com/HKUDS/Vibe-Trading/pull/1671)). + - **2026-10-01** ✅ **دقة البيانات واختبارات خلفية قابلة للتكرار**: يرفض تحويل الأسعار المعدلة لأسهم A الحالات الملتبسة ذات الشمعة الواحدة ([#1551](https://github.com/HKUDS/Vibe-Trading/pull/1551)). تُحوَّل مبالغ التدفقات جنوباً من Eastmoney من وحدة مليون دولار هونغ كونغ، ولا تُعامل الردود الفاشلة كبيانات فارغة ([#1486](https://github.com/HKUDS/Vibe-Trading/pull/1486)). يميز المصدر البديل للتدفقات شمالاً بين قيمة التداول وصافي التدفق بعد 2024-08-19 ([#1484](https://github.com/HKUDS/Vibe-Trading/pull/1484)). تُصنَّف مراكز Binance غير المسعّرة وحدها على أنها غير مكتملة دون التأثير على الوسطاء الآخرين ([#1505](https://github.com/HKUDS/Vibe-Trading/pull/1505)). تسجل بطاقات التشغيل مصدر النموذج وتحذر عندما يكون تاريخ انتهاء التدريب مجهولاً أو خارج فترة الاختبار ([#1618](https://github.com/HKUDS/Vibe-Trading/pull/1618)). - **2026-09-30** 🛠️ **إعداد Feishu في واجهة الويب، وعامل زخم كان يقرأ سعر اليوم، وعامل الربح لاختبار لم يخسر أي صفقة**: انضم Feishu إلى إعداد القنوات الموجَّه، مع اختبار اتصال مستقل وإعادة تحميل فورية تُغلق اتصال WebSocket القديم ([#1572](https://github.com/HKUDS/Vibe-Trading/pull/1572)). كان `academic_carhart_mom` يطرح عائد شهر واحد من عائد 12 شهراً فيتحرك مع سعر إغلاق اليوم، وأصبح الآن العائد من قبل 12 شهراً إلى قبل شهر واحد ([#1578](https://github.com/HKUDS/Vibe-Trading/pull/1578)). الاختبار الذي لم يخسر أي صفقة يُبلغ الآن عن عامل الربح على أنه غير معرَّف بدلاً من 0.0 الذي كان يضعه في المرتبة الأخيرة ([#1602](https://github.com/HKUDS/Vibe-Trading/pull/1602)). صفحة الحماية من الروبوتات في Stooq توقف الآن كل الطلبات اللاحقة في العملية، لا سطر السجل فقط ([#1637](https://github.com/HKUDS/Vibe-Trading/pull/1637))؛ وتصل نتائج أدوات MCP إلى الوكيل مرة واحدة بدلاً من أربع مرات ([#1634](https://github.com/HKUDS/Vibe-Trading/pull/1634))؛ وانفصلت أول وحدة من `loop.py` ([#1636](https://github.com/HKUDS/Vibe-Trading/pull/1636)). diff --git a/README_es.md b/README_es.md index 7eee102c..d40e9afc 100644 --- a/README_es.md +++ b/README_es.md @@ -52,16 +52,18 @@ > ⚠️ **Advertencia de seguridad:** la cuenta de X `VibeTrading_HKU`, el proyecto de Virtuals `101845` y el contrato de token `0x640BDBF77b6447E8b7DB7894cED84BD1c40571f4` no son activos oficiales de Vibe-Trading. Nunca hemos lanzado ni respaldado ningún token o memecoin. No compres, conectes una wallet ni firmes nada. [Detalles](SECURITY.md#official-channels--impersonation). +- **2026-10-05** 🛠️ **Consultas de investigación y cálculos corregidos**: El chat admite consultas más largas e indica en el idioma actual cómo acortarlas cuando superan el límite ([#1701](https://github.com/HKUDS/Vibe-Trading/pull/1701)). Sortino usa la desviación a la baja de todos los períodos; la validación por grupos excluye etiquetas solapadas. También mejoran los pesos de covarianza, el RSI con media inicial de Wilder y la eliminación de memorias por nombre de archivo. + - **2026-10-04** 🛠️ **Informes programados y flujos de investigación**: Edita tareas programadas y elige un destino configurado; los informes por email admiten HTML o archivos PDF adjuntos ([#1649](https://github.com/HKUDS/Vibe-Trading/pull/1649), [#1680](https://github.com/HKUDS/Vibe-Trading/pull/1680)). Los backtests ofrecen resúmenes estructurados y lectura paginada de resultados ([#1646](https://github.com/HKUDS/Vibe-Trading/pull/1646), [#1647](https://github.com/HKUDS/Vibe-Trading/pull/1647)). Se corrigen fragmentos de búsqueda, indicaciones de exportación, avisos de datos macro truncados, riesgo mensual, rotación al invertir posiciones o reinvertir tras pasar a efectivo, precisión de IV, huecos de VaR, actualizaciones desde VCS y el bloqueo de órdenes de opciones no admitidas en Robinhood. - **2026-10-03** 🛠️ **Más fiabilidad en investigación, informes y datos**: se corrigen problemas de búsqueda de sesiones en chino, japonés y coreano, configuración de canales, valoración de posiciones del bróker y escritura de archivos que bloqueaban el uso diario. Los PDF incluyen fuentes CJK, Swarm valida las entradas de sus plantillas y separa los archivos por tarea, y los resultados reutilizados de herramientas sobreviven a la compactación del contexto ([#1683](https://github.com/HKUDS/Vibe-Trading/pull/1683), [#1455](https://github.com/HKUDS/Vibe-Trading/pull/1455), [#1635](https://github.com/HKUDS/Vibe-Trading/pull/1635)). Los backtests mantienen una base de ajuste uniforme, la caché local distingue las fuentes y los límites de un solo activo y el riesgo semanal/mensual respetan la configuración declarada. Las auditorías conservan el signo de las pérdidas, la verificación numérica usa la salida actual del motor y referencias exactas a listas, y Stooq vuelve a intentar tras la espera por rechazo ([#1684](https://github.com/HKUDS/Vibe-Trading/pull/1684), [#1650](https://github.com/HKUDS/Vibe-Trading/pull/1650), [#1685](https://github.com/HKUDS/Vibe-Trading/pull/1685), [#1640](https://github.com/HKUDS/Vibe-Trading/pull/1640)). -- **2026-10-02** 🛠️ **Correcciones en backtests y auditorías**: la escritura de estrategias conserva la procedencia del modelo sin fallar ([#1673](https://github.com/HKUDS/Vibe-Trading/pull/1673)), y el drawdown y Sharpe de Monte Carlo incluyen el capital inicial ([#1664](https://github.com/HKUDS/Vibe-Trading/pull/1664)). La auditoría conserva los negativos contables entre paréntesis y sus unidades ([#1663](https://github.com/HKUDS/Vibe-Trading/pull/1663)); los artefactos de verificación numérica registran las comprobaciones declaradas activadas ([#1661](https://github.com/HKUDS/Vibe-Trading/pull/1661)); los informes públicos de salud de fuentes incluyen motivos de fallo sin información sensible ([#1643](https://github.com/HKUDS/Vibe-Trading/pull/1643)). La documentación de herramientas en indonesio coincide con el registro ([#1671](https://github.com/HKUDS/Vibe-Trading/pull/1671)). -
Noticias anteriores +- **2026-10-02** 🛠️ **Correcciones en backtests y auditorías**: la escritura de estrategias conserva la procedencia del modelo sin fallar ([#1673](https://github.com/HKUDS/Vibe-Trading/pull/1673)), y el drawdown y Sharpe de Monte Carlo incluyen el capital inicial ([#1664](https://github.com/HKUDS/Vibe-Trading/pull/1664)). La auditoría conserva los negativos contables entre paréntesis y sus unidades ([#1663](https://github.com/HKUDS/Vibe-Trading/pull/1663)); los artefactos de verificación numérica registran las comprobaciones declaradas activadas ([#1661](https://github.com/HKUDS/Vibe-Trading/pull/1661)); los informes públicos de salud de fuentes incluyen motivos de fallo sin información sensible ([#1643](https://github.com/HKUDS/Vibe-Trading/pull/1643)). La documentación de herramientas en indonesio coincide con el registro ([#1671](https://github.com/HKUDS/Vibe-Trading/pull/1671)). + - **2026-10-01** ✅ **Datos correctos y backtests reproducibles**: la conversión de precios ajustados de acciones A rechaza casos ambiguos de una sola vela ([#1551](https://github.com/HKUDS/Vibe-Trading/pull/1551)). Los importes de flujos hacia Hong Kong de Eastmoney se convierten desde millones de HKD y las respuestas fallidas ya no parecen vacías ([#1486](https://github.com/HKUDS/Vibe-Trading/pull/1486)). La fuente alternativa de flujos hacia China distingue el volumen monetario del flujo neto después del 2024-08-19 ([#1484](https://github.com/HKUDS/Vibe-Trading/pull/1484)). Solo las posiciones sin precio de Binance se marcan como incompletas, sin penalizar a otros brókeres ([#1505](https://github.com/HKUDS/Vibe-Trading/pull/1505)). Las tarjetas de ejecución registran la procedencia del modelo y advierten si la fecha límite de entrenamiento es desconocida o está fuera del período probado ([#1618](https://github.com/HKUDS/Vibe-Trading/pull/1618)). - **2026-09-30** 🛠️ **Feishu en la interfaz web, un factor de momentum que leía el precio de hoy y el profit factor de una ejecución sin pérdidas**: Feishu se suma a la configuración guiada de canales, con una prueba de conexión independiente y una recarga en caliente que cierra el WebSocket anterior ([#1572](https://github.com/HKUDS/Vibe-Trading/pull/1572)). `academic_carhart_mom` restaba el rendimiento de 1 mes al de 12 meses, así que se movía con el cierre de hoy; ahora es el rendimiento desde hace 12 meses hasta hace 1 mes ([#1578](https://github.com/HKUDS/Vibe-Trading/pull/1578)). Una ejecución sin operaciones perdedoras informa su profit factor como indefinido en lugar de 0.0, que la colocaba en último lugar ([#1602](https://github.com/HKUDS/Vibe-Trading/pull/1602)). La página antibots de Stooq ahora detiene todas las solicitudes posteriores del proceso, no solo la línea de registro ([#1637](https://github.com/HKUDS/Vibe-Trading/pull/1637)); los resultados de herramientas MCP llegan al agente una vez en lugar de hasta cuatro ([#1634](https://github.com/HKUDS/Vibe-Trading/pull/1634)); y la primera parte de `loop.py` pasa a su propio módulo ([#1636](https://github.com/HKUDS/Vibe-Trading/pull/1636)). diff --git a/README_id.md b/README_id.md index 4f047457..aed77820 100644 --- a/README_id.md +++ b/README_id.md @@ -53,16 +53,18 @@ > ⚠️ **Peringatan keamanan:** Akun X `VibeTrading_HKU`, proyek Virtuals `101845`, dan kontrak token `0x640BDBF77b6447E8b7DB7894cED84BD1c40571f4` bukan aset resmi Vibe-Trading. Kami tidak pernah meluncurkan atau mendukung token maupun memecoin apa pun. Jangan membeli, menghubungkan wallet, atau menandatangani apa pun. [Detail](SECURITY.md#official-channels--impersonation). +- **2026-10-05** 🛠️ **Input riset dan perbaikan perhitungan**: Chat menerima input riset lebih panjang dan memberi petunjuk dalam bahasa aktif untuk mempersingkat teks yang melampaui batas ([#1701](https://github.com/HKUDS/Vibe-Trading/pull/1701)). Sortino memakai deviasi penurunan dari seluruh periode; validasi grup membuang label yang tumpang tindih. Bobot kovarians, RSI dengan rata-rata awal Wilder, dan penghapusan memori lewat nama file juga diperbaiki. + - **2026-10-04** 🛠️ **Laporan terjadwal dan alur riset**: Edit tugas terjadwal dan pilih tujuan yang sudah dikonfigurasi; laporan email mendukung HTML atau lampiran PDF ([#1649](https://github.com/HKUDS/Vibe-Trading/pull/1649), [#1680](https://github.com/HKUDS/Vibe-Trading/pull/1680)). Backtest menyediakan ringkasan terstruktur dan pembacaan artefak per halaman ([#1646](https://github.com/HKUDS/Vibe-Trading/pull/1646), [#1647](https://github.com/HKUDS/Vibe-Trading/pull/1647)). Perbaikan mencakup cuplikan pencarian memori, panduan lokasi ekspor, pemberitahuan pemangkasan data makro, risiko bulanan, turnover saat membalik posisi atau masuk kembali setelah menjadi kas, akurasi IV, celah VaR, pembaruan instalasi VCS, serta pemblokiran order opsi Robinhood yang belum didukung. - **2026-10-03** 🛠️ **Keandalan riset, laporan, dan data**: masalah pencarian sesi berbahasa Mandarin, Jepang, dan Korea, pengaturan kanal, penilaian posisi broker, serta penulisan berkas yang menghambat penggunaan sehari-hari telah diperbaiki. PDF menyertakan font CJK, Swarm memvalidasi masukan preset dan memisahkan keluaran tiap tugas, serta hasil alat yang digunakan kembali tetap tersedia setelah pemadatan konteks ([#1683](https://github.com/HKUDS/Vibe-Trading/pull/1683), [#1455](https://github.com/HKUDS/Vibe-Trading/pull/1455), [#1635](https://github.com/HKUDS/Vibe-Trading/pull/1635)). Backtest memakai dasar penyesuaian harga yang konsisten, cache lokal membedakan sumber, dan batas aset tunggal serta risiko mingguan/bulanan mengikuti pengaturan yang dinyatakan. Audit mempertahankan tanda kerugian, verifikasi angka memakai keluaran mesin saat ini dan referensi daftar yang tepat, serta Stooq mencoba lagi setelah masa tunggu akibat penolakan ([#1684](https://github.com/HKUDS/Vibe-Trading/pull/1684), [#1650](https://github.com/HKUDS/Vibe-Trading/pull/1650), [#1685](https://github.com/HKUDS/Vibe-Trading/pull/1685), [#1640](https://github.com/HKUDS/Vibe-Trading/pull/1640)). -- **2026-10-02** 🛠️ **Perbaikan backtest dan audit laporan**: penulisan berkas strategi mempertahankan asal model tanpa gagal ([#1673](https://github.com/HKUDS/Vibe-Trading/pull/1673)), dan drawdown serta Sharpe Monte Carlo mencakup modal awal ([#1664](https://github.com/HKUDS/Vibe-Trading/pull/1664)). Audit mempertahankan angka negatif akuntansi dalam tanda kurung beserta satuannya ([#1663](https://github.com/HKUDS/Vibe-Trading/pull/1663)); artefak verifikasi angka mencatat pemeriksaan deklaratif yang terpicu ([#1661](https://github.com/HKUDS/Vibe-Trading/pull/1661)); laporan kesehatan sumber data publik memuat alasan kegagalan tanpa informasi sensitif ([#1643](https://github.com/HKUDS/Vibe-Trading/pull/1643)). Dokumentasi alat berbahasa Indonesia diselaraskan dengan registri ([#1671](https://github.com/HKUDS/Vibe-Trading/pull/1671)). -
Berita sebelumnya +- **2026-10-02** 🛠️ **Perbaikan backtest dan audit laporan**: penulisan berkas strategi mempertahankan asal model tanpa gagal ([#1673](https://github.com/HKUDS/Vibe-Trading/pull/1673)), dan drawdown serta Sharpe Monte Carlo mencakup modal awal ([#1664](https://github.com/HKUDS/Vibe-Trading/pull/1664)). Audit mempertahankan angka negatif akuntansi dalam tanda kurung beserta satuannya ([#1663](https://github.com/HKUDS/Vibe-Trading/pull/1663)); artefak verifikasi angka mencatat pemeriksaan deklaratif yang terpicu ([#1661](https://github.com/HKUDS/Vibe-Trading/pull/1661)); laporan kesehatan sumber data publik memuat alasan kegagalan tanpa informasi sensitif ([#1643](https://github.com/HKUDS/Vibe-Trading/pull/1643)). Dokumentasi alat berbahasa Indonesia diselaraskan dengan registri ([#1671](https://github.com/HKUDS/Vibe-Trading/pull/1671)). + - **2026-10-01** ✅ **Ketepatan data dan backtest yang dapat direproduksi**: konversi harga tersesuaikan saham A menolak kasus ambigu dengan satu bar ([#1551](https://github.com/HKUDS/Vibe-Trading/pull/1551)). Nilai arus Southbound Eastmoney dikonversi dari juta HKD dan respons gagal tidak lagi dianggap data kosong ([#1486](https://github.com/HKUDS/Vibe-Trading/pull/1486)). Sumber alternatif Northbound membedakan nilai transaksi dari arus bersih setelah 2024-08-19 ([#1484](https://github.com/HKUDS/Vibe-Trading/pull/1484)). Hanya posisi Binance tanpa harga yang ditandai tidak lengkap, tanpa memengaruhi broker lain ([#1505](https://github.com/HKUDS/Vibe-Trading/pull/1505)). Kartu eksekusi mencatat asal model dan memperingatkan jika batas waktu pelatihan tidak diketahui atau di luar periode pengujian ([#1618](https://github.com/HKUDS/Vibe-Trading/pull/1618)). - **2026-09-30** 🛠️ **Feishu di Web UI, faktor momentum yang membaca harga hari ini, dan profit factor untuk backtest tanpa kerugian**: Feishu bergabung dengan pengaturan kanal terpandu, dengan uji koneksi mandiri dan hot reload yang menutup WebSocket lama ([#1572](https://github.com/HKUDS/Vibe-Trading/pull/1572)). `academic_carhart_mom` mengurangkan return 1 bulan dari return 12 bulan sehingga bergerak mengikuti harga penutupan hari ini; kini nilainya adalah return dari 12 bulan lalu hingga 1 bulan lalu ([#1578](https://github.com/HKUDS/Vibe-Trading/pull/1578)). Backtest tanpa transaksi rugi melaporkan profit factor sebagai tidak terdefinisi, bukan 0.0 yang menempatkannya di urutan terakhir ([#1602](https://github.com/HKUDS/Vibe-Trading/pull/1602)). Halaman anti-bot Stooq kini menghentikan semua permintaan berikutnya dalam proses, bukan hanya baris log ([#1637](https://github.com/HKUDS/Vibe-Trading/pull/1637)); hasil alat MCP sampai ke agen satu kali, bukan hingga empat kali ([#1634](https://github.com/HKUDS/Vibe-Trading/pull/1634)); dan bagian pertama `loop.py` dipindahkan ke modulnya sendiri ([#1636](https://github.com/HKUDS/Vibe-Trading/pull/1636)). diff --git a/README_ja.md b/README_ja.md index f3685dec..3099ea7d 100644 --- a/README_ja.md +++ b/README_ja.md @@ -52,16 +52,18 @@ > ⚠️ **セキュリティ警告:** Xアカウント `VibeTrading_HKU`、Virtualsプロジェクト `101845`、およびトークンコントラクト `0x640BDBF77b6447E8b7DB7894cED84BD1c40571f4` は、いずれもVibe-Trading公式のものではありません。Vibe-Tradingはこれまで、いかなるトークンやミームコインも発行・公認していません。購入、ウォレットの接続、署名は行わないでください。[詳細](SECURITY.md#official-channels--impersonation) +- **2026-10-05** 🛠️ **調査入力と計算の修正**:チャットで長い調査入力に対応し、上限を超えた場合は現在の言語で短縮を案内します([#1701](https://github.com/HKUDS/Vibe-Trading/pull/1701))。Sortino は全期間の下方偏差を使用し、グループ検証は重複ラベルを除外します。共分散ウェイト、Wilder 初期平均による RSI、ファイル名によるメモリ削除も改善しました。 + - **2026-10-04** 🛠️ **定期レポートと調査フローの改善**:定期タスクを編集し、設定済みの送信先を選択できます。メールレポートは HTML または PDF 添付に対応しました([#1649](https://github.com/HKUDS/Vibe-Trading/pull/1649), [#1680](https://github.com/HKUDS/Vibe-Trading/pull/1680))。 バックテストに構造化サマリーと成果物のページ読み取りを追加([#1646](https://github.com/HKUDS/Vibe-Trading/pull/1646), [#1647](https://github.com/HKUDS/Vibe-Trading/pull/1647))。メモリ検索の抜粋、エクスポート先の案内、マクロデータの切り詰め表示、月足リスク、売買方向の反転・現金化後の再投資の回転率、IV 精度、VaR の欠損区間、VCS インストールの更新、Robinhood の未対応オプション注文の遮断も修正しました。 - **2026-10-03** 🛠️ **調査・レポート・データの信頼性を改善**:中国語・日本語・韓国語のセッション検索、チャネル設定、証券会社の保有資産評価、ファイル書き込みで日常利用を妨げていた問題を修正。PDF 配信には CJK フォントを埋め込み、Swarm はプリセット入力を検証してタスクごとの成果物を分離し、再利用したツール結果はコンテキスト圧縮後も保持されます([#1683](https://github.com/HKUDS/Vibe-Trading/pull/1683), [#1455](https://github.com/HKUDS/Vibe-Trading/pull/1455), [#1635](https://github.com/HKUDS/Vibe-Trading/pull/1635))。バックテストは価格調整方式を統一し、ローカルキャッシュはデータソースを区別、単一資産の上限と週次・月次リスクは指定設定に従います。監査は損失の符号を保持し、数値検証は今回のエンジン出力と正確なリスト参照を使い、Stooq は拒否後の待機期間が過ぎると再試行できます([#1684](https://github.com/HKUDS/Vibe-Trading/pull/1684), [#1650](https://github.com/HKUDS/Vibe-Trading/pull/1650), [#1685](https://github.com/HKUDS/Vibe-Trading/pull/1685), [#1640](https://github.com/HKUDS/Vibe-Trading/pull/1640))。 -- **2026-10-02** 🛠️ **バックテストとレポート検証の修正**:戦略ファイルへの書き込みがクラッシュせずモデルの出所を保持します ([#1673](https://github.com/HKUDS/Vibe-Trading/pull/1673))。モンテカルロのドローダウンと Sharpe 計算に初期資金を含めます ([#1664](https://github.com/HKUDS/Vibe-Trading/pull/1664))。レポート監査は会計上の括弧付き負数と単位を保持し ([#1663](https://github.com/HKUDS/Vibe-Trading/pull/1663))、数値検証の成果物には実際に発火した宣言型チェックを記録します ([#1661](https://github.com/HKUDS/Vibe-Trading/pull/1661))。公開データソースのヘルスレポートに機密情報を除いた失敗理由を添え ([#1643](https://github.com/HKUDS/Vibe-Trading/pull/1643))、インドネシア語のツール文書をレジストリに合わせました ([#1671](https://github.com/HKUDS/Vibe-Trading/pull/1671))。 -
過去のニュース +- **2026-10-02** 🛠️ **バックテストとレポート検証の修正**:戦略ファイルへの書き込みがクラッシュせずモデルの出所を保持します ([#1673](https://github.com/HKUDS/Vibe-Trading/pull/1673))。モンテカルロのドローダウンと Sharpe 計算に初期資金を含めます ([#1664](https://github.com/HKUDS/Vibe-Trading/pull/1664))。レポート監査は会計上の括弧付き負数と単位を保持し ([#1663](https://github.com/HKUDS/Vibe-Trading/pull/1663))、数値検証の成果物には実際に発火した宣言型チェックを記録します ([#1661](https://github.com/HKUDS/Vibe-Trading/pull/1661))。公開データソースのヘルスレポートに機密情報を除いた失敗理由を添え ([#1643](https://github.com/HKUDS/Vibe-Trading/pull/1643))、インドネシア語のツール文書をレジストリに合わせました ([#1671](https://github.com/HKUDS/Vibe-Trading/pull/1671))。 + - **2026-10-01** ✅ **データの正確性と再現可能なバックテスト**:A 株の調整価格変換は曖昧な 1 本のバーの境界ケースを拒否します ([#1551](https://github.com/HKUDS/Vibe-Trading/pull/1551))。南向き資金の東方財富データは百万香港ドルから換算し、失敗応答を空データと扱いません ([#1486](https://github.com/HKUDS/Vibe-Trading/pull/1486))。北向き資金の代替ソースは 2024-08-19 以降の売買代金と純流入を区別します ([#1484](https://github.com/HKUDS/Vibe-Trading/pull/1484))。Binance の未評価ポジションだけを不完全とし、他のブローカーには影響させません ([#1505](https://github.com/HKUDS/Vibe-Trading/pull/1505))。実行カードにモデルの出所を記録し、学習期限が不明またはテスト期間外の場合に警告します ([#1618](https://github.com/HKUDS/Vibe-Trading/pull/1618))。 - **2026-09-30** 🛠️ **Web UI での Feishu 設定、当日の価格を読んでいたモメンタム因子、負けトレードのないバックテストのプロフィットファクター**:Feishu がガイド付きチャンネル設定に加わり、単独の接続テストと、ホットリロード時に古い WebSocket を閉じる処理を備えました([#1572](https://github.com/HKUDS/Vibe-Trading/pull/1572))。`academic_carhart_mom` は 12 か月リターンから 1 か月リターンを引いていたため、当日の終値で動いていました。現在は 12 か月前から 1 か月前までのリターンです([#1578](https://github.com/HKUDS/Vibe-Trading/pull/1578))。負けトレードのない実行では、プロフィットファクターを最下位に並べてしまう 0.0 ではなく「未定義」と報告します([#1602](https://github.com/HKUDS/Vibe-Trading/pull/1602))。Stooq のボット対策ページを検出すると、ログだけでなくそのプロセス内の以降のリクエストもすべて止めます([#1637](https://github.com/HKUDS/Vibe-Trading/pull/1637))。MCP ツールの結果は最大 4 重ではなく 1 回だけエージェントに渡り([#1634](https://github.com/HKUDS/Vibe-Trading/pull/1634))、`loop.py` から最初のモジュールが分離されました([#1636](https://github.com/HKUDS/Vibe-Trading/pull/1636))。 diff --git a/README_ko.md b/README_ko.md index 90703bef..ec2291c9 100644 --- a/README_ko.md +++ b/README_ko.md @@ -52,16 +52,18 @@ > ⚠️ **보안 경고:** X 계정 `VibeTrading_HKU`, Virtuals 프로젝트 `101845`, 토큰 컨트랙트 `0x640BDBF77b6447E8b7DB7894cED84BD1c40571f4`는 모두 Vibe-Trading 공식과 무관합니다. Vibe-Trading은 어떠한 토큰이나 밈코인도 발행하거나 공식적으로 지지한 적이 없습니다. 해당 토큰을 구매하거나 지갑을 연결하거나 어떠한 서명도 하지 마세요. [자세히 보기](SECURITY.md#official-channels--impersonation). +- **2026-10-05** 🛠️ **리서치 입력과 계산 수정**: 더 긴 리서치 입력을 지원하며 한도를 넘으면 현재 언어로 줄이도록 안내합니다([#1701](https://github.com/HKUDS/Vibe-Trading/pull/1701)). Sortino는 전체 기간의 하방 편차를 사용하고 그룹 검증은 겹치는 레이블을 제외합니다. 공분산 가중치, Wilder 초기 평균을 사용하는 RSI, 파일명으로 메모리 삭제도 개선했습니다. + - **2026-10-04** 🛠️ **예약 보고서와 리서치 흐름 개선**: 예약 작업을 편집하고 설정된 수신 대상을 선택할 수 있습니다. 이메일 보고서는 HTML 또는 PDF 첨부를 지원합니다([#1649](https://github.com/HKUDS/Vibe-Trading/pull/1649), [#1680](https://github.com/HKUDS/Vibe-Trading/pull/1680)). 백테스트에 구조화된 요약과 결과 파일 페이지 읽기를 추가했습니다([#1646](https://github.com/HKUDS/Vibe-Trading/pull/1646), [#1647](https://github.com/HKUDS/Vibe-Trading/pull/1647)). 메모리 검색 미리보기, 내보내기 경로 안내, 매크로 데이터 잘림 표시, 월봉 리스크, 포지션 반전과 현금화 후 재진입의 회전율, IV 정확도, VaR 결측 구간, VCS 설치 업데이트, Robinhood의 미지원 옵션 주문 차단도 수정했습니다. - **2026-10-03** 🛠️ **리서치·보고서·데이터 신뢰성 개선**: 중국어·일본어·한국어 세션 검색, 채널 설정, 증권사 보유 자산 평가와 파일 쓰기에서 일상적인 사용을 막던 문제를 수정했습니다. PDF 전달 시 CJK 글꼴을 포함하고, Swarm은 프리셋 입력을 검증하며 작업별 산출물을 분리하고, 재사용한 도구 결과는 컨텍스트 압축 후에도 유지됩니다([#1683](https://github.com/HKUDS/Vibe-Trading/pull/1683), [#1455](https://github.com/HKUDS/Vibe-Trading/pull/1455), [#1635](https://github.com/HKUDS/Vibe-Trading/pull/1635)). 백테스트의 가격 조정 기준을 통일하고 로컬 캐시는 데이터 소스를 구분하며, 단일 자산 한도와 주간·월간 위험 계산은 지정된 설정을 따릅니다. 감사는 손실 부호를 유지하고 수치 검증은 이번 엔진 출력과 정확한 목록 참조를 사용하며, Stooq은 거부 후 대기 시간이 지나면 재시도합니다([#1684](https://github.com/HKUDS/Vibe-Trading/pull/1684), [#1650](https://github.com/HKUDS/Vibe-Trading/pull/1650), [#1685](https://github.com/HKUDS/Vibe-Trading/pull/1685), [#1640](https://github.com/HKUDS/Vibe-Trading/pull/1640)). -- **2026-10-02** 🛠️ **백테스트와 보고서 검증 수정**: 전략 파일 쓰기가 충돌 없이 모델 출처를 유지하고 ([#1673](https://github.com/HKUDS/Vibe-Trading/pull/1673)), 몬테카를로 낙폭과 Sharpe 계산에 초기 자본을 포함합니다 ([#1664](https://github.com/HKUDS/Vibe-Trading/pull/1664)). 보고서 감사는 회계식 괄호 음수와 단위를 보존하며 ([#1663](https://github.com/HKUDS/Vibe-Trading/pull/1663)), 수치 검증 산출물은 발동된 선언형 검사를 기록합니다 ([#1661](https://github.com/HKUDS/Vibe-Trading/pull/1661)). 공개 데이터 소스 상태 보고서에는 민감 정보를 제거한 실패 이유를 담고 ([#1643](https://github.com/HKUDS/Vibe-Trading/pull/1643)), 인도네시아어 도구 문서를 레지스트리에 맞췄습니다 ([#1671](https://github.com/HKUDS/Vibe-Trading/pull/1671)). -
이전 뉴스 +- **2026-10-02** 🛠️ **백테스트와 보고서 검증 수정**: 전략 파일 쓰기가 충돌 없이 모델 출처를 유지하고 ([#1673](https://github.com/HKUDS/Vibe-Trading/pull/1673)), 몬테카를로 낙폭과 Sharpe 계산에 초기 자본을 포함합니다 ([#1664](https://github.com/HKUDS/Vibe-Trading/pull/1664)). 보고서 감사는 회계식 괄호 음수와 단위를 보존하며 ([#1663](https://github.com/HKUDS/Vibe-Trading/pull/1663)), 수치 검증 산출물은 발동된 선언형 검사를 기록합니다 ([#1661](https://github.com/HKUDS/Vibe-Trading/pull/1661)). 공개 데이터 소스 상태 보고서에는 민감 정보를 제거한 실패 이유를 담고 ([#1643](https://github.com/HKUDS/Vibe-Trading/pull/1643)), 인도네시아어 도구 문서를 레지스트리에 맞췄습니다 ([#1671](https://github.com/HKUDS/Vibe-Trading/pull/1671)). + - **2026-10-01** ✅ **데이터 정확성과 재현 가능한 백테스트**: A주 수정주가 변환은 모호한 단일 봉 경계 사례를 거부합니다 ([#1551](https://github.com/HKUDS/Vibe-Trading/pull/1551)). 남향 자금의 Eastmoney 금액은 백만 HKD 단위에서 환산하고 실패 응답을 빈 데이터로 처리하지 않습니다 ([#1486](https://github.com/HKUDS/Vibe-Trading/pull/1486)). 북향 자금 대체 소스는 2024-08-19 이후 거래대금과 순유입을 구분합니다 ([#1484](https://github.com/HKUDS/Vibe-Trading/pull/1484)). Binance의 미평가 포지션만 불완전으로 표시하며 다른 브로커에는 영향을 주지 않습니다 ([#1505](https://github.com/HKUDS/Vibe-Trading/pull/1505)). 실행 카드는 모델 출처를 기록하고 학습 기준일이 불명확하거나 테스트 기간 밖이면 경고합니다 ([#1618](https://github.com/HKUDS/Vibe-Trading/pull/1618)). - **2026-09-30** 🛠️ **Web UI의 Feishu 설정, 당일 가격을 읽던 모멘텀 팩터, 손실 거래가 없는 백테스트의 수익 팩터**: Feishu가 가이드형 채널 설정에 추가되어 독립 연결 테스트를 제공하고, 핫 리로드 시 이전 WebSocket을 닫습니다([#1572](https://github.com/HKUDS/Vibe-Trading/pull/1572)). `academic_carhart_mom`은 12개월 수익률에서 1개월 수익률을 빼서 당일 종가에 따라 움직였으나, 이제 12개월 전부터 1개월 전까지의 수익률입니다([#1578](https://github.com/HKUDS/Vibe-Trading/pull/1578)). 손실 거래가 없는 실행은 수익 팩터를 최하위로 정렬되게 만드는 0.0 대신 정의되지 않음으로 보고합니다([#1602](https://github.com/HKUDS/Vibe-Trading/pull/1602)). Stooq의 봇 차단 페이지를 감지하면 로그만 남기는 것이 아니라 해당 프로세스의 이후 요청을 모두 중단하고([#1637](https://github.com/HKUDS/Vibe-Trading/pull/1637)), MCP 도구 결과는 최대 네 번이 아니라 한 번만 에이전트에 전달되며([#1634](https://github.com/HKUDS/Vibe-Trading/pull/1634)), `loop.py`에서 첫 번째 모듈이 분리되었습니다([#1636](https://github.com/HKUDS/Vibe-Trading/pull/1636)). diff --git a/README_zh.md b/README_zh.md index 5b3b38e8..a1657757 100644 --- a/README_zh.md +++ b/README_zh.md @@ -52,16 +52,18 @@ > ⚠️ **安全警告:** X 账号 `VibeTrading_HKU`、Virtuals 项目 `101845` 及代币合约 `0x640BDBF77b6447E8b7DB7894cED84BD1c40571f4` 均非 Vibe-Trading 官方。我们从未发行或背书任何代币或 meme 币。请勿购买、连接钱包或签名。[详细说明](SECURITY.md#official-channels--impersonation)。 +- **2026-10-05** 🛠️ **研究输入与计算修复**:聊天支持更长的研究输入,超限时以当前语言提示缩短([#1701](https://github.com/HKUDS/Vibe-Trading/pull/1701))。回测 Sortino 使用全样本下行偏差;分组验证剔除重叠标签,协方差权重保持有限,影子账户 RSI 使用 Wilder 初始均值,记忆支持按文件名删除。 + - **2026-10-04** 🛠️ **定时报告与研究流程完善**:定时任务支持编辑并选择已配置的交付目标,邮件报告可选 HTML 或 PDF 附件([#1649](https://github.com/HKUDS/Vibe-Trading/pull/1649), [#1680](https://github.com/HKUDS/Vibe-Trading/pull/1680))。 回测提供结构化摘要与产物分页读取([#1646](https://github.com/HKUDS/Vibe-Trading/pull/1646), [#1647](https://github.com/HKUDS/Vibe-Trading/pull/1647));同时修复记忆搜索片段、导出路径提示、宏观数据截断说明、月线风险、反向持仓及清仓后重新入场的换手率、隐含波动率精度、VaR 缺口、源码安装更新,以及 Robinhood 不受支持的期权下单拦截。 - **2026-10-03** 🛠️ **研究、报告与数据可靠性修复**:修复中文、日文和韩文会话搜索、渠道设置、券商持仓估值与文件写入中阻碍日常使用的问题。PDF 交付嵌入中日韩字体,Swarm 校验预设输入并隔离各任务产物,回放的工具结果在上下文压缩后仍可用([#1683](https://github.com/HKUDS/Vibe-Trading/pull/1683), [#1455](https://github.com/HKUDS/Vibe-Trading/pull/1455), [#1635](https://github.com/HKUDS/Vibe-Trading/pull/1635))。回测保持一致复权口径,本地缓存区分数据来源,单资产上限与周/月风险计算遵循声明设置,审计保留亏损符号,数值核验绑定本次引擎输出及准确列表引用,Stooq 拒绝冷却后允许重试([#1684](https://github.com/HKUDS/Vibe-Trading/pull/1684), [#1650](https://github.com/HKUDS/Vibe-Trading/pull/1650), [#1685](https://github.com/HKUDS/Vibe-Trading/pull/1685), [#1640](https://github.com/HKUDS/Vibe-Trading/pull/1640))。 -- **2026-10-02** 🛠️ **回测与报告核验修复**:策略文件写入保留模型来源,不再崩溃 ([#1673](https://github.com/HKUDS/Vibe-Trading/pull/1673));蒙特卡洛回撤与 Sharpe 计算纳入初始资金 ([#1664](https://github.com/HKUDS/Vibe-Trading/pull/1664))。报告审计保留会计括号负数及单位 ([#1663](https://github.com/HKUDS/Vibe-Trading/pull/1663)),数值核验制品记录已触发的声明式检查 ([#1661](https://github.com/HKUDS/Vibe-Trading/pull/1661)),公开数据源健康报告附带脱敏后的失败原因 ([#1643](https://github.com/HKUDS/Vibe-Trading/pull/1643))。印尼语工具文档与注册表同步 ([#1671](https://github.com/HKUDS/Vibe-Trading/pull/1671))。 -
更早的新闻 +- **2026-10-02** 🛠️ **回测与报告核验修复**:策略文件写入保留模型来源,不再崩溃 ([#1673](https://github.com/HKUDS/Vibe-Trading/pull/1673));蒙特卡洛回撤与 Sharpe 计算纳入初始资金 ([#1664](https://github.com/HKUDS/Vibe-Trading/pull/1664))。报告审计保留会计括号负数及单位 ([#1663](https://github.com/HKUDS/Vibe-Trading/pull/1663)),数值核验制品记录已触发的声明式检查 ([#1661](https://github.com/HKUDS/Vibe-Trading/pull/1661)),公开数据源健康报告附带脱敏后的失败原因 ([#1643](https://github.com/HKUDS/Vibe-Trading/pull/1643))。印尼语工具文档与注册表同步 ([#1671](https://github.com/HKUDS/Vibe-Trading/pull/1671))。 + - **2026-10-01** ✅ **数据正确性与可复现回测**:A 股复权转换拒绝有歧义的单根 K 线边界 ([#1551](https://github.com/HKUDS/Vibe-Trading/pull/1551));南向东财金额按百万港元缩放,失败响应不再被当作空数据 ([#1486](https://github.com/HKUDS/Vibe-Trading/pull/1486));北向备用源区分 2024-08-19 后的成交额与净流入 ([#1484](https://github.com/HKUDS/Vibe-Trading/pull/1484))。币安未定价持仓标记为不完整,不影响其他券商 ([#1505](https://github.com/HKUDS/Vibe-Trading/pull/1505));回测运行卡记录模型来源,并提示训练截止日期未知或不在回测区间内 ([#1618](https://github.com/HKUDS/Vibe-Trading/pull/1618))。 - **2026-09-30** 🛠️ **飞书进入 Web UI、一个读到当天价格的动量因子,以及从未亏损的回测的盈利因子**:飞书加入引导式频道配置,提供独立的连接测试,热重载时会关闭旧的 WebSocket([#1572](https://github.com/HKUDS/Vibe-Trading/pull/1572))。`academic_carhart_mom` 原本用 12 个月收益减去 1 个月收益,结果随当天收盘价变动;现在改为从 12 个月前到 1 个月前的收益([#1578](https://github.com/HKUDS/Vibe-Trading/pull/1578))。没有亏损交易的回测,盈利因子现在记为未定义,而不是会把它排到最后的 0.0([#1602](https://github.com/HKUDS/Vibe-Trading/pull/1602))。Stooq 的反爬页面现在会让本进程后续请求全部停下,而不只是记一条日志([#1637](https://github.com/HKUDS/Vibe-Trading/pull/1637));MCP 工具结果只传给 agent 一份,不再重复最多四次([#1634](https://github.com/HKUDS/Vibe-Trading/pull/1634));`loop.py` 拆出第一个独立模块([#1636](https://github.com/HKUDS/Vibe-Trading/pull/1636))。 diff --git a/agent/src/api/sessions_routes.py b/agent/src/api/sessions_routes.py index 067c0b08..9e67b464 100644 --- a/agent/src/api/sessions_routes.py +++ b/agent/src/api/sessions_routes.py @@ -13,7 +13,9 @@ from datetime import datetime, timezone from typing import Any, Dict, List, Optional from fastapi import Depends, FastAPI, HTTPException, Query, Request, status -from fastapi.responses import StreamingResponse +from fastapi.exception_handlers import request_validation_exception_handler +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse, StreamingResponse from pydantic import BaseModel, Field from src.session.events import SSEEvent @@ -42,16 +44,14 @@ class SessionResponse(BaseModel): last_attempt_id: Optional[str] = None -# An interactive message is a prompt, not a tool result, so it is capped only to bound -# the request body. 100k characters is roughly 25k tokens - well inside any supported -# model's context. The scheduled-run path (CreateScheduledRunRequest.prompt) has no cap -# at all; this keeps the two within sight of each other rather than 20x apart. +# Bound interactive request size. This character limit is independent of the +# selected model's token window, which the runtime manages separately. MAX_MESSAGE_CHARS = 100_000 class SendMessageRequest(BaseModel): - """Send chat message: natural-language strategy description.""" - content: str = Field(..., description="Natural language strategy description", min_length=1, max_length=MAX_MESSAGE_CHARS) + """Send an interactive research prompt.""" + content: str = Field(..., description="Research prompt", min_length=1, max_length=MAX_MESSAGE_CHARS) class MessageResponse(BaseModel): @@ -325,11 +325,24 @@ def _live_action_frame_from_tool_result(event: Any) -> Optional[str]: # ============================================================================ def register_sessions_routes(app: FastAPI) -> None: - """Mount the session/goal routes onto ``app``. + """Mount session and goal routes using the host api_server dependencies.""" + @app.exception_handler(RequestValidationError) + async def compact_message_validation(request: Request, exc: RequestValidationError): + """Report oversized chat input without echoing the rejected prompt.""" + if re.fullmatch(r"/sessions/[^/]+/messages", request.url.path) and any( + error.get("type") == "string_too_long" and error.get("loc") == ("body", "content") + for error in exc.errors() + ): + return JSONResponse( + status_code=422, + content={"detail": { + "code": "message_too_long", + "max_length": MAX_MESSAGE_CHARS, + "message": f"Message must contain at most {MAX_MESSAGE_CHARS} characters. Shorten it before sending.", + }}, + ) + return await request_validation_exception_handler(request, exc) - Resolves shared dependencies from the host ``api_server`` module via - ``sys.modules``. - """ import sys as _sys host = _sys.modules.get("api_server") or _sys.modules.get("agent.api_server") diff --git a/agent/src/memory/persistent.py b/agent/src/memory/persistent.py index d8803bb9..6529b48b 100644 --- a/agent/src/memory/persistent.py +++ b/agent/src/memory/persistent.py @@ -650,6 +650,9 @@ class PersistentMemory: """ from src.config.accessor import get_env_config + name = name.strip() + if not name: + return False entries = self._scan_entries() matches = [ entry diff --git a/agent/src/quantlib/crossvalidation.py b/agent/src/quantlib/crossvalidation.py index d4729083..be2724f7 100644 --- a/agent/src/quantlib/crossvalidation.py +++ b/agent/src/quantlib/crossvalidation.py @@ -291,9 +291,10 @@ def purged_kfold_splits( def group_purged_kfold_splits( groups: Sequence[object] | pd.Series | np.ndarray, - label_end_times: pd.Series | Sequence[int] | np.ndarray | None = None, n_folds: int = 5, embargo_fraction: float = DEFAULT_EMBARGO_FRACTION, + *, + label_end_times: pd.Series | Sequence[int] | np.ndarray | None = None, ) -> Iterator[Split]: """Purged and embargoed K-fold cross-validation for panel and multi-asset datasets. @@ -303,12 +304,13 @@ def group_purged_kfold_splits( Args: groups: Group identifiers (e.g. dates or bar index) for each sample row in order. - label_end_times: Where each row's label resolves, in the same positional form - :func:`purged_kfold_splits` takes. When None, each label is assumed to - resolve within its own group, so purging removes only the boundary and - the embargo does the remaining work. n_folds: Number of folds, at least :data:`MIN_FOLDS`. embargo_fraction: Fraction of unique ordered groups embargoed after each test block. + label_end_times: Where each row's label resolves, in the same form + :func:`purged_kfold_splits` takes. Groups must be contiguous and + chronological when supplied. Overlap removes a whole group; + embargo starts after the furthest test label ends. When None, + labels resolve within their own group and only embargo applies. Yields: One :class:`Split` per fold, with ``train`` and ``test`` containing row indices. @@ -355,9 +357,11 @@ def group_purged_kfold_splits( # into a later group, so there is nothing for backward purging to do. group_reach = np.arange(n_groups) else: + if np.any(np.diff(row_group_order) < 0): + raise ValueError("groups must be contiguous and chronological when label_end_times is supplied") label_ends = _as_label_spans(label_end_times, n_samples) group_reach = np.array( - [row_group_order[label_ends[group_to_rows[g]]].max() for g in unique_groups] + [row_group_order[np.minimum(label_ends[group_to_rows[g]], n_samples - 1)].max() for g in unique_groups] ) embargo_groups = int(round(n_groups * embargo_fraction)) @@ -369,16 +373,18 @@ def group_purged_kfold_splits( continue test_groups = set(unique_groups[start_g:stop_g]) - embargo_end_g = min(n_groups, stop_g + embargo_groups) - embargo_groups_set = set(unique_groups[stop_g:embargo_end_g]) + test_reach = int(group_reach[start_g:stop_g].max()) + embargo_start_g = test_reach + 1 + embargo_end_g = min(n_groups, embargo_start_g + embargo_groups) + embargo_groups_set = set(unique_groups[embargo_start_g:embargo_end_g]) - # Symmetric purge: an earlier group whose own labels reach forward - # into the test block overlaps it just as much as a later group - # embargoed for starting shortly after -- see the module docstring. + # Closed label intervals overlap on either side of the test block. + # Future groups inside the test labels' horizon must be purged too. purge_groups_set = { unique_groups[g_idx] - for g_idx in range(start_g) - if group_reach[g_idx] >= start_g + for g_idx in range(n_groups) + if g_idx <= test_reach and group_reach[g_idx] >= start_g + and unique_groups[g_idx] not in test_groups } test_rows_list = [group_to_rows[g] for g in unique_groups[start_g:stop_g]] diff --git a/agent/src/shadow_account/extractor.py b/agent/src/shadow_account/extractor.py index dd32b5ba..77394eea 100644 --- a/agent/src/shadow_account/extractor.py +++ b/agent/src/shadow_account/extractor.py @@ -26,6 +26,7 @@ from typing import Any import numpy as np import pandas as pd +from src.shadow_account.indicators import RSI_PERIOD as _RSI_PERIOD, compute_rsi as _compute_rsi from src.shadow_account.models import PRICE_FEATURES, ShadowProfile, ShadowRule from src.shadow_account.storage import hash_journal, new_shadow_id, now_iso from src.tools.trade_journal_parsers import parse_file, records_to_dataframe @@ -43,7 +44,6 @@ _CATEGORICAL_FEATURES = ("market",) # Names live on the data-contract boundary (models.PRICE_FEATURES) so the # extractor, codegen, and scanner cannot drift; aliased here for local use. _PRICE_FEATURES = PRICE_FEATURES -_RSI_PERIOD = 14 _PRIOR_RETURN_WINDOW = 5 # Calendar buffer added before the earliest buy_dt so the RSI warmup has enough # trading bars even across weekends/holidays. @@ -147,73 +147,6 @@ def extract_shadow_profile( # ---------------- Feature engineering ---------------- -def _wilder_average(values: pd.Series, period: int) -> pd.Series: - """True Wilder recursive smoothing: SMA-seeded, then exponential recursion. - - Wilder's original formula seeds the first average as a simple mean of the - first ``period`` valid values, then recurses as - ``avg[t] = (avg[t-1] * (period - 1) + values[t]) / period`` for every bar - after that. This is *not* the same as - ``values.ewm(alpha=1/period, adjust=False).mean()``, which seeds its - recursion from the single first observation instead of that average — - measurably further from Wilder during the warmup-adjacent bars, not - closer, even though the two converge for data far enough past warmup. - - Vectorized as an ``ewm`` over the seed plus the remaining values, which is - mathematically identical to the explicit recursion: seeding an - ``adjust=False`` ewm with the average as its first "observation" and - dropping that duplicate first row reproduces the same recursive update at - every following step. - - Args: - values: Series to smooth (e.g. per-bar gains or losses); may lead - with NaN (as ``close.diff()`` does). - period: Wilder lookback period. - - Returns: - A series aligned with ``values``, NaN before the seed index. - """ - result = pd.Series(np.nan, index=values.index, dtype=float) - valid = values.dropna() - if len(valid) < period: - return result - - seed = valid.iloc[:period].mean() - result.loc[valid.index[period - 1]] = seed - - remaining = valid.iloc[period:] - if len(remaining) > 0: - tail = pd.concat([pd.Series([seed]), remaining]) - smoothed_tail = tail.ewm(alpha=1 / period, adjust=False).mean().iloc[1:] - result.loc[remaining.index] = smoothed_tail.to_numpy() - return result - - -def _compute_rsi(close: pd.Series, period: int = _RSI_PERIOD) -> pd.Series: - """Causal Wilder RSI. - - Mirrors the shape of ``compute_rsi`` in - ``agent/src/skills/technical-basic/example_signal_engine.py:13`` — that - module lives under a hyphenated (non-importable) skills directory, so the - formula is re-implemented here rather than imported. Causal by construction: - ``RSI[t]`` depends only on closes dated ``<= t``. - - Args: - close: Close-price series indexed by date. - period: RSI lookback period. - - Returns: - RSI series (0-100), NaN for the warmup window. - """ - delta = close.diff() - gain = delta.clip(lower=0) - loss = (-delta).clip(lower=0) - avg_gain = _wilder_average(gain, period) - avg_loss = _wilder_average(loss, period) - rs = avg_gain / avg_loss - return 100 - 100 / (1 + rs) - - def _fetch_price_history( symbol: str, market: str, diff --git a/agent/src/shadow_account/indicators.py b/agent/src/shadow_account/indicators.py new file mode 100644 index 00000000..75105286 --- /dev/null +++ b/agent/src/shadow_account/indicators.py @@ -0,0 +1,50 @@ +"""Shared causal price indicators for shadow extraction and scanning.""" + +import numpy as np +import pandas as pd + +RSI_PERIOD = 14 + + +def _wilder_average(values: pd.Series, period: int) -> pd.Series: + """Seed with a simple mean, then recurse over observed values. + + Args: + values: Gains or losses, possibly containing missing observations. + period: Number of observations in the initial seed. + + Returns: + Aligned averages, with warmup and missing observations left as NaN. + Missing observations do not update the recursive state. + """ + result = pd.Series(np.nan, index=values.index, dtype=float) + positions = np.flatnonzero(values.notna().to_numpy()) + if len(positions) < period: + return result + observed = values.iloc[positions].reset_index(drop=True) + seed = observed.iloc[:period].mean() + tail = pd.concat([pd.Series([seed]), observed.iloc[period:]], ignore_index=True) + result.iloc[positions[period - 1 :]] = tail.ewm(alpha=1 / period, adjust=False).mean().to_numpy() + return result + + +def compute_rsi(close: pd.Series, period: int = RSI_PERIOD) -> pd.Series: + """Calculate SMA-seeded Wilder RSI without future bars. + + Args: + close: Close prices in chronological order. + period: Positive number of observed deltas used for warmup. + + Returns: + RSI aligned with close, NaN during warmup or at a missing delta. + Flat prices retain the existing undefined (NaN) convention. + + Raises: + ValueError: If period is not a positive integer. + """ + if isinstance(period, bool) or not isinstance(period, int) or period < 1: + raise ValueError("period must be a positive integer") + delta = close.diff() + avg_gain = _wilder_average(delta.clip(lower=0), period) + avg_loss = _wilder_average((-delta).clip(lower=0), period) + return 100 - 100 / (1 + avg_gain / avg_loss) diff --git a/agent/src/shadow_account/scanner.py b/agent/src/shadow_account/scanner.py index 88e7aa54..024cc948 100644 --- a/agent/src/shadow_account/scanner.py +++ b/agent/src/shadow_account/scanner.py @@ -9,15 +9,14 @@ from __future__ import annotations from datetime import date, datetime from typing import Any, Callable -import numpy as np import pandas as pd from src.shadow_account.backtester import _LIQUID_BASKETS, SUPPORTED_MARKETS +from src.shadow_account.indicators import RSI_PERIOD as _RSI_PERIOD, compute_rsi as _compute_rsi from src.shadow_account.models import ShadowProfile, ShadowRule PriceFetcher = Callable[..., pd.DataFrame | None] -_RSI_PERIOD = 14 def scan_today_signals( @@ -201,47 +200,6 @@ def _compute_features(bars: pd.DataFrame, rule: ShadowRule) -> dict[str, float | return features -def _wilder_average(values: pd.Series, period: int) -> pd.Series: - """True Wilder recursive smoothing: SMA-seeded, then exponential recursion. - - Kept byte-identical to ``_wilder_average`` in ``extractor.py`` — see that - copy's docstring for the full rationale. Duplicated rather than imported - for the same reason ``_compute_rsi`` already was: this module's - ``_compute_rsi`` must evaluate the same RSI that produced the extracted - ``entry_rsi14`` bounds, so the two copies cannot drift apart. - """ - result = pd.Series(np.nan, index=values.index, dtype=float) - valid = values.dropna() - if len(valid) < period: - return result - - seed = valid.iloc[:period].mean() - result.loc[valid.index[period - 1]] = seed - - remaining = valid.iloc[period:] - if len(remaining) > 0: - tail = pd.concat([pd.Series([seed]), remaining]) - smoothed_tail = tail.ewm(alpha=1 / period, adjust=False).mean().iloc[1:] - result.loc[remaining.index] = smoothed_tail.to_numpy() - return result - - -def _compute_rsi(close: pd.Series, period: int = _RSI_PERIOD) -> pd.Series: - """Causal Wilder RSI. - - Mirrors ``_compute_rsi`` in ``extractor.py`` so the scanner evaluates the - same RSI that produced the extracted ``entry_rsi14`` bounds. Causal by - construction: ``RSI[t]`` depends only on closes dated ``<= t``. - """ - delta = close.diff() - gain = delta.clip(lower=0) - loss = (-delta).clip(lower=0) - avg_gain = _wilder_average(gain, period) - avg_loss = _wilder_average(loss, period) - rs = avg_gain / avg_loss - return 100 - 100 / (1 + rs) - - def _window_from_rule(rule: ShadowRule, default: int) -> int: """Infer a recent-return window from common condition keys.""" for key, value in rule.entry_condition.items(): diff --git a/agent/src/strategy_store/metrics.py b/agent/src/strategy_store/metrics.py index ec2fba6c..466c9d18 100644 --- a/agent/src/strategy_store/metrics.py +++ b/agent/src/strategy_store/metrics.py @@ -74,11 +74,9 @@ def compute_decay_metrics( result["rolling_ic_mean"] = round(rolling_mean, 6) # baseline_ics and rolling_ics are the oldest/newest 5 entries, so - # with fewer than 10 they share observations -- at 3-9 entries - # they're the same window read twice, making ic_ratio exactly 1.0 - # regardless of how much the IC actually moved. Require the two - # windows to be disjoint before trusting their ratio. - # + # with fewer than 10 they share observations. At 3-5 entries they + # are identical; at 6-9 the overlap dilutes the measured change. + # Require disjoint windows before comparing baseline and recent IC. # baseline_mean > 0, not != 0: with a negative baseline, dividing # two negatives gives a positive ratio, so a rolling IC that got # much MORE negative (real decay) produces a large ic_ratio that diff --git a/agent/src/tools/remember_tool.py b/agent/src/tools/remember_tool.py index 04b581d3..cbc9fff8 100644 --- a/agent/src/tools/remember_tool.py +++ b/agent/src/tools/remember_tool.py @@ -21,7 +21,7 @@ class RememberTool(BaseTool): "Persistent cross-session memory. " "save: store user preferences, strategy insights, or project context. " "recall: search past memories by keyword. " - "forget: remove a memory by title. " + "forget: remove a memory by title or filename stem. " "reinforce: provide quality feedback on a memory." ) is_readonly = False @@ -35,7 +35,7 @@ class RememberTool(BaseTool): }, "title": { "type": "string", - "description": "Memory title (for save/forget)", + "description": "Memory title (save), or title/filename stem (forget)", }, "content": { "type": "string", diff --git a/agent/tests/quantlib/test_crossvalidation.py b/agent/tests/quantlib/test_crossvalidation.py index 2702d911..14ea5da6 100644 --- a/agent/tests/quantlib/test_crossvalidation.py +++ b/agent/tests/quantlib/test_crossvalidation.py @@ -419,7 +419,7 @@ def test_group_purged_kfold_splits_purges_a_forward_looking_label(): splits = list( group_purged_kfold_splits( - dates, label_end_times, n_folds=5, embargo_fraction=0.3 + dates, label_end_times=label_end_times, n_folds=5, embargo_fraction=0.3 ) ) @@ -438,7 +438,7 @@ def test_group_purged_kfold_splits_reports_purged_count(): splits = list( group_purged_kfold_splits( - dates, label_end_times, n_folds=5, embargo_fraction=0.3 + dates, label_end_times=label_end_times, n_folds=5, embargo_fraction=0.3 ) ) diff --git a/agent/tests/test_channel_authoring_contract.py b/agent/tests/test_channel_authoring_contract.py index ae5c8b4c..614d838e 100644 --- a/agent/tests/test_channel_authoring_contract.py +++ b/agent/tests/test_channel_authoring_contract.py @@ -9,54 +9,60 @@ here instead of in the browser. from __future__ import annotations -from src.channels.config_meta import SECRET_KEY_RE, channel_field_hints, is_secret_key +import asyncio +import re +import tomllib +from pathlib import Path + +import pytest + +from src.channels.config_meta import channel_field_hints, split_values_secrets from src.channels.registry import discover_channel_names, load_channel_class -def test_every_discovered_channel_satisfies_the_authoring_contract() -> None: - names = discover_channel_names() - assert names, "registry discovered no channels" +@pytest.mark.parametrize("name", discover_channel_names()) +def test_every_discovered_channel_satisfies_the_authoring_contract(name) -> None: + try: + cls = load_channel_class(name) + except ImportError as exc: + # Only the adapter's explicit missing-dependency envelope can skip. + # An arbitrary import or missing BaseChannel subclass must fail. + project = tomllib.loads((Path(__file__).resolve().parents[2] / "pyproject.toml").read_text())["project"] + optional = project["optional-dependencies"].get(name, []) + modules = {re.split(r"[<>=!\[; ]", requirement)[0].replace("-", "_") for requirement in optional} + direct_missing = isinstance(exc, ModuleNotFoundError) and (exc.name or "").split(".")[0] in modules + declared_missing = isinstance(exc.__cause__, ModuleNotFoundError) and "dependencies not installed" in str(exc) + if not (direct_missing or declared_missing): + raise + pytest.skip(str(exc)) - checked = 0 - for name in names: - try: - cls = load_channel_class(name) - except ImportError: - # Optional stack not installed here (e.g. matrix needs [nio]); an - # environment gate, not a contract violation. - continue - checked += 1 + # A concrete adapter: start/stop/send are abstract on BaseChannel. + assert not cls.__abstractmethods__, f"{name} leaves BaseChannel abstracts unset" - # A concrete adapter: start/stop/send are abstract on BaseChannel. - assert not cls.__abstractmethods__, f"{name} leaves BaseChannel abstracts unset" + default_config = cls.default_config() + assert isinstance(default_config, dict), f"{name} default_config is not a dict" - default_config = cls.default_config() - assert isinstance(default_config, dict), f"{name} default_config is not a dict" + # The generic form renders from hints; every configurable scalar key + # must resolve one. Dict-valued fields are exempt by design: the form + # cannot edit them, so they stay file-configured (see _derive_hints). + hints = channel_field_hints(name) + configurable = {k for k, v in default_config.items() if k != "enabled" and not isinstance(v, dict)} + hint_keys = {h["key"] for h in hints} + missing = configurable - hint_keys + assert not missing, f"{name} has scalar config keys with no field hint: {sorted(missing)}" - # The generic form renders from hints; every configurable scalar key - # must resolve one. Dict-valued fields are exempt by design: the form - # cannot edit them, so they stay file-configured (see _derive_hints). - hints = channel_field_hints(name) - configurable = {k for k, v in default_config.items() if k != "enabled" and not isinstance(v, dict)} - hint_keys = {h["key"] for h in hints} - missing = configurable - hint_keys - assert not missing, f"{name} has scalar config keys with no field hint: {sorted(missing)}" - - # Secret masking: hand-written hints are authoritative for their keys - # (an audited declaration may subtract an over-match, e.g. websocket's - # token_* path names); for every key no hint covers, the SECRET_KEY_RE - # fail-safe must fire. - hint_flags = {h["key"]: bool(h["secret"]) for h in hints} - for key in configurable: - if key in hint_flags: - continue - if SECRET_KEY_RE.search(key): - assert is_secret_key(name, key), f"{name}.{key} is credential-shaped, uncovered by hints, and unmasked" - for hint in hints: - if hint["type"] == "password": - assert hint["secret"], f"{name}.{hint['key']} is a password field not marked secret" - - assert checked > 0, "every discovered channel failed to import" + # Test the serialized form consumed by the settings UI, rather than + # only asking the same classifier that supplied the hints. + sentinel = "credential-contract-sentinel" + configured = {hint["key"]: sentinel for hint in hints if hint["secret"]} + configured["extra_api_token"] = sentinel + values, secrets = split_values_secrets(name, configured) + assert not (set(configured) & set(values)), f"{name} exposes a credential in values" + assert set(configured) <= set(secrets) + assert sentinel not in str(secrets) + for hint in hints: + if hint["type"] == "password": + assert hint["secret"], f"{name}.{hint['key']} is a password field not marked secret" def test_test_connection_envelope_shape_is_documented_by_default() -> None: @@ -65,3 +71,4 @@ def test_test_connection_envelope_shape_is_documented_by_default() -> None: from src.channels.base import BaseChannel assert "test_connection" not in BaseChannel.__abstractmethods__ + assert asyncio.run(BaseChannel.test_connection(None)) == {"ok": False, "code": "unsupported"} diff --git a/agent/tests/test_interactive_message_limit.py b/agent/tests/test_interactive_message_limit.py new file mode 100644 index 00000000..b4cde9be --- /dev/null +++ b/agent/tests/test_interactive_message_limit.py @@ -0,0 +1,48 @@ +"""Exercise the actual chat route's validation without invoking a model.""" + +import re +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + +import api_server +from src.api.sessions_routes import MAX_MESSAGE_CHARS + + +@pytest.fixture +def client(tmp_path, monkeypatch): + monkeypatch.delenv("VIBE_TRADING_API_AUTH_KEY", raising=False) + monkeypatch.setattr(api_server, "_session_service", None) + monkeypatch.setattr(api_server, "SESSIONS_DIR", tmp_path / "sessions") + monkeypatch.setattr(api_server, "RUNS_DIR", tmp_path / "runs") + return TestClient(api_server.app, client=("127.0.0.1", 50000)) + + +@pytest.mark.parametrize("content", ["x" * 5001, "研" * 100_000, "📈" * 100_000]) +def test_long_prompt_passes_validation_and_reaches_session_lookup(client, content): + response = client.post("/sessions/nosuch/messages", json={"content": content}) + assert response.status_code == 404 + + +def test_oversized_prompt_returns_compact_actionable_error_without_input(client): + content = "private_research_" + "x" * MAX_MESSAGE_CHARS + response = client.post("/sessions/nosuch/messages", json={"content": content}) + assert response.status_code == 422 + assert response.json()["detail"]["code"] == "message_too_long" + assert response.json()["detail"]["max_length"] == MAX_MESSAGE_CHARS + assert len(response.content) < 1000 + assert "private_research_" not in response.text + + +def test_empty_prompt_keeps_normal_validation(client): + response = client.post("/sessions/nosuch/messages", json={"content": ""}) + assert response.status_code == 422 + assert response.json()["detail"][0]["type"] == "string_too_short" + + +def test_frontend_message_bound_matches_the_server_schema(): + frontend = Path(__file__).resolve().parents[2] / "frontend/src/lib/chatPrompt.ts" + constant = re.search(r"MAX_MESSAGE_CHARS = ([\d_]+)", frontend.read_text()) + assert constant is not None + assert int(constant[1].replace("_", "")) == MAX_MESSAGE_CHARS diff --git a/agent/tests/test_sdm_finite_metrics.py b/agent/tests/test_sdm_finite_metrics.py index f6612dc6..77dcb848 100644 --- a/agent/tests/test_sdm_finite_metrics.py +++ b/agent/tests/test_sdm_finite_metrics.py @@ -69,11 +69,11 @@ def test_ic_ratio_still_computed_for_a_positive_baseline(): assert metrics["ic_ratio"] == pytest.approx(0.5) -@pytest.mark.parametrize("n", [3, 4, 5, 6, 9]) +@pytest.mark.parametrize("n", range(3, 10)) def test_ic_ratio_is_none_when_baseline_and_rolling_windows_overlap(n): """With fewer than 10 entries, the oldest-5 and newest-5 windows share - observations -- at 3-9 entries they're the same data read twice, which - made ic_ratio exactly 1.0 no matter how much the IC actually moved. A + observations. At 3-5 entries they are identical; at 6-9 the overlap + dilutes the measured change. A strategy whose IC just collapsed from 0.08 to 0.001 must not come back reading as unchanged.""" history_newest_first = [BenchResult(ic_mean=0.001)] + [ diff --git a/agent/tests/test_ux_batch_regressions.py b/agent/tests/test_ux_batch_regressions.py new file mode 100644 index 00000000..e7579d06 --- /dev/null +++ b/agent/tests/test_ux_batch_regressions.py @@ -0,0 +1,91 @@ +"""Integration boundaries found while reviewing the October 5 UX batch.""" + +import numpy as np +import pandas as pd +import pytest +import json + +from src.memory.persistent import PersistentMemory +from src.quantlib.crossvalidation import detect_boundary_leakage, group_purged_kfold_splits +from src.shadow_account.extractor import _compute_rsi as extracted_rsi +from src.shadow_account.scanner import _compute_rsi as scanned_rsi +from src.tools.remember_tool import RememberTool + + +def test_group_split_preserves_legacy_positional_arguments(): + groups = np.repeat(np.arange(10), 2) + legacy = list(group_purged_kfold_splits(groups, 5, 0.1)) + named = list(group_purged_kfold_splits(groups, n_folds=5, embargo_fraction=0.1)) + for left, right in zip(legacy, named, strict=True): + np.testing.assert_array_equal(left.train, right.train) + np.testing.assert_array_equal(left.test, right.test) + + +def test_group_split_purges_future_groups_inside_the_test_label_horizon(): + groups = np.repeat(np.arange(10), 2) + ends = np.minimum(np.arange(20) + 5, 19) + for split in group_purged_kfold_splits(groups, label_end_times=ends, n_folds=5, embargo_fraction=0): + assert detect_boundary_leakage(split, ends, n_samples=20).clean + for group in set(groups[split.train]): + assert np.isin(np.flatnonzero(groups == group), split.train).all() + + +def test_group_split_accepts_a_label_horizon_past_the_last_sample(): + groups = np.repeat(np.arange(10), 2) + ends = np.arange(20) + ends[-1] = 25 + splits = list(group_purged_kfold_splits(groups, label_end_times=ends, n_folds=5, embargo_fraction=0)) + assert all(detect_boundary_leakage(split, ends, n_samples=20).clean for split in splits) + + +def test_group_embargo_starts_after_the_test_label_horizon(): + groups = np.repeat(np.arange(10), 2) + ends = np.arange(20) + ends[2:4] = 7 + first = next(group_purged_kfold_splits(groups, label_end_times=ends, n_folds=5, embargo_fraction=0.1)) + assert not np.isin([8, 9], first.train).any() + assert first.purged == 4 + assert first.embargoed == 2 + + +@pytest.mark.parametrize("name", ["project_q2_planning", "q2_planning", " Q2 Planning "]) +def test_memory_forget_resolves_the_same_trimmed_names_as_find(tmp_path, name): + memory = PersistentMemory(memory_dir=tmp_path) + memory.add("Q2 Planning", "planning notes", "project") + assert memory.find(name) is not None + assert memory.remove(name) + assert memory.list_entries() == [] + + +def test_memory_forget_ambiguous_stem_preserves_both_entries(tmp_path): + memory = PersistentMemory(memory_dir=tmp_path) + memory.add("Q2 Planning", "project notes", "project") + memory.add("Q2 Planning", "feedback notes", "feedback") + with pytest.raises(ValueError, match="memory_type"): + memory.remove("q2_planning") + assert len(memory.list_entries()) == 2 + assert memory.remove("q2_planning", memory_type="project") + assert memory.find("feedback_q2_planning") is not None + + +def test_remember_tool_forgets_by_filename_stem(tmp_path): + memory = PersistentMemory(memory_dir=tmp_path) + memory.add("Q2 Planning", "project notes", "project") + result = json.loads(RememberTool(memory=memory).execute(action="forget", title="project_q2_planning")) + assert result["status"] == "ok" + assert memory.list_entries() == [] + + +def test_shadow_rsi_keeps_positional_warmup_with_duplicate_index_labels(): + closes = pd.Series([100, 101, 99, 102, 98, 104, 103], index=["bar"] * 7, dtype=float) + expected = extracted_rsi(closes.reset_index(drop=True), period=3) + for calculate in (extracted_rsi, scanned_rsi): + actual = calculate(closes, period=3) + np.testing.assert_allclose(actual.to_numpy(), expected.to_numpy(), equal_nan=True) + assert actual.iloc[:3].isna().all() + + +def test_shadow_scanning_and_extraction_share_seeded_rsi_and_gap_policy(): + closes = pd.Series([100, 101, 99, 102, 98, 104, np.nan, 103, 102, 105], dtype=float) + pd.testing.assert_series_equal(extracted_rsi(closes, period=3), scanned_rsi(closes, period=3)) + assert extracted_rsi(closes, period=3).iloc[6:8].isna().all() diff --git a/frontend/src/components/chat/Composer.tsx b/frontend/src/components/chat/Composer.tsx index 4d99ffdc..49a78bde 100644 --- a/frontend/src/components/chat/Composer.tsx +++ b/frontend/src/components/chat/Composer.tsx @@ -28,6 +28,7 @@ import { import { toast } from "sonner"; import { api } from "@/lib/api"; import type { UploadedAttachment } from "@/lib/attachments"; +import { buildChatPrompt, MAX_GOAL_CHARS, MAX_MESSAGE_CHARS, promptExceedsLimit } from "@/lib/chatPrompt"; import type { AgentActivity } from "@/stores/agent"; import { LiveRuntimeControl, @@ -115,13 +116,20 @@ export const Composer = memo(forwardRef(function Composer const submitPrompt = useCallback((prompt: string) => { if ((!prompt.trim() && attachments.length === 0) || streaming || uploading) return; + const submitted = prompt.trim(); + const limit = goalComposerActive ? MAX_GOAL_CHARS : MAX_MESSAGE_CHARS; + const requestText = goalComposerActive ? submitted : buildChatPrompt(submitted, attachments, Boolean(swarmPreset)); + if (promptExceedsLimit(requestText, limit)) { + toast.error(t('agent.messageTooLong', { limit: limit.toLocaleString() })); + return; + } setInput(""); if (inputRef.current) inputRef.current.style.height = "auto"; const submittedAttachments = [...attachments]; if (!goalComposerActive) setAttachments([]); - onSubmit(prompt.trim(), submittedAttachments); + onSubmit(submitted, submittedAttachments); inputRef.current?.focus(); - }, [attachments, goalComposerActive, onSubmit, streaming, uploading]); + }, [attachments, goalComposerActive, onSubmit, streaming, swarmPreset, t, uploading]); useImperativeHandle(ref, () => ({ fill(prompt: string) { diff --git a/frontend/src/components/chat/__tests__/Composer.test.tsx b/frontend/src/components/chat/__tests__/Composer.test.tsx index ef7a0565..ea8ee6f9 100644 --- a/frontend/src/components/chat/__tests__/Composer.test.tsx +++ b/frontend/src/components/chat/__tests__/Composer.test.tsx @@ -4,6 +4,8 @@ import userEvent from "@testing-library/user-event"; import i18n from "../../../i18n"; import { api } from "@/lib/api"; import { Composer } from "../Composer"; +import { toast } from "sonner"; +import { MAX_MESSAGE_CHARS, SWARM_PROMPT_PREFIX } from "@/lib/chatPrompt"; vi.mock("@/lib/api", () => ({ api: { uploadFile: vi.fn() }, @@ -30,6 +32,34 @@ const baseProps = { }; describe("Composer", () => { + it("retains oversized input and gives an actionable error", () => { + render(); + const textarea = screen.getByRole("textbox"); + const prompt = "x".repeat(MAX_MESSAGE_CHARS + 1); + fireEvent.change(textarea, { target: { value: prompt } }); + fireEvent.keyDown(textarea, { key: "Enter", code: "Enter" }); + expect(baseProps.onSubmit).not.toHaveBeenCalled(); + expect(textarea).toHaveValue(prompt); + expect(toast.error).toHaveBeenCalledWith(expect.stringContaining("Shorten it")); + }); + + it("counts team instructions in the submitted request", () => { + render(); + const prompt = "x".repeat(MAX_MESSAGE_CHARS - SWARM_PROMPT_PREFIX.length + 1); + fireEvent.change(screen.getByRole("textbox"), { target: { value: prompt } }); + fireEvent.keyDown(screen.getByRole("textbox"), { key: "Enter", code: "Enter" }); + expect(baseProps.onSubmit).not.toHaveBeenCalled(); + expect(screen.getByRole("textbox")).toHaveValue(prompt); + }); + + it("accepts the exact Unicode code point boundary", () => { + render(); + const prompt = "📈".repeat(MAX_MESSAGE_CHARS); + fireEvent.change(screen.getByRole("textbox"), { target: { value: prompt } }); + fireEvent.keyDown(screen.getByRole("textbox"), { key: "Enter", code: "Enter" }); + expect(baseProps.onSubmit).toHaveBeenCalledWith(prompt, []); + }); + beforeAll(async () => { await i18n.changeLanguage("en"); }); diff --git a/frontend/src/i18n/locales/ar.json b/frontend/src/i18n/locales/ar.json index e961555a..3d60d464 100644 --- a/frontend/src/i18n/locales/ar.json +++ b/frontend/src/i18n/locales/ar.json @@ -1007,6 +1007,7 @@ "dropFiles": "أفلت الملفات هنا", "removeAttachment": "إزالة {{filename}}", "uploadFailed": "فشل الرفع: {{error}}", + "messageTooLong": "تتجاوز الرسالة {{limit}} حرفًا بما في ذلك معلومات المرفقات وتعليمات الفريق. اختصرها قبل الإرسال.", "failedToSend": "فشل إرسال الرسالة، يُرجى المحاولة مرة أخرى.", "failedToContinue": "فشل متابعة الهدف، يُرجى المحاولة مرة أخرى.", "runtimeBadge": "مراقبة التداول", diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 5408d244..f4017c54 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -1012,6 +1012,7 @@ "dropFiles": "Dateien hier ablegen", "removeAttachment": "{{filename}} entfernen", "uploadFailed": "Hochladen fehlgeschlagen: {{error}}", + "messageTooLong": "Die Nachricht überschreitet {{limit}} Zeichen einschließlich Anhangsinformationen und Teamanweisungen. Kürze sie vor dem Senden.", "failedToSend": "Nachricht konnte nicht gesendet werden, bitte erneut versuchen.", "failedToContinue": "Ziel konnte nicht fortgesetzt werden, bitte erneut versuchen.", "runtimeBadge": "LAUFZEIT", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index d6c1e1b2..5bca1683 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -1012,6 +1012,7 @@ "dropFiles": "Drop files here", "removeAttachment": "Remove {{filename}}", "uploadFailed": "Upload failed: {{error}}", + "messageTooLong": "Message exceeds {{limit}} characters including attachments and team instructions. Shorten it before sending.", "failedToSend": "Failed to send message, please retry.", "failedToContinue": "Failed to continue goal, please retry.", "runtimeBadge": "RUNTIME", diff --git a/frontend/src/i18n/locales/es.json b/frontend/src/i18n/locales/es.json index 6392565d..6af6d627 100644 --- a/frontend/src/i18n/locales/es.json +++ b/frontend/src/i18n/locales/es.json @@ -1012,6 +1012,7 @@ "dropFiles": "Suelta los archivos aquí", "removeAttachment": "Eliminar {{filename}}", "uploadFailed": "Error al subir: {{error}}", + "messageTooLong": "El mensaje supera {{limit}} caracteres, incluidos los datos de los adjuntos y las instrucciones del equipo. Acórtalo antes de enviarlo.", "failedToSend": "Error al enviar el mensaje, intenta de nuevo.", "failedToContinue": "Error al continuar el objetivo, intenta de nuevo.", "runtimeBadge": "RUNTIME", diff --git a/frontend/src/i18n/locales/id.json b/frontend/src/i18n/locales/id.json index 82dc3380..1bde55c2 100644 --- a/frontend/src/i18n/locales/id.json +++ b/frontend/src/i18n/locales/id.json @@ -1012,6 +1012,7 @@ "dropFiles": "Letakkan file di sini", "removeAttachment": "Hapus {{filename}}", "uploadFailed": "Gagal mengunggah: {{error}}", + "messageTooLong": "Pesan melebihi {{limit}} karakter, termasuk informasi lampiran dan instruksi tim. Persingkat sebelum mengirim.", "failedToSend": "Gagal mengirim pesan, silakan coba lagi.", "failedToContinue": "Gagal melanjutkan tujuan, silakan coba lagi.", "runtimeBadge": "RUNTIME", diff --git a/frontend/src/i18n/locales/ja.json b/frontend/src/i18n/locales/ja.json index 023b93c0..ec6cdcc9 100644 --- a/frontend/src/i18n/locales/ja.json +++ b/frontend/src/i18n/locales/ja.json @@ -1007,6 +1007,7 @@ "dropFiles": "ここにファイルをドロップ", "removeAttachment": "{{filename}} を削除", "uploadFailed": "アップロードに失敗しました: {{error}}", + "messageTooLong": "添付情報とチーム指示を含むメッセージが {{limit}} 文字を超えています。短くしてから送信してください。", "failedToSend": "メッセージ送信に失敗しました。再試行してください。", "failedToContinue": "目標の続行に失敗しました。再試行してください。", "runtimeBadge": "取引モニター", diff --git a/frontend/src/i18n/locales/ko.json b/frontend/src/i18n/locales/ko.json index 4748de8a..d88e6c0a 100644 --- a/frontend/src/i18n/locales/ko.json +++ b/frontend/src/i18n/locales/ko.json @@ -1007,6 +1007,7 @@ "dropFiles": "여기에 파일 놓기", "removeAttachment": "{{filename}} 제거", "uploadFailed": "업로드 실패: {{error}}", + "messageTooLong": "첨부 정보와 팀 지침을 포함한 메시지가 {{limit}}자를 초과합니다. 줄인 후 보내세요.", "failedToSend": "메시지 전송에 실패했습니다. 다시 시도해 주세요.", "failedToContinue": "목표 진행에 실패했습니다. 다시 시도해 주세요.", "runtimeBadge": "거래 모니터", diff --git a/frontend/src/i18n/locales/pt-BR.json b/frontend/src/i18n/locales/pt-BR.json index c7823ba3..e5691d11 100644 --- a/frontend/src/i18n/locales/pt-BR.json +++ b/frontend/src/i18n/locales/pt-BR.json @@ -1012,6 +1012,7 @@ "dropFiles": "Solte os arquivos aqui", "removeAttachment": "Remover {{filename}}", "uploadFailed": "Falha no upload: {{error}}", + "messageTooLong": "A mensagem excede {{limit}} caracteres, incluindo informações dos anexos e instruções da equipe. Reduza-a antes de enviar.", "failedToSend": "Falha ao enviar mensagem, tente novamente.", "failedToContinue": "Falha ao continuar a meta. Tente novamente.", "runtimeBadge": "RUNTIME", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index ef39949e..8b8cee53 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -1007,6 +1007,7 @@ "dropFiles": "拖放文件到这里", "removeAttachment": "移除 {{filename}}", "uploadFailed": "上传失败:{{error}}", + "messageTooLong": "消息(含附件信息和团队指令)超过 {{limit}} 个字符,请缩短后再发送。", "failedToSend": "发送消息失败,请重试。", "failedToContinue": "继续目标失败,请重试。", "runtimeBadge": "交易监控", diff --git a/frontend/src/lib/__tests__/api.test.ts b/frontend/src/lib/__tests__/api.test.ts index e0e36db9..aebdcdd7 100644 --- a/frontend/src/lib/__tests__/api.test.ts +++ b/frontend/src/lib/__tests__/api.test.ts @@ -25,6 +25,15 @@ describe("generated report download", () => { }); describe("api request helper", () => { + it("translates the server's message-size error into a recovery instruction", async () => { + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(JSON.stringify({ + detail: { code: "message_too_long", max_length: 100_000, message: "Shorten input" }, + }), { status: 422, headers: { "content-type": "application/json" } }))); + const { api } = await loadApiModule(); + await expect(api.sendMessage("session", "oversized")).rejects.toMatchObject({ + status: 422, code: "message_too_long", message: expect.stringContaining("Shorten it"), + }); + }); beforeEach(() => { vi.stubGlobal("localStorage", { getItem: vi.fn(() => ""), diff --git a/frontend/src/lib/__tests__/chatPrompt.test.ts b/frontend/src/lib/__tests__/chatPrompt.test.ts new file mode 100644 index 00000000..19cbe1a4 --- /dev/null +++ b/frontend/src/lib/__tests__/chatPrompt.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from "vitest"; +import { buildChatPrompt, promptExceedsLimit } from "../chatPrompt"; + +describe("chat request length", () => { + it("includes attachment paths and counts Unicode code points", () => { + const attachments = [{ filename: "研究.pdf", filePath: "uploads/研究.pdf" }]; + const prompt = buildChatPrompt("📈", attachments, false); + const length = Array.from(prompt).length; + expect(promptExceedsLimit(prompt, length)).toBe(false); + expect(promptExceedsLimit(prompt, length - 1)).toBe(true); + expect(prompt).toContain("uploads/研究.pdf"); + }); +}); diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 76c48761..200c0677 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -317,10 +317,13 @@ async function errorFromResponse(res: Response): Promise { if (typeof raw === "string" && raw) { detail = raw; } else if (raw && typeof raw === "object") { - const structured = raw as { code?: unknown; message?: unknown }; + const structured = raw as { code?: unknown; message?: unknown; max_length?: unknown }; if (typeof structured.code === "string" && structured.code) code = structured.code; if (typeof structured.message === "string" && structured.message) detail = structured.message; else if (code) detail = code; + if (code === "message_too_long" && typeof structured.max_length === "number") { + detail = i18n.t("agent.messageTooLong", { limit: structured.max_length.toLocaleString() }); + } } } catch { /* ignore */ } if (res.status === 401 || res.status === 403) { diff --git a/frontend/src/lib/chatPrompt.ts b/frontend/src/lib/chatPrompt.ts new file mode 100644 index 00000000..04f3a678 --- /dev/null +++ b/frontend/src/lib/chatPrompt.ts @@ -0,0 +1,20 @@ +import { prependUploadedAttachments, type UploadedAttachment } from "@/lib/attachments"; + +// Match the interactive and goal request schemas in sessions_routes.py. +export const MAX_MESSAGE_CHARS = 100_000; +export const MAX_GOAL_CHARS = 5_000; +export const SWARM_PROMPT_PREFIX = + "[Swarm Team Mode] Use the swarm tool to assemble the best specialist team for this task. Auto-select the most appropriate preset.\n\n"; + +export function buildChatPrompt(prompt: string, attachments: UploadedAttachment[], swarm: boolean): string { + return prependUploadedAttachments(swarm ? `${SWARM_PROMPT_PREFIX}${prompt}` : prompt, attachments); +} + +/** Python validates Unicode code points; JS string.length counts UTF-16 units. */ +export function promptExceedsLimit(prompt: string, limit: number): boolean { + let count = 0; + for (const _character of prompt) { + if (++count > limit) return true; + } + return false; +} diff --git a/frontend/src/pages/Agent.tsx b/frontend/src/pages/Agent.tsx index 599ba1a1..f87a15de 100644 --- a/frontend/src/pages/Agent.tsx +++ b/frontend/src/pages/Agent.tsx @@ -13,8 +13,8 @@ import { useSSE } from "@/hooks/useSSE"; import { ApiError, AUTH_REQUIRED_MESSAGE, api, isAuthRequiredError, type GoalSnapshot, type MandateProposal, type MandateCommitted, type ScheduledResearchProposal, type LiveAction, type LiveHalted, type LLMSettings } from "@/lib/api"; import { extractUploadedAttachments, - prependUploadedAttachments, } from "@/lib/attachments"; +import { buildChatPrompt, MAX_GOAL_CHARS, MAX_MESSAGE_CHARS, promptExceedsLimit, SWARM_PROMPT_PREFIX } from "@/lib/chatPrompt"; import { isReportWorthyRun } from "@/lib/runReports"; import type { AgentMessage, SwarmRunStatus, ToolCallEntry } from "@/types/agent"; import { AgentAvatar } from "@/components/chat/AgentAvatar"; @@ -110,8 +110,6 @@ function toolProgressKey(callId: string | undefined, tool: string): string { const STREAM_FLUSH_INTERVAL_MS = 80; const TIMELINE_WINDOW_SIZE = 160; -const SWARM_PROMPT_PREFIX = - "[Swarm Team Mode] Use the swarm tool to assemble the best specialist team for this task. Auto-select the most appropriate preset.\n\n"; const GOAL_KICKOFF_PREFIX = [ "Start working on this research goal now.", "Keep it research-only, use available tools when evidence is needed, add concrete evidence to the goal ledger, and keep going until the goal is complete, blocked, waiting for user input, or budget-limited.", @@ -1421,6 +1419,13 @@ export function Agent() { attachments: ComposerAttachment[] = [], ) => { if ((!prompt.trim() && attachments.length === 0) || status === "streaming") return; + const finalPrompt = buildChatPrompt(prompt, attachments, Boolean(swarmPreset)); + const limit = goalComposerActive ? MAX_GOAL_CHARS : MAX_MESSAGE_CHARS; + if (promptExceedsLimit(goalComposerActive ? prompt : finalPrompt, limit)) { + toast.error(t('agent.messageTooLong', { limit: limit.toLocaleString() })); + composerRef.current?.fill(prompt); + return; + } clearStreamingView(); if (goalComposerActive) { @@ -1458,7 +1463,6 @@ export function Agent() { return; } - let finalPrompt = prompt; const displayPrompt = toDisplayPrompt(prompt); const messageMeta: AgentMessageMeta = { ...displayPrompt.meta }; @@ -1466,12 +1470,10 @@ export function Agent() { if (swarmPreset) { messageMeta.swarmMode = true; setSwarmPreset(null); - finalPrompt = `${SWARM_PROMPT_PREFIX}${prompt}`; } if (attachments.length > 0) { messageMeta.attachments = attachments.map(({ filename }) => ({ filename })); - finalPrompt = prependUploadedAttachments(finalPrompt, attachments); } messageMeta.requestText = finalPrompt; act().addMessage({ @@ -1504,7 +1506,8 @@ export function Agent() { } catch (error) { archiveActivity("failed"); act().setStatus("error"); - const message = isAuthRequiredError(error) ? AUTH_REQUIRED_MESSAGE : t('agent.failedToSend'); + const message = isAuthRequiredError(error) ? AUTH_REQUIRED_MESSAGE + : error instanceof ApiError && error.code === 'message_too_long' ? error.message : t('agent.failedToSend'); toast.error(message); act().addMessage({ id: "", type: "error", content: message, timestamp: Date.now() }); }