fix(live): stop active attempts and isolate durable schedules

Wait for actual session terminal events, cancel in-flight analysis on runner stop, and finish scheduler teardown across startup, concurrent stops and API shutdown. Correct heartbeat status units and remove stopped sentinels.

Isolate broker job stores with legacy migration and complete atomic writes under concurrency. Skip malformed emergency-close symbols and overflowing quantities, and make capped grounding feedback explicit about omitted findings.

Validation: 16,817 backend tests passed (190 skipped), 38 frontend status tests passed, and changed Python syntax/lint passed. Update seven README News entries.
This commit is contained in:
Haozhe Wu committed 2026-10-06 01:28:07 +08:00
1 parent 785c757cd5
commit 7f6908b7e1
20 files changed
+674 -73

No files matched your search

+4 -2
View File
@@ -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-06** 🛠️ **Live controls and report corrections**: Stopping a live runner cancels its current analysis and waits for scheduler cleanup, including startup cancellation and API shutdown; status timestamps use the correct units ([#1704](https://github.com/HKUDS/Vibe-Trading/pull/1704)). Emergency cancel/flatten scans skip malformed records without abandoning the remaining book ([#1703](https://github.com/HKUDS/Vibe-Trading/pull/1703)); broker schedules are stored separately and writes handle concurrency and partial writes. US equity half-days respect the early closing bell ([#1706](https://github.com/HKUDS/Vibe-Trading/pull/1706)). Report corrections preserve figures that passed validation, and bounded feedback no longer implies that unlisted figures passed ([#1702](https://github.com/HKUDS/Vibe-Trading/pull/1702)).
- **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. Invalid call aliases now receive exact source references for correction ([#1638](https://github.com/HKUDS/Vibe-Trading/pull/1638)); numeric validation stays unchanged.
- **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)).
<details>
<summary>Earlier news</summary>
- **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)).
- **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)).
+4 -2
View File
@@ -52,16 +52,18 @@
> ⚠️ **تحذير أمني:** حساب X باسم `VibeTrading_HKU`، ومشروع Virtuals رقم `101845`، وعقد التوكن `0x640BDBF77b6447E8b7DB7894cED84BD1c40571f4` كلّها غير رسمية ولا تتبع Vibe-Trading. لم نُطلق أو نؤيد مطلقًا أي توكن أو عملة ميم. لا تشترِ هذا التوكن، ولا تربط محفظتك، ولا توقّع أي شيء. [التفاصيل](SECURITY.md#official-channels--impersonation).
- **2026-10-06** 🛠️ **إصلاح التحكم بالتداول وتصحيح التقارير**: يلغي إيقاف مشغّل التداول الفعلي التحليل الجاري وينتظر انتهاء المجدول، بما يشمل الإلغاء أثناء بدء التشغيل وإغلاق API، مع تصحيح وحدات الوقت في الحالة ([#1704](https://github.com/HKUDS/Vibe-Trading/pull/1704)). يتجاوز إلغاء الأوامر وإغلاق المراكز في الطوارئ السجلات غير الصالحة ويواصل معالجة الباقي ([#1703](https://github.com/HKUDS/Vibe-Trading/pull/1703)). تُحفظ جداول الوسطاء بصورة منفصلة مع معالجة الكتابات المتزامنة والجزئية. تُراعى ساعات الإغلاق المبكر للأسهم الأمريكية ([#1706](https://github.com/HKUDS/Vibe-Trading/pull/1706)). تحافظ تصحيحات التقارير على الأرقام التي اجتازت التحقق، ولا توحي الملاحظات المحدودة بأن الأرقام غير المذكورة قد اجتازته ([#1702](https://github.com/HKUDS/Vibe-Trading/pull/1702)).
- **2026-10-05** 🛠️ **إدخال البحث وتصحيح الحسابات**: تدعم المحادثة نصوص بحث أطول وتوضح باللغة الحالية كيفية تقصير النص عند تجاوز الحد ([#1701](https://github.com/HKUDS/Vibe-Trading/pull/1701)). يستخدم Sortino انحراف الجانب السلبي عبر جميع الفترات، ويستبعد التحقق الجماعي التسميات المتداخلة. تحسنت أوزان التباين وRSI بمتوسط Wilder الأولي وحذف الذاكرة باسم الملف. عند استخدام اسم مستعار غير صحيح للاستدعاء، يقترح التصحيح مراجع الحقول الفعلية مع بقاء التحقق من الأرقام كما هو ([#1638](https://github.com/HKUDS/Vibe-Trading/pull/1638)).
- **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)).
<details>
<summary>أخبار سابقة</summary>
- **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-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)).
+4 -2
View File
@@ -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-06** 🛠️ **Control de operaciones reales y corrección de informes**: Detener el agente de operaciones reales cancela el análisis en curso y espera a que termine el planificador, incluso al cancelar durante el inicio o cerrar la API; se corrigen también las unidades de tiempo del estado ([#1704](https://github.com/HKUDS/Vibe-Trading/pull/1704)). La cancelación y el cierre de posiciones de emergencia omiten registros inválidos y continúan con los restantes ([#1703](https://github.com/HKUDS/Vibe-Trading/pull/1703)); cada bróker conserva su propia programación y las escrituras manejan concurrencia y escrituras parciales. Las sesiones reducidas de acciones estadounidenses respetan el cierre anticipado ([#1706](https://github.com/HKUDS/Vibe-Trading/pull/1706)). Las correcciones conservan las cifras verificadas y no presentan las cifras omitidas de los comentarios limitados como aprobadas ([#1702](https://github.com/HKUDS/Vibe-Trading/pull/1702)).
- **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. Los alias de llamada incorrectos reciben referencias exactas para corregirlos, conservando la validación numérica ([#1638](https://github.com/HKUDS/Vibe-Trading/pull/1638)).
- **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)).
<details>
<summary>Noticias anteriores</summary>
- **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)).
- **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)).
+4 -2
View File
@@ -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-06** 🛠️ **Kontrol trading langsung dan koreksi laporan**: Menghentikan runner trading langsung membatalkan analisis aktif dan menunggu penjadwal selesai, termasuk pembatalan saat mulai dan penutupan API; satuan waktu pada status juga diperbaiki ([#1704](https://github.com/HKUDS/Vibe-Trading/pull/1704)). Pembatalan order dan penutupan posisi darurat melewati catatan tidak valid lalu melanjutkan catatan lainnya ([#1703](https://github.com/HKUDS/Vibe-Trading/pull/1703)); jadwal setiap broker disimpan terpisah dengan penanganan penulisan bersamaan dan parsial. Sesi saham AS yang dipersingkat mengikuti waktu tutup lebih awal ([#1706](https://github.com/HKUDS/Vibe-Trading/pull/1706)). Koreksi laporan mempertahankan angka yang lolos verifikasi, dan umpan balik terbatas tidak menyiratkan bahwa angka yang tidak tercantum sudah lolos ([#1702](https://github.com/HKUDS/Vibe-Trading/pull/1702)).
- **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. Alias panggilan yang keliru kini mendapat referensi sumber yang tepat untuk koreksi, dengan validasi angka tetap berlaku ([#1638](https://github.com/HKUDS/Vibe-Trading/pull/1638)).
- **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)).
<details>
<summary>Berita sebelumnya</summary>
- **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)).
- **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)).
+4 -2
View File
@@ -52,16 +52,18 @@
> ⚠️ **セキュリティ警告:** Xアカウント `VibeTrading_HKU`、Virtualsプロジェクト `101845`、およびトークンコントラクト `0x640BDBF77b6447E8b7DB7894cED84BD1c40571f4` は、いずれもVibe-Trading公式のものではありません。Vibe-Tradingはこれまで、いかなるトークンやミームコインも発行・公認していません。購入、ウォレットの接続、署名は行わないでください。[詳細](SECURITY.md#official-channels--impersonation)
- **2026-10-06** 🛠️ **ライブ運用制御とレポート修正**:ライブランナーの停止は実行中の分析をキャンセルしてスケジューラーの終了を待ち、起動中のキャンセルと API 終了にも対応します。状態の時刻単位も修正しました([#1704](https://github.com/HKUDS/Vibe-Trading/pull/1704))。緊急キャンセル・決済処理は不正なレコードを除外して残りの注文・保有を処理します([#1703](https://github.com/HKUDS/Vibe-Trading/pull/1703))。ブローカーごとにスケジュールを保存し、並行書き込みと部分書き込みに対応。米国株の短縮取引日は早い閉場時刻を守ります([#1706](https://github.com/HKUDS/Vibe-Trading/pull/1706))。レポート修正は検証済みの数値を保持し、省略された指摘を検証済みと誤認させません([#1702](https://github.com/HKUDS/Vibe-Trading/pull/1702))。
- **2026-10-05** 🛠️ **調査入力と計算の修正**:チャットで長い調査入力に対応し、上限を超えた場合は現在の言語で短縮を案内します([#1701](https://github.com/HKUDS/Vibe-Trading/pull/1701))。Sortino は全期間の下方偏差を使用し、グループ検証は重複ラベルを除外します。共分散ウェイト、Wilder 初期平均による RSI、ファイル名によるメモリ削除も改善しました。 呼び出しの別名を誤って引用した場合、修正用に実際のフィールド参照を提示します([#1638](https://github.com/HKUDS/Vibe-Trading/pull/1638))。数値の検証規則は維持されます。
- **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))。
<details>
<summary>過去のニュース</summary>
- **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-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))。
+4 -2
View File
@@ -52,16 +52,18 @@
> ⚠️ **보안 경고:** X 계정 `VibeTrading_HKU`, Virtuals 프로젝트 `101845`, 토큰 컨트랙트 `0x640BDBF77b6447E8b7DB7894cED84BD1c40571f4`는 모두 Vibe-Trading 공식과 무관합니다. Vibe-Trading은 어떠한 토큰이나 밈코인도 발행하거나 공식적으로 지지한 적이 없습니다. 해당 토큰을 구매하거나 지갑을 연결하거나 어떠한 서명도 하지 마세요. [자세히 보기](SECURITY.md#official-channels--impersonation).
- **2026-10-06** 🛠️ **실거래 제어와 보고서 수정**: 실거래 러너 중지는 현재 분석을 취소하고 스케줄러 종료를 기다리며 시작 중 취소와 API 종료도 처리합니다. 상태 시간의 단위도 수정했습니다([#1704](https://github.com/HKUDS/Vibe-Trading/pull/1704)). 긴급 취소·청산은 잘못된 레코드를 건너뛰고 나머지 주문과 포지션을 계속 처리합니다([#1703](https://github.com/HKUDS/Vibe-Trading/pull/1703)). 증권사별 일정을 따로 저장하고 동시 쓰기와 부분 쓰기를 처리합니다. 미국 주식 단축 거래일의 조기 폐장 시간을 반영합니다([#1706](https://github.com/HKUDS/Vibe-Trading/pull/1706)). 보고서 수정은 검증된 수치를 유지하며 생략된 오류가 검증을 통과한 것처럼 안내하지 않습니다([#1702](https://github.com/HKUDS/Vibe-Trading/pull/1702)).
- **2026-10-05** 🛠️ **리서치 입력과 계산 수정**: 더 긴 리서치 입력을 지원하며 한도를 넘으면 현재 언어로 줄이도록 안내합니다([#1701](https://github.com/HKUDS/Vibe-Trading/pull/1701)). Sortino는 전체 기간의 하방 편차를 사용하고 그룹 검증은 겹치는 레이블을 제외합니다. 공분산 가중치, Wilder 초기 평균을 사용하는 RSI, 파일명으로 메모리 삭제도 개선했습니다. 잘못된 호출 별칭을 인용하면 수정할 수 있도록 실제 필드 참조를 안내하며 수치 검증 규칙은 유지합니다([#1638](https://github.com/HKUDS/Vibe-Trading/pull/1638)).
- **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)).
<details>
<summary>이전 뉴스</summary>
- **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-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)).
+4 -2
View File
@@ -52,16 +52,18 @@
> ⚠️ **安全警告:** X 账号 `VibeTrading_HKU`、Virtuals 项目 `101845` 及代币合约 `0x640BDBF77b6447E8b7DB7894cED84BD1c40571f4` 均非 Vibe-Trading 官方。我们从未发行或背书任何代币或 meme 币。请勿购买、连接钱包或签名。[详细说明](SECURITY.md#official-channels--impersonation)。
- **2026-10-06** 🛠️ **实盘控制与报告纠错修复**:停止实盘运行会取消当前分析并等待调度器完成清理,覆盖启动期间取消及 API 关闭,运行状态时间单位也已修正([#1704](https://github.com/HKUDS/Vibe-Trading/pull/1704))。紧急撤单/平仓扫描跳过异常记录,继续处理其余订单和持仓([#1703](https://github.com/HKUDS/Vibe-Trading/pull/1703));不同券商的调度独立保存,持久化正确处理并发写入与短写入。美股半日交易遵循提前收市时间([#1706](https://github.com/HKUDS/Vibe-Trading/pull/1706))。报告纠错保留已核验通过的数字,反馈截断也不会暗示未列出的数字已通过([#1702](https://github.com/HKUDS/Vibe-Trading/pull/1702))。
- **2026-10-05** 🛠️ **研究输入与计算修复**:聊天支持更长的研究输入,超限时以当前语言提示缩短([#1701](https://github.com/HKUDS/Vibe-Trading/pull/1701))。回测 Sortino 使用全样本下行偏差;分组验证剔除重叠标签,协方差权重保持有限,影子账户 RSI 使用 Wilder 初始均值,记忆支持按文件名删除。 报告误用调用别名时,纠正提示提供真实字段引用([#1638](https://github.com/HKUDS/Vibe-Trading/pull/1638)),数值校验保持不变。
- **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))。
<details>
<summary>更早的新闻</summary>
- **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-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))。
+4 -10
View File
@@ -114,13 +114,14 @@ from src.api.state import ( # noqa: F401, E402
console = Console()
logger = logging.getLogger(__name__)
from src.api.channels_routes import ( # noqa: E402
from src.api.channels_routes import ( # noqa: F401, E402
_start_channel_runtime,
_stop_channel_runtime,
)
from src.api.scheduled_routes import ( # noqa: E402
from src.api.scheduled_routes import ( # noqa: F401, E402
_start_scheduled_research_executor,
_stop_scheduled_research_executor,
_stop_scheduled_research_on_shutdown,
)
@@ -142,14 +143,6 @@ async def _run_startup_preflight() -> None:
await _start_channel_runtime()
async def _stop_scheduled_research_on_shutdown() -> None:
"""Stop the scheduled research executor on server shutdown."""
try:
await _stop_channel_runtime()
finally:
await _stop_scheduled_research_executor()
@asynccontextmanager
async def _lifespan(_: FastAPI) -> AsyncIterator[None]:
"""Run API startup and guaranteed reverse-order shutdown."""
@@ -288,6 +281,7 @@ from src.api.live_routes import ( # noqa: F401, E402
_live_broker_adapter,
_build_live_runner,
_drive_runner,
_stop_live_runners,
_connector_verify_cache,
_check_connector_status,
)
+8 -2
View File
@@ -198,6 +198,12 @@ class _ReleaseMixin:
lines.extend(
f"- {issue.get('message', issue.get('code', 'grounding error'))}" for issue in others
)
omitted = len(validation.issues) - 24
if omitted > 0:
lines.append(
f"{omitted} additional findings are not shown in this bounded feedback. "
"Do not assume unlisted figures passed; check their declarations and evidence too."
)
if figures:
lines.extend(
[
@@ -223,10 +229,10 @@ class _ReleaseMixin:
keep += f", and {len(passed) - len(shown)} more"
lines.extend(
[
"Every other measured figure in the draft checked clean. Keep these "
"These measured figures in the draft checked clean. Keep these "
"values exactly as written, where they stand: " + keep + ".",
"Cutting them or swapping whole sections for qualitative prose is not "
"a fix; only the figures listed above need work.",
"a fix; repair the rejected claims while preserving the clean figures.",
]
)
lines.extend(
+127 -20
View File
@@ -22,6 +22,7 @@ event through the EXISTING session EventBus, so the frontend's already-wired
from __future__ import annotations
import asyncio
import inspect
import logging
import sys as _sys
import time
@@ -543,16 +544,17 @@ def _active_mandate_state(broker: str) -> Optional[ActiveMandateState]:
def _runner_liveness_state(broker: str) -> RunnerLivenessState:
"""Build the runner-liveness snapshot for a broker (SPEC §7.5 contract)."""
alive = False
task = _host()._runner_tasks.get(broker)
alive = task is not None and not task.done()
tick: Optional[float] = None
age: Optional[float] = None
try:
from src.live.runtime import liveness
alive = bool(liveness.is_runner_alive(broker))
alive = alive or bool(liveness.is_runner_alive(broker))
raw_tick = liveness.last_tick(broker)
if raw_tick is not None:
tick = float(raw_tick)
tick = float(raw_tick) / 1000
age = max(0.0, time.time() - tick)
except Exception: # pragma: no cover - liveness module is built concurrently
logger.debug("runner liveness lookup failed for %s", broker, exc_info=True)
@@ -560,6 +562,68 @@ def _runner_liveness_state(broker: str) -> RunnerLivenessState:
return RunnerLivenessState(broker=broker, alive=alive, last_tick=tick, last_tick_age_seconds=age)
async def _run_live_session_attempt(svc: Any, session_id: str, prompt: str) -> Dict[str, Any]:
"""Own a queued session attempt until completion or cooperative cancellation.
Args:
svc: The public session service used by the live runner.
session_id: The runner's dedicated session.
prompt: The mandate-bound autonomous tick prompt.
Returns:
The submission receipt plus the completed attempt's result.
Raises:
RuntimeError: If submission has no attempt id or execution fails.
asyncio.CancelledError: After a stopped tick's attempt has terminated.
"""
loop = asyncio.get_running_loop()
completed = loop.create_future()
receipt: Dict[str, Any] = {}
early_results: Dict[str, Dict[str, Any]] = {}
def on_terminal(event: Any) -> None:
if event.session_id != session_id or event.event_type not in {
"attempt.completed", "attempt.failed", "attempt.cancelled", "attempt.interrupted"
}:
return
data = {**event.data, "status": event.event_type.split(".", 1)[1]}
def settle() -> None:
attempt_id = data.get("attempt_id")
if not receipt:
early_results[attempt_id] = data
elif attempt_id == receipt.get("attempt_id") and not completed.done():
completed.set_result(data)
loop.call_soon_threadsafe(settle)
svc.event_bus.add_listener(on_terminal)
try:
receipt = await svc.send_message(session_id, prompt)
attempt_id = receipt.get("attempt_id")
if not isinstance(attempt_id, str) or not attempt_id:
raise RuntimeError("live session submission returned no attempt id")
if attempt_id in early_results:
completed.set_result(early_results[attempt_id])
try:
terminal = await asyncio.shield(completed)
except asyncio.CancelledError:
svc.cancel_current(session_id)
# Session execution is a separate task (and may own an AgentLoop
# thread). Cancelling only this waiter cannot stop that execution.
await asyncio.shield(completed)
raise
if terminal["status"] != "completed":
raise RuntimeError(str(terminal.get("error") or terminal["status"]))
return {
**receipt, "status": "success", "content": terminal.get("summary", ""),
"run_dir": terminal.get("run_dir"),
}
finally:
svc.event_bus.remove_listener(on_terminal)
def _build_live_runner(broker: str) -> Any:
"""Construct a fully-wired ``LiveRunner`` for a broker (SPEC §7.5 R-INT).
@@ -688,7 +752,7 @@ def _build_live_runner(broker: str) -> Any:
session_id = session.session_id
async def _agent_caller(sid: str, prompt: str) -> Dict[str, Any]:
return await svc.send_message(sid, prompt)
return await _run_live_session_attempt(svc, sid, prompt)
def _audit_with_bus(event: Any) -> Dict[str, Any]:
return write_live_action(
@@ -752,7 +816,7 @@ async def _stop_scheduler(runner: Any) -> None:
return
try:
result = stop()
if asyncio.iscoroutine(result):
if inspect.isawaitable(result):
await result
except Exception: # noqa: BLE001 - teardown is best-effort by contract
logger.warning(
@@ -776,24 +840,65 @@ async def _drive_runner(runner: Any) -> None:
Keeping this coroutine parked on the scheduler task makes the driver's
lifetime the scheduler's lifetime, so cancelling it actually stops trading.
"""
result = runner.run_loop()
if asyncio.iscoroutine(result):
await result
else:
await asyncio.get_running_loop().run_in_executor(None, lambda: result)
scheduler_task = _runner_scheduler_task(runner)
if scheduler_task is None:
# ``run_loop`` declined to start (missing or expired mandate): there is
# no scheduler to own, so the driver returns immediately as before.
return
scheduler_task = None
try:
result = runner.run_loop()
if inspect.isawaitable(result):
await result
scheduler_task = _runner_scheduler_task(runner)
if scheduler_task is None:
return
# Shielded: cancelling this task is precisely what ``stop`` does, and the
# teardown below is what has to reach the scheduler loop task.
await asyncio.shield(scheduler_task)
finally:
await _stop_scheduler(runner)
# Starting an async runner may itself be cancelled after it spawned its
# scheduler. Cleanup must cover startup as well as the steady-state wait.
if scheduler_task is not None or getattr(getattr(runner, "_scheduler", None), "_task", None) is not None:
await _stop_scheduler(runner)
from src.live.runtime.liveness import heartbeat_path
try:
heartbeat_path(runner.broker).unlink(missing_ok=True)
except (AttributeError, OSError, ValueError):
logger.warning("could not clear stopped runner heartbeat", exc_info=True)
async def _cancel_runner_task(task: Any) -> None:
"""Cancel a driver once and wait for its scheduler cleanup.
Concurrent stop requests share the same driver. A second cancellation
must not interrupt the first request's asynchronous scheduler teardown.
Shielding also lets teardown finish if an HTTP client disconnects.
Args:
task: The registered runner driver task.
"""
if not getattr(task, "cancelling", lambda: 0)():
task.cancel()
if isinstance(task, asyncio.Future):
try:
await asyncio.shield(task)
except asyncio.CancelledError:
if not task.done():
raise
async def _stop_live_runners() -> None:
"""Await all registered live drivers before API shutdown completes."""
tasks = _host()._runner_tasks
registered = list(tasks.items())
results = await asyncio.gather(
*(_cancel_runner_task(task) for _, task in registered), return_exceptions=True
)
for (broker, task), result in zip(registered, results):
if tasks.get(broker) is task and task.done():
tasks.pop(broker, None)
if isinstance(result, BaseException):
logger.error(
"live runner shutdown failed for %s", broker,
exc_info=(type(result), result, result.__traceback__),
)
# ============================================================================
@@ -1248,11 +1353,13 @@ def register_live_routes(
h = _host()
tasks = h._runner_tasks
task = tasks.pop(broker, None)
task = tasks.get(broker)
if task is None or task.done():
return {"broker": broker, "stopped": False, "was_running": False}
task.cancel()
await _cancel_runner_task(task)
if tasks.get(broker) is task:
tasks.pop(broker, None)
h._emit_live_event(
payload.session_id,
"live.action",
+15
View File
@@ -173,6 +173,21 @@ def _start_scheduled_research_executor() -> None:
_get_scheduled_research_executor().start()
async def _stop_scheduled_research_on_shutdown() -> None:
"""Stop live drivers, channels and research even if one service fails."""
from src.api.channels_routes import _stop_channel_runtime
from src.api.live_routes import _stop_live_runners
host = _sys.modules.get("api_server")
try:
await getattr(host, "_stop_live_runners", _stop_live_runners)()
finally:
try:
await getattr(host, "_stop_channel_runtime", _stop_channel_runtime)()
finally:
await getattr(host, "_stop_scheduled_research_executor", _stop_scheduled_research_executor)()
async def _stop_scheduled_research_executor() -> None:
"""Stop scheduled research execution if it was started."""
executor = _scheduled_research_executor
+6 -1
View File
@@ -362,6 +362,11 @@ def _flatten_open_positions(
continue
if qty == 0.0:
continue
if not isinstance(symbol, str) or not symbol.strip():
report["errors"].append(
{"phase": "flatten", "error": f"invalid or missing position symbol: {symbol!r}"}
)
continue
side = "sell" if qty > 0 else "buy"
close_qty = abs(qty)
request = {
@@ -438,7 +443,7 @@ def _coerce_position_qty(raw: Any) -> float | None:
return None
try:
qty = float(raw)
except (TypeError, ValueError):
except (TypeError, ValueError, OverflowError):
return None
return qty if math.isfinite(qty) else None
+25 -13
View File
@@ -16,7 +16,9 @@ stop watching the mandate's cadence. This store guarantees:
all activity). A genuinely-missing store (first boot) loads as empty — that
is the only blank-start path.
State lives under ``live_root()/runtime/`` (see :mod:`src.live.paths`).
Runner state lives under ``live_root()/<broker>/runtime/``; the legacy shared
``live_root()/runtime/`` store remains readable for migration (see
:mod:`src.live.paths`).
"""
from __future__ import annotations
@@ -24,10 +26,11 @@ from __future__ import annotations
import json
import logging
import os
import tempfile
from datetime import datetime, timezone
from pathlib import Path
from src.live.paths import live_root
from src.live.paths import broker_dir, live_root
from src.live.runtime.scheduler import Job
logger = logging.getLogger(__name__)
@@ -84,16 +87,20 @@ class JobStore:
path: Absolute path of the JSON store file.
"""
def __init__(self, path: Path | None = None) -> None:
def __init__(self, path: Path | None = None, *, broker: str | None = None) -> None:
"""Initialize the store.
Args:
path: Explicit store path. When ``None`` (default), resolves to
``runtime_dir()/jobs.json``. Resolved lazily-but-once here so a
``runtime_dir()/jobs.json``, or the broker's runtime directory
when ``broker`` is supplied. Resolved lazily-but-once here so a
test that monkeypatches ``get_runtime_root`` before construction
gets the isolated path.
broker: Optional broker key for an isolated job store. The legacy
unscoped store remains readable for migration.
"""
self.path = path if path is not None else runtime_dir() / _STORE_FILENAME
directory = broker_dir(broker) / _RUNTIME_SUBDIR if broker is not None else runtime_dir()
self.path = path if path is not None else directory / _STORE_FILENAME
def load(self) -> list[Job]:
"""Load the persisted jobs.
@@ -139,16 +146,21 @@ class JobStore:
target.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
payload = json.dumps(self._envelope(jobs), ensure_ascii=False, indent=2)
tmp = target.with_name(f".{target.name}.{os.getpid()}.tmp")
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
fd, tmp_name = tempfile.mkstemp(prefix=f".{target.name}.", suffix=".tmp", dir=target.parent)
tmp = Path(tmp_name)
try:
os.write(fd, payload.encode("utf-8"))
os.fsync(fd)
with os.fdopen(fd, "wb", buffering=0) as handle:
remaining = memoryview(payload.encode("utf-8"))
while remaining:
written = os.write(handle.fileno(), remaining)
if written == 0:
raise OSError("job store write made no progress")
remaining = remaining[written:]
os.fsync(handle.fileno())
os.replace(tmp, target)
self._fsync_dir(target.parent)
finally:
os.close(fd)
os.replace(tmp, target)
self._fsync_dir(target.parent)
tmp.unlink(missing_ok=True)
def _quarantine(self, cause: str) -> Path:
"""Rename the corrupt store aside as ``<name>.corrupt-<ts>``.
+23 -5
View File
@@ -35,6 +35,7 @@ broker. See the live-trading SPEC §7.5.
from __future__ import annotations
import inspect
import logging
from dataclasses import dataclass
from datetime import datetime, timezone
@@ -930,9 +931,20 @@ class LiveRunner:
if jobs is not None:
return list(jobs)
store = self._job_store or JobStore()
store = self._job_store or JobStore(broker=self.broker)
try:
persisted = store.load()
if self._job_store is None and not store.path.exists():
# Older runners shared one file. Migrate only explicitly bound
# jobs, retaining that file for other brokers' migrations.
persisted = [
job for job in JobStore().load()
if isinstance(job.payload, dict)
and job.payload.get("broker") == self.broker
]
if persisted:
store.save(persisted)
else:
persisted = store.load()
except Exception: # noqa: BLE001 — a corrupt store must not wedge start
logger.exception("job store load failed for %s; recomputing", self.broker)
persisted = []
@@ -1004,7 +1016,13 @@ class LiveRunner:
)
return built
def stop_loop(self) -> None:
"""Stop the scheduler if one was injected (idempotent)."""
async def stop_loop(self) -> None:
"""Stop and await an injected scheduler (idempotent).
Callers must await this method so the scheduler has stopped before
they restart the runner or report a completed stop.
"""
if self._scheduler is not None:
self._scheduler.stop()
result = self._scheduler.stop()
if inspect.isawaitable(result):
await result
+271 -1
View File
@@ -15,6 +15,7 @@ from __future__ import annotations
import asyncio
import json
import time
from pathlib import Path
from types import SimpleNamespace
@@ -99,6 +100,28 @@ def test_live_status_blank_broker_rejected(tmp_path: Path, monkeypatch) -> None:
assert response.status_code == 400
def test_live_status_converts_heartbeat_milliseconds_to_api_seconds(tmp_path, monkeypatch) -> None:
from src.live.runtime import liveness
_live_env(tmp_path, monkeypatch)
now = 1_700_000_000
monkeypatch.setattr("src.api.live_routes.time.time", lambda: now)
liveness.write_heartbeat("robinhood", now_ms=(now - 45) * 1000)
state = api_server._runner_liveness_state("robinhood")
assert state.alive is True
assert state.last_tick == now - 45
assert state.last_tick_age_seconds == 45
def test_live_status_keeps_an_owned_long_running_tick_alive(tmp_path, monkeypatch) -> None:
from src.live.runtime import liveness
_live_env(tmp_path, monkeypatch)
liveness.write_heartbeat("robinhood", now_ms=int(time.time() * 1000) - 300_000)
api_server._runner_tasks["robinhood"] = SimpleNamespace(done=lambda: False)
assert api_server._runner_liveness_state("robinhood").alive is True
def test_live_status_reflects_active_mandate(tmp_path: Path, monkeypatch) -> None:
client = _client(tmp_path, monkeypatch)
monkeypatch.setattr(
@@ -472,8 +495,9 @@ def test_runner_start_without_a_scheduler_task_still_returns(
asyncio.run(scenario())
def test_drive_runner_awaits_an_async_run_loop_and_stops_the_scheduler() -> None:
def test_drive_runner_awaits_an_async_run_loop_and_stops_the_scheduler(tmp_path, monkeypatch) -> None:
"""An ``async def run_loop`` is awaited, and its scheduler is still stopped."""
_live_env(tmp_path, monkeypatch)
scheduler = _StubScheduler()
class _AsyncRunner:
@@ -508,6 +532,175 @@ def test_drive_runner_awaits_an_async_run_loop_and_stops_the_scheduler() -> None
assert scheduler.stop_calls == 1
def test_stop_waits_for_teardown_and_blocks_a_concurrent_restart(tmp_path, monkeypatch) -> None:
_live_env(tmp_path, monkeypatch)
scheduler = _install_runner(monkeypatch)
async def scenario() -> None:
entered, release = asyncio.Event(), asyncio.Event()
original_stop = scheduler.stop
async def slow_stop() -> None:
entered.set()
await release.wait()
await original_stop()
monkeypatch.setattr(scheduler, "stop", slow_stop)
async with _live_client() as client:
await client.post("/live/runner/start", json={"broker": "robinhood"})
await _await_ticks(scheduler)
stop = asyncio.create_task(client.post("/live/runner/stop", json={"broker": "robinhood"}))
try:
await asyncio.wait_for(entered.wait(), 1)
assert not stop.done(), "stop returned before scheduler teardown"
restart = await client.post("/live/runner/start", json={"broker": "robinhood"})
assert restart.json()["already_running"] is True
finally:
release.set()
await stop
assert scheduler.running is False
assert scheduler.stop_calls == 1
assert not api_server._runner_tasks
asyncio.run(scenario())
def test_concurrent_stops_do_not_cancel_teardown_twice(tmp_path, monkeypatch) -> None:
_live_env(tmp_path, monkeypatch)
scheduler = _install_runner(monkeypatch)
async def scenario() -> None:
entered, release = asyncio.Event(), asyncio.Event()
original_stop = scheduler.stop
async def slow_stop() -> None:
entered.set()
await release.wait()
await original_stop()
monkeypatch.setattr(scheduler, "stop", slow_stop)
async with _live_client() as client:
await client.post("/live/runner/start", json={"broker": "robinhood"})
await _await_ticks(scheduler)
first = asyncio.create_task(client.post("/live/runner/stop", json={"broker": "robinhood"}))
await asyncio.wait_for(entered.wait(), 1)
second = asyncio.create_task(client.post("/live/runner/stop", json={"broker": "robinhood"}))
try:
await asyncio.sleep(0.01)
assert not second.done(), "second stop bypassed unfinished teardown"
finally:
release.set()
await asyncio.gather(first, second)
assert not scheduler.running
assert scheduler.stop_calls == 1
asyncio.run(scenario())
def test_api_shutdown_stops_live_schedulers_even_when_channel_stop_fails(tmp_path, monkeypatch) -> None:
_live_env(tmp_path, monkeypatch)
scheduler = _install_runner(monkeypatch)
async def startup() -> None:
pass
async def broken_channels() -> None:
raise RuntimeError("channel stop failed")
async def stop_research() -> None:
pass
monkeypatch.setattr(api_server, "_run_startup_preflight", startup)
monkeypatch.setattr(api_server, "_stop_channel_runtime", broken_channels)
monkeypatch.setattr(api_server, "_stop_scheduled_research_executor", stop_research)
async def scenario() -> None:
with pytest.raises(RuntimeError, match="channel stop failed"):
async with api_server._lifespan(api_server.app):
async with _live_client() as client:
await client.post("/live/runner/start", json={"broker": "robinhood"})
await _await_ticks(scheduler)
try:
assert not scheduler.running, "live scheduler survived API shutdown"
assert not api_server._runner_tasks
finally:
for task in list(api_server._runner_tasks.values()):
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
asyncio.run(scenario())
def test_stop_during_async_start_still_cleans_up_its_scheduler(tmp_path, monkeypatch) -> None:
_live_env(tmp_path, monkeypatch)
monkeypatch.setattr(api_server, "_active_mandate_state", lambda b: _valid_mandate_state(b))
scheduler = _StubScheduler()
async def scenario() -> None:
started = asyncio.Event()
async def run_loop() -> None:
scheduler.start()
started.set()
await asyncio.Event().wait()
monkeypatch.setattr(api_server, "_runner_factory", lambda broker: SimpleNamespace(
broker=broker, _scheduler=scheduler, run_loop=run_loop,
))
async with _live_client() as client:
await client.post("/live/runner/start", json={"broker": "robinhood"})
await asyncio.wait_for(started.wait(), 1)
try:
response = await client.post("/live/runner/stop", json={"broker": "robinhood"})
assert response.json()["stopped"] is True
assert not scheduler.running
finally:
await scheduler.stop()
asyncio.run(scenario())
def test_api_stop_terminates_the_real_scheduler_task(tmp_path, monkeypatch) -> None:
from src.live.runtime.scheduler import Job, Scheduler
from src.live.runtime import liveness
_live_env(tmp_path, monkeypatch)
monkeypatch.setattr(api_server, "_active_mandate_state", lambda b: _valid_mandate_state(b))
async def scenario() -> None:
fired = asyncio.Event()
ticks = []
async def on_fire(job) -> None:
ticks.append(job.id)
liveness.write_heartbeat("robinhood")
fired.set()
scheduler = Scheduler(on_fire)
scheduler.add_job(Job("lifecycle-check", 0, "interval:10"))
monkeypatch.setattr(api_server, "_runner_factory", lambda broker: SimpleNamespace(
broker=broker, _scheduler=scheduler, run_loop=scheduler.start,
))
async with _live_client() as client:
await client.post("/live/runner/start", json={"broker": "robinhood"})
await asyncio.wait_for(fired.wait(), 1)
scheduler_task = scheduler._task
response = await client.post("/live/runner/stop", json={"broker": "robinhood"})
assert response.json()["stopped"] is True
assert scheduler_task.done()
count = len(ticks)
await asyncio.sleep(0.03)
assert len(ticks) == count
assert not api_server._runner_tasks
state = await client.get("/live/status", params={"broker": "robinhood"})
assert state.json()["brokers"][0]["runner"]["alive"] is False
asyncio.run(scenario())
# --------------------------------------------------------------------------- #
# C1 — propose_mandate_profiles tool_result -> mandate.proposal SSE frame
# --------------------------------------------------------------------------- #
@@ -629,6 +822,83 @@ def test_build_live_runner_wires_a_real_runner(tmp_path, monkeypatch) -> None:
assert runner.runner_id == "robinhood"
@pytest.mark.parametrize("cancel,phase,fails", [
(False, "registry", False), (True, "registry", False),
(True, "agent", False), (False, "agent", True),
])
def test_live_runner_owns_its_actual_background_session_attempt(tmp_path, monkeypatch, cancel, phase, fails) -> None:
from src.session.events import EventBus
from src.session.service import SessionService
from src.session.store import SessionStore
monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path), raising=False)
monkeypatch.setattr(api_server, "_runner_factory", None, raising=False)
monkeypatch.setattr(api_server, "_mandate_account_ref", lambda broker: "acct_test")
monkeypatch.setattr(api_server, "_live_broker_adapter", lambda broker: SimpleNamespace(
call_tool=lambda name, args: {"status": "ok", "data": {}},
))
async def scenario() -> None:
bus = EventBus()
bus.set_loop(asyncio.get_running_loop())
service = SessionService(SessionStore(base_dir=tmp_path / "sessions"), bus,
runs_dir=tmp_path / "runs")
started, release = asyncio.Event(), asyncio.Event()
cancelled = False
def cooperative_cancel() -> None:
nonlocal cancelled
cancelled = True
release.set()
async def controlled_attempt(attempt, **kwargs):
if phase == "agent":
service._active_loops[attempt.session_id] = SimpleNamespace(cancel=cooperative_cancel)
started.set()
try:
await release.wait()
if fails:
return {"status": "failed", "reason": "execution failed"}
return {"status": "cancelled" if cancelled else "success", "content": "completed analysis"}
finally:
service._active_loops.pop(attempt.session_id, None)
monkeypatch.setattr(service, "_run_with_agent", controlled_attempt)
monkeypatch.setattr(api_server, "_get_session_service", lambda: service)
runner = api_server._build_live_runner("robinhood")
driver = asyncio.create_task(runner._agent_caller(runner._session_id, "test tick"))
try:
await asyncio.wait_for(started.wait(), 1)
assert not driver.done(), "queued attempt was mistaken for a completed tick"
if cancel:
driver.cancel()
with pytest.raises(asyncio.CancelledError):
await driver
session = service.get_session(runner._session_id)
attempt = service.store.get_attempt(session.session_id, session.last_attempt_id)
assert attempt.status.value == "cancelled"
assert not service._active_tasks
assert not service._inflight
assert not service._active_loops
else:
release.set()
if fails:
with pytest.raises(RuntimeError, match="execution failed"):
await asyncio.wait_for(driver, 1)
else:
result = await asyncio.wait_for(driver, 1)
assert result["status"] == "success"
assert result["content"] == "completed analysis"
assert not bus._listeners
finally:
release.set()
pending = list(service._active_tasks.values())
if pending:
await asyncio.gather(*pending, return_exceptions=True)
asyncio.run(scenario())
def test_runner_start_returns_503_when_broker_unavailable(tmp_path, monkeypatch) -> None:
client = _client(tmp_path, monkeypatch)
monkeypatch.setattr(
+16 -1
View File
@@ -2394,7 +2394,7 @@ def test_the_correction_prompt_names_the_figures_to_keep(tmp_path: Path) -> None
assert "1.171, 1.137, 1.150, 1.090, 1.136" in keep
for rejected in ("2.50", "9.99", "0.97"):
assert rejected not in keep
assert "only the figures listed above need work" in prompt
assert "repair the rejected claims while preserving the clean figures" in prompt
def test_the_keep_list_is_absent_when_nothing_passed(tmp_path: Path) -> None:
@@ -2422,3 +2422,18 @@ def test_the_keep_list_caps_and_counts_the_rest(tmp_path: Path) -> None:
assert "and 3 more" in keep
assert "11.23" in keep
assert "11.24" not in keep
def test_keep_list_does_not_endorse_rejections_hidden_by_the_feedback_cap(tmp_path: Path) -> None:
ledger = _ledger(tmp_path)
bad_values = [f"{90 + i / 100:.2f}" for i in range(30)]
draft = HDR + " Unverified metrics: " + ", ".join(bad_values) + "." + _block(HDR_ROW)
result = ledger.validate_final_answer(draft)
assert len(result.issues) > 24
prompt = ledger.correction_prompt(result)
keep = next(line for line in prompt.splitlines() if "checked clean" in line)
assert "1.171" in keep
assert all(value not in keep for value in bad_values)
assert "Every other measured figure" not in prompt
assert "only the figures listed above need work" not in prompt
assert f"{len(result.issues) - 24} additional" in prompt
@@ -312,3 +312,48 @@ def test_agent_loop_sends_the_hint_and_releases_the_corrected_answer(tmp_path: P
assert rejected[0]["issues"][0]["reason"] == "unknown_call_id"
assert rejected[0]["issues"][0]["field_ref_candidates"] == [exact_ref]
assert any(event == "grounding_status" and data["stage"] == "revising" for event, data in events)
def test_agent_loop_keeps_clean_figures_and_revalidates_the_repair(tmp_path: Path) -> None:
"""Exercise keep feedback through the actual AgentLoop without a paid model."""
class RepairingLLM:
calls = 0
def stream_chat(self, messages, on_text_chunk=None, **_kwargs):
self.calls += 1
if self.calls == 1:
return LLMResponse(tool_calls=[ToolCallRequest("call_report", "report_tool", {})])
if self.calls == 2:
drawdown = "99.9"
else:
assert self.calls == 3
keep = next(line for line in messages[-1]["content"].splitlines() if "checked clean" in line)
assert "12.5" in keep and "99.9" not in keep
drawdown = "23.75"
content = (
f"Total return 12.5. Maximum drawdown {drawdown}.\n"
"```figures\n12.5 | observed | total return | call_report::data.summary.total_return\n"
f"{drawdown} | observed | maximum drawdown | call_report::data.summary.max_drawdown\n```"
)
if on_text_chunk:
on_text_chunk(content)
return LLMResponse(content=content)
def chat(self, _messages, **_kwargs):
return LLMResponse(content="")
registry = ToolRegistry()
registry.register(_ReportTool(json.dumps({"data": {"summary": {"total_return": 12.5, "max_drawdown": 23.75}}})))
llm = RepairingLLM()
agent = AgentLoop(registry=registry, llm=llm, max_iterations=5)
run_dir = tmp_path / "run"
run_dir.mkdir()
agent.memory.run_dir = str(run_dir)
result = agent.run("Report the total return and maximum drawdown")
assert result["status"] == "success"
assert result["content"] == "Total return 12.5. Maximum drawdown 23.75."
assert not result.get("degraded")
assert llm.calls == 3
rejected = [event for event in TraceWriter.read(run_dir) if event.get("type") == "answer_rejected"]
assert len(rejected) == 1
assert rejected[0]["issues"][0]["value"] == "99.9"
+14 -2
View File
@@ -556,8 +556,8 @@ def test_missing_or_blank_order_id_is_not_submitted(
@pytest.mark.parametrize(
"dirty_qty",
["n/a", "", None, float("nan"), float("inf"), {"a": 1}, True],
ids=["str", "blank", "none", "nan", "inf", "dict", "bool"],
["n/a", "", None, float("nan"), float("inf"), {"a": 1}, True, 10**400],
ids=["str", "blank", "none", "nan", "inf", "dict", "bool", "overflow"],
)
def test_dirty_position_qty_is_recorded_and_sweep_continues(
live_runtime: Path, dirty_qty: Any
@@ -625,3 +625,15 @@ def test_zero_qty_still_skipped_without_error(live_runtime: Path) -> None:
)
assert report["flatten_orders_submitted"] == []
assert report["errors"] == []
@pytest.mark.parametrize("symbol", [None, "", " ", True, {"symbol": "AAPL"}])
def test_invalid_symbol_never_reaches_a_close_submission(live_runtime, symbol) -> None:
broker = _Broker([], [{"symbol": symbol, "qty": 1}, {"symbol": "AAPL", "qty": -2}])
report = flatten.flatten_and_cancel(
"robinhood", broker.submit, broker.read_positions, broker.read_open_orders,
allow_flatten=True,
)
assert [request["symbol"] for _, request in broker.calls] == ["AAPL"]
assert report["flatten_orders_submitted"][0]["side"] == "buy"
assert any(error["phase"] == "flatten" and "symbol" in error["error"] for error in report["errors"])
+28
View File
@@ -12,7 +12,9 @@ from __future__ import annotations
import json
import os
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from threading import Barrier
import pytest
@@ -70,6 +72,32 @@ def test_save_is_atomic_no_temp_left_behind(live_runtime: Path) -> None:
assert leftovers == [], f"temp files leaked: {leftovers}"
def test_save_completes_short_writes(live_runtime, monkeypatch) -> None:
store = JobStore()
real_write = os.write
monkeypatch.setattr("src.live.runtime.jobstore.os.write", lambda fd, data: real_write(fd, data[:7]))
store.save([_job("full-payload")])
assert [job.id for job in store.load()] == ["full-payload"]
def test_concurrent_saves_use_distinct_temporary_files(live_runtime, monkeypatch) -> None:
store = JobStore()
barrier = Barrier(2)
real_replace = os.replace
def replace(src, dst):
barrier.wait(timeout=2)
real_replace(src, dst)
monkeypatch.setattr("src.live.runtime.jobstore.os.replace", replace)
with ThreadPoolExecutor(max_workers=2) as pool:
results = [pool.submit(store.save, [_job(name)]) for name in ("a", "b")]
for result in results:
result.result(timeout=3)
assert store.load()[0].id in {"a", "b"}
assert list(runtime_dir().glob(".*tmp")) == []
def test_save_overwrites_in_place(live_runtime: Path) -> None:
store = JobStore()
store.save([_job("a", 1000)])
+64 -4
View File
@@ -158,8 +158,9 @@ def _build_runner(tracker: _OrderTracker, **overrides) -> LiveRunner:
trip_halt_fn=tracker.trip_halt,
session_id="live-test",
)
broker = overrides.pop("broker", BROKER)
kwargs.update(overrides)
return LiveRunner(BROKER, **kwargs)
return LiveRunner(broker, **kwargs)
# --------------------------------------------------------------------------- #
@@ -529,10 +530,69 @@ def test_stop_loop_idempotent() -> None:
tracker = _OrderTracker()
sched = _FakeScheduler()
runner = _build_runner(tracker, scheduler=sched)
runner.stop_loop()
asyncio.run(runner.stop_loop())
assert sched.stopped is True
# No scheduler => no-op, no raise.
_build_runner(tracker).stop_loop()
asyncio.run(_build_runner(tracker).stop_loop())
def test_stop_loop_awaits_the_real_scheduler() -> None:
from src.live.runtime.scheduler import Scheduler
async def scenario() -> None:
async def fire(job) -> None:
pass
scheduler = Scheduler(fire)
runner = _build_runner(_OrderTracker(), scheduler=scheduler)
scheduler.start()
task = scheduler._task
await runner.stop_loop()
assert task.done()
assert scheduler._task is None
await runner.stop_loop()
asyncio.run(scenario())
def test_default_job_stores_keep_each_brokers_cadence_separate(tmp_path, monkeypatch) -> None:
from src.live import paths
from src.live.runtime.triggers import Trigger
monkeypatch.setattr(paths, "get_runtime_root", lambda: tmp_path)
first, second = _FakeScheduler(), _FakeScheduler()
_build_runner(_OrderTracker(), broker="robinhood", scheduler=first,
triggers=[Trigger.interval(30_000)]).run_loop()
_build_runner(_OrderTracker(), broker="alpaca", scheduler=second,
triggers=[Trigger.interval(90_000)]).run_loop()
assert [job.schedule for job in first.jobs] == ["interval:30000"]
assert [job.schedule for job in second.jobs] == ["interval:90000"]
assert all(job.payload["broker"] == "alpaca" for job in second.jobs)
restarted = _FakeScheduler()
_build_runner(_OrderTracker(), broker="robinhood", scheduler=restarted,
triggers=[Trigger.interval(120_000)]).run_loop()
assert [job.schedule for job in restarted.jobs] == ["interval:30000"]
def test_default_job_store_migrates_only_its_own_legacy_jobs(tmp_path, monkeypatch) -> None:
from src.live import paths
from src.live.runtime.jobstore import JobStore
from src.live.runtime.scheduler import Job
from src.live.runtime.triggers import Trigger
monkeypatch.setattr(paths, "get_runtime_root", lambda: tmp_path)
legacy = JobStore()
legacy.save([
Job("robinhood-watch", 1, "interval:5000", {"broker": "robinhood"}),
Job("alpaca-watch", 2, "interval:6000", {"broker": "alpaca"}),
Job("unbound", 3, "interval:7000"),
])
for broker, expected in [("robinhood", "robinhood-watch"), ("alpaca", "alpaca-watch")]:
scheduler = _FakeScheduler()
_build_runner(_OrderTracker(), broker=broker, scheduler=scheduler,
triggers=[Trigger.interval(30_000)]).run_loop()
assert [job.id for job in scheduler.jobs] == [expected]
assert len(legacy.load()) == 3
# --------------------------------------------------------------------------- #
@@ -544,7 +604,7 @@ def test_real_audit_write_on_halt(monkeypatch, tmp_path) -> None:
# Isolate the runtime root so the real ledger lands under tmp.
monkeypatch.setattr("src.config.paths.Path.home", lambda: tmp_path)
from src.live.audit import audit_ledger_path, write_live_action
from src.live.halt import halt_flag_set, trip_halt
from src.live.halt import trip_halt
tracker = _OrderTracker()
runner = LiveRunner(