Finish thirteen-PR integration and repair uncovered boundary cases
Signed-off-by: Haozhe Wu <haozhe_wu@connect.hku.hk>
This commit is contained in:
1 parent
c0a3ad2cc7
commit
ce73fff5a4
35 files changed
+460
-66
No files matched your search
@@ -5,6 +5,41 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Maintenance — 2026-10-09
|
||||
|
||||
- Channel edits to live-read settings apply without reconnecting, including a
|
||||
full Web form that echoes unchanged connection values (#1737; #1734/#1736).
|
||||
Web results distinguish refresh, reconnect, deferred application and degraded
|
||||
reset in all nine languages. Reset reasons redact URL userinfo and old/new
|
||||
secret values; a best-effort notice goes to the changed channel's last chat.
|
||||
Signal refreshes its writable nested policies, not its computed allowlist.
|
||||
- Strip URL credentials even with malformed ports or multiple `@` characters
|
||||
(#1742; #1741). Saving or testing an echoed configuration preserves the stored
|
||||
URL credentials; explicit edits and clear flags remain effective (#1744; #1743).
|
||||
Enable and connection-test failures also redact credentials, including raised
|
||||
SDK exceptions, and leave saved configuration unchanged.
|
||||
- Recognize known slash-USD crypto pairs before forex (#1748), normalize
|
||||
suffixed Hong Kong aliases for duplicate detection (#1746; #1745), and treat
|
||||
unusable persisted cache metadata as a recoverable cache miss (#1747).
|
||||
Five-digit display aliases share an identity with their four-digit spelling;
|
||||
distinct currency counters stay separate. Missing cache index metadata is a
|
||||
miss too, with valid entries still reused after repair.
|
||||
- Options drawdown includes the initial-cash high-water mark (#1750; #1749).
|
||||
Pricing volatility receives the run's annualization factor, including the
|
||||
cross-market convention (#1753). Bootstrap and walk-forward validation use
|
||||
only observed adjacent equity returns across missing data (#1754).
|
||||
A walk-forward window without observed endpoints or any adjacent returns
|
||||
reports an error instead of a fabricated zero score or an incomplete aggregate.
|
||||
- Cash-flow ingestion refuses ambiguous single-dot amounts unless the decimal
|
||||
separator is supplied (#1739). American waterfalls reject a bare `as_of`
|
||||
date rather than silently ignoring it; per-deal dates remain supported (#1732).
|
||||
- Retire mootdx from the default A-share chain and public canary (#1730; #1729),
|
||||
with explicit-source access retained. Old source-order overrides containing
|
||||
the retired slot fall back to the current default with a warning.
|
||||
- Restore main CI through the configured environment accessor and corrected
|
||||
correlation routing (#1740; #1751). Repair the outdated Alpha Zoo test double
|
||||
while retaining production factor-metadata validation.
|
||||
|
||||
### Maintenance — 2026-10-08
|
||||
|
||||
- Optional AES-256 protection for Email PDF reports (#1709): Web, CLI
|
||||
|
||||
+13
-5
@@ -195,6 +195,13 @@ configuration with no per-channel frontend code.
|
||||
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()`.
|
||||
Declare `hot_reload_noop_keys` only for writable fields consumed live from
|
||||
`self.config`. Such edits are revalidated and applied without reconnecting;
|
||||
connection settings and enable transitions still reload the adapter. A
|
||||
computed field is not writable (Signal's policies live under `dm`/`group`).
|
||||
The Web UI reports the apply outcome. A failed hot swap that requires a full
|
||||
reset returns a bounded, sanitized reason and sends a best-effort notice to
|
||||
the changed channel's last outbound chat.
|
||||
3. Run the authoring contract locally:
|
||||
```bash
|
||||
pytest agent/tests/test_channel_authoring_contract.py -q
|
||||
@@ -275,12 +282,13 @@ cd agent
|
||||
/tmp/vibe-loader-health-venv/bin/python -m backtest.loader_health --output /tmp/loader-health.json
|
||||
```
|
||||
|
||||
Use the isolated environment: mootdx's HTTP client requirement conflicts with
|
||||
the application's dependency range. The canary imports the checkout's loader
|
||||
code directly, without installing the full application dependency set.
|
||||
Use the isolated environment to keep optional SDK pins separate from the
|
||||
application's dependency range. The canary imports the checkout's loader code
|
||||
directly, without installing the full application dependency set.
|
||||
|
||||
Each unauthenticated network loader has a liquid canary symbol; the local-file
|
||||
loader is explicitly excluded. Adding a public loader requires updating the
|
||||
Each active unauthenticated network loader has a liquid canary symbol. The
|
||||
local-file loader and retired mootdx source are explicitly excluded with a
|
||||
reason; mootdx remains available through explicit source selection. Adding a public loader requires updating the
|
||||
canary catalog, enforced by the offline test suite. Probes bypass the loader
|
||||
cache and run with a temporary home and no inherited credentials. Each source
|
||||
has a 120-second total subprocess deadline, including two attempts, and the
|
||||
|
||||
@@ -52,15 +52,17 @@
|
||||
|
||||
> ⚠️ **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-09** 🛠️ **Channel updates and backtest correctness**: Channel settings preserve embedded credentials and apply live-read edits without reconnecting; Web results explain resets in all nine languages ([#1737](https://github.com/HKUDS/Vibe-Trading/pull/1737), [#1742](https://github.com/HKUDS/Vibe-Trading/pull/1742), [#1744](https://github.com/HKUDS/Vibe-Trading/pull/1744)). Options pricing and drawdown use consistent annualization and starting capital; validation excludes missing equity returns. Crypto/HK identity and cache recovery are corrected, and mootdx leaves the default A-share chain. See [CHANGELOG](CHANGELOG.md) for all thirteen PRs.
|
||||
|
||||
- **2026-10-08** 🔐 **Protected PDF reports and clearer broker reads**: Scheduled Email PDFs support optional AES-256 password protection across Web, CLI playbooks and agent confirmations; the password stays in private channel configuration ([#1709](https://github.com/HKUDS/Vibe-Trading/pull/1709)). KIS queries all three venue buckets, refreshes an explicitly expired token once, and reports incomplete reads as errors ([#1726](https://github.com/HKUDS/Vibe-Trading/pull/1726)). Derived formulas keep their arithmetic after descriptive labels while evidence checks remain enforced ([#1728](https://github.com/HKUDS/Vibe-Trading/pull/1728)).
|
||||
|
||||
- **2026-10-07** 🛠️ **Research continuity and reliable risk metrics**: Volatile workspace state now travels outside the system prompt, preserving its stable prefix ([#1708](https://github.com/HKUDS/Vibe-Trading/pull/1708)). Cross-market dates align correctly, missing prices no longer bridge return observations, and options Sortino uses full-sample downside deviation ([#1710](https://github.com/HKUDS/Vibe-Trading/pull/1710), [#1717](https://github.com/HKUDS/Vibe-Trading/pull/1717), [#1727](https://github.com/HKUDS/Vibe-Trading/pull/1727)). Loader fixtures share one OHLCV contract; credit valuation, style exposures, walk-forward folds and audit diagnostics are corrected.
|
||||
|
||||
- **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)).
|
||||
|
||||
<details>
|
||||
<summary>Earlier news</summary>
|
||||
|
||||
- **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)).
|
||||
|
||||
+5
-3
@@ -52,15 +52,17 @@
|
||||
|
||||
> ⚠️ **تحذير أمني:** حساب X باسم `VibeTrading_HKU`، ومشروع Virtuals رقم `101845`، وعقد التوكن `0x640BDBF77b6447E8b7DB7894cED84BD1c40571f4` كلّها غير رسمية ولا تتبع Vibe-Trading. لم نُطلق أو نؤيد مطلقًا أي توكن أو عملة ميم. لا تشترِ هذا التوكن، ولا تربط محفظتك، ولا توقّع أي شيء. [التفاصيل](SECURITY.md#official-channels--impersonation).
|
||||
|
||||
- **2026-10-09** 🛠️ **إعدادات القنوات ودقة الاختبارات التاريخية**: تُحفظ بيانات الاعتماد المضمّنة في الروابط وتُطبّق الإعدادات المقروءة مباشرة دون إعادة الاتصال. تعرض الواجهة نتائج الحفظ وإعادة التشغيل بتسع لغات ([#1737](https://github.com/HKUDS/Vibe-Trading/pull/1737)، [#1742](https://github.com/HKUDS/Vibe-Trading/pull/1742)، [#1744](https://github.com/HKUDS/Vibe-Trading/pull/1744)). صُححت سنوية تقلب الخيارات والتراجع من رأس المال الأولي، ولا تُنشأ عوائد وهمية من قيم مفقودة. تحسّن التعرف على العملات الرقمية وأسهم هونغ كونغ واستعادة الذاكرة المؤقتة، وأُزيل mootdx من سلسلة أسهم A الافتراضية. تفاصيل طلبات الدمج الثلاثة عشر في [CHANGELOG](CHANGELOG.md).
|
||||
|
||||
- **2026-10-08** 🔐 **حماية تقارير PDF وتصحيح قراءات الوسيط**: تدعم تقارير البريد المجدولة حماية اختيارية بكلمة مرور عبر AES-256 في الويب وCLI وتأكيدات الوكيل؛ تُحفظ كلمة المرور في إعدادات القناة الخاصة فقط ([#1709](https://github.com/HKUDS/Vibe-Trading/pull/1709)). يستعلم KIS عن مسارات التداول الثلاثة ويجدد الرمز المنتهي صراحةً مرة واحدة ويرفض القراءة غير المكتملة ([#1726](https://github.com/HKUDS/Vibe-Trading/pull/1726)). تُحفظ الصيغة الحسابية بعد الوصف مع استمرار التحقق من الأدلة ([#1728](https://github.com/HKUDS/Vibe-Trading/pull/1728)).
|
||||
|
||||
- **2026-10-07** 🛠️ **استمرارية البحث وتصحيح مقاييس المخاطر**: نُقلت حالة التشغيل خارج تعليمات النظام للحفاظ على بادئة ثابتة ([#1708](https://github.com/HKUDS/Vibe-Trading/pull/1708)). صُححت محاذاة التواريخ بين الأسواق وعوائد الأسعار المفقودة وحساب Sortino للخيارات باستخدام العينة كاملةً ([#1710](https://github.com/HKUDS/Vibe-Trading/pull/1710)، [#1717](https://github.com/HKUDS/Vibe-Trading/pull/1717)، [#1727](https://github.com/HKUDS/Vibe-Trading/pull/1727)). تشترك اختبارات مصادر البيانات في عقد OHLCV واحد، مع إصلاحات لتقييم الائتمان والتعرضات والتحقق الزمني وتشخيص التدقيق.
|
||||
|
||||
- **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)).
|
||||
|
||||
<details>
|
||||
<summary>أخبار سابقة</summary>
|
||||
|
||||
- **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)).
|
||||
@@ -461,7 +463,7 @@ vibe-trading connector install /tmp/my-broker
|
||||
| Source | Markets | Auth | Role |
|
||||
|--------|---------|------|------|
|
||||
| `tencent` | A-share + HK | none | never IP-banned |
|
||||
| `mootdx` | A-share (explicit-only) | none | retired from the default chain 2026-10: TDX servers stopped answering the client protocol it speaks (#1729) |
|
||||
| `mootdx` | أسهم A (اختيار صريح فقط) | none | أُزيل من السلسلة الافتراضية في أكتوبر 2026 بعد توقف خوادم TDX عن الاستجابة لبروتوكول العميل (#1729) |
|
||||
| `eastmoney` | A / US / HK | none | OHLCV + deep fundamentals & flow tools (throttled) |
|
||||
| `baostock` · `akshare` | A (+ US/HK/futures/macro/fx) | none | free fallbacks |
|
||||
| `tushare` | A / HK / futures / fund / macro | token | richest A-share |
|
||||
|
||||
+5
-3
@@ -52,15 +52,17 @@
|
||||
|
||||
> ⚠️ **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-09** 🛠️ **Configuración de canales y correcciones de backtests**: Se conservan las credenciales de las URL y los ajustes consultados en cada mensaje se aplican sin reconectar. La web explica guardados y reinicios en nueve idiomas ([#1737](https://github.com/HKUDS/Vibe-Trading/pull/1737), [#1742](https://github.com/HKUDS/Vibe-Trading/pull/1742), [#1744](https://github.com/HKUDS/Vibe-Trading/pull/1744)). Corregimos la anualización de opciones y la caída desde el capital inicial; la validación no inventa rendimientos para valores ausentes. Mejoran la identificación de criptoactivos/acciones de Hong Kong y la recuperación de caché; mootdx sale de la cadena predeterminada de acciones A. Los trece PR están en [CHANGELOG](CHANGELOG.md).
|
||||
|
||||
- **2026-10-08** 🔐 **Informes PDF protegidos y lecturas del bróker corregidas**: Los PDF de correo programado admiten protección AES-256 opcional en Web, CLI y confirmaciones del agente; la contraseña permanece en la configuración privada del canal ([#1709](https://github.com/HKUDS/Vibe-Trading/pull/1709)). KIS consulta las tres rutas, renueva una sola vez un token explícitamente caducado y rechaza lecturas incompletas ([#1726](https://github.com/HKUDS/Vibe-Trading/pull/1726)). Las fórmulas conservan la expresión tras las etiquetas descriptivas y mantienen la verificación de evidencias ([#1728](https://github.com/HKUDS/Vibe-Trading/pull/1728)).
|
||||
|
||||
- **2026-10-07** 🛠️ **Continuidad de investigación y métricas de riesgo corregidas**: El estado de ejecución se separa del mensaje del sistema para preservar su prefijo estable ([#1708](https://github.com/HKUDS/Vibe-Trading/pull/1708)). Se corrigen las fechas entre mercados, los retornos alrededor de precios ausentes y el Sortino de opciones con toda la muestra ([#1710](https://github.com/HKUDS/Vibe-Trading/pull/1710), [#1717](https://github.com/HKUDS/Vibe-Trading/pull/1717), [#1727](https://github.com/HKUDS/Vibe-Trading/pull/1727)). Los cargadores comparten un contrato OHLCV; también se corrigen valoración crediticia, exposiciones, validación temporal y diagnóstico de auditoría.
|
||||
|
||||
- **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)).
|
||||
|
||||
<details>
|
||||
<summary>Noticias anteriores</summary>
|
||||
|
||||
- **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)).
|
||||
@@ -460,7 +462,7 @@ Una sola llamada `get_market_data`, **28 fuentes de datos de mercado**, una de e
|
||||
| Fuente | Mercados | Autenticación | Rol |
|
||||
|--------|---------|------|------|
|
||||
| `tencent` | A-share + HK | ninguna | nunca bloqueada por IP |
|
||||
| `mootdx` | A-share (explicit-only) | ninguna | retired from the default chain 2026-10: TDX servers stopped answering the client protocol it speaks (#1729) |
|
||||
| `mootdx` | Acciones A (solo selección explícita) | ninguna | retirado de la cadena predeterminada en octubre de 2026: los servidores TDX dejaron de responder al protocolo del cliente (#1729) |
|
||||
| `eastmoney` | A / EE. UU. / HK | ninguna | OHLCV + herramientas de fundamentales y flujo profundas (limitada) |
|
||||
| `baostock` · `akshare` | A (+ EE. UU./HK/futuros/macro/fx) | ninguna | fallbacks gratuitos |
|
||||
| `tushare` | A / HK / futuros / fondos / macro | token | la más completa para A-share |
|
||||
|
||||
+5
-3
@@ -53,15 +53,17 @@
|
||||
|
||||
> ⚠️ **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-09** 🛠️ **Pengaturan kanal dan perbaikan backtest**: Kredensial dalam URL tetap tersimpan, dan pengaturan yang dibaca setiap pesan diterapkan tanpa menyambungkan ulang. Hasil penyimpanan dan mulai ulang dijelaskan dalam sembilan bahasa ([#1737](https://github.com/HKUDS/Vibe-Trading/pull/1737), [#1742](https://github.com/HKUDS/Vibe-Trading/pull/1742), [#1744](https://github.com/HKUDS/Vibe-Trading/pull/1744)). Annualisasi opsi dan penurunan dari modal awal diperbaiki; validasi tidak menciptakan imbal hasil dari nilai yang hilang. Identifikasi kripto/saham Hong Kong dan pemulihan cache diperbaiki, sementara mootdx dikeluarkan dari rantai A-share bawaan. Lihat ketiga belas PR di [CHANGELOG](CHANGELOG.md).
|
||||
|
||||
- **2026-10-08** 🔐 **Laporan PDF terlindungi dan pembacaan broker diperbaiki**: PDF Email terjadwal mendukung perlindungan AES-256 opsional melalui Web, CLI, dan konfirmasi agen; kata sandi hanya disimpan dalam konfigurasi privat kanal ([#1709](https://github.com/HKUDS/Vibe-Trading/pull/1709)). KIS menanyakan ketiga jalur perdagangan, memperbarui token yang secara eksplisit kedaluwarsa satu kali, dan menolak pembacaan yang tidak lengkap ([#1726](https://github.com/HKUDS/Vibe-Trading/pull/1726)). Rumus turunan tetap utuh setelah label deskriptif dengan pemeriksaan bukti tetap berlaku ([#1728](https://github.com/HKUDS/Vibe-Trading/pull/1728)).
|
||||
|
||||
- **2026-10-07** 🛠️ **Kesinambungan riset dan koreksi metrik risiko**: Status eksekusi dipindahkan dari prompt sistem agar prefiksnya tetap stabil ([#1708](https://github.com/HKUDS/Vibe-Trading/pull/1708)). Tanggal lintas pasar, imbal hasil di sekitar harga yang hilang, dan Sortino opsi berdasarkan seluruh sampel diperbaiki ([#1710](https://github.com/HKUDS/Vibe-Trading/pull/1710), [#1717](https://github.com/HKUDS/Vibe-Trading/pull/1717), [#1727](https://github.com/HKUDS/Vibe-Trading/pull/1727)). Pengujian loader memakai satu kontrak OHLCV, disertai koreksi valuasi kredit, eksposur, validasi berjalan, dan diagnosis audit.
|
||||
|
||||
- **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)).
|
||||
|
||||
<details>
|
||||
<summary>Berita sebelumnya</summary>
|
||||
|
||||
- **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)).
|
||||
@@ -622,7 +624,7 @@ Satu call `get_market_data`, **28 sumber data market**, salah satunya marketplac
|
||||
| Sumber | Market | Auth | Peran |
|
||||
|--------|---------|------|------|
|
||||
| `tencent` | A-share + HK | tidak ada | tidak terkena IP-ban |
|
||||
| `mootdx` | A-share (explicit-only) | tidak ada | retired from the default chain 2026-10: TDX servers stopped answering the client protocol it speaks (#1729) |
|
||||
| `mootdx` | A-share (hanya pemilihan eksplisit) | tidak ada | dihapus dari rantai bawaan pada Oktober 2026: server TDX berhenti merespons protokol klien (#1729) |
|
||||
| `eastmoney` | A / AS / HK | tidak ada | OHLCV + fundamental mendalam & tool flow (throttled) |
|
||||
| `baostock` · `akshare` | A (+ AS/HK/futures/makro/fx) | tidak ada | fallback gratis |
|
||||
| `tushare` | A / HK / futures / fund / makro | token | A-share paling kaya |
|
||||
|
||||
+5
-3
@@ -52,15 +52,17 @@
|
||||
|
||||
> ⚠️ **セキュリティ警告:** Xアカウント `VibeTrading_HKU`、Virtualsプロジェクト `101845`、およびトークンコントラクト `0x640BDBF77b6447E8b7DB7894cED84BD1c40571f4` は、いずれもVibe-Trading公式のものではありません。Vibe-Tradingはこれまで、いかなるトークンやミームコインも発行・公認していません。購入、ウォレットの接続、署名は行わないでください。[詳細](SECURITY.md#official-channels--impersonation)
|
||||
|
||||
- **2026-10-09** 🛠️ **チャンネル設定とバックテストの修正**:URL 内の認証情報を保持し、接続に影響しない設定は再接続せず反映。保存・再起動の結果を Web で9言語表示します([#1737](https://github.com/HKUDS/Vibe-Trading/pull/1737)、[#1742](https://github.com/HKUDS/Vibe-Trading/pull/1742)、[#1744](https://github.com/HKUDS/Vibe-Trading/pull/1744))。オプション価格の年率換算と初期資金からのドローダウンを修正し、欠損資産額から架空のリターンを作りません。暗号資産・香港株の識別とキャッシュ復旧を改善し、mootdx を標準の A 株取得経路から外しました。全13件は [CHANGELOG](CHANGELOG.md)。
|
||||
|
||||
- **2026-10-08** 🔐 **PDF レポートのパスワード保護とブローカー読み取り修正**:定期メール PDF に AES-256 保護を選択でき、Web、CLI、エージェント確認画面に反映されます。パスワードは非公開のチャネル設定だけに保存します([#1709](https://github.com/HKUDS/Vibe-Trading/pull/1709))。KIS は三つの取引経路を照会し、明示的なトークン失効を一度だけ再取得、不完全な読み取りはエラーにします([#1726](https://github.com/HKUDS/Vibe-Trading/pull/1726))。説明ラベルの後の導出式を保持し、証拠検証を維持します([#1728](https://github.com/HKUDS/Vibe-Trading/pull/1728))。
|
||||
|
||||
- **2026-10-07** 🛠️ **リサーチの継続性とリスク指標の修正**:実行状態をシステムプロンプトから分離し、安定したプレフィックスを保ちます([#1708](https://github.com/HKUDS/Vibe-Trading/pull/1708))。市場間の日付整列、欠損価格前後のリターン、全標本によるオプションのソルティノ比率を修正しました([#1710](https://github.com/HKUDS/Vibe-Trading/pull/1710)、[#1717](https://github.com/HKUDS/Vibe-Trading/pull/1717)、[#1727](https://github.com/HKUDS/Vibe-Trading/pull/1727))。データ取得テストは共通の OHLCV 契約を使い、信用評価、スタイル・エクスポージャー、時系列検証、監査診断も改善しました。
|
||||
|
||||
- **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))。
|
||||
|
||||
<details>
|
||||
<summary>過去のニュース</summary>
|
||||
|
||||
- **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))。
|
||||
@@ -461,7 +463,7 @@ vibe-trading connector install /tmp/my-broker
|
||||
| Source | Markets | Auth | Role |
|
||||
|--------|---------|------|------|
|
||||
| `tencent` | A-share + HK | none | never IP-banned |
|
||||
| `mootdx` | A-share (explicit-only) | none | retired from the default chain 2026-10: TDX servers stopped answering the client protocol it speaks (#1729) |
|
||||
| `mootdx` | A株(明示指定のみ) | none | 2026年10月に標準経路から除外:TDX サーバーがクライアントのプロトコルに応答しなくなったため(#1729) |
|
||||
| `eastmoney` | A / US / HK | none | OHLCV + deep fundamentals & flow tools (throttled) |
|
||||
| `baostock` · `akshare` | A (+ US/HK/futures/macro/fx) | none | free fallbacks |
|
||||
| `tushare` | A / HK / futures / fund / macro | token | richest A-share |
|
||||
|
||||
+5
-3
@@ -52,15 +52,17 @@
|
||||
|
||||
> ⚠️ **보안 경고:** X 계정 `VibeTrading_HKU`, Virtuals 프로젝트 `101845`, 토큰 컨트랙트 `0x640BDBF77b6447E8b7DB7894cED84BD1c40571f4`는 모두 Vibe-Trading 공식과 무관합니다. Vibe-Trading은 어떠한 토큰이나 밈코인도 발행하거나 공식적으로 지지한 적이 없습니다. 해당 토큰을 구매하거나 지갑을 연결하거나 어떠한 서명도 하지 마세요. [자세히 보기](SECURITY.md#official-channels--impersonation).
|
||||
|
||||
- **2026-10-09** 🛠️ **채널 설정 및 백테스트 수정**: URL에 포함된 인증 정보를 보존하고 연결에 영향을 주지 않는 설정은 재연결 없이 적용합니다. 저장·재시작 결과를 웹에서 9개 언어로 표시합니다 ([#1737](https://github.com/HKUDS/Vibe-Trading/pull/1737), [#1742](https://github.com/HKUDS/Vibe-Trading/pull/1742), [#1744](https://github.com/HKUDS/Vibe-Trading/pull/1744)). 옵션 가격의 연율화와 초기 자금을 반영한 낙폭을 수정하고, 누락된 자산 값에서 수익률을 만들어 내지 않습니다. 암호화폐·홍콩 주식 식별과 캐시 복구를 개선하고 기본 A주 경로에서 mootdx를 제외했습니다. 13개 PR은 [CHANGELOG](CHANGELOG.md)를 참조하세요.
|
||||
|
||||
- **2026-10-08** 🔐 **PDF 보고서 비밀번호 보호와 증권사 조회 수정**: 예약 이메일 PDF에 AES-256 보호를 선택할 수 있으며 Web, CLI 및 에이전트 확인에 반영됩니다. 비밀번호는 비공개 채널 설정에만 저장합니다([#1709](https://github.com/HKUDS/Vibe-Trading/pull/1709)). KIS는 세 거래 경로를 조회하고 명시적으로 만료된 토큰을 한 번만 갱신하며 불완전한 조회는 오류로 처리합니다([#1726](https://github.com/HKUDS/Vibe-Trading/pull/1726)). 설명 뒤의 전체 파생 수식을 보존하고 증거 검증을 유지합니다([#1728](https://github.com/HKUDS/Vibe-Trading/pull/1728)).
|
||||
|
||||
- **2026-10-07** 🛠️ **리서치 연속성과 위험 지표 수정**: 실행 상태를 시스템 프롬프트 밖으로 옮겨 안정적인 접두부를 유지합니다([#1708](https://github.com/HKUDS/Vibe-Trading/pull/1708)). 시장 간 날짜 정렬, 누락 가격 주변의 수익률, 전체 표본을 사용하는 옵션 Sortino 계산을 수정했습니다([#1710](https://github.com/HKUDS/Vibe-Trading/pull/1710), [#1717](https://github.com/HKUDS/Vibe-Trading/pull/1717), [#1727](https://github.com/HKUDS/Vibe-Trading/pull/1727)). 데이터 로더 테스트는 공통 OHLCV 계약을 사용하며 신용 평가, 스타일 노출, 순차 검증 및 감사 진단도 개선했습니다.
|
||||
|
||||
- **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)).
|
||||
|
||||
<details>
|
||||
<summary>이전 뉴스</summary>
|
||||
|
||||
- **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)).
|
||||
@@ -461,7 +463,7 @@ vibe-trading connector install /tmp/my-broker
|
||||
| Source | Markets | Auth | Role |
|
||||
|--------|---------|------|------|
|
||||
| `tencent` | A-share + HK | none | never IP-banned |
|
||||
| `mootdx` | A-share (explicit-only) | none | retired from the default chain 2026-10: TDX servers stopped answering the client protocol it speaks (#1729) |
|
||||
| `mootdx` | A주 (명시적 선택만) | none | 2026년 10월 기본 경로에서 제외: TDX 서버가 클라이언트 프로토콜에 응답하지 않음 (#1729) |
|
||||
| `eastmoney` | A / US / HK | none | OHLCV + deep fundamentals & flow tools (throttled) |
|
||||
| `baostock` · `akshare` | A (+ US/HK/futures/macro/fx) | none | free fallbacks |
|
||||
| `tushare` | A / HK / futures / fund / macro | token | richest A-share |
|
||||
|
||||
+5
-3
@@ -52,15 +52,17 @@
|
||||
|
||||
> ⚠️ **安全警告:** X 账号 `VibeTrading_HKU`、Virtuals 项目 `101845` 及代币合约 `0x640BDBF77b6447E8b7DB7894cED84BD1c40571f4` 均非 Vibe-Trading 官方。我们从未发行或背书任何代币或 meme 币。请勿购买、连接钱包或签名。[详细说明](SECURITY.md#official-channels--impersonation)。
|
||||
|
||||
- **2026-10-09** 🛠️ **渠道设置与回测修正**:渠道设置保留 URL 内嵌凭证,允许名单等实时读取的配置可不中断连接地更新;Web 以九种语言说明保存和重启结果([#1737](https://github.com/HKUDS/Vibe-Trading/pull/1737)、[#1742](https://github.com/HKUDS/Vibe-Trading/pull/1742)、[#1744](https://github.com/HKUDS/Vibe-Trading/pull/1744))。期权定价年化与运行口径一致,回撤纳入初始资金,验证不再为缺失净值制造收益。修正加密货币/港股识别和缓存恢复,mootdx 移出默认 A 股链。全部十三项 PR 见 [CHANGELOG](CHANGELOG.md)。
|
||||
|
||||
- **2026-10-08** 🔐 **PDF 报告密码保护与券商读取修复**:定时邮件 PDF 可选 AES-256 密码保护,覆盖 Web、CLI 模板和 Agent 确认入口;密码只保存在渠道私有配置中([#1709](https://github.com/HKUDS/Vibe-Trading/pull/1709))。KIS 读取三个交易场所的订单,明确过期的 token 只刷新一次,不完整的读取返回错误([#1726](https://github.com/HKUDS/Vibe-Trading/pull/1726))。派生公式保留说明标签之后的完整算式,证据核验规则保持不变([#1728](https://github.com/HKUDS/Vibe-Trading/pull/1728))。
|
||||
|
||||
- **2026-10-07** 🛠️ **研究连续性与风险指标修正**:运行状态移出系统提示词,保留稳定前缀以利于缓存([#1708](https://github.com/HKUDS/Vibe-Trading/pull/1708))。跨市场日期正确对齐,缺失价格不再拼接跨期收益,期权 Sortino 按全样本计算下行偏差([#1710](https://github.com/HKUDS/Vibe-Trading/pull/1710)、[#1717](https://github.com/HKUDS/Vibe-Trading/pull/1717)、[#1727](https://github.com/HKUDS/Vibe-Trading/pull/1727))。数据源测试共用 OHLCV 契约,并修正信用估值、风格敞口、滚动验证和审计诊断。
|
||||
|
||||
- **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))。
|
||||
|
||||
<details>
|
||||
<summary>更早的新闻</summary>
|
||||
|
||||
- **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))。
|
||||
@@ -476,7 +478,7 @@ vibe-trading connector install /tmp/my-broker
|
||||
| Source | Markets | Auth | Role |
|
||||
|--------|---------|------|------|
|
||||
| `tencent` | A-share + HK | none | never IP-banned |
|
||||
| `mootdx` | A-share (explicit-only) | none | retired from the default chain 2026-10: TDX servers stopped answering the client protocol it speaks (#1729) |
|
||||
| `mootdx` | A 股(仅显式指定) | none | 2026-10 移出默认链:TDX 服务器不再响应其客户端协议(#1729) |
|
||||
| `eastmoney` | A / US / HK | none | OHLCV + deep fundamentals & flow tools (throttled) |
|
||||
| `baostock` · `akshare` | A (+ US/HK/futures/macro/fx) | none | free fallbacks |
|
||||
| `tushare` | A / HK / futures / fund / macro | token | richest A-share |
|
||||
|
||||
@@ -120,18 +120,14 @@ def _normalize_symbol(code: str, market: str) -> str:
|
||||
if re.fullmatch(r"[A-Z]{2,}USDT", cleaned):
|
||||
return f"{cleaned[:-4]}-USDT"
|
||||
return cleaned
|
||||
if market == "hk_equity":
|
||||
hk = re.fullmatch(r"(\d{1,5})(?:\.HK)?", cleaned)
|
||||
if hk:
|
||||
# HKEX's five-digit display and four-digit Yahoo spelling can
|
||||
# name the same counter. Only discard redundant leading zeroes;
|
||||
# genuine five-digit counters (e.g. 80700) remain distinct.
|
||||
return f"{int(hk.group(1)):04d}.HK"
|
||||
if re.search(r"\.(US|HK|SH|SZ|BJ|KS|KQ|NS|BO|TO|V|BA|L|VN|FX)$", cleaned):
|
||||
# HK codes are zero-padded to four digits on the bare path below; the
|
||||
# suffixed spelling must land on the same key, or the duplicate-asset
|
||||
# guard misses one instrument written as `700` and `700.HK`.
|
||||
hk = re.fullmatch(r"(\d{1,5})\.HK", cleaned)
|
||||
if market == "hk_equity" and hk:
|
||||
# zfill pads a 4-digit code and never truncates, so a 5-digit HKEX
|
||||
# spelling (``00700.HK``) keeps its own key while ``700``, ``0700``
|
||||
# and ``700.HK`` all land on ``0700.HK``. Collapsing the 5-digit
|
||||
# form would change the symbol sent to the loaders, so that is a
|
||||
# market-data decision rather than part of this normalization.
|
||||
return f"{hk.group(1).zfill(4)}.HK"
|
||||
return cleaned
|
||||
upper = cleaned.upper()
|
||||
if market == "us_equity":
|
||||
|
||||
@@ -714,6 +714,26 @@ def _read_loader_cache_frame(cache_path: Path) -> pd.DataFrame | None:
|
||||
metadata_path = _loader_cache_metadata_path(cache_path)
|
||||
try:
|
||||
metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
|
||||
# Every cache frame, including one with an unnamed RangeIndex, writes
|
||||
# explicit index metadata. Accepting {} or [] here silently turns a
|
||||
# dated series into row numbers and drops its adjustment/currency.
|
||||
if not isinstance(metadata, dict) or metadata.get("version") != _LOADER_CACHE_VERSION:
|
||||
raise ValueError("missing or incompatible cache metadata version")
|
||||
index_columns = metadata.get("index_columns")
|
||||
index_names = metadata.get("index_names")
|
||||
frame_attrs = metadata.get("frame_attrs")
|
||||
if (
|
||||
not isinstance(index_columns, list) or not index_columns
|
||||
or not all(isinstance(column, str) for column in index_columns)
|
||||
or len(set(index_columns)) != len(index_columns)
|
||||
or not isinstance(index_names, list) or len(index_names) != len(index_columns)
|
||||
or not isinstance(frame_attrs, dict)
|
||||
or any(
|
||||
name in frame_attrs and not isinstance(frame_attrs[name], str)
|
||||
for name in _LOADER_FRAME_METADATA_ATTRS
|
||||
)
|
||||
):
|
||||
raise ValueError("invalid cache index or frame metadata")
|
||||
except Exception as exc: # noqa: BLE001 - local cache miss is non-fatal
|
||||
logger.warning(
|
||||
"loader cache metadata read failed for %s: %s", cache_path.name, exc
|
||||
|
||||
@@ -253,6 +253,8 @@ def walk_forward_analysis(
|
||||
win_eq = equity_curve.iloc[start_idx:end_idx]
|
||||
win_start = indices[start_idx]
|
||||
win_end = indices[end_idx - 1]
|
||||
if not np.isfinite(win_eq.iloc[[0, -1]]).all():
|
||||
return {"error": f"window {i + 1} requires observed start and end equity"}
|
||||
|
||||
# Per-window trades
|
||||
win_trades = [t for t in trades if win_start <= t.entry_time <= win_end]
|
||||
@@ -266,6 +268,8 @@ def walk_forward_analysis(
|
||||
.dropna()
|
||||
.values
|
||||
)
|
||||
if not len(win_returns):
|
||||
return {"error": f"window {i + 1} has no observed adjacent equity returns"}
|
||||
sharpe = _sharpe(win_returns, bars_per_year) if len(win_returns) > 1 else 0.0
|
||||
|
||||
peak = win_eq.cummax()
|
||||
|
||||
@@ -408,7 +408,7 @@ def register_alpha_routes(
|
||||
from src.factors.registry import get_default_registry
|
||||
|
||||
registry = get_default_registry()
|
||||
configured = bool(get_env_config().data.tushare_token)
|
||||
configured = bool(get_env_config().data.tushare_token.strip())
|
||||
installed = importlib.util.find_spec("tushare") is not None
|
||||
return {"universes": {
|
||||
"csi300": {"ready": configured and installed, "reason": "tushare_ready" if configured and installed else "tushare_token_missing" if not configured else "tushare_dependency_missing"},
|
||||
|
||||
@@ -21,7 +21,6 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
import sys as _sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
@@ -34,6 +33,7 @@ from src.api import state as _state
|
||||
from src.channels import config as _channels_config
|
||||
from src.channels.bus.events import OutboundMessage
|
||||
from src.channels.bus.queue import MessageBus
|
||||
from src.channels.manager import _strip_url_userinfo as _strip_error_url_userinfo
|
||||
from src.channels.config_meta import (
|
||||
_strip_url_userinfo,
|
||||
channel_field_hints,
|
||||
@@ -59,7 +59,6 @@ _FALLBACK_STOP_TIMEOUT_S = 10.0
|
||||
|
||||
# Sanitized reset_reason bounds: exception text may embed credentials in URLs.
|
||||
_RESET_REASON_MAX_LEN = 200
|
||||
_URL_USERINFO_RE = re.compile(r"(\w[\w+.-]*://)[^\s/@]+@")
|
||||
|
||||
# Per-channel lifecycle keys the manager consumes from the section; they are
|
||||
# not adapter model fields but are legitimate config content (#341 lineage).
|
||||
@@ -293,9 +292,17 @@ def _scrub_detail(name: str, text: Any, section: dict[str, Any]) -> str:
|
||||
"""Mask every secret value of *section* found in *text* (defense in depth)."""
|
||||
cleaned = str(text or "")
|
||||
for key, value in section.items():
|
||||
if _is_secret_key(name, key) and isinstance(value, str) and value:
|
||||
cleaned = cleaned.replace(value, "***")
|
||||
return cleaned
|
||||
if isinstance(value, str) and value:
|
||||
if _is_secret_key(name, key):
|
||||
cleaned = cleaned.replace(value, "***")
|
||||
else:
|
||||
# A known config URL can contain malformed userinfo, including
|
||||
# spaces. Scrub the whole value before parsing free-text URLs,
|
||||
# whose boundary must stop at prose whitespace.
|
||||
redacted = _strip_url_userinfo(value)
|
||||
if redacted != value:
|
||||
cleaned = cleaned.replace(value, redacted)
|
||||
return _strip_error_url_userinfo(cleaned)
|
||||
|
||||
|
||||
def _validation_fields(exc: ValidationError) -> list[str]:
|
||||
@@ -360,9 +367,15 @@ def format_reset_notice() -> str:
|
||||
return "Channel configuration updated; connections were reset."
|
||||
|
||||
|
||||
def _sanitize_reset_reason(exc: BaseException) -> str:
|
||||
def _sanitize_reset_reason(
|
||||
exc: BaseException, *, name: str = "", sections: tuple[dict[str, Any], ...] = ()
|
||||
) -> str:
|
||||
"""Return a bounded, credential-free one-line reason for a degraded reset (#1625)."""
|
||||
return _URL_USERINFO_RE.sub(r"\1", f"{type(exc).__name__}: {exc}")[:_RESET_REASON_MAX_LEN]
|
||||
text = f"{type(exc).__name__}: {exc}"
|
||||
for section in sections:
|
||||
text = _scrub_detail(name, text, section)
|
||||
text = _strip_error_url_userinfo(text)
|
||||
return " ".join(text.split())[:_RESET_REASON_MAX_LEN]
|
||||
|
||||
|
||||
async def _publish_reset_notice(runtime: Any, name: str, chat_id: str) -> None:
|
||||
@@ -385,7 +398,10 @@ async def _publish_reset_notice(runtime: Any, name: str, chat_id: str) -> None:
|
||||
logger.warning("Reset notice for %s was not published", name, exc_info=True)
|
||||
|
||||
|
||||
async def _hot_apply(name: str, section: dict[str, Any] | None) -> tuple[str, str | None]:
|
||||
async def _hot_apply(
|
||||
name: str, section: dict[str, Any] | None,
|
||||
*, previous_section: dict[str, Any] | None = None,
|
||||
) -> tuple[str, str | None]:
|
||||
"""Apply a written section to the cached runtime; return ``(applied, reason)``.
|
||||
|
||||
Running runtime → per-channel hot swap; built-but-stopped → drop the stale
|
||||
@@ -408,10 +424,12 @@ async def _hot_apply(name: str, section: dict[str, Any] | None) -> tuple[str, st
|
||||
await runtime.manager.reload_channel(name, section)
|
||||
return "hot_swapped", None
|
||||
except Exception as exc: # noqa: BLE001 - a failed swap must not strand the runtime
|
||||
reason = _sanitize_reset_reason(
|
||||
exc, name=name, sections=(previous_section or {}, section or {}),
|
||||
)
|
||||
logger.warning(
|
||||
"Hot reload failed for channel %s; falling back to a runtime reset",
|
||||
name,
|
||||
exc_info=True,
|
||||
"Hot reload failed for channel %s; falling back to a runtime reset: %s",
|
||||
name, reason,
|
||||
)
|
||||
# The reset destroys the old manager, so capture the notify target —
|
||||
# the last chat we sent to — before its state is gone.
|
||||
@@ -430,7 +448,7 @@ async def _hot_apply(name: str, section: dict[str, Any] | None) -> tuple[str, st
|
||||
await fresh.start(start_manager=True)
|
||||
if last_chat_id is not None:
|
||||
await _publish_reset_notice(fresh, name, last_chat_id)
|
||||
return "reset", _sanitize_reset_reason(exc)
|
||||
return "reset", reason
|
||||
|
||||
|
||||
async def _apply_update(name: str, payload: ChannelConfigUpdateRequest) -> dict[str, Any]:
|
||||
@@ -492,19 +510,30 @@ async def _apply_update(name: str, payload: ChannelConfigUpdateRequest) -> dict[
|
||||
fresh = _stored_section(name)
|
||||
# Noop-key edits (declared keys + manager bool overrides) are read live per
|
||||
# message, so refresh in place; ``enabled`` never qualifies (start/stop, #1625).
|
||||
changed = set(patch) | set(clears)
|
||||
before = _effective_section(name, stored)
|
||||
after = _effective_section(name, fresh)
|
||||
model = type(instance.config)
|
||||
if hasattr(model, "model_validate"):
|
||||
# Web text widgets serialize numbers as strings; compare validated
|
||||
# values and defaults so an echoed connection field is not an edit.
|
||||
computed = set(model.model_computed_fields)
|
||||
try:
|
||||
before.update(model.model_validate(stored).model_dump(by_alias=True, exclude=computed))
|
||||
except Exception: # an edit may be repairing an invalid stored section
|
||||
pass
|
||||
after.update(instance.config.model_dump(by_alias=True, exclude=computed))
|
||||
changed = {key for key in before.keys() | after.keys() if before.get(key) != after.get(key)}
|
||||
noop_keys = cls.hot_reload_noop_keys | _MANAGER_OVERRIDE_KEYS
|
||||
runtime = getattr(_host(), "_channel_runtime", None)
|
||||
if (
|
||||
changed
|
||||
and changed <= noop_keys
|
||||
changed <= noop_keys
|
||||
and runtime is not None
|
||||
and runtime.status().get("running")
|
||||
and runtime.manager.get_channel(name) is not None
|
||||
and await runtime.manager.refresh_channel_config(name, fresh)
|
||||
):
|
||||
return {"channel": _fresh_entry(name), "applied": "refreshed"}
|
||||
applied, reset_reason = await _hot_apply(name, fresh or None)
|
||||
applied, reset_reason = await _hot_apply(name, fresh or None, previous_section=stored)
|
||||
response: dict[str, Any] = {"channel": _fresh_entry(name), "applied": applied}
|
||||
if reset_reason is not None:
|
||||
response["reset_reason"] = reset_reason
|
||||
@@ -514,7 +543,9 @@ async def _apply_update(name: str, payload: ChannelConfigUpdateRequest) -> dict[
|
||||
async def _run_test(name: str, body: dict[str, Any] | None, clears: list[str]) -> dict[str, Any]:
|
||||
"""Probe credentials on an ephemeral instance; never persists anything."""
|
||||
stored = _stored_section(name)
|
||||
patch = _normalize_nullable_blanks(name, _patch_of(name, body or {}))
|
||||
patch = _drop_echoed_url_rewrites(
|
||||
stored, _normalize_nullable_blanks(name, _patch_of(name, body or {})),
|
||||
)
|
||||
merged = {**stored, **patch}
|
||||
for key in clears:
|
||||
merged.pop(key, None)
|
||||
@@ -550,7 +581,10 @@ async def _run_test(name: str, body: dict[str, Any] | None, clears: list[str]) -
|
||||
except Exception as exc: # noqa: BLE001 - a broken section cannot be probed
|
||||
return _result(False, "invalid_credentials", f"validation_error: {type(exc).__name__}", sdk_fallback)
|
||||
|
||||
probe = await instance.test_connection()
|
||||
try:
|
||||
probe = await instance.test_connection()
|
||||
except Exception as exc: # probe adapters may also raise SDK/network errors
|
||||
probe = {"ok": False, "code": "network", "detail": f"{type(exc).__name__}: {exc}"}
|
||||
code = probe.get("code")
|
||||
sdk_available = probe.get("sdk_available")
|
||||
return _result(
|
||||
|
||||
@@ -369,14 +369,15 @@ class SignalChannel(BaseChannel):
|
||||
|
||||
name = "signal"
|
||||
display_name = "Signal"
|
||||
hot_reload_noop_keys = frozenset({"allow_from"})
|
||||
# The top-level allow_from is computed; the writable policies are nested.
|
||||
hot_reload_noop_keys = frozenset({"dm", "group"})
|
||||
_TYPING_REFRESH_SECONDS = 10.0
|
||||
_MAX_MESSAGE_LEN = 64_000 # signal-cli practical limit (protocol max ~64 KB)
|
||||
_HTTP_TIMEOUT_SECONDS = 60.0
|
||||
|
||||
@classmethod
|
||||
def default_config(cls) -> dict[str, Any]:
|
||||
return SignalConfig().model_dump(by_alias=True)
|
||||
return SignalConfig().model_dump(by_alias=True, exclude={"allow_from"})
|
||||
|
||||
def __init__(self, config: SignalConfig, bus: MessageBus):
|
||||
if isinstance(config, dict):
|
||||
|
||||
@@ -692,7 +692,7 @@ def _validate_bench_data_requirements(registry: Any, alpha_ids: list[str], unive
|
||||
ValueError: If US-only SEC inputs are requested from another universe.
|
||||
"""
|
||||
columns = sorted({column for aid in alpha_ids
|
||||
for column in (getattr(registry.get(aid), "meta", None) or {}).get("columns_required", [])
|
||||
for column in (registry.get(aid).meta or {}).get("columns_required", [])
|
||||
if column.startswith("fund:")})
|
||||
if columns and universe != "sp500":
|
||||
raise ValueError("Fundamental factor evaluation currently uses SEC US filings. Choose the S&P 500 universe.")
|
||||
|
||||
@@ -99,7 +99,10 @@ def test_duplicate_aliases_rejected_before_any_fetch(monkeypatch):
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"pair", [("700", "700.HK"), ("9988", "9988.HK"), ("00700", "00700.HK")]
|
||||
"pair", [
|
||||
("700", "700.HK"), ("9988", "9988.HK"), ("00700", "00700.HK"),
|
||||
("00700.HK", "0700.HK"), ("00005.HK", "5"),
|
||||
]
|
||||
)
|
||||
def test_hk_spellings_of_one_instrument_are_rejected_as_duplicates(pair, monkeypatch):
|
||||
"""A bare HK code and its suffixed spelling must normalize to one key.
|
||||
@@ -121,6 +124,13 @@ def test_hk_spellings_of_one_instrument_are_rejected_as_duplicates(pair, monkeyp
|
||||
fetch.assert_not_called()
|
||||
|
||||
|
||||
def test_hk_normalization_preserves_distinct_currency_counters():
|
||||
"""Remove redundant zeroes without truncating a real five-digit counter."""
|
||||
assert correlation._normalize_symbol("80700.HK", "hk_equity") == "80700.HK"
|
||||
assert correlation._normalize_symbol("80700", "hk_equity") == "80700.HK"
|
||||
assert correlation._normalize_symbol("00700.HK", "hk_equity") == "0700.HK"
|
||||
|
||||
|
||||
def test_readiness_snapshots_and_single_asset_validation():
|
||||
client = TestClient(api_server.app, client=("127.0.0.1", 50000))
|
||||
assert client.get("/alpha/readiness").json()["universes"]["btc-usdt"]["ready"] is False
|
||||
|
||||
@@ -286,3 +286,58 @@ def test_route_falls_back_to_hot_apply(
|
||||
assert response.json()["applied"] == "hot_swapped"
|
||||
assert [name for name, _ in manager.reload_calls] == ["dingtalk"]
|
||||
assert bool(manager.refresh_calls) is expect_refresh_called
|
||||
|
||||
|
||||
@pytest.mark.parametrize("allow_from", [["alice"], ["*"]])
|
||||
def test_full_form_echo_does_not_restart_for_unchanged_connection_fields(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, allow_from: list[str]
|
||||
) -> None:
|
||||
manager = _FakeManager()
|
||||
client = _client(
|
||||
tmp_path, monkeypatch, channels={"dingtalk": _dingtalk_section()},
|
||||
runtime=_FakeRuntime(running=True, manager=manager),
|
||||
)
|
||||
entry = client.get("/channels/config").json()["channels"]["dingtalk"]
|
||||
patch = dict(entry["values"], allow_from=allow_from)
|
||||
response = client.put("/channels/config/dingtalk", json={"config": patch})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json()["applied"] == "refreshed"
|
||||
assert manager.refresh_calls[0][1]["allow_from"] == allow_from
|
||||
assert manager.reload_calls == []
|
||||
|
||||
|
||||
def test_signal_refresh_changes_the_real_nested_allowlist() -> None:
|
||||
from src.channels.signal import SignalChannel
|
||||
|
||||
channel = SignalChannel({"dm": {"allow_from": ["alice"]}}, MessageBus())
|
||||
assert "allow_from" not in channel.hot_reload_noop_keys
|
||||
assert {"dm", "group"} <= channel.hot_reload_noop_keys
|
||||
assert channel.refresh_config({"dm": {"allow_from": ["bob"]}})
|
||||
assert channel.is_allowed("bob")
|
||||
assert not channel.is_allowed("alice")
|
||||
|
||||
|
||||
def test_signal_policy_put_refreshes_without_counting_computed_fields(tmp_path, monkeypatch):
|
||||
from src.channels.signal import SignalChannel
|
||||
|
||||
section = {"enabled": True, "dm": {"allow_from": ["alice"]}}
|
||||
channel = SignalChannel(section, MessageBus())
|
||||
manager = _FakeManager()
|
||||
monkeypatch.setattr(manager, "get_channel", lambda name: channel)
|
||||
|
||||
async def refresh(name, fresh):
|
||||
manager.refresh_calls.append((name, fresh))
|
||||
return channel.refresh_config(fresh)
|
||||
|
||||
monkeypatch.setattr(manager, "refresh_channel_config", refresh)
|
||||
client = _client(
|
||||
tmp_path, monkeypatch, channels={"signal": section},
|
||||
runtime=_FakeRuntime(running=True, manager=manager),
|
||||
)
|
||||
response = client.put(
|
||||
"/channels/config/signal", json={"config": {"dm": {"allow_from": ["bob"]}}},
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json()["applied"] == "refreshed"
|
||||
assert manager.reload_calls == []
|
||||
assert channel.is_allowed("bob") and not channel.is_allowed("alice")
|
||||
@@ -286,3 +286,30 @@ def test_notice_content_never_echoes_config_values(
|
||||
for item in items:
|
||||
if isinstance(item, str) and item:
|
||||
assert item not in notice.content
|
||||
|
||||
|
||||
@pytest.mark.parametrize("url", [
|
||||
"https://user:pass@word@host:99999/x",
|
||||
"https://user:pass@word@[::1]:8443/x",
|
||||
])
|
||||
def test_reset_reason_strips_the_entire_userinfo(url: str) -> None:
|
||||
reason = routes._sanitize_reset_reason(RuntimeError(f"failed ({url})\nretry later"))
|
||||
assert "pass" not in reason
|
||||
assert "word" not in reason
|
||||
assert "\n" not in reason
|
||||
|
||||
|
||||
def test_reset_reason_does_not_expose_a_stored_secret(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
client, old_runtime, _ = _arrange(
|
||||
tmp_path, monkeypatch, last_chat=None, fail_reload=True,
|
||||
)
|
||||
|
||||
async def fail(name, section):
|
||||
raise RuntimeError(f"SDK refused credential {STORED_SECRET}")
|
||||
|
||||
old_runtime.manager.reload_channel = fail
|
||||
body = _put_rotated_id(client)
|
||||
assert body["applied"] == "reset"
|
||||
assert STORED_SECRET not in body["reset_reason"]
|
||||
@@ -654,6 +654,30 @@ def test_enable_probe_uses_the_stored_url_not_the_echoed_stripped_one(
|
||||
assert "proxy-secret" not in response.text
|
||||
|
||||
|
||||
def test_connection_test_keeps_echoed_url_credentials(tmp_path, monkeypatch) -> None:
|
||||
proxy = "http://proxy-user:proxy-secret@proxy.local:8080"
|
||||
client, path = _client(
|
||||
tmp_path, monkeypatch,
|
||||
channels={"discord": {"enabled": False, "proxy": proxy}},
|
||||
)
|
||||
probed = []
|
||||
|
||||
async def probe(self):
|
||||
probed.append(self.config.proxy)
|
||||
return {"ok": True, "code": "ok", "detail": ""}
|
||||
|
||||
cls = routes.load_channel_class("discord")
|
||||
monkeypatch.setattr(cls, "supports_connection_test", True)
|
||||
monkeypatch.setattr(cls, "test_connection", probe)
|
||||
values = client.get("/channels/config").json()["channels"]["discord"]["values"]
|
||||
response = client.post("/channels/discord/test", json={"config": values})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json()["ok"]
|
||||
assert probed == [proxy]
|
||||
assert "proxy-secret" not in response.text
|
||||
assert json.loads(path.read_text())["channels"]["discord"]["proxy"] == proxy
|
||||
|
||||
|
||||
def test_put_feishu_enable_with_bad_credentials_is_blocked_before_write(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
@@ -1507,3 +1531,41 @@ def test_email_pdf_password_config_exposes_presence_only(tmp_path, monkeypatch):
|
||||
client, _path = _client(tmp_path, monkeypatch, channels={"email": _email_section()})
|
||||
entry = client.get("/channels/config").json()["channels"]["email"]
|
||||
assert entry["pdf_password_configured"] is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("endpoint", ["test", "enable"])
|
||||
@pytest.mark.parametrize("raises", [False, True])
|
||||
@pytest.mark.parametrize("password", ["pass@word", "pass word"])
|
||||
def test_probe_failures_redact_url_credentials_and_do_not_write(
|
||||
tmp_path, monkeypatch, endpoint, raises, password,
|
||||
) -> None:
|
||||
"""Both probe entry points sanitize returned errors and raised exceptions."""
|
||||
proxy = f"http://proxy-user:{password}@proxy.local:notaport"
|
||||
client, path = _client(
|
||||
tmp_path, monkeypatch,
|
||||
channels={"discord": {"enabled": False, "proxy": proxy, "token": STORED_SECRET}},
|
||||
)
|
||||
before = path.read_bytes()
|
||||
|
||||
async def probe(self):
|
||||
detail = f"Failed via {self.config.proxy}; token={self.config.token}"
|
||||
if raises:
|
||||
raise RuntimeError(detail)
|
||||
return {"ok": False, "code": "network", "detail": detail}
|
||||
|
||||
cls = routes.load_channel_class("discord")
|
||||
monkeypatch.setattr(cls, "supports_connection_test", True)
|
||||
monkeypatch.setattr(cls, "test_connection", probe)
|
||||
if endpoint == "test":
|
||||
response = client.post("/channels/discord/test", json={"config": {}})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json()["code"] == "network"
|
||||
assert response.json()["ok"] is False
|
||||
else:
|
||||
response = client.put("/channels/config/discord", json={"config": {"enabled": True}})
|
||||
assert response.status_code == 422, response.text
|
||||
assert "proxy-user" not in response.text
|
||||
assert password not in response.text
|
||||
assert STORED_SECRET not in response.text
|
||||
assert "proxy.local:notaport" in response.text
|
||||
assert path.read_bytes() == before
|
||||
@@ -125,7 +125,11 @@ def test_ignored_yaml_metadata_does_not_break_local_fetch(
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"corruption",
|
||||
["null", "array", "index_names_length", "unhashable_index_column"],
|
||||
[
|
||||
"null", "array", "index_names_length", "unhashable_index_column",
|
||||
"empty_object", "empty_index", "missing_index", "missing_names",
|
||||
"wrong_version", "missing_attrs", "invalid_attrs",
|
||||
],
|
||||
)
|
||||
def test_corrupt_local_cache_metadata_refetches_and_repairs(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, corruption: str
|
||||
@@ -157,6 +161,20 @@ def test_corrupt_local_cache_metadata_refetches_and_repairs(
|
||||
malformed = []
|
||||
elif corruption == "index_names_length":
|
||||
malformed["index_names"] = ["date", "unexpected_extra_level"]
|
||||
elif corruption == "empty_object":
|
||||
malformed = {}
|
||||
elif corruption == "empty_index":
|
||||
malformed["index_columns"] = []
|
||||
elif corruption == "missing_index":
|
||||
malformed.pop("index_columns")
|
||||
elif corruption == "missing_names":
|
||||
malformed.pop("index_names")
|
||||
elif corruption == "wrong_version":
|
||||
malformed["version"] = -1
|
||||
elif corruption == "missing_attrs":
|
||||
malformed.pop("frame_attrs")
|
||||
elif corruption == "invalid_attrs":
|
||||
malformed["frame_attrs"] = {"adjustment": ["qfq"]}
|
||||
else:
|
||||
malformed["index_columns"] = [{"not": "a column name"}]
|
||||
metadata_path.write_text(json.dumps(malformed))
|
||||
|
||||
@@ -7,6 +7,7 @@ import json
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import mcp_server
|
||||
import pytest
|
||||
@@ -210,7 +211,7 @@ def test_alpha_bench_rejects_output_outside_allowed_roots(monkeypatch) -> None:
|
||||
def test_alpha_bench_does_not_follow_preexisting_report_symlink(monkeypatch, tmp_path: Path) -> None:
|
||||
class _BenchRegistry:
|
||||
def get(self, alpha_id: str) -> object:
|
||||
return object()
|
||||
return SimpleNamespace(meta=None)
|
||||
|
||||
class _FixedDateTime:
|
||||
@classmethod
|
||||
|
||||
@@ -78,3 +78,29 @@ def test_loaded_equity_gap_reaches_validation_sample_guard(tmp_path) -> None:
|
||||
)
|
||||
|
||||
assert result["bootstrap"] == {"error": "need at least 5 return observations"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"values",
|
||||
[
|
||||
[float("nan"), 100, 110, 99],
|
||||
[100, 110, 99, float("nan")],
|
||||
[float("nan")] * 4,
|
||||
[100, float("nan"), float("nan"), 110],
|
||||
],
|
||||
ids=["missing-start", "missing-end", "empty-window", "no-adjacent-pair"],
|
||||
)
|
||||
def test_walk_forward_refuses_unmeasurable_window(values) -> None:
|
||||
"""No boundary value or no observed period cannot become a zero score."""
|
||||
equity = pd.Series(values, index=pd.date_range("2026-01-01", periods=4))
|
||||
result = walk_forward_analysis(equity, [], n_windows=1)
|
||||
assert "error" in result
|
||||
assert "window 1" in result["error"]
|
||||
assert "sharpe_mean" not in result
|
||||
|
||||
|
||||
def test_walk_forward_does_not_silently_drop_an_unmeasurable_window() -> None:
|
||||
equity = pd.Series([100, 110, 99, 108.9, float("nan"), 120, 132, 118.8])
|
||||
result = walk_forward_analysis(equity, [], n_windows=2)
|
||||
assert "error" in result and "window 2" in result["error"]
|
||||
assert "consistency_rate" not in result
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
type ChannelConfigEntry,
|
||||
type ChannelFieldHint,
|
||||
type ChannelPutBody,
|
||||
type ChannelPutResult,
|
||||
type ChannelTestBody,
|
||||
type ChannelTestResult,
|
||||
} from "@/lib/api";
|
||||
@@ -195,6 +196,7 @@ export function ChannelConfigPanel({
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [errorDetail, setErrorDetail] = useState<string | null>(null);
|
||||
const [enableAnyway, setEnableAnyway] = useState(false);
|
||||
const [applyResult, setApplyResult] = useState<ChannelPutResult | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// A freshly fetched entry re-seeds non-secret fields (typed secret drafts
|
||||
@@ -377,21 +379,35 @@ export function ChannelConfigPanel({
|
||||
setErrorDetail(detail || null);
|
||||
};
|
||||
|
||||
const applyMessage = (result: ChannelPutResult) => {
|
||||
if (result.applied === "refreshed") return t("settings.channels.config.appliedInPlace");
|
||||
if (result.applied === "reset" && result.reset_reason) return t("settings.channels.config.appliedReset");
|
||||
if (result.applied === "deferred" || result.applied === "reset") return t("settings.channels.config.appliedOnStart");
|
||||
return t("settings.channels.config.appliedReconnect");
|
||||
};
|
||||
|
||||
const showApplied = (result: ChannelPutResult, fallback: string) => {
|
||||
setApplyResult(result);
|
||||
if (result.applied === "reset" && result.reset_reason) toast.info(applyMessage(result));
|
||||
else toast.success(result.applied === "refreshed" ? applyMessage(result) : fallback);
|
||||
};
|
||||
|
||||
const submit = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
setErrorDetail(null);
|
||||
setEnableAnyway(false);
|
||||
setApplyResult(null);
|
||||
const body: ChannelPutBody = { config: buildPatch() };
|
||||
for (const field of entry.fields) {
|
||||
if (field.secret && clears[field.key]) body[`clear_${field.key}`] = true;
|
||||
}
|
||||
try {
|
||||
await api.putChannelConfig(name, body);
|
||||
const result = await api.putChannelConfig(name, body);
|
||||
setSecretDrafts({});
|
||||
setClears({});
|
||||
toast.success(t("settings.channels.config.saved"));
|
||||
showApplied(result, t("settings.channels.config.saved"));
|
||||
await onChanged();
|
||||
} catch (saveError) {
|
||||
const failure = describeApiFailure(saveError);
|
||||
@@ -439,12 +455,13 @@ export function ChannelConfigPanel({
|
||||
setError(null);
|
||||
setErrorDetail(null);
|
||||
setEnableAnyway(false);
|
||||
setApplyResult(null);
|
||||
try {
|
||||
await api.putChannelConfig(name, {
|
||||
const result = await api.putChannelConfig(name, {
|
||||
config: { enabled: next },
|
||||
skip_verify: options?.skipVerify,
|
||||
});
|
||||
toast.success(
|
||||
showApplied(result,
|
||||
next
|
||||
? t("settings.channels.config.enabledToast")
|
||||
: t("settings.channels.config.disabledToast"),
|
||||
@@ -669,6 +686,18 @@ export function ChannelConfigPanel({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{applyResult ? (
|
||||
<div
|
||||
role="status"
|
||||
className={`rounded-md border px-3 py-2 text-sm ${applyResult.reset_reason
|
||||
? "border-warning/40 bg-warning/10 text-warning-foreground"
|
||||
: "border-success/30 bg-success/5 text-success"}`}
|
||||
>
|
||||
<div>{applyMessage(applyResult)}</div>
|
||||
{applyResult.reset_reason ? <div className="mt-1 break-words text-xs">{applyResult.reset_reason}</div> : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<form onSubmit={submit} className="grid gap-4">
|
||||
{entry.fields.length ? (
|
||||
entry.fields.map(renderField)
|
||||
|
||||
@@ -553,6 +553,23 @@ describe("ChannelSettings config panel", () => {
|
||||
expect((secondBody.config as Record<string, unknown>).client_secret).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["refreshed", undefined, "Settings saved without reconnecting the channel."],
|
||||
["reset", "RuntimeError: upstream unavailable", "Settings saved. Channel connections were restarted."],
|
||||
["deferred", undefined, "Settings saved. Changes will apply when channels start."],
|
||||
])("explains the %s apply result after saving", async (applied, resetReason, message) => {
|
||||
apiMock.putChannelConfig.mockResolvedValue({
|
||||
channel: dingtalkEntry(), applied, reset_reason: resetReason,
|
||||
});
|
||||
await renderExpanded();
|
||||
fireEvent.change(screen.getByPlaceholderText("Keep current (****abcd)"), {
|
||||
target: { value: "new-secret" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save configuration" }));
|
||||
expect(await screen.findByText(message!)).toBeInTheDocument();
|
||||
if (resetReason) expect(screen.getByText(resetReason)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("disables Save until the form is dirty", async () => {
|
||||
await renderExpanded();
|
||||
|
||||
|
||||
@@ -512,6 +512,10 @@
|
||||
"save": "حفظ الإعدادات",
|
||||
"hotApply": "يُطبَّق الحفظ فورًا — دون حاجة إلى إعادة التشغيل.",
|
||||
"saved": "تم حفظ إعدادات القناة",
|
||||
"appliedInPlace": "تم حفظ الإعدادات دون إعادة توصيل القناة.",
|
||||
"appliedReset": "تم حفظ الإعدادات وإعادة تشغيل اتصالات القنوات.",
|
||||
"appliedOnStart": "تم حفظ الإعدادات. ستُطبّق التغييرات عند بدء القنوات.",
|
||||
"appliedReconnect": "تم حفظ الإعدادات وإعادة توصيل هذه القناة.",
|
||||
"saveFailed": "تعذّر حفظ إعدادات القناة: {{message}}",
|
||||
"test": "اختبار الاتصال",
|
||||
"testing": "جارٍ الاختبار…",
|
||||
|
||||
@@ -517,6 +517,10 @@
|
||||
"save": "Konfiguration speichern",
|
||||
"hotApply": "Speichern wirkt sofort – kein Neustart nötig.",
|
||||
"saved": "Kanal-Konfiguration gespeichert",
|
||||
"appliedInPlace": "Einstellungen ohne erneuten Verbindungsaufbau gespeichert.",
|
||||
"appliedReset": "Einstellungen gespeichert. Die Kanalverbindungen wurden neu gestartet.",
|
||||
"appliedOnStart": "Einstellungen gespeichert. Die Änderungen werden beim Start der Kanäle angewendet.",
|
||||
"appliedReconnect": "Einstellungen gespeichert. Dieser Kanal wurde erneut verbunden.",
|
||||
"saveFailed": "Kanal-Konfiguration konnte nicht gespeichert werden: {{message}}",
|
||||
"test": "Verbindung testen",
|
||||
"testing": "Wird getestet…",
|
||||
|
||||
@@ -517,6 +517,10 @@
|
||||
"save": "Save configuration",
|
||||
"hotApply": "Saving applies immediately — no restart needed.",
|
||||
"saved": "Channel configuration saved",
|
||||
"appliedInPlace": "Settings saved without reconnecting the channel.",
|
||||
"appliedReset": "Settings saved. Channel connections were restarted.",
|
||||
"appliedOnStart": "Settings saved. Changes will apply when channels start.",
|
||||
"appliedReconnect": "Settings saved. This channel was reconnected.",
|
||||
"saveFailed": "Failed to save the channel configuration: {{message}}",
|
||||
"test": "Test connection",
|
||||
"testing": "Testing…",
|
||||
|
||||
@@ -517,6 +517,10 @@
|
||||
"save": "Guardar configuración",
|
||||
"hotApply": "Guardar se aplica al instante: no hace falta reiniciar.",
|
||||
"saved": "Configuración del canal guardada",
|
||||
"appliedInPlace": "Ajustes guardados sin reconectar el canal.",
|
||||
"appliedReset": "Ajustes guardados. Se reiniciaron las conexiones de los canales.",
|
||||
"appliedOnStart": "Ajustes guardados. Los cambios se aplicarán al iniciar los canales.",
|
||||
"appliedReconnect": "Ajustes guardados. Se reconectó este canal.",
|
||||
"saveFailed": "No se pudo guardar la configuración del canal: {{message}}",
|
||||
"test": "Probar conexión",
|
||||
"testing": "Probando…",
|
||||
|
||||
@@ -517,6 +517,10 @@
|
||||
"save": "Simpan konfigurasi",
|
||||
"hotApply": "Penyimpanan langsung berlaku — tidak perlu restart.",
|
||||
"saved": "Konfigurasi channel tersimpan",
|
||||
"appliedInPlace": "Pengaturan disimpan tanpa menyambungkan ulang kanal.",
|
||||
"appliedReset": "Pengaturan disimpan. Koneksi kanal telah dimulai ulang.",
|
||||
"appliedOnStart": "Pengaturan disimpan. Perubahan akan diterapkan saat kanal dimulai.",
|
||||
"appliedReconnect": "Pengaturan disimpan. Kanal ini telah tersambung kembali.",
|
||||
"saveFailed": "Gagal menyimpan konfigurasi channel: {{message}}",
|
||||
"test": "Uji koneksi",
|
||||
"testing": "Menguji…",
|
||||
|
||||
@@ -512,6 +512,10 @@
|
||||
"save": "設定を保存",
|
||||
"hotApply": "保存するとすぐに反映されます。再起動は不要です。",
|
||||
"saved": "チャンネル設定を保存しました",
|
||||
"appliedInPlace": "設定を保存しました。チャンネルの再接続は不要でした。",
|
||||
"appliedReset": "設定を保存しました。チャンネル接続を再起動しました。",
|
||||
"appliedOnStart": "設定を保存しました。チャンネルの起動時に反映されます。",
|
||||
"appliedReconnect": "設定を保存し、このチャンネルを再接続しました。",
|
||||
"saveFailed": "チャンネル設定の保存に失敗しました: {{message}}",
|
||||
"test": "接続テスト",
|
||||
"testing": "テスト中…",
|
||||
|
||||
@@ -512,6 +512,10 @@
|
||||
"save": "설정 저장",
|
||||
"hotApply": "저장하면 즉시 적용됩니다. 재시작이 필요하지 않습니다.",
|
||||
"saved": "채널 설정을 저장했습니다",
|
||||
"appliedInPlace": "채널을 다시 연결하지 않고 설정을 저장했습니다.",
|
||||
"appliedReset": "설정을 저장하고 채널 연결을 다시 시작했습니다.",
|
||||
"appliedOnStart": "설정을 저장했습니다. 채널을 시작하면 적용됩니다.",
|
||||
"appliedReconnect": "설정을 저장하고 이 채널을 다시 연결했습니다.",
|
||||
"saveFailed": "채널 설정 저장 실패: {{message}}",
|
||||
"test": "연결 테스트",
|
||||
"testing": "테스트 중…",
|
||||
|
||||
@@ -517,6 +517,10 @@
|
||||
"save": "Salvar configuração",
|
||||
"hotApply": "Salvar é aplicado imediatamente — não é preciso reiniciar.",
|
||||
"saved": "Configuração do canal salva",
|
||||
"appliedInPlace": "Configurações salvas sem reconectar o canal.",
|
||||
"appliedReset": "Configurações salvas. As conexões dos canais foram reiniciadas.",
|
||||
"appliedOnStart": "Configurações salvas. As alterações serão aplicadas quando os canais iniciarem.",
|
||||
"appliedReconnect": "Configurações salvas. Este canal foi reconectado.",
|
||||
"saveFailed": "Falha ao salvar a configuração do canal: {{message}}",
|
||||
"test": "Testar conexão",
|
||||
"testing": "Testando…",
|
||||
|
||||
@@ -512,6 +512,10 @@
|
||||
"save": "保存配置",
|
||||
"hotApply": "保存后立即生效,无需重启。",
|
||||
"saved": "渠道配置已保存",
|
||||
"appliedInPlace": "设置已保存,渠道连接保持不中断。",
|
||||
"appliedReset": "设置已保存,渠道连接已重启。",
|
||||
"appliedOnStart": "设置已保存,将在渠道启动时生效。",
|
||||
"appliedReconnect": "设置已保存,此渠道已重新连接。",
|
||||
"saveFailed": "保存渠道配置失败:{{message}}",
|
||||
"test": "测试连接",
|
||||
"testing": "测试中…",
|
||||
|
||||
@@ -1019,7 +1019,8 @@ export type ChannelPutBody = {
|
||||
|
||||
export interface ChannelPutResult {
|
||||
channel: ChannelConfigEntry;
|
||||
applied: "hot_swapped" | "reset" | "deferred";
|
||||
applied: "refreshed" | "hot_swapped" | "reset" | "deferred";
|
||||
reset_reason?: string;
|
||||
}
|
||||
|
||||
export type ChannelTestBody = {
|
||||
|
||||
Reference in new issue
Block a user