fix: complete September 27 reviews and surface verified run-card evidence
This commit is contained in:
1 parent
71a8c2b005
commit
0244eceaaf
36 files changed
+1046
-215
No files matched your search
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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-",
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"(下单|市价单|限价单|马上买|立即买|现在买|马上卖|立即卖|现在卖)"),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -11,8 +11,6 @@ Validates:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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": "الحجم",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "サイズ",
|
||||
|
||||
@@ -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": "크기",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "大小",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user