fix: complete September 27 reviews and surface verified run-card evidence

This commit is contained in:
Haozhe Wu committed 2026-09-27 10:57:33 +08:00
1 parent 71a8c2b005
commit 0244eceaaf
36 files changed
+1046 -215

No files matched your search

+4 -2
View File
@@ -52,6 +52,8 @@
> ⚠️ **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-09-27** 🛠️ **Verifiable run cards and safer research**: Run cards now show hashed backtest execution records and verified metric-to-CSV references in JSON, Markdown and Run Detail ([#1612](https://github.com/HKUDS/Vibe-Trading/pull/1612)). GTJA high/low recency factors use the correct day count and consistent tie handling ([#1604](https://github.com/HKUDS/Vibe-Trading/pull/1604)); India short covers check the buy-side circuit band ([#1608](https://github.com/HKUDS/Vibe-Trading/pull/1608)). Token usage remains readable without exempting arbitrary credential strings ([#1606](https://github.com/HKUDS/Vibe-Trading/pull/1606)), malformed MCP schemas retain their object properties ([#1607](https://github.com/HKUDS/Vibe-Trading/pull/1607)), and immediate-order goal checks preserve research questions ([#1605](https://github.com/HKUDS/Vibe-Trading/pull/1605)). Setup documentation now correctly identifies OpenRouter as the shipped default ([#1609](https://github.com/HKUDS/Vibe-Trading/issues/1609)).
- **2026-09-26** 🛠️ **More reliable indicators and trading limits**: Technical indicators retain their observation date and can reuse results lost during conversation compaction ([#1590](https://github.com/HKUDS/Vibe-Trading/pull/1590), [#1601](https://github.com/HKUDS/Vibe-Trading/pull/1601)). All five Qlib158 WVMA windows now use absolute returns in the numerator ([#1594](https://github.com/HKUDS/Vibe-Trading/pull/1594)); Benford checks preserve the correct leading digit at numeric boundaries ([#1591](https://github.com/HKUDS/Vibe-Trading/pull/1591)). An explicitly zero exposure limit stays zero ([#1593](https://github.com/HKUDS/Vibe-Trading/pull/1593)), and Dhan paper orders reject fractional or invalid quantities without rounding away input precision ([#1595](https://github.com/HKUDS/Vibe-Trading/pull/1595)). Invalid optional numeric environment variables fall back to defaults while explicit configuration constraints remain enforced ([#1592](https://github.com/HKUDS/Vibe-Trading/pull/1592)). Runtime triggers keep the correct field defaults and subclass factory behavior ([#1599](https://github.com/HKUDS/Vibe-Trading/pull/1599)).
- **2026-09-25** 🛠️ **More reliable research sessions and channel diagnostics**: A text column in a backtest CSV no longer discards the numeric metrics ([#1579](https://github.com/HKUDS/Vibe-Trading/pull/1579)), and conversation compaction now counts reasoning content in its retained-message budget ([#1582](https://github.com/HKUDS/Vibe-Trading/pull/1582)). Failed channel loads now log the actual exception ([#1580](https://github.com/HKUDS/Vibe-Trading/pull/1580)); the Telegram guide clarifies that CLI and Web controls share the same API runtime ([#1583](https://github.com/HKUDS/Vibe-Trading/pull/1583)). Robinhood portfolio reads reject malformed buying-power objects while keeping omitted or null values unknown ([#1526](https://github.com/HKUDS/Vibe-Trading/pull/1526)). Leaving a chat also cancels pending history-scroll timers. Thanks [@Shizoqua](https://github.com/Shizoqua) and [@lorenzozanee](https://github.com/lorenzozanee)!
@@ -1021,7 +1023,7 @@ Vibe-Trading is a tool-heavy agent — skills, backtests, memory, and swarms all
| **Sweet spot** (default) | `deepseek-v4-pro`, `deepseek/deepseek-v4-pro`, `x-ai/grok-4.20`, `z-ai/glm-5.1`, `moonshotai/kimi-k2.6`, `qwen/qwen3-max-thinking` | Daily driver — reliable tool-calling at ~1/10 the cost |
| **Avoid for agent use** | `*-nano`, `*-flash-lite`, `*-coder-next`, small / distilled variants | Tool-calling is unreliable — the agent will appear to "answer from memory" instead of loading skills or running backtests |
The default `agent/.env.example` ships with DeepSeek official API + `deepseek-v4-pro`; OpenRouter users can use `deepseek/deepseek-v4-pro`.
The default `agent/.env.example` uses OpenRouter + `deepseek/deepseek-v4-pro`; for the DeepSeek official API, select `LANGCHAIN_PROVIDER=deepseek` and `deepseek-v4-pro`.
---
@@ -1978,7 +1980,7 @@ Vibe-Trading is part of the **[HKUDS](https://github.com/HKUDS)** agent ecosyste
| Phase | Feature | Status |
|-------|---------|--------|
| **Trust Layer** | Reproducible run cards are emitted and shown in Run Detail; v1 adds tool traces and citations | v0 Shipped |
| **Trust Layer** | Run cards display hashed backtest execution records and verified CSV metric references in Run Detail and Markdown; existing cards remain readable | Backtest trace + citation display shipped |
| **Hypothesis Registry** | Durable research hypotheses with lifecycle status, data sources, skills, run-card links, and invalidation notes | Backend MVP Shipped |
| **Research Autopilot** | Manual-first research loop: hypothesis → deterministic backtest → evidence report | Phase 1–3 Shipped |
| **Data Bridge** | Bring-your-own data: local CSV/Parquet/SQL connectors with schema mapping | Local loader Shipped |
+4 -2
View File
@@ -52,6 +52,8 @@
> ⚠️ **تحذير أمني:** حساب X باسم `VibeTrading_HKU`، ومشروع Virtuals رقم `101845`، وعقد التوكن `0x640BDBF77b6447E8b7DB7894cED84BD1c40571f4` كلّها غير رسمية ولا تتبع Vibe-Trading. لم نُطلق أو نؤيد مطلقًا أي توكن أو عملة ميم. لا تشترِ هذا التوكن، ولا تربط محفظتك، ولا توقّع أي شيء. [التفاصيل](SECURITY.md#official-channels--impersonation).
- **2026-09-27** 🛠️ **بطاقات تشغيل قابلة للتحقق وتحسينات للبحث**: تعرض JSON وMarkdown وصفحة تفاصيل التشغيل سجلات تنفيذ الاختبار التاريخي بصماتٍ مشفّرة ومراجع موثقة لمقاييس CSV ([#1612](https://github.com/HKUDS/Vibe-Trading/pull/1612)). صُحّح حساب الأيام وحالات التعادل في عوامل GTJA ([#1604](https://github.com/HKUDS/Vibe-Trading/pull/1604))، ويُفحص إغلاق المراكز القصيرة في الهند وفق حد السعر لجهة الشراء ([#1608](https://github.com/HKUDS/Vibe-Trading/pull/1608)). تبقى أعداد استخدام الرموز مقروءة دون السماح بسلاسل اعتماد عشوائية ([#1606](https://github.com/HKUDS/Vibe-Trading/pull/1606))، وتُستعاد خصائص مخططات MCP غير الصالحة ([#1607](https://github.com/HKUDS/Vibe-Trading/pull/1607)). يسمح فحص أهداف الأوامر الفورية بأسئلة البحث ([#1605](https://github.com/HKUDS/Vibe-Trading/pull/1605))، وتوضح الوثائق أن OpenRouter هو الإعداد الافتراضي ([#1609](https://github.com/HKUDS/Vibe-Trading/issues/1609)).
- **2026-09-26** 🛠️ **مؤشرات وحدود تداول أكثر موثوقية**: تحتفظ المؤشرات الفنية بتاريخ الرصد ويمكنها إعادة استخدام النتائج التي أزيلت أثناء ضغط المحادثة ([#1590](https://github.com/HKUDS/Vibe-Trading/pull/1590)، [#1601](https://github.com/HKUDS/Vibe-Trading/pull/1601)). تستخدم نوافذ WVMA الخمس في Qlib158 العوائد المطلقة في البسط ([#1594](https://github.com/HKUDS/Vibe-Trading/pull/1594))، ويحافظ فحص Benford على الرقم الأول الصحيح عند الحدود العددية ([#1591](https://github.com/HKUDS/Vibe-Trading/pull/1591)). يبقى حد التعرض المحدد صراحةً بصفر صفراً ([#1593](https://github.com/HKUDS/Vibe-Trading/pull/1593))، وترفض أوامر Dhan التجريبية الكميات الكسرية أو غير الصالحة دون فقدان دقة المدخلات بسبب التقريب ([#1595](https://github.com/HKUDS/Vibe-Trading/pull/1595)). تعود متغيرات البيئة العددية الاختيارية غير الصالحة إلى القيم الافتراضية مع استمرار تطبيق قيود الإعدادات الصريحة ([#1592](https://github.com/HKUDS/Vibe-Trading/pull/1592)). كما أُصلحت القيم الافتراضية لحقول مشغلات وقت التشغيل وسلوك إنشاء مثيلاتها في الأصناف الفرعية ([#1599](https://github.com/HKUDS/Vibe-Trading/pull/1599)).
- **2026-09-25** 🛠️ **جلسات بحث وتشخيص قنوات أكثر موثوقية**: لم يعد وجود عمود نصي في ملف CSV للاختبار التاريخي يؤدي إلى فقدان المقاييس الرقمية ([#1579](https://github.com/HKUDS/Vibe-Trading/pull/1579))، وأصبح ضغط المحادثة يحتسب محتوى الاستدلال ضمن ميزانية الرسائل المحتفظ بها ([#1582](https://github.com/HKUDS/Vibe-Trading/pull/1582)). تُسجَّل الآن الاستثناءات الفعلية عند فشل تحميل القنوات ([#1580](https://github.com/HKUDS/Vibe-Trading/pull/1580))، ويوضح دليل Telegram أن أوامر CLI وعناصر تحكم الويب تشترك في بيئة تشغيل API نفسها ([#1583](https://github.com/HKUDS/Vibe-Trading/pull/1583)). ترفض قراءة محفظة Robinhood كائنات القوة الشرائية ذات البنية غير الصحيحة، مع إبقاء القيم المحذوفة أو null غير معروفة ([#1526](https://github.com/HKUDS/Vibe-Trading/pull/1526)). كما تُلغى مؤقتات تمرير السجل المعلقة عند مغادرة المحادثة. شكرًا [@Shizoqua](https://github.com/Shizoqua) و[@lorenzozanee](https://github.com/lorenzozanee)!
@@ -926,7 +928,7 @@ Vibe-Trading وكيل كثيف الأدوات؛ المهارات والاختب
| **النقطة المثلى** (افتراضي) | `deepseek-v4-pro`, `deepseek/deepseek-v4-pro`, `x-ai/grok-4.20`, `z-ai/glm-5.1`, `moonshotai/kimi-k2.6`, `qwen/qwen3-max-thinking` | الاستخدام اليومي، tool-calling موثوق بنحو عُشر التكلفة |
| **تجنبها لاستخدام الوكيل** | `*-nano`, `*-flash-lite`, `*-coder-next`, small / distilled variants | tool-calling غير موثوق؛ سيبدو الوكيل وكأنه "يجيب من الذاكرة" بدلاً من تحميل المهارات أو تشغيل الاختبارات الرجعية |
يأتي `agent/.env.example` افتراضياً مع DeepSeek official API + `deepseek-v4-pro`; ويمكن لمستخدمي OpenRouter استخدام `deepseek/deepseek-v4-pro`.
يستخدم `agent/.env.example` افتراضياً OpenRouter + `deepseek/deepseek-v4-pro`؛ لاستخدام واجهة DeepSeek الرسمية، اختر `LANGCHAIN_PROVIDER=deepseek` و`deepseek-v4-pro`.
---
@@ -1765,7 +1767,7 @@ Vibe-Trading جزء من نظام وكلاء **[HKUDS](https://github.com/HKUDS)
| المرحلة | الميزة | الحالة |
|-------|---------|--------|
| **Trust Layer** | بطاقات تشغيل قابلة لإعادة الإنتاج تُنتج وتظهر في Run Detail؛ يضيف v1 آثار الأدوات والاستشهادات | v0 شُحن |
| **Trust Layer** | سجلات بصمات تنفيذ الاختبار التاريخي ومراجع مقاييس CSV الموثقة في تفاصيل التشغيل وMarkdown، مع دعم البطاقات السابقة | عرض السجلات والمراجع متاح |
| **Hypothesis Registry** | فرضيات بحثية دائمة مع حالة lifecycle ومصادر بيانات ومهارات وروابط run-card وملاحظات إبطال | Backend MVP شُحن |
| **Research Autopilot** | حلقة بحث يدوية أولاً: فرضية → اختبار رجعي حتمي → تقرير أدلة | المراحل 1–3 شُحنت |
| **Data Bridge** | أحضر بياناتك: موصلات CSV/Parquet/SQL محلية مع schema mapping | المُحمِّل المحلي شُحن |
+4 -2
View File
@@ -52,6 +52,8 @@
> ⚠️ **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-09-27** 🛠️ **Tarjetas verificables y mejoras en el análisis**: JSON, Markdown y Run Detail muestran registros hash de ejecución del backtest y referencias verificadas a métricas CSV ([#1612](https://github.com/HKUDS/Vibe-Trading/pull/1612)). Se corrigen el recuento de días y los empates en factores GTJA ([#1604](https://github.com/HKUDS/Vibe-Trading/pull/1604)), y el cierre de cortos en India comprueba el límite de precio del lado comprador ([#1608](https://github.com/HKUDS/Vibe-Trading/pull/1608)). El uso de tokens sigue visible sin permitir cadenas de credenciales arbitrarias ([#1606](https://github.com/HKUDS/Vibe-Trading/pull/1606)); se recuperan las propiedades de esquemas MCP malformados ([#1607](https://github.com/HKUDS/Vibe-Trading/pull/1607)). El filtro de órdenes inmediatas permite preguntas de análisis ([#1605](https://github.com/HKUDS/Vibe-Trading/pull/1605)), y la documentación identifica OpenRouter como configuración predeterminada ([#1609](https://github.com/HKUDS/Vibe-Trading/issues/1609)).
- **2026-09-26** 🛠️ **Indicadores y límites de negociación más fiables**: Los indicadores técnicos conservan la fecha de observación y pueden reutilizar resultados eliminados al compactar la conversación ([#1590](https://github.com/HKUDS/Vibe-Trading/pull/1590), [#1601](https://github.com/HKUDS/Vibe-Trading/pull/1601)). Las cinco ventanas WVMA de Qlib158 usan rendimientos absolutos en el numerador ([#1594](https://github.com/HKUDS/Vibe-Trading/pull/1594)); la comprobación de Benford conserva el primer dígito correcto en los límites numéricos ([#1591](https://github.com/HKUDS/Vibe-Trading/pull/1591)). Un límite de exposición fijado explícitamente en cero permanece en cero ([#1593](https://github.com/HKUDS/Vibe-Trading/pull/1593)), y las órdenes simuladas de Dhan rechazan cantidades fraccionarias o inválidas sin perder precisión por redondeo ([#1595](https://github.com/HKUDS/Vibe-Trading/pull/1595)). Las variables de entorno numéricas opcionales inválidas vuelven a sus valores predeterminados, manteniendo las restricciones de la configuración explícita ([#1592](https://github.com/HKUDS/Vibe-Trading/pull/1592)). Los activadores de ejecución conservan los valores predeterminados de sus campos y el comportamiento correcto de sus métodos de creación en subclases ([#1599](https://github.com/HKUDS/Vibe-Trading/pull/1599)).
- **2026-09-25** 🛠️ **Sesiones de análisis y diagnóstico de canales más fiables**: Una columna de texto en el CSV de un backtest ya no descarta las métricas numéricas ([#1579](https://github.com/HKUDS/Vibe-Trading/pull/1579)), y la compactación de conversaciones incluye el contenido de razonamiento en el presupuesto de mensajes conservados ([#1582](https://github.com/HKUDS/Vibe-Trading/pull/1582)). Los fallos al cargar canales registran la excepción real ([#1580](https://github.com/HKUDS/Vibe-Trading/pull/1580)); la guía de Telegram aclara que los controles CLI y Web comparten el mismo entorno de ejecución de la API ([#1583](https://github.com/HKUDS/Vibe-Trading/pull/1583)). La lectura de carteras de Robinhood rechaza objetos de poder de compra con formato incorrecto y mantiene como desconocidos los valores omitidos o null ([#1526](https://github.com/HKUDS/Vibe-Trading/pull/1526)). Al salir del chat también se cancelan los temporizadores pendientes de desplazamiento del historial. ¡Gracias a [@Shizoqua](https://github.com/Shizoqua) y [@lorenzozanee](https://github.com/lorenzozanee)!
@@ -962,7 +964,7 @@ Vibe-Trading es un agente que depende intensamente de herramientas: skills, back
| **Sweet spot** (predeterminado) | `deepseek-v4-pro`, `deepseek/deepseek-v4-pro`, `x-ai/grok-4.20`, `z-ai/glm-5.1`, `moonshotai/kimi-k2.6`, `qwen/qwen3-max-thinking` | Opción diaria — llamadas a herramientas confiables a ~1/10 del costo |
| **Evitar para uso en agentes** | `*-nano`, `*-flash-lite`, `*-coder-next`, variantes pequeñas / destiladas | Las llamadas a herramientas son poco confiables — el agente parecerá "responder de memoria" en lugar de cargar skills o ejecutar backtests |
El `agent/.env.example` predeterminado viene configurado con la API oficial de DeepSeek + `deepseek-v4-pro`; los usuarios de OpenRouter pueden usar `deepseek/deepseek-v4-pro`.
El `agent/.env.example` predeterminado usa OpenRouter + `deepseek/deepseek-v4-pro`; para la API oficial de DeepSeek, selecciona `LANGCHAIN_PROVIDER=deepseek` y `deepseek-v4-pro`.
---
@@ -1853,7 +1855,7 @@ Vibe-Trading forma parte del ecosistema de agentes de **[HKUDS](https://github.c
| Fase | Función | Estado |
|-------|---------|--------|
| **Trust Layer** | Se emiten run cards reproducibles y se muestran en Run Detail; v1 añade tool traces y citations | v0 publicado |
| **Trust Layer** | Registros hash del backtest y referencias verificadas a métricas CSV en Run Detail y Markdown; compatibilidad con tarjetas existentes | Registros y referencias de backtest disponibles |
| **Hypothesis Registry** | Hipótesis de investigación duraderas con lifecycle status, data sources, skills, enlaces a run-card y notas de invalidación | MVP de backend publicado |
| **Research Autopilot** | Bucle de investigación manual-primero: hipótesis → backtest determinista → evidence report | Fases 1–3 publicadas |
| **Data Bridge** | Trae tus propios datos: conectores locales CSV/Parquet/SQL con mapeo de esquema | Cargador local publicado |
+4 -2
View File
@@ -53,6 +53,8 @@
> ⚠️ **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-09-27** 🛠️ **Kartu eksekusi yang dapat diverifikasi dan perbaikan riset**: JSON, Markdown, dan Run Detail menampilkan catatan hash eksekusi backtest serta referensi metrik CSV yang terverifikasi ([#1612](https://github.com/HKUDS/Vibe-Trading/pull/1612)). Hitungan hari dan nilai ekstrem seri pada faktor GTJA diperbaiki ([#1604](https://github.com/HKUDS/Vibe-Trading/pull/1604)); penutupan posisi short India memeriksa batas harga sisi beli ([#1608](https://github.com/HKUDS/Vibe-Trading/pull/1608)). Penggunaan token tetap terbaca tanpa meloloskan string kredensial sembarang ([#1606](https://github.com/HKUDS/Vibe-Trading/pull/1606)), properti skema MCP yang rusak dipulihkan ([#1607](https://github.com/HKUDS/Vibe-Trading/pull/1607)), dan pemeriksaan tujuan pesanan langsung tetap mengizinkan pertanyaan riset ([#1605](https://github.com/HKUDS/Vibe-Trading/pull/1605)). Dokumentasi kini menyatakan OpenRouter sebagai pengaturan bawaan ([#1609](https://github.com/HKUDS/Vibe-Trading/issues/1609)).
- **2026-09-26** 🛠️ **Indikator dan batas perdagangan lebih andal**: Indikator teknis mempertahankan tanggal pengamatan dan dapat memakai kembali hasil yang terhapus saat percakapan dipadatkan ([#1590](https://github.com/HKUDS/Vibe-Trading/pull/1590), [#1601](https://github.com/HKUDS/Vibe-Trading/pull/1601)). Kelima jendela WVMA Qlib158 memakai imbal hasil absolut pada pembilang ([#1594](https://github.com/HKUDS/Vibe-Trading/pull/1594)); pemeriksaan Benford mempertahankan digit pertama yang benar pada batas numerik ([#1591](https://github.com/HKUDS/Vibe-Trading/pull/1591)). Batas eksposur yang secara eksplisit ditetapkan nol tetap nol ([#1593](https://github.com/HKUDS/Vibe-Trading/pull/1593)), dan pesanan simulasi Dhan menolak jumlah pecahan atau tidak valid tanpa kehilangan presisi akibat pembulatan ([#1595](https://github.com/HKUDS/Vibe-Trading/pull/1595)). Variabel lingkungan numerik opsional yang tidak valid kembali ke nilai bawaan, sementara batasan konfigurasi eksplisit tetap berlaku ([#1592](https://github.com/HKUDS/Vibe-Trading/pull/1592)). Pemicu runtime juga mempertahankan nilai bawaan kolom dan perilaku metode pembuat objek pada subkelas dengan benar ([#1599](https://github.com/HKUDS/Vibe-Trading/pull/1599)).
- **2026-09-25** 🛠️ **Sesi riset dan diagnostik channel lebih andal**: Kolom teks dalam CSV backtest tidak lagi menghilangkan seluruh metrik numerik ([#1579](https://github.com/HKUDS/Vibe-Trading/pull/1579)), dan pemadatan percakapan kini menghitung konten penalaran dalam anggaran pesan yang dipertahankan ([#1582](https://github.com/HKUDS/Vibe-Trading/pull/1582)). Kegagalan memuat channel mencatat pengecualian yang sebenarnya ([#1580](https://github.com/HKUDS/Vibe-Trading/pull/1580)); panduan Telegram memperjelas bahwa kontrol CLI dan Web berbagi runtime API yang sama ([#1583](https://github.com/HKUDS/Vibe-Trading/pull/1583)). Pembacaan portofolio Robinhood menolak objek daya beli dengan format yang salah, sementara nilai yang tidak disertakan atau null tetap dinyatakan tidak diketahui ([#1526](https://github.com/HKUDS/Vibe-Trading/pull/1526)). Meninggalkan halaman chat juga membatalkan timer pengguliran riwayat yang masih tertunda. Terima kasih [@Shizoqua](https://github.com/Shizoqua) dan [@lorenzozanee](https://github.com/lorenzozanee)!
@@ -1149,7 +1151,7 @@ Vibe-Trading adalah agent yang sangat bergantung pada tool — skill, backtest,
| **Sweet spot** (default) | `deepseek-v4-pro`, `deepseek/deepseek-v4-pro`, `x-ai/grok-4.20`, `z-ai/glm-5.1`, `moonshotai/kimi-k2.6`, `qwen/qwen3-max-thinking` | Daily driver — tool-calling andal dengan biaya sekitar 1/10 |
| **Hindari untuk penggunaan agent** | `*-nano`, `*-flash-lite`, `*-coder-next`, varian kecil / distilled | Tool-calling tidak andal — agent akan terlihat "menjawab dari memory" alih-alih memuat skill atau menjalankan backtest |
Default `agent/.env.example` menggunakan API resmi DeepSeek + `deepseek-v4-pro`; pengguna OpenRouter dapat memakai `deepseek/deepseek-v4-pro`.
Default `agent/.env.example` memakai OpenRouter + `deepseek/deepseek-v4-pro`; untuk API resmi DeepSeek, pilih `LANGCHAIN_PROVIDER=deepseek` dan `deepseek-v4-pro`.
---
@@ -2099,7 +2101,7 @@ Vibe-Trading adalah bagian dari ekosistem agent **[HKUDS](https://github.com/HKU
| Fase | Fitur | Status |
|-------|---------|--------|
| **Trust Layer** | Run card reproducible dibuat dan ditampilkan di Run Detail; v1 menambahkan tool trace dan citation | v0 Dirilis |
| **Trust Layer** | Catatan hash eksekusi backtest dan referensi metrik CSV terverifikasi di Run Detail dan Markdown; kartu lama tetap terbaca | Tampilan catatan dan referensi backtest tersedia |
| **Hypothesis Registry** | Hipotesis riset persisten dengan status lifecycle, sumber data, skill, link run-card, dan catatan invalidasi | Backend MVP Dirilis |
| **Research Autopilot** | Loop riset manual-first: hipotesis → backtest deterministik → laporan bukti | Fase 1–3 Dirilis |
| **Data Bridge** | Bawa data sendiri: connector CSV/Parquet/SQL lokal dengan schema mapping | Loader lokal Dirilis |
+4 -2
View File
@@ -52,6 +52,8 @@
> ⚠️ **セキュリティ警告:** Xアカウント `VibeTrading_HKU`、Virtualsプロジェクト `101845`、およびトークンコントラクト `0x640BDBF77b6447E8b7DB7894cED84BD1c40571f4` は、いずれもVibe-Trading公式のものではありません。Vibe-Tradingはこれまで、いかなるトークンやミームコインも発行・公認していません。購入、ウォレットの接続、署名は行わないでください。[詳細](SECURITY.md#official-channels--impersonation)
- **2026-09-27** 🛠️ **検証可能な実行カードと研究フローの改善**:JSON、Markdown、実行詳細に、ハッシュ化したバックテスト実行記録と検証済みの指標 CSV 参照を表示します([#1612](https://github.com/HKUDS/Vibe-Trading/pull/1612))。GTJA の高値・安値経過日数と同値時の処理を修正し([#1604](https://github.com/HKUDS/Vibe-Trading/pull/1604))、インド市場の空売り買い戻しは買い側の値幅制限を確認します([#1608](https://github.com/HKUDS/Vibe-Trading/pull/1608))。認証情報を露出せずトークン使用量を保持し([#1606](https://github.com/HKUDS/Vibe-Trading/pull/1606))、不正な MCP スキーマのオブジェクト属性を補完します([#1607](https://github.com/HKUDS/Vibe-Trading/pull/1607))。即時注文の目標チェックは研究目的の質問を許可し([#1605](https://github.com/HKUDS/Vibe-Trading/pull/1605))、設定文書の既定経路を OpenRouter と明記しました([#1609](https://github.com/HKUDS/Vibe-Trading/issues/1609))。
- **2026-09-26** 🛠️ **指標計算と取引上限の信頼性を改善**:テクニカル指標は観測日を保持し、会話の圧縮で失われた結果を再利用できます([#1590](https://github.com/HKUDS/Vibe-Trading/pull/1590)、[#1601](https://github.com/HKUDS/Vibe-Trading/pull/1601))。Qlib158 の全5種類の WVMA 窓で分子に絶対リターンを使用し([#1594](https://github.com/HKUDS/Vibe-Trading/pull/1594))、Benford 検査は数値の境界でも正しい先頭桁を保持します([#1591](https://github.com/HKUDS/Vibe-Trading/pull/1591))。明示的にゼロに設定したエクスポージャー上限はゼロのまま維持され([#1593](https://github.com/HKUDS/Vibe-Trading/pull/1593))、Dhan の模擬注文は入力精度を丸めず、小数や無効な数量を拒否します([#1595](https://github.com/HKUDS/Vibe-Trading/pull/1595))。任意の数値環境変数が無効な場合はデフォルト値に戻り、明示的な設定の制約は引き続き適用されます([#1592](https://github.com/HKUDS/Vibe-Trading/pull/1592))。実行時トリガーのフィールド初期値とサブクラスのファクトリ動作も修正しました([#1599](https://github.com/HKUDS/Vibe-Trading/pull/1599))。
- **2026-09-25** 🛠️ **リサーチセッションとチャネル診断を改善**:バックテスト CSV に文字列の列があっても数値指標が失われなくなり([#1579](https://github.com/HKUDS/Vibe-Trading/pull/1579))、会話圧縮で保持するメッセージの容量計算に推論内容を含めるようになりました([#1582](https://github.com/HKUDS/Vibe-Trading/pull/1582))。チャネルの読み込み失敗時には実際の例外を記録し([#1580](https://github.com/HKUDS/Vibe-Trading/pull/1580))、Telegram ガイドでは CLI と Web の操作が同じ API ランタイムを共有することを明記しました([#1583](https://github.com/HKUDS/Vibe-Trading/pull/1583))。Robinhood のポートフォリオ取得は不正な形式の購買力オブジェクトを拒否し、省略値や null は不明のまま保持します([#1526](https://github.com/HKUDS/Vibe-Trading/pull/1526))。チャット画面を離れると、履歴のスクロール待機タイマーも解除されます。[@Shizoqua](https://github.com/Shizoqua) と [@lorenzozanee](https://github.com/lorenzozanee) に感謝!
@@ -931,7 +933,7 @@ Vibe-Trading は tool-heavy agent です。skills、backtests、memory、swarms
| **Sweet spot** (default) | `deepseek-v4-pro`, `deepseek/deepseek-v4-pro`, `x-ai/grok-4.20`, `z-ai/glm-5.1`, `moonshotai/kimi-k2.6`, `qwen/qwen3-max-thinking` | 日常使い。信頼できる tool-calling を約 1/10 の cost で |
| **agent 用途では避ける** | `*-nano`, `*-flash-lite`, `*-coder-next`, small / distilled variants | Tool-calling が不安定です。agent は skills 読み込みや backtests 実行ではなく「記憶から答えている」ように見えます |
既定の `agent/.env.example` は DeepSeek official API + `deepseek-v4-pro` で出荷されています。OpenRouter users は `deepseek/deepseek-v4-pro` を利用できます。
既定の `agent/.env.example` は OpenRouter + `deepseek/deepseek-v4-pro` を使用します。DeepSeek 公式 API を使う場合は `LANGCHAIN_PROVIDER=deepseek` と `deepseek-v4-pro` を選択してください。
---
@@ -1773,7 +1775,7 @@ Vibe-Trading は **[HKUDS](https://github.com/HKUDS)** agent ecosystem の一部
| Phase | Feature | Status |
|-------|---------|--------|
| **Trust Layer** | 再現可能な run cards は出力・Run Detail 表示まで完了。v1 では tool traces と citations を追加 | v0 出荷済み |
| **Trust Layer** | 実行詳細と Markdown にバックテスト実行ハッシュと検証済み CSV 指標参照を表示。既存カードも閲覧可能 | バックテスト記録・参照表示を実装 |
| **Hypothesis Registry** | lifecycle status、data sources、skills、run-card links、invalidation notes を持つ永続リサーチ仮説 | Backend MVP 出荷済み |
| **Research Autopilot** | 手動実行から始める research loop: hypothesis → deterministic backtest → evidence report | フェーズ1–3 出荷済み |
| **Data Bridge** | Bring-your-own data: local CSV/Parquet/SQL connectors with schema mapping | ローカルローダー出荷済み |
+4 -2
View File
@@ -52,6 +52,8 @@
> ⚠️ **보안 경고:** X 계정 `VibeTrading_HKU`, Virtuals 프로젝트 `101845`, 토큰 컨트랙트 `0x640BDBF77b6447E8b7DB7894cED84BD1c40571f4`는 모두 Vibe-Trading 공식과 무관합니다. Vibe-Trading은 어떠한 토큰이나 밈코인도 발행하거나 공식적으로 지지한 적이 없습니다. 해당 토큰을 구매하거나 지갑을 연결하거나 어떠한 서명도 하지 마세요. [자세히 보기](SECURITY.md#official-channels--impersonation).
- **2026-09-27** 🛠️ **검증 가능한 실행 카드와 연구 흐름 개선**: JSON, Markdown, 실행 상세 화면에 해시 기반 백테스트 실행 기록과 검증된 지표 CSV 참조를 표시합니다([#1612](https://github.com/HKUDS/Vibe-Trading/pull/1612)). GTJA 고점·저점 경과 일수와 동률 처리를 수정하고([#1604](https://github.com/HKUDS/Vibe-Trading/pull/1604)), 인도 시장 숏 청산은 매수 방향의 가격 제한을 확인합니다([#1608](https://github.com/HKUDS/Vibe-Trading/pull/1608)). 자격 증명 문자열을 노출하지 않고 토큰 사용량을 유지하며([#1606](https://github.com/HKUDS/Vibe-Trading/pull/1606)), 잘못된 MCP 스키마의 객체 속성을 복구합니다([#1607](https://github.com/HKUDS/Vibe-Trading/pull/1607)). 즉시 주문 목표 검사는 연구 질문을 허용하고([#1605](https://github.com/HKUDS/Vibe-Trading/pull/1605)), 설정 문서에 기본 경로가 OpenRouter임을 명시했습니다([#1609](https://github.com/HKUDS/Vibe-Trading/issues/1609)).
- **2026-09-26** 🛠️ **지표 계산과 거래 한도 개선**: 기술적 지표는 관측 날짜를 유지하고 대화 압축으로 사라진 결과를 재사용할 수 있습니다([#1590](https://github.com/HKUDS/Vibe-Trading/pull/1590), [#1601](https://github.com/HKUDS/Vibe-Trading/pull/1601)). Qlib158의 다섯 WVMA 기간 모두 분자에 절대 수익률을 사용하며([#1594](https://github.com/HKUDS/Vibe-Trading/pull/1594)), Benford 검사는 수치 경계에서도 올바른 첫 자릿수를 유지합니다([#1591](https://github.com/HKUDS/Vibe-Trading/pull/1591)). 명시적으로 0으로 설정한 익스포저 한도는 그대로 유지되고([#1593](https://github.com/HKUDS/Vibe-Trading/pull/1593)), Dhan 모의 주문은 입력 정밀도를 반올림으로 잃지 않으면서 소수 및 잘못된 수량을 거부합니다([#1595](https://github.com/HKUDS/Vibe-Trading/pull/1595)). 잘못된 선택적 숫자 환경 변수는 기본값으로 돌아가며 명시적 설정의 제약은 계속 적용됩니다([#1592](https://github.com/HKUDS/Vibe-Trading/pull/1592)). 런타임 트리거의 필드 기본값과 하위 클래스 팩터리 동작도 수정했습니다([#1599](https://github.com/HKUDS/Vibe-Trading/pull/1599)).
- **2026-09-25** 🛠️ **리서치 세션과 채널 진단 개선**: 백테스트 CSV에 텍스트 열이 있어도 숫자 지표가 통째로 사라지지 않으며([#1579](https://github.com/HKUDS/Vibe-Trading/pull/1579)), 대화 압축 시 보존할 메시지의 용량 계산에 추론 내용도 포함합니다([#1582](https://github.com/HKUDS/Vibe-Trading/pull/1582)). 채널 로드 실패 시 실제 예외를 기록하고([#1580](https://github.com/HKUDS/Vibe-Trading/pull/1580)), Telegram 가이드에 CLI와 Web 제어가 같은 API 런타임을 공유한다는 점을 명시했습니다([#1583](https://github.com/HKUDS/Vibe-Trading/pull/1583)). Robinhood 포트폴리오 조회는 잘못된 형식의 구매력 객체를 거부하되, 누락되거나 null인 값은 알 수 없는 값으로 유지합니다([#1526](https://github.com/HKUDS/Vibe-Trading/pull/1526)). 채팅 화면을 떠나면 대기 중인 기록 스크롤 타이머도 취소합니다. [@Shizoqua](https://github.com/Shizoqua), [@lorenzozanee](https://github.com/lorenzozanee) 감사합니다!
@@ -929,7 +931,7 @@ Vibe-Trading은 tool-heavy agent입니다. skills, backtests, memory, swarms가
| **Sweet spot**(기본값) | `deepseek-v4-pro`, `deepseek/deepseek-v4-pro`, `x-ai/grok-4.20`, `z-ai/glm-5.1`, `moonshotai/kimi-k2.6`, `qwen/qwen3-max-thinking` | Daily driver — 약 1/10 비용으로 안정적인 tool-calling |
| **Agent 사용 시 피할 것** | `*-nano`, `*-flash-lite`, `*-coder-next`, small / distilled variants | tool-calling이 불안정합니다. agent가 skills를 로드하거나 backtest를 실행하는 대신 "기억에서 답하는" 것처럼 보일 수 있습니다. |
기본 `agent/.env.example`은 DeepSeek official API + `deepseek-v4-pro`를 포함합니다. OpenRouter 사용자는 `deepseek/deepseek-v4-pro`를 사용할 수 있습니다.
기본 `agent/.env.example`은 OpenRouter + `deepseek/deepseek-v4-pro`를 사용합니다. DeepSeek 공식 API를 사용하려면 `LANGCHAIN_PROVIDER=deepseek`와 `deepseek-v4-pro`를 선택하세요.
---
@@ -1770,7 +1772,7 @@ Vibe-Trading은 **[HKUDS](https://github.com/HKUDS)** agent ecosystem의 일부
| Phase | Feature | Status |
|-------|---------|--------|
| **Trust Layer** | 재현 가능한 run cards는 생성 및 Run Detail 표시까지 완료. v1은 tool traces와 citations 추가 | v0 출시 |
| **Trust Layer** | 실행 상세와 Markdown에 백테스트 실행 해시 및 검증된 CSV 지표 참조 표시, 기존 카드 호환 | 백테스트 기록·참조 표시 구현 |
| **Hypothesis Registry** | lifecycle status, data sources, skills, run-card links, invalidation notes를 가진 durable research hypotheses | Backend MVP 출시 |
| **Research Autopilot** | 수동 실행 우선 research loop: hypothesis → deterministic backtest → evidence report | 1–3단계 출시 |
| **Data Bridge** | Bring-your-own data: local CSV/Parquet/SQL connectors with schema mapping | 로컬 로더 출시 |
+5 -3
View File
@@ -52,6 +52,8 @@
> ⚠️ **安全警告:** X 账号 `VibeTrading_HKU`、Virtuals 项目 `101845` 及代币合约 `0x640BDBF77b6447E8b7DB7894cED84BD1c40571f4` 均非 Vibe-Trading 官方。我们从未发行或背书任何代币或 meme 币。请勿购买、连接钱包或签名。[详细说明](SECURITY.md#official-channels--impersonation)。
- **2026-09-27** 🛠️ **可复核的运行卡片与更可靠的研究流程**:运行卡片在 JSON、Markdown 和运行详情页展示回测执行哈希记录及已核验的指标 CSV 引用([#1612](https://github.com/HKUDS/Vibe-Trading/pull/1612))。GTJA 高低点远近因子修正计日与并列极值处理([#1604](https://github.com/HKUDS/Vibe-Trading/pull/1604)),印度市场回测平空仓按买入方向检查涨跌停([#1608](https://github.com/HKUDS/Vibe-Trading/pull/1608))。token 用量可读,同时不放行任意凭据字符串([#1606](https://github.com/HKUDS/Vibe-Trading/pull/1606));异常 MCP schema 保留对象属性([#1607](https://github.com/HKUDS/Vibe-Trading/pull/1607));即时下单目标检查不再误拦研究问题([#1605](https://github.com/HKUDS/Vibe-Trading/pull/1605))。配置文档明确默认使用 OpenRouter([#1609](https://github.com/HKUDS/Vibe-Trading/issues/1609))。
- **2026-09-26** 🛠️ **指标计算与交易限额更可靠**:技术指标保留观测日期,并能恢复对话压缩时移除的结果,避免重复取数([#1590](https://github.com/HKUDS/Vibe-Trading/pull/1590)、[#1601](https://github.com/HKUDS/Vibe-Trading/pull/1601))。Qlib158 的五个 WVMA 窗口统一使用绝对收益率计算分子([#1594](https://github.com/HKUDS/Vibe-Trading/pull/1594));Benford 检查在数值边界也能保留正确首位数字([#1591](https://github.com/HKUDS/Vibe-Trading/pull/1591))。明确设为零的敞口上限保持为零([#1593](https://github.com/HKUDS/Vibe-Trading/pull/1593)),Dhan 模拟订单拒绝小数及无效数量,不再因舍入丢失输入精度([#1595](https://github.com/HKUDS/Vibe-Trading/pull/1595))。无效的可选数值环境变量回退到默认值,显式配置约束仍然生效([#1592](https://github.com/HKUDS/Vibe-Trading/pull/1592))。运行时触发器修复字段默认值,并保留子类工厂的正确行为([#1599](https://github.com/HKUDS/Vibe-Trading/pull/1599))。
- **2026-09-25** 🛠️ **研究会话与频道诊断更可靠**:回测 CSV 中的文本列不再导致数值指标整行丢失([#1579](https://github.com/HKUDS/Vibe-Trading/pull/1579)),对话压缩的保留消息预算现在计入推理内容([#1582](https://github.com/HKUDS/Vibe-Trading/pull/1582))。频道加载失败时会正确记录异常([#1580](https://github.com/HKUDS/Vibe-Trading/pull/1580));Telegram 指南澄清了 CLI 与 Web 控件共享同一个 API 运行时([#1583](https://github.com/HKUDS/Vibe-Trading/pull/1583))。Robinhood 持仓读取拒绝格式错误的购买力对象,同时保留缺失或空值为未知([#1526](https://github.com/HKUDS/Vibe-Trading/pull/1526))。离开聊天页面时也会取消未执行的历史消息滚动定时器。感谢 [@Shizoqua](https://github.com/Shizoqua) 和 [@lorenzozanee](https://github.com/lorenzozanee)!
@@ -936,7 +938,7 @@ Vibe-Trading 是高度依赖工具的智能体:skills、backtests、memory 和
| **Sweet spot**(默认) | `deepseek-v4-pro`, `deepseek/deepseek-v4-pro`, `x-ai/grok-4.20`, `z-ai/glm-5.1`, `moonshotai/kimi-k2.6`, `qwen/qwen3-max-thinking` | 日常主力,约 1/10 成本下具备可靠工具调用 |
| **避免用于 agent** | `*-nano`, `*-flash-lite`, `*-coder-next`, 小型 / 蒸馏变体 | 工具调用不可靠,智能体会看起来像是在“凭记忆回答”,而不是加载 skills 或运行回测 |
默认 `agent/.env.example` 使用 DeepSeek 官方 API + `deepseek-v4-pro`;OpenRouter 用户可以使用 `deepseek/deepseek-v4-pro`。
默认 `agent/.env.example` 使用 OpenRouter + `deepseek/deepseek-v4-pro`;使用 DeepSeek 官方 API 时,请选择 `LANGCHAIN_PROVIDER=deepseek` 和 `deepseek-v4-pro`。
---
@@ -1768,7 +1770,7 @@ Vibe-Trading 是 **[HKUDS](https://github.com/HKUDS)** 智能体生态的一部
| 阶段 | 功能 | 状态 |
|------|------|------|
| **Trust Layer** | 可复现 run cards 已输出并展示在 Run Detail;v1 会补充 tool traces 与 citations | v0 已发布 |
| **Trust Layer** | 运行详情与 Markdown 展示回测执行哈希记录、已核验的 CSV 指标引用;兼容已有卡片 | 回测记录与引用展示已实现 |
| **Hypothesis Registry** | 持久化研究假设:lifecycle status、data sources、skills、run-card links 与 invalidation notes | Backend MVP 已发布 |
| **Research Autopilot** | 手动触发优先的研究循环:hypothesis → deterministic backtest → evidence report | 第 1–3 阶段已发布 |
| **Data Bridge** | 自带数据:本地 CSV/Parquet/SQL connectors 与 schema mapping | 本地加载器已发布 |
@@ -1950,4 +1952,4 @@ MIT License — see [LICENSE](LICENSE)
</p>
<p align="center">
<img src="https://visitor-badge.laobi.icu/badge?page_id=HKUDS.Vibe-Trading&style=flat" alt="visitors"/>
</p>
</p>
+83 -13
View File
@@ -2,7 +2,10 @@
from __future__ import annotations
import csv
import hashlib
import re
from decimal import Decimal, InvalidOperation
import json
import math
from datetime import datetime, timezone
@@ -99,7 +102,7 @@ def write_run_card(
normalized_traces = _normalize_tool_traces(tool_traces)
if normalized_traces:
card["tool_traces"] = normalized_traces
citations = _metric_citations(metrics, card["artifacts"])
citations = _metric_citations(run_dir, metrics, card["artifacts"])
if citations:
card["citations"] = citations
structured = _structured_metrics(metrics)
@@ -284,6 +287,21 @@ def _normalize_artifact_refs(artifact_refs: Sequence[Mapping[str, Any]] | None)
def _normalize_tool_traces(tool_traces: Sequence[Mapping[str, Any]] | None) -> list[dict[str, Any]]:
traces = []
for trace in tool_traces or []:
# Metadata is not a free-text channel for arguments or exception text.
tool = str(trace["tool"])
status = str(trace["status"])
if tool not in {"backtest", "load_data", "generate_signals"}:
raise ValueError("unsupported run-card trace operation")
if status not in {"ok", "error", "cancelled"}:
raise ValueError("unsupported run-card trace status")
times = []
for key in ("started_at", "ended_at"):
stamp = str(trace[key])
if not re.fullmatch(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z", stamp):
raise ValueError("trace timestamps must be UTC ISO timestamps")
times.append(datetime.fromisoformat(stamp.replace("Z", "+00:00")))
if times[1] < times[0]:
raise ValueError("trace ends before it starts")
args = trace["args"]
result = trace["result"]
if not isinstance(args, Mapping) or not isinstance(result, Mapping):
@@ -313,19 +331,55 @@ def _tool_payload_hash(value: Mapping[str, Any]) -> str:
def _metric_citations(
metrics: Mapping[str, Any], artifacts: Sequence[Mapping[str, Any]]
) -> list[dict[str, str]]:
metrics_artifact = next(
(artifact for artifact in artifacts if artifact.get("path") == "artifacts/metrics.csv"),
None,
)
if metrics_artifact is None:
run_dir: Path, metrics: Mapping[str, Any], artifacts: Sequence[Mapping[str, Any]]
) -> list[dict[str, Any]]:
"""Cite only scalar values actually present in the single metrics CSV row.
Args:
run_dir: Run artifact root.
metrics: Values displayed on the card.
artifacts: Artifact manifest with checksums.
Returns:
Verified column references, or no references for missing/malformed CSV.
"""
artifact_id = "artifacts/metrics.csv"
artifact = next((a for a in artifacts if a.get("path") == artifact_id), None)
if artifact is None:
return []
return [
{"metric": key, "artifact_id": "artifacts/metrics.csv"}
for key, value in metrics.items()
if key != "validation" and _is_scalar(value)
]
path = run_dir / artifact_id
if path.is_symlink() or not path.resolve().is_relative_to(run_dir.resolve()):
return []
try:
with path.open(encoding="utf-8", newline="") as stream:
reader = csv.DictReader(stream)
columns = reader.fieldnames or []
row = next(reader, None)
if row is None or next(reader, None) is not None or len(set(columns)) != len(columns):
return []
if _file_hash(path) != artifact["sha256"]:
return []
except (OSError, UnicodeError, csv.Error):
return []
citations = []
for key, value in _scalar_metrics(metrics).items():
if key not in row or value is None or row[key] is None:
continue
cell = row[key]
if isinstance(value, bool):
matches = cell == str(value)
elif isinstance(value, (int, float)):
try:
number = Decimal(cell)
matches = number.is_finite() and number == Decimal(str(value))
except InvalidOperation:
matches = False
else:
matches = cell == str(value)
if matches:
citations.append({"metric": key, "artifact_id": artifact_id,
"column": key, "row": 1, "sha256": artifact["sha256"]})
return citations
def _render_markdown(card: Mapping[str, Any]) -> str:
@@ -388,6 +442,22 @@ def _render_markdown(card: Mapping[str, Any]) -> str:
lines.extend(["", "## Warnings"])
lines.extend(f"- {warning}" for warning in warnings)
lines.extend(["", "## Execution records"])
traces = card.get("tool_traces", [])
if not traces:
lines.append("- No execution records available for this run.")
for trace in traces:
lines.append(f"- {trace['tool']} ({trace['status']}): {trace['started_at']} → {trace['ended_at']}; "
f"args sha256 `{trace['args_hash']}`, result sha256 `{trace['result_hash']}`")
lines.extend(["", "## Metric evidence"])
citations = card.get("citations", [])
if not citations:
lines.append("- No verified metric references available.")
for citation in citations:
lines.append(f"- {citation['metric']}: `{citation['artifact_id']}`, "
f"column `{citation['column']}`, data row {citation['row']}, "
f"sha256 `{citation['sha256']}`")
lines.extend(["", "## Artifacts"])
artifacts = card.get("artifacts", [])
if artifacts:
+1 -1
View File
@@ -73,7 +73,7 @@ PROVIDERS: Final[tuple[Provider, ...]] = (
"cheapest tier — good for batch backtest research",
"deepseek-v4-pro", "DEEPSEEK_API_KEY", "DEEPSEEK_BASE_URL",
"https://api.deepseek.com/v1", "sk-",
("deepseek-v4-pro", "deepseek-v4-flash")),
("deepseek-v4-pro", "deepseek-flash")),
Provider("opencode", "OpenCode", "Go / Zen relay — DeepSeek, GLM, Kimi, Qwen under one key",
"deepseek-v4.1-flash", "OPENCODE_API_KEY", "OPENCODE_BASE_URL",
"https://opencode.ai/zen/go/v1", "sk-",
+20 -14
View File
@@ -4,21 +4,19 @@
## Source
国泰君安证券 (Guotai Junan Securities) 2014 research report
*"191 个短周期交易型 alpha 因子" — 191 Short-Period Transactional Alpha Factors*.
The report is part of Guotai Junan's published Chinese-A-share quantitative research series
and was distributed publicly through the firm's research portal and partner channels.
国泰君安证券 (Guotai Junan Securities),
*基于短周期价量特征的多因子选股体系——数量化专题之九十三*,
2017-06-15. The appendix lists the 191 alpha formulas.
[Public copy of the original report](https://guorn.com/static/upload/file/3/134065454575605.pdf).
## Authors
The report is attributed to the Guotai Junan quantitative research team. Where
individual author names appear on the cover page (光辉 / Guang Hui and 彭祖虎 /
Peng Zuhu in some circulating copies), they are credited to the original work;
this repository does not claim derived authorship of the formulas themselves.
李辰 (Li Chen) and 刘富兵 (Liu Fubing), as credited on the report cover.
This repository does not claim authorship of the formulas.
## Year
2014.
2017.
## What is reproduced here
@@ -71,10 +69,17 @@ The most common substitutions:
propagate NaN rather than silently zero-fill.
- `SUMIF(x, cond, n)` — implemented as `(x * cond).rolling(n).sum()` with
boolean → float coercion documented where it appears.
- `HIGHDAY` / `LOWDAY` use `ts_argmax` / `ts_argmin` with **0-based** positions
inside the window (vs. the report's 1-based convention); a `(n - argmax)`-style
re-base is applied where the formula's downstream arithmetic depends on the
count rather than the index.
- `HIGHDAY` / `LOWDAY` — the operator table on report page 31 defines
the distance from the extreme to the current bar, so today is 0 and the
oldest bar in an n-bar window is n-1. For a 0-based index `i` measured from
the oldest bar, days-since is `(n - 1) - i`, not `n - i`. Thus alphas 103
(page 14) and 177 (page 16) range from 5 to 100 for n=20; alpha 133
(page 15) subtracts the low-recency score from the high-recency score.
The report does not specify ties. These three ports explicitly choose
the **most recent** tied extreme, independently of optional acceleration.
The original table repeats “maximum” on the LOWDAY row; LOWDAY is interpreted
as the minimum, consistent with its name and the low-price formula.
When a benchmark index series (`benchmark_close`) is referenced by the original
formula but unavailable in this panel, we fall back to the per-day
@@ -92,5 +97,6 @@ the look-ahead guard in `tests/factors/test_lookahead.py` (no `Ref(x, -n)`,
## Display name and citation
When citing this re-implementation, please reference:
- Guotai Junan Securities, "191 个短周期交易型 alpha 因子", 2014.
- Li Chen and Liu Fubing, Guotai Junan Securities,
"基于短周期价量特征的多因子选股体系——数量化专题之九十三", 2017-06-15.
- The display name **"GTJA Alpha 191"** for the directory in this repository.
+12 -31
View File
@@ -4,38 +4,19 @@
# 简要说明: 国泰君安191短周期交易型alpha因子第103号,详见公式定义。
# 典型用途: 在A股市场经中性化处理后用于选股或股指期货日内交易。
# ============================================================
"""GTJA Alpha 103 (国泰君安 191 短周期交易型 alpha 因子, 2014).
"""GTJA Alpha 103 (Guotai Junan, 2017-06-15).
Formula (verbatim from the report):
((20-LOWDAY(low,20))/20)*100
Formula (report appendix; source linked in gtja191/LICENSE.md):
((20-LOWDAY(LOW,20))/20)*100
Notes: this repo's LOWDAY convention (see gtja191/LICENSE.md) rebases the
0-based ts_argmin index to the report's day-count via ``n - ts_argmin``, so
substituting that into the formula's own ``n - LOWDAY`` term cancels back to
the raw index: ``(n - (n - ts_argmin)) / n * 100 == ts_argmin / n * 100``.
HIGHDAY/LOWDAY count bars since the extreme: today is 0, not 1.
The report does not specify ties; this port chooses the most recent extreme.
Reversing each complete window makes that convention independent of the
optional bottleneck backend used by the shared ts_argmax/ts_argmin operators.
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from src.factors.base import (
decay_linear,
delta,
rank,
safe_div,
scale,
signed_power,
ts_argmax,
ts_argmin,
ts_corr,
ts_cov,
ts_max,
ts_mean,
ts_min,
ts_rank,
ts_std,
)
ALPHA_ID = "gtja191_103"
@@ -49,7 +30,7 @@ __alpha_meta__ = {
'frequency': ['1d'],
'decay_horizon': 20,
'min_warmup_bars': 20,
'notes': 'LOWDAY rebase cancels against the formula\'s own (20-LOWDAY) term, leaving ts_argmin/20*100.',
'notes': 'Days since the most recent extreme; today=0. Ties choose the latest bar (port policy).',
}
@@ -62,7 +43,7 @@ def compute(panel):
Returns:
pd.DataFrame with index = panel["close"].index, columns = panel["close"].columns.
"""
l = panel["low"]
am = ts_argmin(l, 20)
out = am / 20.0 * 100.0
return out
lowday = panel["low"].rolling(20, min_periods=20).apply(
lambda window: np.argmin(window[::-1]), raw=True
)
return (20.0 - lowday) / 20.0 * 100.0
+14 -30
View File
@@ -4,38 +4,19 @@
# 简要说明: 国泰君安191短周期交易型alpha因子第133号,详见公式定义。
# 典型用途: 在A股市场经中性化处理后用于选股或股指期货日内交易。
# ============================================================
"""GTJA Alpha 133 (国泰君安 191 短周期交易型 alpha 因子, 2014).
"""GTJA Alpha 133 (Guotai Junan, 2017-06-15).
Formula (verbatim from the report):
Formula (report appendix; source linked in gtja191/LICENSE.md):
((20-HIGHDAY(HIGH,20))/20)*100 - ((20-LOWDAY(LOW,20))/20)*100
Notes: this repo's HIGHDAY/LOWDAY convention (see gtja191/LICENSE.md) rebases
the 0-based ts_argmax/ts_argmin index to the report's day-count via
``n - ts_argmax``, so substituting that into each term's own ``n - HIGHDAY``
(or LOWDAY) cancels back to the raw index: ``ts_argmax/n*100 - ts_argmin/n*100``.
HIGHDAY/LOWDAY count bars since the extreme: today is 0, not 1.
The report does not specify ties; this port chooses the most recent extreme.
Reversing each complete window makes that convention independent of the
optional bottleneck backend used by the shared ts_argmax/ts_argmin operators.
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from src.factors.base import (
decay_linear,
delta,
rank,
safe_div,
scale,
signed_power,
ts_argmax,
ts_argmin,
ts_corr,
ts_cov,
ts_max,
ts_mean,
ts_min,
ts_rank,
ts_std,
)
ALPHA_ID = "gtja191_133"
@@ -49,7 +30,7 @@ __alpha_meta__ = {
'frequency': ['1d'],
'decay_horizon': 20,
'min_warmup_bars': 20,
'notes': 'HIGHDAY/LOWDAY rebase cancels against each term\'s own (20-x) wrapper, leaving ts_argmax/20*100 - ts_argmin/20*100.',
'notes': 'Days since the most recent extreme; today=0. Ties choose the latest bar (port policy).',
}
@@ -62,7 +43,10 @@ def compute(panel):
Returns:
pd.DataFrame with index = panel["close"].index, columns = panel["close"].columns.
"""
h = panel["high"]
l = panel["low"]
out = ts_argmax(h, 20) / 20.0 * 100.0 - ts_argmin(l, 20) / 20.0 * 100.0
return out
highday = panel["high"].rolling(20, min_periods=20).apply(
lambda window: np.argmax(window[::-1]), raw=True
)
lowday = panel["low"].rolling(20, min_periods=20).apply(
lambda window: np.argmin(window[::-1]), raw=True
)
return (20.0 - highday) / 20.0 * 100.0 - (20.0 - lowday) / 20.0 * 100.0
+11 -29
View File
@@ -4,38 +4,19 @@
# 简要说明: 国泰君安191短周期交易型alpha因子第177号,详见公式定义。
# 典型用途: 在A股市场经中性化处理后用于选股或股指期货日内交易。
# ============================================================
"""GTJA Alpha 177 (国泰君安 191 短周期交易型 alpha 因子, 2014).
"""GTJA Alpha 177 (Guotai Junan, 2017-06-15).
Formula (verbatim from the report):
Formula (report appendix; source linked in gtja191/LICENSE.md):
((20-HIGHDAY(HIGH,20))/20)*100
Notes: this repo's HIGHDAY convention (see gtja191/LICENSE.md) rebases the
0-based ts_argmax index to the report's day-count via ``n - ts_argmax``, so
substituting that into the formula's own ``n - HIGHDAY`` term cancels back to
the raw index: ``(n - (n - ts_argmax)) / n * 100 == ts_argmax / n * 100``.
HIGHDAY/LOWDAY count bars since the extreme: today is 0, not 1.
The report does not specify ties; this port chooses the most recent extreme.
Reversing each complete window makes that convention independent of the
optional bottleneck backend used by the shared ts_argmax/ts_argmin operators.
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from src.factors.base import (
decay_linear,
delta,
rank,
safe_div,
scale,
signed_power,
ts_argmax,
ts_argmin,
ts_corr,
ts_cov,
ts_max,
ts_mean,
ts_min,
ts_rank,
ts_std,
)
ALPHA_ID = "gtja191_177"
@@ -49,7 +30,7 @@ __alpha_meta__ = {
'frequency': ['1d'],
'decay_horizon': 20,
'min_warmup_bars': 20,
'notes': 'HIGHDAY rebase cancels against the formula\'s own (20-HIGHDAY) term, leaving ts_argmax/20*100.',
'notes': 'Days since the most recent extreme; today=0. Ties choose the latest bar (port policy).',
}
@@ -62,6 +43,7 @@ def compute(panel):
Returns:
pd.DataFrame with index = panel["close"].index, columns = panel["close"].columns.
"""
h = panel["high"]
out = ts_argmax(h, 20) / 20.0 * 100.0
return out
highday = panel["high"].rolling(20, min_periods=20).apply(
lambda window: np.argmax(window[::-1]), raw=True
)
return (20.0 - highday) / 20.0 * 100.0
+1 -1
View File
@@ -18,7 +18,7 @@ _EXECUTION_PATTERNS = (
# Same cue, reversed order ("immediately buy TSLA", "right away sell
# AAPL"): the pattern above only fires when the verb comes first.
re.compile(
r"\b(now|immediately|right away)\b.{0,40}\b(buy|sell|short|long)\b",
r"\b(now|immediately|right away)\b[\s,:-]+(?:please\s+)?(?:buy|sell|short|long)\b",
re.I,
),
re.compile(r"(下单|市价单|限价单|马上买|立即买|现在买|马上卖|立即卖|现在卖)"),
+3 -6
View File
@@ -35,7 +35,7 @@ from src.swarm.models import (
)
from src.tools import build_swarm_registry
from src.tools.mcp import MCPRemoteTool
from src.tools.redaction import is_sensitive_arg, redact_payload, redact_tool_result
from src.tools.redaction import redact_payload, redact_tool_result
logger = logging.getLogger(__name__)
@@ -1156,13 +1156,10 @@ def _tool_arguments(
def _preview_tool_arguments(arguments: dict) -> dict[str, str]:
"""Return a short, redacted argument preview for streamed events."""
preview: dict[str, str] = {}
for key, value in arguments.items():
for key, value in redact_payload(arguments).items():
if key == "run_dir":
continue
if is_sensitive_arg(key):
preview[key] = "[redacted]"
continue
preview[key] = _truncate_preview(redact_payload(value))
preview[key] = _truncate_preview(value)
return preview
+58 -25
View File
@@ -53,6 +53,7 @@ _REDACTED = "[redacted]"
#: degrades to the strict behaviour (fail closed).
ARGUMENTS_SINK = "arguments"
RESULT_SINK = "result"
_VALUE_UNAVAILABLE = object()
#: Curated EXACT-MATCH PII / account-identifier field names (SPEC Consent §5 /
#: §8 #2). These are scrubbed in addition to the credential-marker keys below.
@@ -113,16 +114,11 @@ _SENSITIVE_ARG_MARKERS = ("api_key", "authorization", "password", "secret", "tok
#: (an env dump in a result leaks exactly what an env argument would).
_RESULT_SAFE_KEYS = {"content"}
#: Curated EXACT-MATCH fields that legitimately contain the substring
#: "token" without holding a credential (token counts/budgets, and
#: Polymarket's public CLOB outcome-token id). The "token" marker below has
#: to stay a substring match to catch arbitrary credential-key spellings
#: (``access_token``, ``refresh_token``, ``bearer_token`` …), so — mirroring
#: how :data:`_PII_EXACT_KEYS` keeps ``account_ref`` from being caught by an
#: ``"account"`` substring — these known-benign fields are exempted by exact
#: name instead of being carved out of the marker itself. Unlike
#: :data:`_RESULT_SAFE_KEYS`, this exemption is not sink-dependent: none of
#: these hold a secret in the arguments sink or the live audit ledger either.
#: Exact token-metadata names eligible for a VALUE-SHAPED exemption in every
#: sink. A familiar name alone is never permission to release its value:
#: counts must be nonnegative integers (or absent), usage may be a mapping of
#: counts, and CLOB ids must have the public decimal uint256 shape. Unknown
#: shapes fail closed, including unprefixed secrets no text regex can detect.
_BENIGN_TOKEN_FIELD_KEYS = {
"token_budget",
"token_usage",
@@ -266,7 +262,9 @@ def redact_internal_paths(text: object) -> str:
return s
def is_sensitive_arg(name: str, *, sink: str = ARGUMENTS_SINK) -> bool:
def is_sensitive_arg(
name: str, *, sink: str = ARGUMENTS_SINK, value: Any = _VALUE_UNAVAILABLE
) -> bool:
"""Return whether a tool-argument / payload key name should be redacted.
A key is sensitive when its normalized (stripped, lower-cased) form either
@@ -281,13 +279,15 @@ def is_sensitive_arg(name: str, *, sink: str = ARGUMENTS_SINK) -> bool:
``"account"`` substring — so benign fields are not over-redacted and the
audit record's opaque ``account_ref`` provenance field (the
mandate→consent accountability chain, SPEC §5) is preserved. The same
reasoning exempts :data:`_BENIGN_TOKEN_FIELD_KEYS` (``token_budget``,
``token_usage`` totals, ``clob_token_id`` …) from the ``"token"``
credential marker, which otherwise over-redacts any key merely
containing that substring.
reasoning permits :data:`_BENIGN_TOKEN_FIELD_KEYS` only when ``value``
has the expected metadata shape. Without a value these names remain
sensitive. Usage mappings still require recursive redaction by
:func:`redact_payload`; this predicate never approves an entire subtree.
Args:
name: Argument or payload key name to classify.
value: Candidate value for the token-metadata exception. Omission
fails closed for token-like names.
sink: Where the value is headed. :data:`ARGUMENTS_SINK` (the default)
is the strict set used for tool-call arguments and the live audit
ledger; :data:`RESULT_SINK` additionally releases the
@@ -302,7 +302,17 @@ def is_sensitive_arg(name: str, *, sink: str = ARGUMENTS_SINK) -> bool:
normalized = name.strip().lower()
folded = _fold_key(name)
if folded in _BENIGN_TOKEN_FIELD_KEYS_FOLDED:
return False
if folded == "clobtokenid":
return not (
isinstance(value, str)
and value.isascii()
and value.isdecimal()
and len(value) <= 78
and int(value) < 2**256
)
if folded == "tokenusage" and isinstance(value, dict):
return False
return not (value is None or (type(value) is int and value >= 0))
if sink == RESULT_SINK and folded in _RESULT_SAFE_KEYS_FOLDED:
return False
if normalized in _SENSITIVE_ARG_KEYS or any(
@@ -321,9 +331,11 @@ def redact_payload(obj: Any, *, sink: str = ARGUMENTS_SINK) -> Any:
"""Recursively redact sensitive keys in a structured payload.
Walks dicts and lists; any dict value whose key :func:`is_sensitive_arg`
is replaced with the ``'[redacted]'`` sentinel. Non-container values pass
through unchanged. Used before event-preview stringification and before
writing broker request/response payloads to the live audit ledger.
marks sensitive is replaced with the ``'[redacted]'`` sentinel. Token
metadata exceptions additionally validate value shape, and usage mappings
retain only numeric leaves after sensitive-key scrubbing. String leaves
are pattern-scrubbed in every sink. Used before event-preview stringification
and before writing broker payloads to the live audit ledger.
Scrubs two key classes (see :func:`is_sensitive_arg`): credential keys
(OAuth tokens, ``api_key``, ``authorization``, ``password``/``secret``,
@@ -335,11 +347,10 @@ def redact_payload(obj: Any, *, sink: str = ARGUMENTS_SINK) -> Any:
Args:
obj: Arbitrary payload (dict / list / scalar) to scrub.
sink: Where the payload is headed; forwarded to
:func:`is_sensitive_arg`. In :data:`RESULT_SINK` the surviving
string leaves are additionally pattern-scrubbed with
:func:`redact_text`, because a JSON tool result routinely carries
free text (shell ``stdout``, error bodies) under a benign key that
no key-based rule can classify.
:func:`is_sensitive_arg`. In every sink surviving string leaves
are additionally pattern-scrubbed with :func:`redact_text`, since
shell commands, output and error bodies can carry credentials
under a benign key that no key-based rule can classify.
Returns:
A new structure of the same shape with sensitive values replaced. The
@@ -348,7 +359,9 @@ def redact_payload(obj: Any, *, sink: str = ARGUMENTS_SINK) -> Any:
if isinstance(obj, dict):
return {
key: _REDACTED
if is_sensitive_arg(str(key), sink=sink)
if is_sensitive_arg(str(key), sink=sink, value=item)
else _redact_token_usage(item, sink=sink)
if _fold_key(str(key)) == "tokenusage"
else redact_payload(item, sink=sink)
for key, item in obj.items()
}
@@ -362,6 +375,26 @@ def redact_payload(obj: Any, *, sink: str = ARGUMENTS_SINK) -> Any:
return obj
def _redact_token_usage(value: Any, *, sink: str) -> Any:
"""Keep usage counts without admitting credentials through a usage map.
Args:
value: Usage count or nested mapping to scrub.
sink: Sink policy for nested sensitive keys.
Returns:
A fresh mapping containing only counts, nulls, and redaction sentinels.
"""
if isinstance(value, dict):
return {
key: _REDACTED
if is_sensitive_arg(str(key), sink=sink, value=item)
else _redact_token_usage(item, sink=sink)
for key, item in value.items()
}
return value if value is None or (type(value) is int and value >= 0) else _REDACTED
#: Credential-shaped key names recognized in FREE TEXT. Longer alternatives
#: come first so ``access_token`` is not clipped to ``token`` and
#: ``secret_key`` is not clipped to ``secret``. The list is deliberately
@@ -1,19 +1,18 @@
"""Semantic tests for ``zoo/gtja191/alpha_103``, ``alpha_133``, ``alpha_177``.
"""GTJA report p31 counts the distance to today, which is zero for today.
These factors are the report's HIGHDAY/LOWDAY-based Aroon-style oscillators:
a high or low that happened *today* must score near 100, one that happened at
the oldest edge of the window must score near 0. ``ts_argmax``/``ts_argmin``
are 0-based from the start of the window (index ``n-1`` is the most recent
bar), and this repo's documented rebase to the report's day-count convention
(``n - ts_argmax``) cancels algebraically against the formula's own
``n - HIGHDAY`` term, leaving the raw index scaled by ``100/n``.
The original 2017-06-15 report is linked in gtja191/LICENSE.md. It does not
specify ties; these ports choose the most recent occurrence. Expected values
come from explicit bar ages or a scalar timestamp oracle, never the shared
arg-extreme operators or another alpha.
"""
from __future__ import annotations
import numpy as np
import pandas as pd
import pytest
from src.factors import base
from src.factors.zoo.gtja191.alpha_103 import compute as alpha_103
from src.factors.zoo.gtja191.alpha_133 import compute as alpha_133
from src.factors.zoo.gtja191.alpha_177 import compute as alpha_177
@@ -21,52 +20,114 @@ from src.factors.zoo.gtja191.alpha_177 import compute as alpha_177
N = 20
def _panel_with_extreme_at(offset_from_end: int) -> dict[str, pd.DataFrame]:
"""A 20-bar window whose low and high both sit at ``offset_from_end``.
@pytest.fixture(params=[False, True], ids=["pandas", "bottleneck"])
def backend(request, monkeypatch):
"""Optional acceleration must not change the GTJA tie convention."""
if request.param and not base.HAS_BOTTLENECK:
pytest.skip("bottleneck is not installed")
monkeypatch.setattr(base, "HAS_BOTTLENECK", request.param)
``offset_from_end=0`` puts the extreme on the most recent bar (today);
``offset_from_end=N-1`` puts it on the oldest bar in the window.
"""
close = [100.0] * N
low = [90.0] * N
high = [110.0] * N
idx = N - 1 - offset_from_end
low[idx] = 1.0
high[idx] = 1000.0
return {
"close": pd.DataFrame({"AAA": close}),
"low": pd.DataFrame({"AAA": low}),
"high": pd.DataFrame({"AAA": high}),
def _panel(high_age=0, low_age=0, rows=N):
index = pd.bdate_range("2026-01-01", periods=rows, name="date")
panel = {
name: pd.DataFrame(value, index=index, columns=["AAA", "BBB"])
for name, value in (("close", 100.0), ("low", 90.0), ("high", 110.0))
}
panel["high"].iloc[rows - 1 - high_age] = 1000.0
panel["low"].iloc[rows - 1 - low_age] = 1.0
return panel
def test_alpha_103_scores_a_todays_low_near_the_maximum() -> None:
panel = _panel_with_extreme_at(offset_from_end=0)
out = alpha_103(panel)
assert out["AAA"].iloc[-1] == pytest.approx(95.0)
@pytest.mark.parametrize("age", range(N))
@pytest.mark.parametrize("compute", [alpha_103, alpha_177], ids=["103", "177"])
def test_single_recency_uses_zero_days_since_today(compute, age, backend):
out = compute(_panel(high_age=age, low_age=age))
# Today=100; yesterday=95; oldest member of the 20-bar window=5.
assert out.iloc[-1].tolist() == pytest.approx([100.0 - age * 5.0] * 2)
assert out.iloc[:-1].isna().all().all()
def test_alpha_103_scores_the_oldest_low_near_the_minimum() -> None:
panel = _panel_with_extreme_at(offset_from_end=N - 1)
out = alpha_103(panel)
assert out["AAA"].iloc[-1] == pytest.approx(0.0)
@pytest.mark.parametrize(
"high_age, low_age, expected",
[(0, 19, 95.0), (19, 0, -95.0), (3, 14, 55.0), (14, 3, -55.0), (7, 7, 0.0)],
)
def test_133_preserves_the_direction_of_distinct_extrema(
high_age, low_age, expected, backend
):
out = alpha_133(_panel(high_age=high_age, low_age=low_age))
assert out.iloc[-1].tolist() == pytest.approx([expected] * 2)
def test_alpha_177_scores_todays_high_near_the_maximum() -> None:
panel = _panel_with_extreme_at(offset_from_end=0)
out = alpha_177(panel)
assert out["AAA"].iloc[-1] == pytest.approx(95.0)
@pytest.mark.parametrize(
"compute, expected", [(alpha_103, 80.0), (alpha_177, 90.0), (alpha_133, 10.0)],
ids=["103", "177", "133"],
)
def test_tied_extrema_use_the_most_recent_occurrence(compute, expected, backend):
panel = _panel(high_age=2, low_age=4)
panel["high"].iloc[0] = 1000.0
panel["low"].iloc[1] = 1.0
assert compute(panel).iloc[-1].tolist() == pytest.approx([expected] * 2)
def test_alpha_177_scores_the_oldest_high_near_the_minimum() -> None:
panel = _panel_with_extreme_at(offset_from_end=N - 1)
out = alpha_177(panel)
assert out["AAA"].iloc[-1] == pytest.approx(0.0)
@pytest.mark.parametrize(
"compute, expected", [(alpha_103, 100.0), (alpha_177, 100.0), (alpha_133, 0.0)],
ids=["103", "177", "133"],
)
def test_constant_window_chooses_today(compute, expected, backend):
panel = _panel()
panel["high"].iloc[:] = 110.0
panel["low"].iloc[:] = 90.0
assert compute(panel).iloc[-1].tolist() == pytest.approx([expected] * 2)
def test_alpha_133_is_the_difference_of_the_two_recency_scores() -> None:
panel = _panel_with_extreme_at(offset_from_end=0)
out = alpha_133(panel)
# Both the high and the low sit on today's bar, so the Aroon-up and
# Aroon-down terms are equal and cancel.
assert out["AAA"].iloc[-1] == pytest.approx(0.0)
@pytest.mark.parametrize(
"compute, missing_field",
[(alpha_103, "low"), (alpha_177, "high"), (alpha_133, "low"), (alpha_133, "high")],
ids=["103-low", "177-high", "133-low", "133-high"],
)
def test_gap_invalidates_exactly_the_windows_that_contain_it(
compute, missing_field, backend
):
panel = _panel(rows=65)
expected = compute(panel)
gap = 30
panel[missing_field].iloc[gap, 0] = np.nan
actual = compute(panel)
assert actual.iloc[:N - 1].isna().all().all()
assert actual["AAA"].iloc[gap:gap + N].isna().all()
pd.testing.assert_series_equal(actual["BBB"], expected["BBB"])
pd.testing.assert_series_equal(actual["AAA"].iloc[:gap], expected["AAA"].iloc[:gap])
pd.testing.assert_series_equal(actual["AAA"].iloc[gap + N:], expected["AAA"].iloc[gap + N:])
assert actual["AAA"].iloc[gap + N] == expected["AAA"].iloc[gap + N]
def _scalar_score(values, row, maximum):
"""Find the last absolute row holding the extreme, then count its age."""
window = values[row - N + 1:row + 1]
if row < N - 1 or any(pd.isna(value) for value in window):
return np.nan
extreme = max(window) if maximum else min(window)
last_seen = max(i for i in range(row - N + 1, row + 1) if values[i] == extreme)
days_since = row - last_seen
return (N - days_since) * 100.0 / N
@pytest.mark.parametrize("compute, which", [(alpha_103, "low"), (alpha_177, "high"), (alpha_133, "both")])
def test_rolling_output_matches_independent_timestamp_oracle(compute, which, backend):
panel = _panel(rows=73)
rng = np.random.default_rng(27)
# Small integer ranges intentionally create repeated extrema.
panel["high"].iloc[:] = rng.integers(110, 116, size=(73, 2))
panel["low"].iloc[:] = rng.integers(85, 91, size=(73, 2))
panel["low"].iloc[31, 0] = np.nan
panel["high"].iloc[40, 1] = np.nan
expected = pd.DataFrame(np.nan, index=panel["close"].index, columns=panel["close"].columns)
for symbol in expected.columns:
for row in range(len(expected)):
high = _scalar_score(panel["high"][symbol].tolist(), row, True)
low = _scalar_score(panel["low"][symbol].tolist(), row, False)
expected.loc[expected.index[row], symbol] = (
high - low if which == "both" else high if which == "high" else low
)
pd.testing.assert_frame_equal(compute(panel), expected)
+7
View File
@@ -19,6 +19,10 @@ from src.goal.policy import reject_live_execution_objective
"Short interest trends in regional banks",
"Short-term momentum in semiconductors",
"研究茅台是否值得买入",
"Now research whether to buy AAPL",
"Now analyze short interest in TSLA",
"Immediately evaluate a long AAPL thesis",
"Right away assess whether to sell TSLA",
],
)
def test_ordinary_research_objectives_are_accepted(objective: str) -> None:
@@ -47,6 +51,9 @@ def test_ordinary_research_objectives_are_accepted(objective: str) -> None:
"Immediately buy TSLA",
"Right away sell my AAPL position",
"Now sell GOOGL",
"Now, please buy AAPL",
"Research the setup; immediately sell TSLA",
"Please immediately short TSLA",
],
)
def test_genuine_execution_requests_are_still_rejected(objective: str) -> None:
+106
View File
@@ -0,0 +1,106 @@
"""India circuit checks use the fill side, including shared composite positions."""
from __future__ import annotations
import numpy as np
import pandas as pd
import pytest
from backtest.engines.composite import CompositeEngine
from backtest.engines.india_equity import IndiaEquityEngine
from backtest.models import Position
SYMBOL = "RELIANCE.NS"
ENTRY_DATE = pd.Timestamp("2024-04-01")
NEXT_DATE = pd.Timestamp("2024-04-02")
@pytest.fixture(params=["single", "composite"])
def engine(request):
config = {"initial_cash": 1_000_000, "allow_short": True, "slippage": 0.001}
if request.param == "composite":
result = CompositeEngine(config, [SYMBOL, "TCS.NS"])
assert not result._rule_engines["india_equity"].positions
else:
result = IndiaEquityEngine(config)
result._active_symbol = SYMBOL
# Exercise the previous-close panel used by real runs: no pre_close field.
result._close_arr = np.array([[100.0]])
result._code_to_col = {SYMBOL: 0}
result._bar_idx = 1
return result
def _hold(engine, direction):
engine.positions[SYMBOL] = Position(
symbol=SYMBOL, direction=direction, size=10, entry_price=100.0,
entry_time=ENTRY_DATE,
)
@pytest.mark.parametrize(
("direction", "position_direction", "buying"),
[(1, None, True), (-1, None, False), (0, 1, False), (0, -1, True)],
ids=["open-long", "open-short", "close-long", "cover-short"],
)
@pytest.mark.parametrize(
("open_price", "buy_allowed", "sell_allowed"),
[
(119.9, False, True), # Buy slippage crosses 120; raw open is inside.
(119.8, True, True),
(80.05, True, False), # Sell slippage crosses 80; raw open is inside.
(80.2, True, True),
],
)
def test_fill_side_controls_the_band(
engine, direction, position_direction, buying, open_price, buy_allowed, sell_allowed,
):
if position_direction is not None:
_hold(engine, position_direction)
# The later close must not rescue or veto an execution at the open.
bar = pd.Series({"open": open_price, "close": 100.0}, name=NEXT_DATE)
expected = buy_allowed if buying else sell_allowed
assert engine.can_execute(SYMBOL, direction, bar) is expected
@pytest.mark.parametrize("position_direction", [1, -1], ids=["long", "short"])
@pytest.mark.parametrize("date_field", [None, "date", "trade_date"])
def test_existing_same_day_close_guard_is_preserved(engine, position_direction, date_field):
"""The delivery engine blocks same-day closes even with allow_short enabled."""
_hold(engine, position_direction)
bar = pd.Series({"open": 100.0, "close": 100.0}, name=ENTRY_DATE)
if date_field is not None:
bar.name = None
bar[date_field] = ENTRY_DATE.isoformat()
assert engine.can_execute(SYMBOL, 0, bar) is False
def test_blocked_short_cover_retains_position_until_a_fillable_bar(engine):
"""Run the actual open/close path so a rejected cover cannot book a trade."""
dates = pd.bdate_range(ENTRY_DATE, periods=3)
bars = pd.DataFrame(
{"open": [100.0, 119.9, 110.0], "close": [100.0, 105.0, 110.0],
"pre_close": [100.0, 100.0, 105.0]},
index=dates,
)
engine._rebalance(SYMBOL, -0.1, bars, dates[0], 1_000_000)
position = engine.positions[SYMBOL]
assert position.direction == -1
assert position.entry_price == pytest.approx(99.9)
capital_before_cover = engine.capital
engine._rebalance(SYMBOL, 0, bars, dates[1], 1_000_000)
assert engine.positions[SYMBOL] is position
assert engine.capital == capital_before_cover
assert not engine.trades
assert len(engine.fill_records) == 1
engine._rebalance(SYMBOL, 0, bars, dates[2], 1_000_000)
assert SYMBOL not in engine.positions
assert len(engine.trades) == 1
assert engine.trades[0].exit_time == dates[2]
assert engine.trades[0].exit_price == pytest.approx(110.11)
assert len(engine.fill_records) == 2
if isinstance(engine, CompositeEngine):
assert not engine._rule_engines["india_equity"].positions
-2
View File
@@ -11,8 +11,6 @@ Validates:
from __future__ import annotations
import datetime as dt
import pandas as pd
import pytest
+16
View File
@@ -176,3 +176,19 @@ def test_credential_fallback_map_matches_provider_catalog() -> None:
for item in json.loads(providers_path.read_text(encoding="utf-8"))
}
assert _provider_default_base_urls() == catalog
def test_env_example_default_matches_provider_registry():
agent_root = Path(__file__).resolve().parents[1]
assignments = dict(line.split("=", 1) for line in
(agent_root / ".env.example").read_text().splitlines()
if line.startswith(("LANGCHAIN_PROVIDER=", "LANGCHAIN_MODEL_NAME=")))
providers = json.loads((agent_root / "src/providers/llm_providers.json").read_text())
provider = next(p for p in providers if p["name"] == assignments["LANGCHAIN_PROVIDER"])
assert assignments["LANGCHAIN_MODEL_NAME"] == provider["default_model"]
def test_direct_deepseek_suggests_current_flash_name():
provider = next(p for p in ONBOARD_PROVIDERS if p.key == "deepseek")
assert "deepseek-flash" in provider.suggested_models
assert "deepseek-v4-flash" not in provider.suggested_models
+19
View File
@@ -876,3 +876,22 @@ class TestHttpErrorBodyIsReported:
assert "Status failed 500" in str(caught.value)
assert caught.value.response.status_code == 400
@pytest.mark.parametrize("bad", [None, [], "oops", 42, False])
def test_malformed_properties_normalization_is_idempotent(bad):
original = {"type": "object", "properties": bad, "required": ["symbol"]}
normalized = normalize_mcp_tool_schema(original)
assert normalized["properties"] == {}
assert normalized["required"] == ["symbol"]
assert normalize_mcp_tool_schema(normalized) == normalized
assert original["properties"] == bad
@pytest.mark.parametrize("composition", ["anyOf", "oneOf", "allOf"])
def test_malformed_properties_does_not_override_composed_schema(composition):
branch = {"type": "object", "properties": {"symbol": {"type": "string"}}}
result = normalize_mcp_tool_schema({"type": "object", "properties": "oops", composition: [branch, {"type": "object", "properties": {"code": {"type": "integer"}}}]})
assert result[composition][0] == branch
assert len(result[composition]) == 2
assert "properties" not in result
+131 -1
View File
@@ -190,7 +190,9 @@ def test_is_sensitive_arg_preserves_benign_token_fields(key: str) -> None:
``access_token``, ``refresh_token`` …), so token counts/budgets and the
public CLOB outcome-token id need an explicit exemption to avoid being
over-redacted."""
assert is_sensitive_arg(key) is False
value = "123456789012345678901234567890" if key == "clob_token_id" else 50000
assert is_sensitive_arg(key, value=value) is False
assert is_sensitive_arg(key) is True # No value evidence means no exemption.
@pytest.mark.parametrize(
@@ -487,3 +489,131 @@ def test_bare_token_scrub_leaves_ordinary_output_alone() -> None:
"""Short sk-prefixed words and usage counters are not credentials."""
text = "tokens: 1204 in / 318 out, sk-ok, account 12345"
assert redact_text(text) == text
_TOKEN_METADATA_KEYS = [
"token_budget", "tokenBudget", "Token Budget", "token-budget",
"token_usage", "tokenUsage", "Token Usage", "token-usage",
"total_input_tokens", "totalInputTokens", "Total Input Tokens",
"total_output_tokens", "totalOutputTokens", "Total Output Tokens",
"clob_token_id", "clobTokenId", "CLOB Token ID", "clob-token-id",
]
@pytest.mark.parametrize("sink", ["arguments", RESULT_SINK, "unknown"])
@pytest.mark.parametrize("key", _TOKEN_METADATA_KEYS)
@pytest.mark.parametrize(
"value",
["opaque-test-credential", "Bearer planted-test-token", "sk-proj-" + "x" * 24,
{"accessToken": "nested-test-credential"}, ["opaque-test-credential"], True, -1],
)
def test_token_metadata_exceptions_never_release_unexpected_values(
sink: str, key: str, value: Any,
) -> None:
"""Benign names cannot launder credentials, including unrecognizable ones."""
out = redact_payload({key: value}, sink=sink)
if "usage" in key.lower() and isinstance(value, dict):
assert out == {key: {"accessToken": "[redacted]"}}
else:
assert out == {key: "[redacted]"}
assert redact_payload(out, sink=sink) == out
@pytest.mark.parametrize("sink", ["arguments", RESULT_SINK, "unknown"])
def test_token_metadata_keeps_counts_and_public_ids_in_every_sink(sink: str) -> None:
payload = {
"tokenBudget": 50000,
"total_input_tokens": 0,
"totalOutputTokens": 412,
"token_usage": {"totalInputTokens": 1024, "total_output_tokens": 512,
"details": {"cached": 32, "reasoning": 16}},
"clobTokenId": str(2**256 - 1),
}
assert redact_payload(payload, sink=sink) == payload
assert json.loads(redact_tool_result(json.dumps(payload))) == payload
@pytest.mark.parametrize("sink", ["arguments", RESULT_SINK, "unknown"])
def test_usage_mapping_recurses_but_never_treats_nested_credentials_as_counts(sink: str) -> None:
payload = {"tokenUsage": {
"totalInputTokens": 128,
"details": {"cached": 64, "apiKey": 12345,
"refreshToken": "opaque-test-credential", "note": "unlabelled-private-value"},
"accessToken": {"count": 4},
"content": "private body",
"rows": [{"password": "list-test-credential"}],
}}
expected = {"tokenUsage": {
"totalInputTokens": 128,
"details": {"cached": 64, "apiKey": "[redacted]",
"refreshToken": "[redacted]", "note": "[redacted]"},
"accessToken": "[redacted]", "content": "[redacted]", "rows": "[redacted]",
}}
assert redact_payload(payload, sink=sink) == expected
assert payload["tokenUsage"]["details"]["apiKey"] == 12345
@pytest.mark.parametrize("value", ["²", "123", str(2**256), "1" * 5000, "12.3", "0x123"])
def test_clob_id_exception_only_accepts_decimal_uint256(value: str) -> None:
assert redact_payload({"clob_token_id": value}) == {"clob_token_id": "[redacted]"}
@pytest.mark.parametrize("key", ["secretTokenBudget", "tokenUsageAccessToken", "clobTokenIdSecret"])
def test_metadata_exception_is_exact_and_cannot_hide_a_credential_suffix(key: str) -> None:
assert redact_payload({key: 123}) == {key: "[redacted]"}
def test_metadata_validation_reaches_swarm_argument_previews() -> None:
from src.swarm.worker import _preview_tool_arguments
preview = _preview_tool_arguments({
"token_budget": "opaque-test-credential",
"token_usage": {"total_input_tokens": 7, "accessToken": "nested-test-credential"},
"totalOutputTokens": 12,
})
assert preview["token_budget"] == "[redacted]"
assert "nested-test-credential" not in json.dumps(preview)
assert "7" in preview["token_usage"]
assert preview["totalOutputTokens"] == "12"
def test_metadata_validation_reaches_trace_events_and_result_previews(tmp_path: Path) -> None:
payload = {"tokenBudget": "opaque-test-credential", "totalInputTokens": 23,
"tokenUsage": {"refreshToken": "nested-test-credential", "cached": 12}}
run_dir = tmp_path / "run"
run_dir.mkdir()
tc = SimpleNamespace(id="tc_metadata", name="fetch_quote", arguments=payload)
events, _, react_trace = _run_one_tool_call(_StubRegistry(json.dumps(payload)), tc, run_dir)
for surface in [_persisted_text(run_dir), json.dumps(events), json.dumps(react_trace)]:
assert "opaque-test-credential" not in surface
assert "nested-test-credential" not in surface
entries = TraceWriter.read(run_dir, resolve_offloads=True)
call = next(entry for entry in entries if entry["type"] == "tool_call")
result = next(entry for entry in entries if entry["type"] == "tool_result")
assert call["args"]["totalInputTokens"] == 23
assert json.loads(result["result"])["tokenUsage"]["cached"] == 12
def test_metadata_validation_reaches_all_audit_sinks(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
from src.live import audit, paths
monkeypatch.setattr(paths, "get_runtime_root", lambda: tmp_path)
records: list[dict] = []
events: list[dict] = []
payload = {"tokenBudget": "opaque-test-credential", "tokenUsage": {
"totalInputTokens": 77, "accessToken": "nested-test-credential"}}
event = audit.LiveActionEvent(kind="order_placed", session_id="s1", outcome="accepted",
server="test-broker", broker_request=payload, broker_response=payload)
returned = audit.write_live_action(
event, event_callback=lambda name, data: events.append(data),
trace_writer=SimpleNamespace(write=records.append),
)
ledger = [json.loads(line) for line in audit.audit_ledger_path().read_text().splitlines()]
assert ledger == events == [returned]
assert records == [{"type": "live_action", **returned}]
chain = [json.loads(line) for line in audit.audit_chain_ledger_path().read_text().splitlines()]
assert len(chain) == 1
for record in ledger + records + events + chain:
assert "opaque-test-credential" not in json.dumps(record)
assert "nested-test-credential" not in json.dumps(record)
assert record["broker_request"]["tokenUsage"]["totalInputTokens"] == 77
+54 -1
View File
@@ -52,7 +52,8 @@ def test_run_card_v1_hashes_tool_inputs_and_cites_metric_artifact(tmp_path) -> N
expected_hash = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
assert trace_record[f"{key}_hash"] == expected_hash
assert card["citations"] == [
{"metric": "sharpe", "artifact_id": "artifacts/metrics.csv"}
{"metric": "sharpe", "artifact_id": "artifacts/metrics.csv", "column": "sharpe", "row": 1,
"sha256": hashlib.sha256((metrics_dir / "metrics.csv").read_bytes()).hexdigest()}
]
serialized = json.dumps(card)
@@ -109,3 +110,55 @@ def test_run_card_v1_hashes_tool_inputs_and_cites_metric_artifact(tmp_path) -> N
changed_card["tool_traces"][0]["result_hash"]
!= same_shape_card["tool_traces"][0]["result_hash"]
)
def test_citations_require_matching_single_row_and_bind_checksum(tmp_path):
artifacts = tmp_path / "artifacts"
artifacts.mkdir()
path = artifacts / "metrics.csv"
path.write_text("sharpe,win_rate\n1.23,0.5\n")
card = write_run_card(tmp_path, {}, {"sharpe": 9.99, "win_rate": 0.5, "absent": 3.0})
assert [ref["metric"] for ref in card["citations"]] == ["win_rate"]
assert card["citations"][0]["sha256"] == hashlib.sha256(path.read_bytes()).hexdigest()
markdown = (tmp_path / "run_card.md").read_text()
assert "column `win_rate`, data row 1" in markdown
for content in ("sharpe\n1.23\n2.0\n", "sharpe,sharpe\n1.23,1.23\n", "sharpe\n"):
path.write_text(content)
assert "citations" not in write_run_card(tmp_path, {}, {"sharpe": 1.23})
def test_trace_metadata_cannot_be_a_raw_payload_channel(tmp_path):
import pytest
trace = {"tool": "backtest", "status": "ok", "args": {}, "result": {},
"started_at": "2026-09-27T00:00:00Z", "ended_at": "2026-09-27T00:00:01Z"}
for field, value in (("tool", "private credential text"), ("status", "secret failure text"),
("started_at", "/private/run"), ("ended_at", "2026-09-26T00:00:00Z")):
with pytest.raises(ValueError):
write_run_card(tmp_path, {}, {}, tool_traces=[{**trace, field: value}])
card = write_run_card(tmp_path, {}, {}, tool_traces=[trace])
markdown = (tmp_path / "run_card.md").read_text()
assert "backtest (ok)" in markdown
assert card["tool_traces"][0]["args_hash"] in markdown
def test_old_style_card_has_explicit_missing_trace_and_evidence_text(tmp_path):
card = write_run_card(tmp_path, {}, {"sharpe": 1.23})
assert "tool_traces" not in card and "citations" not in card
text = (tmp_path / "run_card.md").read_text()
assert "No execution records available" in text
assert "No verified metric references" in text
def test_run_detail_api_preserves_verified_traces_and_citations(tmp_path):
import api_server
(tmp_path / "artifacts").mkdir()
(tmp_path / "artifacts/metrics.csv").write_text("sharpe\n1.23\n")
trace = {"tool": "backtest", "status": "ok", "args": {}, "result": {"sharpe": 1.23},
"started_at": "2026-09-27T00:00:00Z", "ended_at": "2026-09-27T00:00:01Z"}
card = write_run_card(tmp_path, {}, {"sharpe": 1.23}, tool_traces=[trace])
response = api_server._build_response_from_run_dir(tmp_path, elapsed=0.0)
payload = response.model_dump(mode="json")
assert payload["run_card"]["tool_traces"] == card["tool_traces"]
assert payload["run_card"]["citations"] == card["citations"]
+27
View File
@@ -1009,6 +1009,33 @@
"reproducibility": "قابلية إعادة الإنتاج",
"metrics": "المقاييس",
"validationPayload": "التحقّق",
"trust": {
"executionRecords": "سجلات التنفيذ",
"executionDescription": "مراحل التنفيذ المسجلة وبصمات مدخلاتها ونتائجها.",
"noExecutionRecords": "لم تُسجَّل سجلات تنفيذ لهذا التشغيل.",
"startedAt": "البدء (UTC)",
"endedAt": "الانتهاء (UTC)",
"argsHash": "بصمة المعاملات SHA-256",
"resultHash": "بصمة النتيجة SHA-256",
"metricEvidence": "أدلة المقاييس",
"evidenceDescription": "مراجع لبيانات المقاييس المحفوظة. تحدد بصمات التحقق الملفات المسجلة؛ لا تتحقق هذه الواجهة منها بشكل مستقل.",
"noMetricEvidence": "لم تُسجَّل أدلة مقاييس لهذا التشغيل.",
"metric": "المقياس",
"artifact": "الملف الناتج",
"column": "العمود",
"row": "صف البيانات (يبدأ من 1، دون الترويسة)",
"artifactHash": "بصمة الملف SHA-256",
"tools": {
"backtest": "الاختبار التاريخي",
"load_data": "تحميل البيانات",
"generate_signals": "توليد الإشارات"
},
"status": {
"ok": "مكتمل",
"error": "فشل",
"cancelled": "ملغى"
}
},
"artifactChecksums": "مجاميع التحقق للمنتجات",
"path": "المسار",
"size": "الحجم",
+27
View File
@@ -1014,6 +1014,33 @@
"reproducibility": "Reproduzierbarkeit",
"metrics": "Metriken",
"validationPayload": "Validierung",
"trust": {
"executionRecords": "Ausführungsprotokolle",
"executionDescription": "Aufgezeichnete Ausführungsschritte und Hashes ihrer Eingaben und Ergebnisse.",
"noExecutionRecords": "Für diesen Lauf wurden keine Ausführungsprotokolle gespeichert.",
"startedAt": "Beginn (UTC)",
"endedAt": "Ende (UTC)",
"argsHash": "SHA-256 der Argumente",
"resultHash": "SHA-256 des Ergebnisses",
"metricEvidence": "Kennzahlennachweise",
"evidenceDescription": "Verweise auf gespeicherte Kennzahlendaten. Prüfsummen identifizieren die aufgezeichneten Dateien; diese Ansicht prüft sie nicht unabhängig.",
"noMetricEvidence": "Für diesen Lauf wurden keine Kennzahlennachweise gespeichert.",
"metric": "Kennzahl",
"artifact": "Artefakt",
"column": "Spalte",
"row": "Datenzeile (ab 1, ohne Kopfzeile)",
"artifactHash": "SHA-256 des Artefakts",
"tools": {
"backtest": "Backtest",
"load_data": "Daten laden",
"generate_signals": "Signale erzeugen"
},
"status": {
"ok": "Abgeschlossen",
"error": "Fehlgeschlagen",
"cancelled": "Abgebrochen"
}
},
"artifactChecksums": "Artefakt-Prüfsummen",
"path": "Pfad",
"size": "Größe",
+27
View File
@@ -1014,6 +1014,33 @@
"reproducibility": "Reproducibility",
"metrics": "Metrics",
"validationPayload": "Validation",
"trust": {
"executionRecords": "Execution records",
"executionDescription": "Recorded execution stages and the hashes of their inputs and results.",
"noExecutionRecords": "Execution records were not recorded for this run.",
"startedAt": "Started (UTC)",
"endedAt": "Ended (UTC)",
"argsHash": "Arguments SHA-256",
"resultHash": "Result SHA-256",
"metricEvidence": "Metric evidence",
"evidenceDescription": "References to saved metric data. Checksums identify the recorded files; this view does not independently verify them.",
"noMetricEvidence": "Metric evidence was not recorded for this run.",
"metric": "Metric",
"artifact": "Artifact",
"column": "Column",
"row": "Data row (1-based, excluding header)",
"artifactHash": "Artifact SHA-256",
"tools": {
"backtest": "Backtest",
"load_data": "Load data",
"generate_signals": "Generate signals"
},
"status": {
"ok": "Completed",
"error": "Failed",
"cancelled": "Cancelled"
}
},
"artifactChecksums": "Artifact Checksums",
"path": "Path",
"size": "Size",
+27
View File
@@ -1014,6 +1014,33 @@
"reproducibility": "Reproducibilidad",
"metrics": "Métricas",
"validationPayload": "Validación",
"trust": {
"executionRecords": "Registros de ejecución",
"executionDescription": "Etapas de ejecución registradas y hashes de sus entradas y resultados.",
"noExecutionRecords": "No se guardaron registros de ejecución para esta ejecución.",
"startedAt": "Inicio (UTC)",
"endedAt": "Fin (UTC)",
"argsHash": "SHA-256 de los argumentos",
"resultHash": "SHA-256 del resultado",
"metricEvidence": "Evidencia de métricas",
"evidenceDescription": "Referencias a los datos de métricas guardados. Las sumas de comprobación identifican los archivos registrados; esta vista no los verifica de forma independiente.",
"noMetricEvidence": "No se registró evidencia de métricas para esta ejecución.",
"metric": "Métrica",
"artifact": "Artefacto",
"column": "Columna",
"row": "Fila de datos (desde 1, sin encabezado)",
"artifactHash": "SHA-256 del artefacto",
"tools": {
"backtest": "Backtest",
"load_data": "Cargar datos",
"generate_signals": "Generar señales"
},
"status": {
"ok": "Completado",
"error": "Fallido",
"cancelled": "Cancelado"
}
},
"artifactChecksums": "Sumas de verificación de artefactos",
"path": "Ruta",
"size": "Tamaño",
+27
View File
@@ -1014,6 +1014,33 @@
"reproducibility": "Reprodusibilitas",
"metrics": "Metrik",
"validationPayload": "Validasi",
"trust": {
"executionRecords": "Catatan eksekusi",
"executionDescription": "Tahap eksekusi yang dicatat beserta hash masukan dan hasilnya.",
"noExecutionRecords": "Catatan eksekusi tidak disimpan untuk proses ini.",
"startedAt": "Mulai (UTC)",
"endedAt": "Selesai (UTC)",
"argsHash": "SHA-256 argumen",
"resultHash": "SHA-256 hasil",
"metricEvidence": "Bukti metrik",
"evidenceDescription": "Referensi ke data metrik tersimpan. Checksum mengidentifikasi berkas yang dicatat; tampilan ini tidak memverifikasinya secara independen.",
"noMetricEvidence": "Bukti metrik tidak dicatat untuk proses ini.",
"metric": "Metrik",
"artifact": "Artefak",
"column": "Kolom",
"row": "Baris data (mulai dari 1, tanpa header)",
"artifactHash": "SHA-256 artefak",
"tools": {
"backtest": "Backtest",
"load_data": "Muat data",
"generate_signals": "Buat sinyal"
},
"status": {
"ok": "Selesai",
"error": "Gagal",
"cancelled": "Dibatalkan"
}
},
"artifactChecksums": "Checksum Artefak",
"path": "Path",
"size": "Ukuran",
+27
View File
@@ -1009,6 +1009,33 @@
"reproducibility": "再現性",
"metrics": "メトリクス",
"validationPayload": "検証",
"trust": {
"executionRecords": "実行記録",
"executionDescription": "記録された実行段階と、入力および結果のハッシュ。",
"noExecutionRecords": "この実行の実行記録は保存されていません。",
"startedAt": "開始(UTC)",
"endedAt": "終了(UTC)",
"argsHash": "引数の SHA-256",
"resultHash": "結果の SHA-256",
"metricEvidence": "指標の根拠",
"evidenceDescription": "保存された指標データへの参照です。チェックサムは記録されたファイルを識別します。この画面では独立した検証は行いません。",
"noMetricEvidence": "この実行の指標の根拠は記録されていません。",
"metric": "指標",
"artifact": "成果物",
"column": "列",
"row": "データ行(1 始まり、ヘッダーを除く)",
"artifactHash": "成果物の SHA-256",
"tools": {
"backtest": "バックテスト",
"load_data": "データ読み込み",
"generate_signals": "シグナル生成"
},
"status": {
"ok": "完了",
"error": "失敗",
"cancelled": "キャンセル済み"
}
},
"artifactChecksums": "成果物チェックサム",
"path": "パス",
"size": "サイズ",
+27
View File
@@ -1009,6 +1009,33 @@
"reproducibility": "재현성",
"metrics": "지표",
"validationPayload": "검증",
"trust": {
"executionRecords": "실행 기록",
"executionDescription": "기록된 실행 단계와 입력 및 결과의 해시입니다.",
"noExecutionRecords": "이 실행의 실행 기록이 저장되지 않았습니다.",
"startedAt": "시작 (UTC)",
"endedAt": "종료 (UTC)",
"argsHash": "인수 SHA-256",
"resultHash": "결과 SHA-256",
"metricEvidence": "지표 근거",
"evidenceDescription": "저장된 지표 데이터에 대한 참조입니다. 체크섬은 기록된 파일을 식별하며, 이 화면에서는 별도로 검증하지 않습니다.",
"noMetricEvidence": "이 실행의 지표 근거가 기록되지 않았습니다.",
"metric": "지표",
"artifact": "산출물",
"column": "열",
"row": "데이터 행 (1부터 시작, 헤더 제외)",
"artifactHash": "산출물 SHA-256",
"tools": {
"backtest": "백테스트",
"load_data": "데이터 로드",
"generate_signals": "신호 생성"
},
"status": {
"ok": "완료",
"error": "실패",
"cancelled": "취소됨"
}
},
"artifactChecksums": "아티팩트 체크섬",
"path": "경로",
"size": "크기",
+27
View File
@@ -1014,6 +1014,33 @@
"reproducibility": "Reprodutibilidade",
"metrics": "Métricas",
"validationPayload": "Validação",
"trust": {
"executionRecords": "Registros de execução",
"executionDescription": "Etapas de execução registradas e hashes de suas entradas e resultados.",
"noExecutionRecords": "Não foram salvos registros de execução para esta execução.",
"startedAt": "Início (UTC)",
"endedAt": "Fim (UTC)",
"argsHash": "SHA-256 dos argumentos",
"resultHash": "SHA-256 do resultado",
"metricEvidence": "Evidências das métricas",
"evidenceDescription": "Referências aos dados de métricas salvos. As somas de verificação identificam os arquivos registrados; esta tela não os verifica de forma independente.",
"noMetricEvidence": "Não foram registradas evidências de métricas para esta execução.",
"metric": "Métrica",
"artifact": "Artefato",
"column": "Coluna",
"row": "Linha de dados (a partir de 1, sem cabeçalho)",
"artifactHash": "SHA-256 do artefato",
"tools": {
"backtest": "Backtest",
"load_data": "Carregar dados",
"generate_signals": "Gerar sinais"
},
"status": {
"ok": "Concluído",
"error": "Falhou",
"cancelled": "Cancelado"
}
},
"artifactChecksums": "Somas de verificação de artefato",
"path": "Caminho",
"size": "Tamanho",
+27
View File
@@ -1009,6 +1009,33 @@
"reproducibility": "可复现性",
"metrics": "指标",
"validationPayload": "验证",
"trust": {
"executionRecords": "执行记录",
"executionDescription": "已记录的执行阶段及其输入和结果的哈希值。",
"noExecutionRecords": "此次运行未记录执行记录。",
"startedAt": "开始时间(UTC)",
"endedAt": "结束时间(UTC)",
"argsHash": "参数 SHA-256",
"resultHash": "结果 SHA-256",
"metricEvidence": "指标证据",
"evidenceDescription": "指向已保存指标数据的引用。校验和标识记录中的文件;此视图不会独立验证文件。",
"noMetricEvidence": "此次运行未记录指标证据。",
"metric": "指标",
"artifact": "产物",
"column": "列",
"row": "数据行(从 1 开始,不含表头)",
"artifactHash": "产物 SHA-256",
"tools": {
"backtest": "回测",
"load_data": "加载数据",
"generate_signals": "生成信号"
},
"status": {
"ok": "已完成",
"error": "失败",
"cancelled": "已取消"
}
},
"artifactChecksums": "制品校验和",
"path": "路径",
"size": "大小",
+20
View File
@@ -1268,6 +1268,24 @@ export function fetchRunSectorMap(runId: string, refresh?: boolean): Promise<Sec
return request<SectorMapResponse>(`/runs/${encodeURIComponent(runId)}/positions/sectors${qs}`);
}
export interface RunCardToolTrace {
tool: "backtest" | "load_data" | "generate_signals";
started_at: string;
ended_at: string;
status: "ok" | "error" | "cancelled";
args_hash: string;
result_hash: string;
}
export interface RunCardCitation {
metric: string;
artifact_id: string;
column: string;
/** One-based data row, excluding the CSV header. */
row: number;
sha256: string;
}
export interface RunCard {
schema_version?: string;
generated_at?: string;
@@ -1282,6 +1300,8 @@ export interface RunCard {
validation?: unknown;
warnings?: string[];
artifacts?: RunCardArtifact[];
tool_traces?: RunCardToolTrace[];
citations?: RunCardCitation[];
[key: string]: unknown;
}
+44
View File
@@ -466,6 +466,7 @@ export function RunDetail() {
}
function RunCardTab({ card }: { card: RunCard }) {
const { t } = useTranslation();
const backtest = card.backtest || {};
const reproducibility = card.reproducibility || {};
const metrics = card.metrics || {};
@@ -531,6 +532,49 @@ function RunCardTab({ card }: { card: RunCard }) {
<KeyValueTable data={structuredMetrics} empty={i18n.t("runDetail.noStructuredMetrics")} monospaceValues />
</RunCardPanel>
<RunCardPanel title={t("runDetail.trust.executionRecords")} icon={List}>
<p className="mb-3 text-xs text-muted-foreground">{t("runDetail.trust.executionDescription")}</p>
{card.tool_traces?.length ? (
<ol className="space-y-3">
{card.tool_traces.map((trace, index) => (
<li key={`${index}-${trace.started_at}`} className="rounded-lg border border-border/60 p-3">
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
<span className="text-sm font-medium">{t(`runDetail.trust.tools.${trace.tool}`)}</span>
<span className={cn("rounded-md px-2 py-1 text-xs", trace.status === "ok" ? "bg-success/10 text-success" : trace.status === "error" ? "bg-danger/10 text-danger" : "bg-muted text-muted-foreground")}>
{t(`runDetail.trust.status.${trace.status}`)}
</span>
</div>
<dl className="grid gap-3 text-xs sm:grid-cols-2">
<div><dt className="text-muted-foreground">{t("runDetail.trust.startedAt")}</dt><dd className="mt-1 break-all"><time dateTime={trace.started_at}>{trace.started_at || t("runDetail.noneRecorded")}</time></dd></div>
<div><dt className="text-muted-foreground">{t("runDetail.trust.endedAt")}</dt><dd className="mt-1 break-all"><time dateTime={trace.ended_at}>{trace.ended_at || t("runDetail.noneRecorded")}</time></dd></div>
<div><dt className="text-muted-foreground">{t("runDetail.trust.argsHash")}</dt><dd className="mt-1 break-all font-mono" dir="ltr">{trace.args_hash || t("runDetail.noneRecorded")}</dd></div>
<div><dt className="text-muted-foreground">{t("runDetail.trust.resultHash")}</dt><dd className="mt-1 break-all font-mono" dir="ltr">{trace.result_hash || t("runDetail.noneRecorded")}</dd></div>
</dl>
</li>
))}
</ol>
) : <p className="text-sm text-muted-foreground">{t("runDetail.trust.noExecutionRecords")}</p>}
</RunCardPanel>
<RunCardPanel title={t("runDetail.trust.metricEvidence")} icon={FileCheck2}>
<p className="mb-3 text-xs text-muted-foreground">{t("runDetail.trust.evidenceDescription")}</p>
{card.citations?.length ? (
<ul className="space-y-3">
{card.citations.map((citation, index) => (
<li key={`${index}-${citation.metric}`} className="rounded-lg border border-border/60 p-3">
<dl className="grid gap-3 text-xs sm:grid-cols-2">
<div><dt className="text-muted-foreground">{t("runDetail.trust.metric")}</dt><dd className="mt-1 break-all font-mono">{citation.metric}</dd></div>
<div><dt className="text-muted-foreground">{t("runDetail.trust.artifact")}</dt><dd className="mt-1 break-all font-mono" dir="ltr">{citation.artifact_id}</dd></div>
<div><dt className="text-muted-foreground">{t("runDetail.trust.column")}</dt><dd className="mt-1 break-all font-mono">{citation.column}</dd></div>
<div><dt className="text-muted-foreground">{t("runDetail.trust.row")}</dt><dd className="mt-1 font-mono">{citation.row}</dd></div>
<div className="sm:col-span-2"><dt className="text-muted-foreground">{t("runDetail.trust.artifactHash")}</dt><dd className="mt-1 break-all font-mono" dir="ltr">{citation.sha256 || t("runDetail.noneRecorded")}</dd></div>
</dl>
</li>
))}
</ul>
) : <p className="text-sm text-muted-foreground">{t("runDetail.trust.noMetricEvidence")}</p>}
</RunCardPanel>
<RunCardPanel title={i18n.t("runDetail.artifactChecksums")} icon={FileCheck2}>
{artifacts.length > 0 ? (
<div className="overflow-x-auto">
@@ -1,4 +1,4 @@
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { createMemoryRouter, RouterProvider } from "react-router";
import { RunDetail } from "../RunDetail";
import type { RunData } from "@/lib/api";
@@ -327,6 +327,73 @@ describe("RunDetail page", () => {
expect(await screen.findByText("No structured metrics recorded.")).toBeInTheDocument();
});
it("renders execution status, UTC times and complete metric evidence hashes", async () => {
const argsHash = "a".repeat(64);
const resultHash = "b".repeat(64);
const artifactHash = "c".repeat(64);
const runCard: NonNullable<RunData["run_card"]> = {
schema_version: "v1",
tool_traces: [
{ tool: "load_data", started_at: "2026-09-27T01:00:00Z", ended_at: "2026-09-27T01:00:01Z", status: "ok", args_hash: argsHash, result_hash: resultHash },
{ tool: "generate_signals", started_at: "2026-09-27T01:00:02Z", ended_at: "2026-09-27T01:00:03Z", status: "error", args_hash: "d".repeat(64), result_hash: "e".repeat(64) },
{ tool: "backtest", started_at: "2026-09-27T01:00:04Z", ended_at: "2026-09-27T01:00:05Z", status: "cancelled", args_hash: "f".repeat(64), result_hash: "0".repeat(64) },
],
citations: [{ metric: "sharpe", artifact_id: "artifacts/metrics.csv", column: "sharpe", row: 1, sha256: artifactHash }],
};
apiMock.getRun.mockResolvedValue({ status: "success", run_id: "trust", prompt: "Evidence run", run_card: runCard });
apiMock.getRunCode.mockResolvedValue({});
renderRunDetail("/runs/trust");
fireEvent.click(await screen.findByRole("tab", { name: "Run Card" }));
const executions = screen.getByText("Execution records").closest("section")!;
expect(within(executions).getByText("Load data")).toBeInTheDocument();
expect(within(executions).getByText("Generate signals")).toBeInTheDocument();
expect(within(executions).getByText("Backtest")).toBeInTheDocument();
for (const status of ["Completed", "Failed", "Cancelled"]) {
expect(within(executions).getByText(status)).toBeInTheDocument();
}
expect(within(executions).getByText("2026-09-27T01:00:00Z")).toHaveAttribute("dateTime", "2026-09-27T01:00:00Z");
expect(within(executions).getByText("2026-09-27T01:00:01Z")).toBeInTheDocument();
expect(within(executions).getByText(argsHash)).toHaveClass("break-all");
expect(within(executions).getByText(resultHash)).toBeInTheDocument();
const evidence = screen.getByText("Metric evidence").closest("section")!;
expect(within(evidence).getAllByText("sharpe")).toHaveLength(2);
expect(within(evidence).getByText("artifacts/metrics.csv")).toBeInTheDocument();
expect(within(evidence).getByText("Data row (1-based, excluding header)")).toBeInTheDocument();
expect(within(evidence).getByText("1")).toBeInTheDocument();
expect(within(evidence).getByText(artifactHash)).toHaveClass("break-all");
expect(within(evidence).getByText(/does not independently verify/)).toBeInTheDocument();
expect(within(evidence).queryByRole("link")).not.toBeInTheDocument();
});
it.each([
{ schema_version: "v0", metrics: { sharpe: 1.42 } },
{ schema_version: "v1", tool_traces: [], citations: [] },
])("explicitly reports absent evidence for $schema_version cards", async (runCard) => {
apiMock.getRun.mockResolvedValue({ status: "success", run_id: "no-evidence", prompt: "No evidence", run_card: runCard });
apiMock.getRunCode.mockResolvedValue({});
renderRunDetail("/runs/no-evidence");
fireEvent.click(await screen.findByRole("tab", { name: "Run Card" }));
expect(screen.getByText("Execution records were not recorded for this run.")).toBeInTheDocument();
expect(screen.getByText("Metric evidence was not recorded for this run.")).toBeInTheDocument();
if (runCard.schema_version === "v0") expect(screen.getByText("1.4200")).toBeInTheDocument();
});
it("renders artifact references as inert text", async () => {
const unsafeReference = "javascript:alert('<img src=x onerror=alert(1)>')";
apiMock.getRun.mockResolvedValue({
status: "success", run_id: "inert", prompt: "Inert references",
run_card: { citations: [{ metric: "return", artifact_id: unsafeReference, column: "return", row: 1, sha256: "a".repeat(64) }] },
});
apiMock.getRunCode.mockResolvedValue({});
renderRunDetail("/runs/inert");
fireEvent.click(await screen.findByRole("tab", { name: "Run Card" }));
const evidence = screen.getByText("Metric evidence").closest("section")!;
expect(within(evidence).getByText(unsafeReference)).toBeInTheDocument();
expect(evidence.querySelector("a, img")).toBeNull();
});
it("renders the Factor Research tab from has_factor_artifacts and lazy-loads the report", async () => {
apiMock.getRun.mockResolvedValue({
status: "success",