Merge branch 'main' into fix/journal-to-float-strip-currency
Resolves the import-list conflict in agent/tests/test_trade_journal.py introduced when #862 landed on main: keep both _to_float (this branch) and load_dataframe (#862).
This commit is contained in:
commit
5c690c7211
36 files changed
+1642
-794
No files matched your search
@@ -12,13 +12,21 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
||||
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
cache: pip
|
||||
|
||||
- name: Verify hash-locked dependencies
|
||||
run: >-
|
||||
pip install
|
||||
--dry-run
|
||||
--ignore-installed
|
||||
--require-hashes
|
||||
-r requirements-lock.txt
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install -e ".[dev]"
|
||||
|
||||
@@ -60,3 +68,41 @@ jobs:
|
||||
run: |
|
||||
cd frontend
|
||||
npx vitest run --reporter=verbose
|
||||
|
||||
windows-background:
|
||||
name: ${{ matrix.name }} / Python 3.14 background lifecycle
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: Windows 11 ARM64
|
||||
runner: windows-11-arm
|
||||
architecture: arm64
|
||||
- name: Windows Server 2025 x64
|
||||
runner: windows-2025
|
||||
architecture: x64
|
||||
|
||||
steps:
|
||||
# actions/checkout v7.0.1
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
|
||||
# actions/setup-python v7.0.0
|
||||
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
|
||||
with:
|
||||
python-version: "3.14"
|
||||
architecture: ${{ matrix.architecture }}
|
||||
cache: pip
|
||||
|
||||
- name: Install focused test dependencies
|
||||
run: python -m pip install pytest pydantic pyyaml python-dotenv
|
||||
|
||||
- name: Run Windows background safety regressions
|
||||
env:
|
||||
PYTHONPATH: agent
|
||||
run: >-
|
||||
python -m pytest
|
||||
agent/tests/test_background_tools.py
|
||||
agent/tests/test_bash_tool.py
|
||||
-q
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
deployments: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Deploy wiki
|
||||
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
|
||||
|
||||
@@ -18,13 +18,13 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
||||
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ agent/tests/test_e2e_*.py
|
||||
|
||||
# Local working notes
|
||||
NOTES.md
|
||||
.workbuddy/
|
||||
|
||||
# Agent tool aliases (auto-generated mirror of CLAUDE.md)
|
||||
AGENTS.md
|
||||
|
||||
@@ -52,13 +52,14 @@
|
||||
|
||||
> ⚠️ **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-07-26** 🔒 **Dependency lock + universe transparency**: Docker’s hash-locked install works again, with a new CI lock check ([#858](https://github.com/HKUDS/Vibe-Trading/pull/858), closes [#847](https://github.com/HKUDS/Vibe-Trading/issues/847)). `alpha bench` now discloses CSI300/SP500 sources, counts, degraded fallbacks, and survivorship bias ([#859](https://github.com/HKUDS/Vibe-Trading/pull/859), closes [#845](https://github.com/HKUDS/Vibe-Trading/issues/845)). Actions and five frontend dependencies were also refreshed ([#850](https://github.com/HKUDS/Vibe-Trading/pull/850)–[#852](https://github.com/HKUDS/Vibe-Trading/pull/852)).
|
||||
- **2026-07-25** 🔧 **Perpetual realism + MCP crash fix + a correctness batch**: USD-M perpetuals gain **margin state contracts** ([#798](https://github.com/HKUDS/Vibe-Trading/pull/798), thanks @honginp) and the engine now consumes **historical funding rates** instead of fetching-and-ignoring them ([#819](https://github.com/HKUDS/Vibe-Trading/pull/819), thanks @g0rdonL). MCP dataclass results no longer crash on a false `Circular reference detected` ([#849](https://github.com/HKUDS/Vibe-Trading/pull/849), thanks @Echoandelementwebsites), and `alpha bench` CLI/HTML forward the `_meta` survivorship disclosure ([#841](https://github.com/HKUDS/Vibe-Trading/pull/841), closes [#797](https://github.com/HKUDS/Vibe-Trading/issues/797), thanks @AmirF194). Plus 12 correctness fixes across journals, connectors, and channels ([#799](https://github.com/HKUDS/Vibe-Trading/pull/799)–[#810](https://github.com/HKUDS/Vibe-Trading/pull/810), thanks @santhreal), and a real account label in CLI balances ([#843](https://github.com/HKUDS/Vibe-Trading/pull/843), closes [#846](https://github.com/HKUDS/Vibe-Trading/issues/846), thanks @Robin1987China).
|
||||
- **2026-07-24** 🔀 **Memory Tier 2, composable optimizer constraints + an interval-handling sweep**: Persistent memory gains **Tier 2 structural organization** ([#815](https://github.com/HKUDS/Vibe-Trading/pull/815), thanks @shadowinlife), and backtest optimizers accept **composable weight constraints** ([#818](https://github.com/HKUDS/Vibe-Trading/pull/818), thanks @he-yufeng). Correctness: the daily-bar validator can opt in to **non-positive prices** — opening on negative bars while still rejecting zero ([#816](https://github.com/HKUDS/Vibe-Trading/pull/816), closes [#571](https://github.com/HKUDS/Vibe-Trading/issues/571), thanks @darkknight4563). Plus a 19-PR loader **interval-normalization sweep**: lowercase `1h/4h/1d/1w` aliases accepted everywhere, unsupported intervals now fail fast instead of silently returning daily bars, Yahoo `4H` maps to `1h`, and MT5 accepts `1W/1M` ([#812](https://github.com/HKUDS/Vibe-Trading/pull/812)–[#838](https://github.com/HKUDS/Vibe-Trading/pull/838), thanks @santhreal), a trade-journal fix for Eastmoney Excel-serial dates ([#811](https://github.com/HKUDS/Vibe-Trading/pull/811), thanks @santhreal), and a README nav-anchor fix ([#840](https://github.com/HKUDS/Vibe-Trading/pull/840), thanks @dvirarad).
|
||||
- **2026-07-23** 🔧 **Reliability sweep + strict alpha-bench surfaced + opt-in memory lifecycle**: A 22-PR contributor batch. A broad **reliability sweep** fixes timeframe handling end to end — yfinance `1M`→monthly (not minute), CCXT `1W`/`1M`, akshare/india-broker rejecting unsupported intervals instead of silent daily, and the Tiger/Alpaca/OKX/Shoonya/Longbridge connectors keeping `1H`/`4H` as hour bars — plus trade-journal Excel-date normalization (eastmoney float `YYYYMMDD`, Futu/Tonghuashun serial dates), finite-JSON `report_audit`, blank `holding_days` validation, and Feishu/CLI markdown table edges ([#778](https://github.com/HKUDS/Vibe-Trading/pull/778)–[#794](https://github.com/HKUDS/Vibe-Trading/pull/794), thanks @santhreal). **MT5** `trading_history` now coerces numpy scalars so JSON serialization no longer dies on `int64` ([#776](https://github.com/HKUDS/Vibe-Trading/pull/776), closes [#774](https://github.com/HKUDS/Vibe-Trading/issues/774), thanks @shadowinlife), and **PIT fundamentals** dedup restated rows and stop the snapshot regressing to an older fiscal period on a late restatement ([#772](https://github.com/HKUDS/Vibe-Trading/pull/772), closes [#771](https://github.com/HKUDS/Vibe-Trading/issues/771), thanks @klmtseng). New: **`alpha bench --strict`** finally wires the strict same-universe random-control + OOS gate that shipped unreachable since 0.1.9 ([#796](https://github.com/HKUDS/Vibe-Trading/pull/796), closes [#773](https://github.com/HKUDS/Vibe-Trading/issues/773), thanks @he-yufeng), an opt-in **memory lifecycle** (quality scoring, Ebbinghaus decay, archive-only GC — all off by default) ([#733](https://github.com/HKUDS/Vibe-Trading/pull/733), closes [#732](https://github.com/HKUDS/Vibe-Trading/issues/732), thanks @shadowinlife), and backtest **rebalance-notes** artifacts + turnover metrics ([#795](https://github.com/HKUDS/Vibe-Trading/pull/795), thanks @he-yufeng).
|
||||
|
||||
<details>
|
||||
<summary>Earlier news</summary>
|
||||
|
||||
- **2026-07-23** 🔧 **Reliability sweep + strict alpha-bench surfaced + opt-in memory lifecycle**: A 22-PR contributor batch. A broad **reliability sweep** fixes timeframe handling end to end — yfinance `1M`→monthly (not minute), CCXT `1W`/`1M`, akshare/india-broker rejecting unsupported intervals instead of silent daily, and the Tiger/Alpaca/OKX/Shoonya/Longbridge connectors keeping `1H`/`4H` as hour bars — plus trade-journal Excel-date normalization (eastmoney float `YYYYMMDD`, Futu/Tonghuashun serial dates), finite-JSON `report_audit`, blank `holding_days` validation, and Feishu/CLI markdown table edges ([#778](https://github.com/HKUDS/Vibe-Trading/pull/778)–[#794](https://github.com/HKUDS/Vibe-Trading/pull/794), thanks @santhreal). **MT5** `trading_history` now coerces numpy scalars so JSON serialization no longer dies on `int64` ([#776](https://github.com/HKUDS/Vibe-Trading/pull/776), closes [#774](https://github.com/HKUDS/Vibe-Trading/issues/774), thanks @shadowinlife), and **PIT fundamentals** dedup restated rows and stop the snapshot regressing to an older fiscal period on a late restatement ([#772](https://github.com/HKUDS/Vibe-Trading/pull/772), closes [#771](https://github.com/HKUDS/Vibe-Trading/issues/771), thanks @klmtseng). New: **`alpha bench --strict`** finally wires the strict same-universe random-control + OOS gate that shipped unreachable since 0.1.9 ([#796](https://github.com/HKUDS/Vibe-Trading/pull/796), closes [#773](https://github.com/HKUDS/Vibe-Trading/issues/773), thanks @he-yufeng), an opt-in **memory lifecycle** (quality scoring, Ebbinghaus decay, archive-only GC — all off by default) ([#733](https://github.com/HKUDS/Vibe-Trading/pull/733), closes [#732](https://github.com/HKUDS/Vibe-Trading/issues/732), thanks @shadowinlife), and backtest **rebalance-notes** artifacts + turnover metrics ([#795](https://github.com/HKUDS/Vibe-Trading/pull/795), thanks @he-yufeng).
|
||||
- **2026-07-22** 🚀 **v0.1.12 released** ([Release notes](https://github.com/HKUDS/Vibe-Trading/releases/tag/v0.1.12), `pip install -U vibe-trading-ai`): The **correlation regime timeline** adds a `GET /correlation/regime` endpoint + an opt-in Correlation-tab strip — edge density run through a causal hysteresis state machine that marks FUSED market episodes, descriptive risk context rather than a signal ([#756](https://github.com/HKUDS/Vibe-Trading/pull/756), closes [#719](https://github.com/HKUDS/Vibe-Trading/issues/719), thanks @ebujinovch). Provider endpoint resolution now falls back to each provider's canonical base URL and gracefully handles non-SSE endpoints, fixing the native **zai** provider on glm-5.1 ([#758](https://github.com/HKUDS/Vibe-Trading/issues/758)). Plus a strict-JSON / finite-number **reliability sweep** across metrics, factors, pattern, session, and journal ([#761](https://github.com/HKUDS/Vibe-Trading/pull/761)–[#770](https://github.com/HKUDS/Vibe-Trading/pull/770), thanks @santhreal) and a Binance maintenance-bracket decouple that keeps `-PERP` backtests zero-credential ([#757](https://github.com/HKUDS/Vibe-Trading/pull/757), thanks @honginp). Rolls up ~90 fixes since 0.1.11.
|
||||
- **2026-07-21** 🔧 **Data-loader completeness + a reliability fix sweep**: Partial market-data results now complete the missing symbols through the fallback chain and fail closed instead of silently shrinking the backtest universe ([#689](https://github.com/HKUDS/Vibe-Trading/pull/689), closes [#681](https://github.com/HKUDS/Vibe-Trading/issues/681), thanks @xkam7ar), and OKX bars use the `history-candles` endpoint with rate-limit retry for deep backfills ([#644](https://github.com/HKUDS/Vibe-Trading/pull/644), thanks @tyj147454413-cmd). Plus a fix sweep: the MCP network guard accepts IPv6 / case-variant hosts ([#750](https://github.com/HKUDS/Vibe-Trading/pull/750), thanks @Robin1987China), trade-journal parsers skip blank/NaN symbol rows ([#749](https://github.com/HKUDS/Vibe-Trading/pull/749), thanks @Robin1987China), the Shadow Account skips the mined entry-hour gate on daily bars ([#748](https://github.com/HKUDS/Vibe-Trading/pull/748), thanks @Robin1987China), and MiniMax regional API endpoints are selectable ([#731](https://github.com/HKUDS/Vibe-Trading/pull/731), thanks @octo-patch).
|
||||
- **2026-07-20** 🔀 **Providers, MetaTrader 5, and a reliability sweep**: Native **Anthropic Messages API** (optional `[anthropic]` extra, [#695](https://github.com/HKUDS/Vibe-Trading/pull/695), thanks @jelech), **SiliconFlow** ([#565](https://github.com/HKUDS/Vibe-Trading/pull/565), thanks @UNHNQ), and **iFlytek Spark** ([#537](https://github.com/HKUDS/Vibe-Trading/pull/537), thanks @FenjuFu) join the provider roster, and a **MetaTrader 5 (Exness)** broker connector + `mt5` forex/metal data source lands (broker connectors → **12**, [#481](https://github.com/HKUDS/Vibe-Trading/pull/481), thanks @StaniellG). Plus a provider-agnostic **`llm-vision` OCR** engine ([#548](https://github.com/HKUDS/Vibe-Trading/pull/548), thanks @shadowinlife), an **80× signal-alignment vectorization** ([#698](https://github.com/HKUDS/Vibe-Trading/pull/698), thanks @shadowinlife), historical **Binance USD-M funding/bracket** data ([#716](https://github.com/HKUDS/Vibe-Trading/pull/716), thanks @honginp), a swarm MCP-discovery cache ([#704](https://github.com/HKUDS/Vibe-Trading/pull/704)), and a reliability consolidation closing **13** SSE/session/CLI/swarm/scheduler issues ([#584](https://github.com/HKUDS/Vibe-Trading/pull/584), thanks @xkam7ar). Correctness: options **partial-close** now honors the requested quantity instead of flattening the lot ([#577](https://github.com/HKUDS/Vibe-Trading/issues/577)), centralized provider credential resolution ([#563](https://github.com/HKUDS/Vibe-Trading/pull/563)), queued-cancel handling ([#641](https://github.com/HKUDS/Vibe-Trading/pull/641)), a frontend streaming-DOM race ([#717](https://github.com/HKUDS/Vibe-Trading/pull/717), thanks @Marnie0415), and the connector CLI renderers ([#726](https://github.com/HKUDS/Vibe-Trading/pull/726), thanks @nareshkps).
|
||||
@@ -984,7 +985,7 @@ Interactive docs: `http://localhost:8899/docs`
|
||||
|
||||
For localhost development, `vibe-trading serve` keeps the browser workflow simple. For any non-local client, sensitive API endpoints require `API_AUTH_KEY`; use `Authorization: Bearer <key>` for JSON/upload requests. Browser EventSource streams are handled by the Web UI after you enter the same key once in Settings.
|
||||
|
||||
Shell-capable tools (`bash` / `background_run`) are enabled only for the interactive local CLI. Every other surface — the HTTP/SSE API and the MCP server on **all** transports (stdio included) — keeps them off unless you explicitly opt in with `VIBE_TRADING_ENABLE_SHELL_TOOLS=1` (or pass `--enable-shell-tools` to `vibe-trading-mcp`). Transport type never implicitly grants shell access. Document and journal readers are limited to upload/import roots by default; place files under `agent/uploads`, `agent/runs`, `./uploads`, `./data`, `~/.vibe-trading/uploads`, or `~/.vibe-trading/imports`, or add a dedicated directory through `VIBE_TRADING_ALLOWED_FILE_ROOTS`.
|
||||
Shell-capable process tools (`bash` / `background_run` / `cancel_background`) are enabled only for the interactive local CLI. Every other surface — the HTTP/SSE API and the MCP server on **all** transports (stdio included) — keeps them off unless you explicitly opt in with `VIBE_TRADING_ENABLE_SHELL_TOOLS=1` (or pass `--enable-shell-tools` to `vibe-trading-mcp`). Transport type never implicitly grants shell access. `cancel_background` stops only the tracked task ID returned by `background_run`; broad Python process-name termination is refused because it could terminate Vibe-Trading itself. Document and journal readers are limited to upload/import roots by default; place files under `agent/uploads`, `agent/runs`, `./uploads`, `./data`, `~/.vibe-trading/uploads`, or add a dedicated directory through `VIBE_TRADING_ALLOWED_FILE_ROOTS`.
|
||||
|
||||
Generated backtest code runs as a local Python subprocess and can make network requests through the configured market-data loaders. Its environment is intentionally narrow: the runner keeps OS/Python basics, proxy/certificate settings, `VIBE_TRADING_ALLOWED_RUN_ROOTS`, and read-only market-data keys such as `TUSHARE_TOKEN`, `FMP_API_KEY`, `FRED_API_KEY`, and `VIBE_TRADING_IWENCAI_KEY`. It does not pass LLM provider keys, API auth tokens, shell-tool switches, broker trading secrets, or live/advisory toggles to generated strategy code by default.
|
||||
|
||||
|
||||
+3
-2
@@ -52,13 +52,14 @@
|
||||
|
||||
> ⚠️ **تحذير أمني:** حساب X باسم `VibeTrading_HKU`، ومشروع Virtuals رقم `101845`، وعقد التوكن `0x640BDBF77b6447E8b7DB7894cED84BD1c40571f4` كلّها غير رسمية ولا تتبع Vibe-Trading. لم نُطلق أو نؤيد مطلقًا أي توكن أو عملة ميم. لا تشترِ هذا التوكن، ولا تربط محفظتك، ولا توقّع أي شيء. [التفاصيل](SECURITY.md#official-channels--impersonation).
|
||||
|
||||
- **2026-07-26** 🔒 **إصلاح قفل الاعتماديات + شفافية مكونات المؤشر**: عاد تثبيت Docker المقيّد بالتجزئات للعمل، مع فحص جديد للقفل في CI ([#858](https://github.com/HKUDS/Vibe-Trading/pull/858)، يغلق [#847](https://github.com/HKUDS/Vibe-Trading/issues/847)). ويكشف `alpha bench` الآن مصادر CSI300/SP500 وأعداد المكونات وحالات الرجوع المخفّضة وانحياز البقاء ([#859](https://github.com/HKUDS/Vibe-Trading/pull/859)، يغلق [#845](https://github.com/HKUDS/Vibe-Trading/issues/845)). كما حُدّثت Actions وخمس اعتماديات للواجهة الأمامية ([#850](https://github.com/HKUDS/Vibe-Trading/pull/850)–[#852](https://github.com/HKUDS/Vibe-Trading/pull/852)).
|
||||
- **2026-07-25** 🔧 **واقعية العقود الدائمة + إصلاح تعطل MCP + حزمة تصحيحات**: حصلت عقود USD-M الدائمة على **عقود حالة الهامش** ([#798](https://github.com/HKUDS/Vibe-Trading/pull/798)، شكرًا @honginp)، وأصبح المحرك يستهلك **معدلات التمويل التاريخية** فعليًا بدلًا من جلبها وتجاهلها ([#819](https://github.com/HKUDS/Vibe-Trading/pull/819)، شكرًا @g0rdonL). لم تعد نتائج dataclass في MCP تتعطل بسبب `Circular reference detected` خاطئ ([#849](https://github.com/HKUDS/Vibe-Trading/pull/849)، شكرًا @Echoandelementwebsites)، ويمرر `alpha bench` في CLI/HTML كتلة `_meta` للإفصاح عن انحياز البقاء ([#841](https://github.com/HKUDS/Vibe-Trading/pull/841)، يغلق [#797](https://github.com/HKUDS/Vibe-Trading/issues/797)، شكرًا @AmirF194). إضافة إلى 12 إصلاحًا للصحة عبر اليوميات والموصلات والقنوات ([#799](https://github.com/HKUDS/Vibe-Trading/pull/799)–[#810](https://github.com/HKUDS/Vibe-Trading/pull/810)، شكرًا @santhreal)، وعرض تسمية حساب حقيقية في عرض الأرصدة في CLI ([#843](https://github.com/HKUDS/Vibe-Trading/pull/843)، يغلق [#846](https://github.com/HKUDS/Vibe-Trading/issues/846)، شكرًا @Robin1987China).
|
||||
- **2026-07-24** 🔀 **الطبقة الثانية من الذاكرة وقيود مُحسِّن قابلة للتركيب + جولة معالجة الفترات**: حصلت الذاكرة الدائمة على **تنظيم هيكلي من الطبقة الثانية (Tier 2)** ([#815](https://github.com/HKUDS/Vibe-Trading/pull/815)، شكرًا @shadowinlife)، وأصبحت مُحسِّنات الاختبار الرجعي تقبل **قيود أوزان قابلة للتركيب** ([#818](https://github.com/HKUDS/Vibe-Trading/pull/818)، شكرًا @he-yufeng). التصحيح: أصبح مدقّق الأعمدة اليومية قادرًا على تفعيل **الأسعار غير الموجبة** اختياريًا — يفتح عند أعمدة الأسعار السالبة مع استمرار رفض الصفر ([#816](https://github.com/HKUDS/Vibe-Trading/pull/816)، يغلق [#571](https://github.com/HKUDS/Vibe-Trading/issues/571)، شكرًا @darkknight4563). بالإضافة إلى **جولة تطبيع فترات** عبر 19 طلب دمج في المحمّلات: قبول الأسماء المستعارة بالأحرف الصغيرة `1h/4h/1d/1w` في كل مكان، والفترات غير المدعومة تفشل الآن بسرعة بدل إرجاع أعمدة يومية بصمت، ويُحوَّل `4H` في Yahoo إلى `1h`، ويقبل MT5 الفترتين `1W/1M` ([#812](https://github.com/HKUDS/Vibe-Trading/pull/812)–[#838](https://github.com/HKUDS/Vibe-Trading/pull/838)، شكرًا @santhreal)، مع إصلاح تواريخ Excel التسلسلية من Eastmoney في سجل التداول ([#811](https://github.com/HKUDS/Vibe-Trading/pull/811)، شكرًا @santhreal)، وإصلاح مرساة التنقل في README ([#840](https://github.com/HKUDS/Vibe-Trading/pull/840)، شكرًا @dvirarad).
|
||||
- **2026-07-23** 🔧 **جولة موثوقية + كشف بوابة alpha-bench الصارمة + دورة حياة اختيارية للذاكرة**: دفعة من 22 طلب دمج من المساهمين. **جولة موثوقية** واسعة تُصلح معالجة الأطر الزمنية من طرف إلى طرف — yfinance `1M`←شهري (لا دقيقة)، وCCXT `1W`/`1M`، وakshare/india-broker يرفضان الفواصل غير المدعومة بدل إرجاع يومي بصمت، وموصّلات Tiger/Alpaca/OKX/Shoonya/Longbridge تُبقي `1H`/`4H` شموعاً بالساعة — إضافة إلى تطبيع تواريخ Excel في سجل التداول (عدد عشري `YYYYMMDD` من eastmoney، وتواريخ تسلسلية من Futu/Tonghuashun)، وJSON منتهي القيم في `report_audit`، والتحقق من `holding_days` الفارغة، وأعمدة حواف جداول markdown في Feishu/CLI ([#778](https://github.com/HKUDS/Vibe-Trading/pull/778)–[#794](https://github.com/HKUDS/Vibe-Trading/pull/794)، شكرًا @santhreal). أصبح **MT5** `trading_history` يحوّل قيم numpy العددية إلى أنواع Python أصلية فلا يفشل تسلسل JSON على `int64` ([#776](https://github.com/HKUDS/Vibe-Trading/pull/776)، يغلق [#774](https://github.com/HKUDS/Vibe-Trading/issues/774)، شكرًا @shadowinlife)، و**البيانات الأساسية PIT** تزيل تكرار الصفوف المُعاد ذكرها وتمنع تراجع اللقطة إلى فترة مالية أقدم عند إعلان تصحيح متأخر ([#772](https://github.com/HKUDS/Vibe-Trading/pull/772)، يغلق [#771](https://github.com/HKUDS/Vibe-Trading/issues/771)، شكرًا @klmtseng). جديد: **`alpha bench --strict`** يربط أخيرًا بوابة الضبط الصارم بالتحكم العشوائي على نفس الكون + OOS التي شُحنت دون منفذ منذ 0.1.9 ([#796](https://github.com/HKUDS/Vibe-Trading/pull/796)، يغلق [#773](https://github.com/HKUDS/Vibe-Trading/issues/773)، شكرًا @he-yufeng)، و**دورة حياة اختيارية للذاكرة** (تسجيل الجودة، تلاشي إبنغهاوس، جمع مهملات بالأرشفة فقط — كلها معطّلة افتراضيًا) ([#733](https://github.com/HKUDS/Vibe-Trading/pull/733)، يغلق [#732](https://github.com/HKUDS/Vibe-Trading/issues/732)، شكرًا @shadowinlife)، ومنتجات **ملاحظات إعادة التوازن** في الاختبار الخلفي + مقاييس معدل الدوران ([#795](https://github.com/HKUDS/Vibe-Trading/pull/795)، شكرًا @he-yufeng).
|
||||
|
||||
<details>
|
||||
<summary>أخبار سابقة</summary>
|
||||
|
||||
- **2026-07-23** 🔧 **جولة موثوقية + كشف بوابة alpha-bench الصارمة + دورة حياة اختيارية للذاكرة**: دفعة من 22 طلب دمج من المساهمين. **جولة موثوقية** واسعة تُصلح معالجة الأطر الزمنية من طرف إلى طرف — yfinance `1M`←شهري (لا دقيقة)، وCCXT `1W`/`1M`، وakshare/india-broker يرفضان الفواصل غير المدعومة بدل إرجاع يومي بصمت، وموصّلات Tiger/Alpaca/OKX/Shoonya/Longbridge تُبقي `1H`/`4H` شموعاً بالساعة — إضافة إلى تطبيع تواريخ Excel في سجل التداول (عدد عشري `YYYYMMDD` من eastmoney، وتواريخ تسلسلية من Futu/Tonghuashun)، وJSON منتهي القيم في `report_audit`، والتحقق من `holding_days` الفارغة، وأعمدة حواف جداول markdown في Feishu/CLI ([#778](https://github.com/HKUDS/Vibe-Trading/pull/778)–[#794](https://github.com/HKUDS/Vibe-Trading/pull/794)، شكرًا @santhreal). أصبح **MT5** `trading_history` يحوّل قيم numpy العددية إلى أنواع Python أصلية فلا يفشل تسلسل JSON على `int64` ([#776](https://github.com/HKUDS/Vibe-Trading/pull/776)، يغلق [#774](https://github.com/HKUDS/Vibe-Trading/issues/774)، شكرًا @shadowinlife)، و**البيانات الأساسية PIT** تزيل تكرار الصفوف المُعاد ذكرها وتمنع تراجع اللقطة إلى فترة مالية أقدم عند إعلان تصحيح متأخر ([#772](https://github.com/HKUDS/Vibe-Trading/pull/772)، يغلق [#771](https://github.com/HKUDS/Vibe-Trading/issues/771)، شكرًا @klmtseng). جديد: **`alpha bench --strict`** يربط أخيرًا بوابة الضبط الصارم بالتحكم العشوائي على نفس الكون + OOS التي شُحنت دون منفذ منذ 0.1.9 ([#796](https://github.com/HKUDS/Vibe-Trading/pull/796)، يغلق [#773](https://github.com/HKUDS/Vibe-Trading/issues/773)، شكرًا @he-yufeng)، و**دورة حياة اختيارية للذاكرة** (تسجيل الجودة، تلاشي إبنغهاوس، جمع مهملات بالأرشفة فقط — كلها معطّلة افتراضيًا) ([#733](https://github.com/HKUDS/Vibe-Trading/pull/733)، يغلق [#732](https://github.com/HKUDS/Vibe-Trading/issues/732)، شكرًا @shadowinlife)، ومنتجات **ملاحظات إعادة التوازن** في الاختبار الخلفي + مقاييس معدل الدوران ([#795](https://github.com/HKUDS/Vibe-Trading/pull/795)، شكرًا @he-yufeng).
|
||||
- **2026-07-22** 🚀 **صدر الإصدار v0.1.12** ([ملاحظات الإصدار](https://github.com/HKUDS/Vibe-Trading/releases/tag/v0.1.12)، `pip install -U vibe-trading-ai`): يضيف **الجدول الزمني لنظام الارتباط (correlation regime)** نقطة نهاية `GET /correlation/regime` + شريطاً اختيارياً في تبويب Correlation — تُمرَّر كثافة الحواف عبر آلة حالة تباطؤ (hysteresis) سببية تُعلّم فترات اندماج السوق (FUSED)، وهو سياق مخاطر وصفي لا إشارة تداول ([#756](https://github.com/HKUDS/Vibe-Trading/pull/756)، يغلق [#719](https://github.com/HKUDS/Vibe-Trading/issues/719)، شكرًا @ebujinovch). وأصبح حلّ نقاط نهاية المزوّد يتراجع الآن إلى عنوان URL الأساسي القانوني لكل مزوّد ويتعامل بسلاسة مع نقاط النهاية غير SSE، ما يصلح مزوّد **zai** الأصلي على glm-5.1 ([#758](https://github.com/HKUDS/Vibe-Trading/issues/758)). بالإضافة إلى **جولة موثوقية** بـ JSON صارم / أرقام منتهية عبر المقاييس والعوامل والأنماط والجلسات والسجل ([#761](https://github.com/HKUDS/Vibe-Trading/pull/761)–[#770](https://github.com/HKUDS/Vibe-Trading/pull/770)، شكرًا @santhreal)، وفصل شريحة صيانة Binance الذي يُبقي اختبارات `-PERP` الرجعية بلا بيانات اعتماد ([#757](https://github.com/HKUDS/Vibe-Trading/pull/757)، شكرًا @honginp). يجمع ~90 إصلاحاً منذ 0.1.11.
|
||||
- **2026-07-21** 🔧 **اكتمال محمّل البيانات + جولة إصلاحات للموثوقية**: أصبحت نتائج بيانات السوق الجزئية تُكمل الرموز المفقودة عبر سلسلة fallback وتفشل بأمان بدلًا من تقليص نطاق الاختبار الخلفي بصمت ([#689](https://github.com/HKUDS/Vibe-Trading/pull/689)، يُغلق [#681](https://github.com/HKUDS/Vibe-Trading/issues/681)، شكرًا @xkam7ar)، وتستخدم شموع OKX نقطة النهاية `history-candles` مع إعادة المحاولة عند حد المعدل لعمليات التعبئة التاريخية العميقة ([#644](https://github.com/HKUDS/Vibe-Trading/pull/644)، شكرًا @tyj147454413-cmd). بالإضافة إلى جولة إصلاحات: يقبل حارس شبكة MCP مضيفي IPv6 / المختلفين في حالة الأحرف ([#750](https://github.com/HKUDS/Vibe-Trading/pull/750)، شكرًا @Robin1987China)، ويتخطى محلّلو سجل التداول صفوف الرموز الفارغة/NaN ([#749](https://github.com/HKUDS/Vibe-Trading/pull/749)، شكرًا @Robin1987China)، ويتخطى Shadow Account بوابة ساعة الدخول المستخرجة على الشموع اليومية ([#748](https://github.com/HKUDS/Vibe-Trading/pull/748)، شكرًا @Robin1987China)، وأصبحت نقاط نهاية MiniMax الإقليمية قابلة للاختيار ([#731](https://github.com/HKUDS/Vibe-Trading/pull/731)، شكرًا @octo-patch).
|
||||
- **2026-07-20** 🔀 **مزوّدو النماذج وMetaTrader 5 ومراجعة للموثوقية**: انضم مزوّد **Anthropic Messages API** الأصلي (إضافة اختيارية `[anthropic]`، [#695](https://github.com/HKUDS/Vibe-Trading/pull/695)، شكرًا @jelech) و**SiliconFlow** ([#565](https://github.com/HKUDS/Vibe-Trading/pull/565)، شكرًا @UNHNQ) و**iFlytek Spark** ([#537](https://github.com/HKUDS/Vibe-Trading/pull/537)، شكرًا @FenjuFu) إلى قائمة المزوّدين، ووصل موصّل وسيط **MetaTrader 5 (Exness)** ومصدر بيانات `mt5` للفوركس/المعادن (موصّلات الوسطاء → **12**، [#481](https://github.com/HKUDS/Vibe-Trading/pull/481)، شكرًا @StaniellG). بالإضافة إلى محرّك **`llm-vision` OCR** المستقل عن المزوّد ([#548](https://github.com/HKUDS/Vibe-Trading/pull/548)، شكرًا @shadowinlife)، و**تحويل محاذاة الإشارات إلى متجهات بسرعة 80×** ([#698](https://github.com/HKUDS/Vibe-Trading/pull/698)، شكرًا @shadowinlife)، وبيانات **تمويل/شرائح USD-M** التاريخية من Binance ([#716](https://github.com/HKUDS/Vibe-Trading/pull/716)، شكرًا @honginp)، وذاكرة تخزين مؤقتة لاكتشاف MCP في swarm ([#704](https://github.com/HKUDS/Vibe-Trading/pull/704))، ودمج للموثوقية يُغلق **13** مشكلة في SSE/الجلسات/CLI/swarm/المجدول ([#584](https://github.com/HKUDS/Vibe-Trading/pull/584)، شكرًا @xkam7ar). تصحيحات الدقّة: أصبح **الإغلاق الجزئي** للخيارات يحترم الكمية المطلوبة بدلًا من تصفية العقدة بالكامل ([#577](https://github.com/HKUDS/Vibe-Trading/issues/577))، وتوحيد حلّ بيانات اعتماد المزوّد ([#563](https://github.com/HKUDS/Vibe-Trading/pull/563))، ومعالجة الإلغاء أثناء الانتظار ([#641](https://github.com/HKUDS/Vibe-Trading/pull/641))، وسباق DOM أثناء البث في الواجهة ([#717](https://github.com/HKUDS/Vibe-Trading/pull/717)، شكرًا @Marnie0415)، وعارضات CLI للموصّلات ([#726](https://github.com/HKUDS/Vibe-Trading/pull/726)، شكرًا @nareshkps).
|
||||
@@ -925,7 +926,7 @@ vibe-trading serve --port 8899
|
||||
|
||||
للتطوير على localhost، يبقي `vibe-trading serve` سير المتصفح بسيطاً. لأي عميل غير محلي، تتطلب نقاط API الحساسة `API_AUTH_KEY`؛ استخدم `Authorization: Bearer <key>` لطلبات JSON/الرفع. تتعامل Web UI مع تدفقات Browser EventSource بعد إدخال المفتاح نفسه مرة واحدة في Settings.
|
||||
|
||||
الأدوات القادرة على shell (`bash` / `background_run`) مفعّلة فقط لواجهة CLI المحلية التفاعلية. أما بقية الأسطح — واجهة HTTP/SSE و MCP server على **جميع** وسائط النقل (بما في ذلك stdio) — فتبقى معطّلة ما لم تفعّلها صراحة عبر `VIBE_TRADING_ENABLE_SHELL_TOOLS=1` (أو تمرير `--enable-shell-tools` إلى `vibe-trading-mcp`). نوع وسيط النقل لا يمنح صلاحية shell ضمنيًا أبدًا. قارئات المستندات والسجلات محدودة افتراضياً بجذور الرفع/الاستيراد؛ ضع الملفات تحت `agent/uploads` أو `agent/runs` أو `./uploads` أو `./data` أو `~/.vibe-trading/uploads` أو `~/.vibe-trading/imports`، أو أضف دليلاً مخصصاً عبر `VIBE_TRADING_ALLOWED_FILE_ROOTS`.
|
||||
الأدوات القادرة على shell (`bash` / `background_run` / `cancel_background`) مفعّلة فقط لواجهة CLI المحلية التفاعلية. أما بقية الأسطح — واجهة HTTP/SSE و MCP server على **جميع** وسائط النقل (بما في ذلك stdio) — فتبقى معطّلة ما لم تفعّلها صراحة عبر `VIBE_TRADING_ENABLE_SHELL_TOOLS=1` (أو تمرير `--enable-shell-tools` إلى `vibe-trading-mcp`). نوع وسيط النقل لا يمنح صلاحية shell ضمنيًا أبدًا. قارئات المستندات والسجلات محدودة افتراضياً بجذور الرفع/الاستيراد؛ ضع الملفات تحت `agent/uploads` أو `agent/runs` أو `./uploads` أو `./data` أو `~/.vibe-trading/uploads` أو `~/.vibe-trading/imports`، أو أضف دليلاً مخصصاً عبر `VIBE_TRADING_ALLOWED_FILE_ROOTS`.
|
||||
|
||||
### إعدادات Web UI
|
||||
|
||||
|
||||
+3
-2
@@ -52,13 +52,14 @@
|
||||
|
||||
> ⚠️ **セキュリティ警告:** Xアカウント `VibeTrading_HKU`、Virtualsプロジェクト `101845`、およびトークンコントラクト `0x640BDBF77b6447E8b7DB7894cED84BD1c40571f4` は、いずれもVibe-Trading公式のものではありません。Vibe-Tradingはこれまで、いかなるトークンやミームコインも発行・公認していません。購入、ウォレットの接続、署名は行わないでください。[詳細](SECURITY.md#official-channels--impersonation)
|
||||
|
||||
- **2026-07-26** 🔒 **依存関係ロック修復 + ベンチマーク・ユニバースの透明性**:Docker のハッシュロック付きインストールが復旧し、CI にロック検証を追加しました([#858](https://github.com/HKUDS/Vibe-Trading/pull/858)、[#847](https://github.com/HKUDS/Vibe-Trading/issues/847) をクローズ)。`alpha bench` は CSI300/SP500 の出典、銘柄数、縮退フォールバック、生存者バイアスを開示します([#859](https://github.com/HKUDS/Vibe-Trading/pull/859)、[#845](https://github.com/HKUDS/Vibe-Trading/issues/845) をクローズ)。Actions とフロントエンド依存関係 5 件も更新しました([#850](https://github.com/HKUDS/Vibe-Trading/pull/850)–[#852](https://github.com/HKUDS/Vibe-Trading/pull/852))。
|
||||
- **2026-07-25** 🔧 **パーペチュアルのリアリティ向上 + MCP クラッシュ修正 + 正確性バッチ**: USD-M パーペチュアルが**証抠金ステートコントラクト**を獲得([#798](https://github.com/HKUDS/Vibe-Trading/pull/798)、@honginp さんに感謝)。エンジンは取得して無視していた**過去の資金調達率**を実際に消費するようになりました([#819](https://github.com/HKUDS/Vibe-Trading/pull/819)、@g0rdonL さんに感謝)。MCP の dataclass 結果が誤検出の `Circular reference detected` でクラッシュしなくなり([#849](https://github.com/HKUDS/Vibe-Trading/pull/849)、@Echoandelementwebsites さんに感謝)、`alpha bench` の CLI/HTML が `_meta` の生存者バイアス開示を転送します([#841](https://github.com/HKUDS/Vibe-Trading/pull/841)、[#797](https://github.com/HKUDS/Vibe-Trading/issues/797) をクローズ、@AmirF194 さんに感謝)。さらにジャーナル・コネクタ・チャネル横断の 12 件の正確性修正([#799](https://github.com/HKUDS/Vibe-Trading/pull/799)–[#810](https://github.com/HKUDS/Vibe-Trading/pull/810)、@santhreal さんに感謝)、CLI 残高ビューに実際のアカウントラベルを表示([#843](https://github.com/HKUDS/Vibe-Trading/pull/843)、[#846](https://github.com/HKUDS/Vibe-Trading/issues/846) をクローズ、@Robin1987China さんに感謝)。
|
||||
- **2026-07-24** 🔀 **メモリ Tier 2、合成可能なオプティマイザ制約 + インターバル処理の総点検**:永続メモリが **Tier 2 の構造的な整理**を獲得しました([#815](https://github.com/HKUDS/Vibe-Trading/pull/815)、@shadowinlife さんに感謝)。バックテストのオプティマイザが**合成可能な重み制約**を受け付けるようになりました([#818](https://github.com/HKUDS/Vibe-Trading/pull/818)、@he-yufeng さんに感謝)。正確性:日次バーのバリデーターが**非正の価格**をオプトインできるようになり、負の価格バーでは始値を取りつつ、ゼロは引き続き拒否します([#816](https://github.com/HKUDS/Vibe-Trading/pull/816)、[#571](https://github.com/HKUDS/Vibe-Trading/issues/571) をクローズ、@darkknight4563 さんに感謝)。さらに 19 件の PR によるローダーの**インターバル正規化の総点検**:小文字の `1h/4h/1d/1w` エイリアスを全域で受け付け、未対応のインターバルは日足を黙って返す代わりに fail fast し、Yahoo の `4H` は `1h` にマップ、MT5 は `1W/1M` を受け付けます([#812](https://github.com/HKUDS/Vibe-Trading/pull/812)–[#838](https://github.com/HKUDS/Vibe-Trading/pull/838)、@santhreal さんに感謝)。加えて、トレードジャーナルの Eastmoney Excel シリアル日付の修正([#811](https://github.com/HKUDS/Vibe-Trading/pull/811)、@santhreal さんに感謝)と README ナビゲーションアンカーの修正([#840](https://github.com/HKUDS/Vibe-Trading/pull/840)、@dvirarad さんに感謝)。
|
||||
- **2026-07-23** 🔧 **信頼性の総点検 + strict alpha-bench の導線追加 + オプトインのメモリライフサイクル**:22 件のコントリビューター PR のバッチ。広範な**信頼性の総点検**がタイムフレーム処理をエンドツーエンドで修正:yfinance `1M`→月足(分ではない)、CCXT `1W`/`1M`、akshare/india-broker がサポート外の間隔を黙って日足にせず拒否、Tiger/Alpaca/OKX/Shoonya/Longbridge コネクタが `1H`/`4H` を時間足として維持。さらに取引ジャーナルの Excel 日付正規化(eastmoney の浮動小数 `YYYYMMDD`、Futu/Tonghuashun のシリアル日付)、`report_audit` の有限数 JSON、空の `holding_days` バリデーション、Feishu/CLI の markdown テーブル端列([#778](https://github.com/HKUDS/Vibe-Trading/pull/778)–[#794](https://github.com/HKUDS/Vibe-Trading/pull/794)、@santhreal さんに感謝)。**MT5** の `trading_history` は numpy スカラーをネイティブ Python 型に変換し、JSON シリアライズが `int64` で失敗しなくなりました([#776](https://github.com/HKUDS/Vibe-Trading/pull/776)、[#774](https://github.com/HKUDS/Vibe-Trading/issues/774) をクローズ、@shadowinlife さんに感謝)。**PIT ファンダメンタルズ**は修正済み行を重複排除し、遅れて届いた修正発表でスナップショットが古い会計期に後退しないようにしました([#772](https://github.com/HKUDS/Vibe-Trading/pull/772)、[#771](https://github.com/HKUDS/Vibe-Trading/issues/771) をクローズ、@klmtseng さんに感謝)。新機能:**`alpha bench --strict`** が、0.1.9 から存在しつつ導線のなかった strict な同一ユニバースのランダム対照 + OOS ゲートをついに接続([#796](https://github.com/HKUDS/Vibe-Trading/pull/796)、[#773](https://github.com/HKUDS/Vibe-Trading/issues/773) をクローズ、@he-yufeng さんに感謝)、オプトインの**メモリライフサイクル**(品質スコアリング、エビングハウス減衰、アーカイブのみの GC——すべてデフォルト無効)([#733](https://github.com/HKUDS/Vibe-Trading/pull/733)、[#732](https://github.com/HKUDS/Vibe-Trading/issues/732) をクローズ、@shadowinlife さんに感謝)、そしてバックテストの**リバランスノート**成果物 + 回転率メトリクス([#795](https://github.com/HKUDS/Vibe-Trading/pull/795)、@he-yufeng さんに感謝)。
|
||||
|
||||
<details>
|
||||
<summary>過去のニュース</summary>
|
||||
|
||||
- **2026-07-23** 🔧 **信頼性の総点検 + strict alpha-bench の導線追加 + オプトインのメモリライフサイクル**:22 件のコントリビューター PR のバッチ。広範な**信頼性の総点検**がタイムフレーム処理をエンドツーエンドで修正:yfinance `1M`→月足(分ではない)、CCXT `1W`/`1M`、akshare/india-broker がサポート外の間隔を黙って日足にせず拒否、Tiger/Alpaca/OKX/Shoonya/Longbridge コネクタが `1H`/`4H` を時間足として維持。さらに取引ジャーナルの Excel 日付正規化(eastmoney の浮動小数 `YYYYMMDD`、Futu/Tonghuashun のシリアル日付)、`report_audit` の有限数 JSON、空の `holding_days` バリデーション、Feishu/CLI の markdown テーブル端列([#778](https://github.com/HKUDS/Vibe-Trading/pull/778)–[#794](https://github.com/HKUDS/Vibe-Trading/pull/794)、@santhreal さんに感謝)。**MT5** の `trading_history` は numpy スカラーをネイティブ Python 型に変換し、JSON シリアライズが `int64` で失敗しなくなりました([#776](https://github.com/HKUDS/Vibe-Trading/pull/776)、[#774](https://github.com/HKUDS/Vibe-Trading/issues/774) をクローズ、@shadowinlife さんに感謝)。**PIT ファンダメンタルズ**は修正済み行を重複排除し、遅れて届いた修正発表でスナップショットが古い会計期に後退しないようにしました([#772](https://github.com/HKUDS/Vibe-Trading/pull/772)、[#771](https://github.com/HKUDS/Vibe-Trading/issues/771) をクローズ、@klmtseng さんに感謝)。新機能:**`alpha bench --strict`** が、0.1.9 から存在しつつ導線のなかった strict な同一ユニバースのランダム対照 + OOS ゲートをついに接続([#796](https://github.com/HKUDS/Vibe-Trading/pull/796)、[#773](https://github.com/HKUDS/Vibe-Trading/issues/773) をクローズ、@he-yufeng さんに感謝)、オプトインの**メモリライフサイクル**(品質スコアリング、エビングハウス減衰、アーカイブのみの GC——すべてデフォルト無効)([#733](https://github.com/HKUDS/Vibe-Trading/pull/733)、[#732](https://github.com/HKUDS/Vibe-Trading/issues/732) をクローズ、@shadowinlife さんに感謝)、そしてバックテストの**リバランスノート**成果物 + 回転率メトリクス([#795](https://github.com/HKUDS/Vibe-Trading/pull/795)、@he-yufeng さんに感謝)。
|
||||
- **2026-07-22** 🚀 **v0.1.12 リリース**([リリースノート](https://github.com/HKUDS/Vibe-Trading/releases/tag/v0.1.12)、`pip install -U vibe-trading-ai`):**correlation regime タイムライン**が `GET /correlation/regime` エンドポイント + オプトインの Correlation タブストリップを追加しました —— エッジ密度を因果的なヒステリシス状態機械に通して FUSED(融合)した市場エピソードをマークするもので、シグナルではなく記述的なリスクコンテキストです([#756](https://github.com/HKUDS/Vibe-Trading/pull/756)、[#719](https://github.com/HKUDS/Vibe-Trading/issues/719) をクローズ、@ebujinovch さんに感謝)。プロバイダーのエンドポイント解決が各プロバイダーの canonical な base URL にフォールバックし、非 SSE エンドポイントを適切に処理するようになり、glm-5.1 上のネイティブ **zai** プロバイダーを修正しました([#758](https://github.com/HKUDS/Vibe-Trading/issues/758))。さらに、metrics・factors・pattern・session・journal にわたる strict-JSON/有限数の**信頼性の総点検**([#761](https://github.com/HKUDS/Vibe-Trading/pull/761)–[#770](https://github.com/HKUDS/Vibe-Trading/pull/770)、@santhreal さんに感謝)と、`-PERP` バックテストを資格情報ゼロに保つ Binance のメンテナンスブラケット分離([#757](https://github.com/HKUDS/Vibe-Trading/pull/757)、@honginp さんに感謝)。0.1.11 以降 約 90 件の修正をまとめています。
|
||||
- **2026-07-21** 🔧 **データローダーの完全性 + 信頼性修正の総点検**:部分的な市場データは、欠損したシンボルを fallback チェーンで補完し、補完できない場合はフェイルクローズするようになり、バックテストのユニバースを黙って縮小しなくなりました([#689](https://github.com/HKUDS/Vibe-Trading/pull/689)、[#681](https://github.com/HKUDS/Vibe-Trading/issues/681) をクローズ、@xkam7ar さんに感謝)。また、OKX のバーは深い履歴バックフィルのためにレート制限リトライ付きで `history-candles` エンドポイントを使用します([#644](https://github.com/HKUDS/Vibe-Trading/pull/644)、@tyj147454413-cmd さんに感謝)。さらに修正の総点検:MCP ネットワークガードが IPv6/大文字小文字違いのホストを受け入れ([#750](https://github.com/HKUDS/Vibe-Trading/pull/750)、@Robin1987China さんに感謝)、取引ジャーナルのパーサーが空白/NaN のシンボル行をスキップし([#749](https://github.com/HKUDS/Vibe-Trading/pull/749)、@Robin1987China さんに感謝)、Shadow Account が日足バーでは抽出された入場時間ゲートをスキップし([#748](https://github.com/HKUDS/Vibe-Trading/pull/748)、@Robin1987China さんに感謝)、MiniMax のリージョン API エンドポイントが選択可能になりました([#731](https://github.com/HKUDS/Vibe-Trading/pull/731)、@octo-patch さんに感謝)。
|
||||
- **2026-07-20** 🔀 **プロバイダー、MetaTrader 5、堅牢性の総点検**:ネイティブの **Anthropic Messages API**(任意の `[anthropic]` extra、[#695](https://github.com/HKUDS/Vibe-Trading/pull/695)、@jelech さんに感謝)、**SiliconFlow**([#565](https://github.com/HKUDS/Vibe-Trading/pull/565)、@UNHNQ さんに感謝)、**iFlytek Spark**([#537](https://github.com/HKUDS/Vibe-Trading/pull/537)、@FenjuFu さんに感謝)がプロバイダーに加わり、**MetaTrader 5(Exness)** ブローカーコネクタ + `mt5` 為替/貴金属データソースが追加されました(ブローカーコネクタ → **12**、[#481](https://github.com/HKUDS/Vibe-Trading/pull/481)、@StaniellG さんに感謝)。さらに、プロバイダー非依存の **`llm-vision` OCR** エンジン([#548](https://github.com/HKUDS/Vibe-Trading/pull/548)、@shadowinlife さんに感謝)、**80× のシグナル整列ベクトル化**([#698](https://github.com/HKUDS/Vibe-Trading/pull/698)、@shadowinlife さんに感謝)、Binance **USD-M ファンディング/ブラケット** の履歴データ([#716](https://github.com/HKUDS/Vibe-Trading/pull/716)、@honginp さんに感謝)、swarm の MCP ディスカバリキャッシュ([#704](https://github.com/HKUDS/Vibe-Trading/pull/704))、そして **13** 件の SSE/セッション/CLI/swarm/スケジューラの問題をクローズする信頼性統合([#584](https://github.com/HKUDS/Vibe-Trading/pull/584)、@xkam7ar さんに感謝)。正確性の修正:オプションの**部分決済** がロット全体を清算せず要求数量だけを決済するように([#577](https://github.com/HKUDS/Vibe-Trading/issues/577))、プロバイダー資格情報解決の一元化([#563](https://github.com/HKUDS/Vibe-Trading/pull/563))、キュー中のキャンセル処理([#641](https://github.com/HKUDS/Vibe-Trading/pull/641))、フロントエンドのストリーミング DOM 競合([#717](https://github.com/HKUDS/Vibe-Trading/pull/717)、@Marnie0415 さんに感謝)、コネクタ CLI レンダラー([#726](https://github.com/HKUDS/Vibe-Trading/pull/726)、@nareshkps さんに感謝)。
|
||||
@@ -922,7 +923,7 @@ Interactive docs: `http://localhost:8899/docs`
|
||||
|
||||
localhost 開発では、`vibe-trading serve` は browser workflow を簡単に保ちます。非ローカル client では、sensitive API endpoints に `API_AUTH_KEY` が必要です。JSON/upload requests には `Authorization: Bearer <key>` を使ってください。Browser EventSource streams は、Settings で同じ key を一度入力した後、Web UI が処理します。
|
||||
|
||||
Shell-capable tools(`bash` / `background_run`)はインタラクティブな local CLI でのみ有効です。それ以外のすべての面 — HTTP/SSE API と MCP server の**すべての** transport(stdio を含む)— は、`VIBE_TRADING_ENABLE_SHELL_TOOLS=1` を明示的に設定する(または `vibe-trading-mcp` に `--enable-shell-tools` を渡す)まで無効のままです。transport の種類が暗黙に shell アクセスを許可することはありません。Document と journal readers は既定で upload/import roots に制限されます。ファイルは `agent/uploads`、`agent/runs`、`./uploads`、`./data`、`~/.vibe-trading/uploads`、`~/.vibe-trading/imports` の下に置くか、`VIBE_TRADING_ALLOWED_FILE_ROOTS` で専用 directory を追加してください。
|
||||
Shell-capable process tools(`bash` / `background_run` / `cancel_background`)はインタラクティブな local CLI でのみ有効です。それ以外のすべての面 — HTTP/SSE API と MCP server の**すべての** transport(stdio を含む)— は、`VIBE_TRADING_ENABLE_SHELL_TOOLS=1` を明示的に設定する(または `vibe-trading-mcp` に `--enable-shell-tools` を渡す)まで無効のままです。transport の種類が暗黙に shell アクセスを許可することはありません。Document と journal readers は既定で upload/import roots に制限されます。ファイルは `agent/uploads`、`agent/runs`、`./uploads`、`./data`、`~/.vibe-trading/uploads`、`~/.vibe-trading/imports` の下に置くか、`VIBE_TRADING_ALLOWED_FILE_ROOTS` で専用 directory を追加してください。
|
||||
|
||||
### Web UI Settings
|
||||
|
||||
|
||||
+3
-2
@@ -52,13 +52,14 @@
|
||||
|
||||
> ⚠️ **보안 경고:** X 계정 `VibeTrading_HKU`, Virtuals 프로젝트 `101845`, 토큰 컨트랙트 `0x640BDBF77b6447E8b7DB7894cED84BD1c40571f4`는 모두 Vibe-Trading 공식과 무관합니다. Vibe-Trading은 어떠한 토큰이나 밈코인도 발행하거나 공식적으로 지지한 적이 없습니다. 해당 토큰을 구매하거나 지갑을 연결하거나 어떠한 서명도 하지 마세요. [자세히 보기](SECURITY.md#official-channels--impersonation).
|
||||
|
||||
- **2026-07-26** 🔒 **의존성 잠금 복구 + 벤치마크 유니버스 투명성**: Docker 해시 잠금 설치가 다시 정상화되고 CI에 잠금 검사가 추가됐습니다([#858](https://github.com/HKUDS/Vibe-Trading/pull/858), [#847](https://github.com/HKUDS/Vibe-Trading/issues/847) 종료). `alpha bench`가 이제 CSI300/SP500의 출처, 구성 종목 수, 축소 폴백, 생존자 편향을 공개합니다([#859](https://github.com/HKUDS/Vibe-Trading/pull/859), [#845](https://github.com/HKUDS/Vibe-Trading/issues/845) 종료). Actions와 프런트엔드 의존성 5건도 업데이트했습니다([#850](https://github.com/HKUDS/Vibe-Trading/pull/850)–[#852](https://github.com/HKUDS/Vibe-Trading/pull/852)).
|
||||
- **2026-07-25** 🔧 **퍼페추얼 리얼리즘 + MCP 크래시 수정 + 정확성 배치**: USD-M 퍼페추얼에 **마진 상태 계약**이 추가되고([#798](https://github.com/HKUDS/Vibe-Trading/pull/798), @honginp 감사합니다), 엔진이 이제 가져오기만 하고 무시하던 **과거 펀딩 비율**을 실제로 소비합니다([#819](https://github.com/HKUDS/Vibe-Trading/pull/819), @g0rdonL 감사합니다). MCP dataclass 결과가 잘못 감지된 `Circular reference detected`로 더 이상 크래시하지 않으며([#849](https://github.com/HKUDS/Vibe-Trading/pull/849), @Echoandelementwebsites 감사합니다), `alpha bench` CLI/HTML이 `_meta` 생존자 편향 공시를 전달합니다([#841](https://github.com/HKUDS/Vibe-Trading/pull/841), [#797](https://github.com/HKUDS/Vibe-Trading/issues/797) 닫힘, @AmirF194 감사합니다). 또한 저널, 커넥터, 채널 전반의 12개 정확성 수정([#799](https://github.com/HKUDS/Vibe-Trading/pull/799)–[#810](https://github.com/HKUDS/Vibe-Trading/pull/810), @santhreal 감사합니다)과 CLI 잔액 보기의 실제 계정 레이블([#843](https://github.com/HKUDS/Vibe-Trading/pull/843), [#846](https://github.com/HKUDS/Vibe-Trading/issues/846) 닫힘, @Robin1987China 감사합니다).
|
||||
- **2026-07-24** 🔀 **메모리 Tier 2, 합성 가능한 옵티마이저 제약 + 인터벌 처리 점검**: 퍼시스턴트 메모리에 **Tier 2 구조적 조직화**가 추가됩니다([#815](https://github.com/HKUDS/Vibe-Trading/pull/815), @shadowinlife 감사합니다). 백테스트 옵티마이저가 **합성 가능한 가중치 제약**을 받아들입니다([#818](https://github.com/HKUDS/Vibe-Trading/pull/818), @he-yufeng 감사합니다). 정확성: 일봉 검증기가 **비양수 가격**을 옵트인할 수 있습니다 — 음수 가격 봉에서는 시가를 취하면서 0은 계속 거부합니다([#816](https://github.com/HKUDS/Vibe-Trading/pull/816), [#571](https://github.com/HKUDS/Vibe-Trading/issues/571) 종료, @darkknight4563 감사합니다). 여기에 19건의 PR로 이뤄진 로더 **인터벌 정규화 점검**: 소문자 `1h/4h/1d/1w` 별칭을 전반적으로 허용하고, 지원하지 않는 인터벌은 조용히 일봉을 반환하는 대신 즉시 실패하며, Yahoo `4H`는 `1h`로 매핑되고 MT5는 `1W/1M`을 허용합니다([#812](https://github.com/HKUDS/Vibe-Trading/pull/812)–[#838](https://github.com/HKUDS/Vibe-Trading/pull/838), @santhreal 감사합니다). 또한 매매 일지의 Eastmoney Excel 시리얼 날짜 수정([#811](https://github.com/HKUDS/Vibe-Trading/pull/811), @santhreal 감사합니다)과 README 네비게이션 앵커 수정([#840](https://github.com/HKUDS/Vibe-Trading/pull/840), @dvirarad 감사합니다)이 포함됩니다.
|
||||
- **2026-07-23** 🔧 **신뢰성 점검 + strict alpha-bench 진입점 노출 + 옷트인 메모리 라이프사이클**: 22개 기여자 PR 배치. 광범위한 **신뢰성 점검**이 타임프레임 처리를 엔드투엔드로 수정합니다 — yfinance `1M`→월봉(분 아님), CCXT `1W`/`1M`, akshare/india-broker가 지원하지 않는 간격을 조용히 일봉으로 만들지 않고 거부, Tiger/Alpaca/OKX/Shoonya/Longbridge 커넥터가 `1H`/`4H`를 시간봉으로 유지 — 여기에 거래 저널 Excel 날짜 정규화(eastmoney 부동소수 `YYYYMMDD`, Futu/Tonghuashun 시리얼 날짜), `report_audit` 유한 수치 JSON, 빈 `holding_days` 검증, Feishu/CLI markdown 테이블 가장자리 열([#778](https://github.com/HKUDS/Vibe-Trading/pull/778)–[#794](https://github.com/HKUDS/Vibe-Trading/pull/794), @santhreal 감사합니다). **MT5** `trading_history`는 이제 numpy 스칼라를 네이티브 Python 타입으로 변환하여 JSON 직렬화가 `int64`에서 실패하지 않습니다([#776](https://github.com/HKUDS/Vibe-Trading/pull/776), [#774](https://github.com/HKUDS/Vibe-Trading/issues/774) 종료, @shadowinlife 감사합니다). **PIT 펀더멘털**은 정정된 행을 중복 제거하고, 늦게 도착한 정정 공시로 스냅샷이 더 오래된 회계 기간으로 후퇴하지 않도록 합니다([#772](https://github.com/HKUDS/Vibe-Trading/pull/772), [#771](https://github.com/HKUDS/Vibe-Trading/issues/771) 종료, @klmtseng 감사합니다). 신규: **`alpha bench --strict`**가 0.1.9부터 존재했지만 진입점이 없던 strict 동일 유니버스 랜덤 대조 + OOS 게이트를 마침내 연결([#796](https://github.com/HKUDS/Vibe-Trading/pull/796), [#773](https://github.com/HKUDS/Vibe-Trading/issues/773) 종료, @he-yufeng 감사합니다), 옷트인 **메모리 라이프사이클**(품질 점수, 에빙하우스 감쇠, 아카이브 전용 GC — 모두 기본 비활성화)([#733](https://github.com/HKUDS/Vibe-Trading/pull/733), [#732](https://github.com/HKUDS/Vibe-Trading/issues/732) 종료, @shadowinlife 감사합니다), 백테스트 **리밸런스 노트** 아티팩트 + 회전율 지표([#795](https://github.com/HKUDS/Vibe-Trading/pull/795), @he-yufeng 감사합니다).
|
||||
|
||||
<details>
|
||||
<summary>이전 뉴스</summary>
|
||||
|
||||
- **2026-07-23** 🔧 **신뢰성 점검 + strict alpha-bench 진입점 노출 + 옷트인 메모리 라이프사이클**: 22개 기여자 PR 배치. 광범위한 **신뢰성 점검**이 타임프레임 처리를 엔드투엔드로 수정합니다 — yfinance `1M`→월봉(분 아님), CCXT `1W`/`1M`, akshare/india-broker가 지원하지 않는 간격을 조용히 일봉으로 만들지 않고 거부, Tiger/Alpaca/OKX/Shoonya/Longbridge 커넥터가 `1H`/`4H`를 시간봉으로 유지 — 여기에 거래 저널 Excel 날짜 정규화(eastmoney 부동소수 `YYYYMMDD`, Futu/Tonghuashun 시리얼 날짜), `report_audit` 유한 수치 JSON, 빈 `holding_days` 검증, Feishu/CLI markdown 테이블 가장자리 열([#778](https://github.com/HKUDS/Vibe-Trading/pull/778)–[#794](https://github.com/HKUDS/Vibe-Trading/pull/794), @santhreal 감사합니다). **MT5** `trading_history`는 이제 numpy 스칼라를 네이티브 Python 타입으로 변환하여 JSON 직렬화가 `int64`에서 실패하지 않습니다([#776](https://github.com/HKUDS/Vibe-Trading/pull/776), [#774](https://github.com/HKUDS/Vibe-Trading/issues/774) 종료, @shadowinlife 감사합니다). **PIT 펀더멘털**은 정정된 행을 중복 제거하고, 늦게 도착한 정정 공시로 스냅샷이 더 오래된 회계 기간으로 후퇴하지 않도록 합니다([#772](https://github.com/HKUDS/Vibe-Trading/pull/772), [#771](https://github.com/HKUDS/Vibe-Trading/issues/771) 종료, @klmtseng 감사합니다). 신규: **`alpha bench --strict`**가 0.1.9부터 존재했지만 진입점이 없던 strict 동일 유니버스 랜덤 대조 + OOS 게이트를 마침내 연결([#796](https://github.com/HKUDS/Vibe-Trading/pull/796), [#773](https://github.com/HKUDS/Vibe-Trading/issues/773) 종료, @he-yufeng 감사합니다), 옷트인 **메모리 라이프사이클**(품질 점수, 에빙하우스 감쇠, 아카이브 전용 GC — 모두 기본 비활성화)([#733](https://github.com/HKUDS/Vibe-Trading/pull/733), [#732](https://github.com/HKUDS/Vibe-Trading/issues/732) 종료, @shadowinlife 감사합니다), 백테스트 **리밸런스 노트** 아티팩트 + 회전율 지표([#795](https://github.com/HKUDS/Vibe-Trading/pull/795), @he-yufeng 감사합니다).
|
||||
- **2026-07-22** 🚀 **v0.1.12 릴리스**([릴리스 노트](https://github.com/HKUDS/Vibe-Trading/releases/tag/v0.1.12), `pip install -U vibe-trading-ai`): **correlation regime 타임라인**이 `GET /correlation/regime` 엔드포인트 + 옵트인 Correlation 탭 스트립을 추가합니다 — 엣지 밀도를 인과적 히스테리시스 상태 머신에 통과시켜 FUSED 시장 국면을 표시하며, 시그널이 아니라 서술적 리스크 컨텍스트입니다([#756](https://github.com/HKUDS/Vibe-Trading/pull/756), [#719](https://github.com/HKUDS/Vibe-Trading/issues/719) 종료, @ebujinovch 감사합니다). 프로바이더 엔드포인트 해석이 이제 각 프로바이더의 canonical base URL로 폴백하고 non-SSE 엔드포인트를 우아하게 처리하여, glm-5.1의 네이티브 **zai** 프로바이더를 수정합니다([#758](https://github.com/HKUDS/Vibe-Trading/issues/758)). 여기에 metrics, factors, pattern, session, journal 전반의 strict-JSON / 유한 수치 **신뢰성 점검**([#761](https://github.com/HKUDS/Vibe-Trading/pull/761)–[#770](https://github.com/HKUDS/Vibe-Trading/pull/770), @santhreal 감사합니다)과 `-PERP` 백테스트를 자격 증명 없이 유지하는 Binance 유지보수 브래킷 분리([#757](https://github.com/HKUDS/Vibe-Trading/pull/757), @honginp 감사합니다)가 더해집니다. 0.1.11 이후 ~90건의 수정을 롤업합니다.
|
||||
- **2026-07-21** 🔧 **데이터 로더 완전성 + 신뢰성 수정 점검**: 부분 시장 데이터는 이제 fallback 체인을 통해 누락된 심볼을 채우고, 채우지 못하면 페일클로즈하여 백테스트 유니버스를 조용히 축소하지 않습니다([#689](https://github.com/HKUDS/Vibe-Trading/pull/689), [#681](https://github.com/HKUDS/Vibe-Trading/issues/681) 종료, @xkam7ar 감사합니다). 또한 OKX 바는 깊은 이력 백필을 위해 레이트리밋 재시도와 함께 `history-candles` 엔드포인트를 사용합니다([#644](https://github.com/HKUDS/Vibe-Trading/pull/644), @tyj147454413-cmd 감사합니다). 여기에 수정 점검: MCP 네트워크 가드가 IPv6 / 대소문자가 다른 호스트를 허용하고([#750](https://github.com/HKUDS/Vibe-Trading/pull/750), @Robin1987China 감사합니다), 거래 저널 파서가 공백/NaN 심볼 행을 건너뛰며([#749](https://github.com/HKUDS/Vibe-Trading/pull/749), @Robin1987China 감사합니다), Shadow Account가 일봉에서는 채굴된 진입 시간 게이트를 건너뛰고([#748](https://github.com/HKUDS/Vibe-Trading/pull/748), @Robin1987China 감사합니다), MiniMax 지역 API 엔드포인트를 선택할 수 있습니다([#731](https://github.com/HKUDS/Vibe-Trading/pull/731), @octo-patch 감사합니다).
|
||||
- **2026-07-20** 🔀 **프로바이더, MetaTrader 5, 견고성 점검**: 네이티브 **Anthropic Messages API**(선택적 `[anthropic]` extra, [#695](https://github.com/HKUDS/Vibe-Trading/pull/695), @jelech 감사합니다), **SiliconFlow**([#565](https://github.com/HKUDS/Vibe-Trading/pull/565), @UNHNQ 감사합니다), **iFlytek Spark**([#537](https://github.com/HKUDS/Vibe-Trading/pull/537), @FenjuFu 감사합니다)가 프로바이더에 추가되고, **MetaTrader 5(Exness)** 브로커 커넥터 + `mt5` 외환/귀금속 데이터 소스가 도입되었습니다(브로커 커넥터 → **12**, [#481](https://github.com/HKUDS/Vibe-Trading/pull/481), @StaniellG 감사합니다). 여기에 프로바이더 독립적인 **`llm-vision` OCR** 엔진([#548](https://github.com/HKUDS/Vibe-Trading/pull/548), @shadowinlife 감사합니다), **80× 시그널 정렬 벡터화**([#698](https://github.com/HKUDS/Vibe-Trading/pull/698), @shadowinlife 감사합니다), Binance **USD-M 펀딩/브래킷** 이력 데이터([#716](https://github.com/HKUDS/Vibe-Trading/pull/716), @honginp 감사합니다), swarm MCP 디스커버리 캐시([#704](https://github.com/HKUDS/Vibe-Trading/pull/704)), 그리고 **13**개의 SSE/세션/CLI/swarm/스케줄러 이슈를 닫는 신뢰성 통합([#584](https://github.com/HKUDS/Vibe-Trading/pull/584), @xkam7ar 감사합니다)이 더해졌습니다. 정확성 수정: 옵션 **부분 청산**이 이제 전체 랏을 청산하지 않고 요청 수량만 청산하며([#577](https://github.com/HKUDS/Vibe-Trading/issues/577)), 프로바이더 자격 증명 해석 일원화([#563](https://github.com/HKUDS/Vibe-Trading/pull/563)), 대기 중 취소 처리([#641](https://github.com/HKUDS/Vibe-Trading/pull/641)), 프런트엔드 스트리밍 DOM 경합([#717](https://github.com/HKUDS/Vibe-Trading/pull/717), @Marnie0415 감사합니다), 커넥터 CLI 렌더러([#726](https://github.com/HKUDS/Vibe-Trading/pull/726), @nareshkps 감사합니다).
|
||||
@@ -925,7 +926,7 @@ Interactive docs: `http://localhost:8899/docs`
|
||||
|
||||
localhost 개발에서 `vibe-trading serve`는 browser workflow를 단순하게 유지합니다. non-local client에서는 민감한 API endpoint에 `API_AUTH_KEY`가 필요합니다. JSON/upload request에는 `Authorization: Bearer <key>`를 사용하세요. Browser EventSource stream은 Web UI Settings에 같은 key를 한 번 입력하면 Web UI가 처리합니다.
|
||||
|
||||
Shell-capable tools(`bash` / `background_run`)는 대화형 local CLI에서만 활성화됩니다. 그 외 모든 표면 — HTTP/SSE API와 MCP server의 **모든** transport(stdio 포함) — 는 `VIBE_TRADING_ENABLE_SHELL_TOOLS=1`을 명시적으로 설정하지 않는 한(또는 `vibe-trading-mcp`에 `--enable-shell-tools`를 전달하지 않는 한) 비활성 상태로 유지됩니다. transport 종류가 암묵적으로 shell 접근을 부여하는 일은 없습니다. Document와 journal reader는 기본적으로 upload/import roots로 제한됩니다. 파일은 `agent/uploads`, `agent/runs`, `./uploads`, `./data`, `~/.vibe-trading/uploads`, `~/.vibe-trading/imports` 아래에 두거나, `VIBE_TRADING_ALLOWED_FILE_ROOTS`로 전용 directory를 추가하세요.
|
||||
Shell-capable process tools(`bash` / `background_run` / `cancel_background`)는 대화형 local CLI에서만 활성화됩니다. 그 외 모든 표면 — HTTP/SSE API와 MCP server의 **모든** transport(stdio 포함) — 는 `VIBE_TRADING_ENABLE_SHELL_TOOLS=1`을 명시적으로 설정하지 않는 한(또는 `vibe-trading-mcp`에 `--enable-shell-tools`를 전달하지 않는 한) 비활성 상태로 유지됩니다. transport 종류가 암묵적으로 shell 접근을 부여하는 일은 없습니다. Document와 journal reader는 기본적으로 upload/import roots로 제한됩니다. 파일은 `agent/uploads`, `agent/runs`, `./uploads`, `./data`, `~/.vibe-trading/uploads`, `~/.vibe-trading/imports` 아래에 두거나, `VIBE_TRADING_ALLOWED_FILE_ROOTS`로 전용 directory를 추가하세요.
|
||||
|
||||
### Web UI Settings
|
||||
|
||||
|
||||
+3
-2
@@ -52,13 +52,14 @@
|
||||
|
||||
> ⚠️ **安全警告:** X 账号 `VibeTrading_HKU`、Virtuals 项目 `101845` 及代币合约 `0x640BDBF77b6447E8b7DB7894cED84BD1c40571f4` 均非 Vibe-Trading 官方。我们从未发行或背书任何代币或 meme 币。请勿购买、连接钱包或签名。[详细说明](SECURITY.md#official-channels--impersonation)。
|
||||
|
||||
- **2026-07-26** 🔒 **依赖锁修复 + 基准成分透明度**:Docker 哈希锁定安装恢复正常,CI 新增锁文件校验([#858](https://github.com/HKUDS/Vibe-Trading/pull/858),关闭 [#847](https://github.com/HKUDS/Vibe-Trading/issues/847))。`alpha bench` 现在披露 CSI300/SP500 的来源、成分数量、降级 fallback 与幸存者偏差([#859](https://github.com/HKUDS/Vibe-Trading/pull/859),关闭 [#845](https://github.com/HKUDS/Vibe-Trading/issues/845))。同时更新了 Actions 和 5 项前端依赖([#850](https://github.com/HKUDS/Vibe-Trading/pull/850)–[#852](https://github.com/HKUDS/Vibe-Trading/pull/852))。
|
||||
- **2026-07-25** 🔧 **永续合约回测更真实 + MCP 崩溃修复 + 正确性批量修复**:USD-M 永续合约获得**保证金状态合约**([#798](https://github.com/HKUDS/Vibe-Trading/pull/798),感谢 @honginp),引擎现在真正消费**历史资金费率**,不再“取了不用”([#819](https://github.com/HKUDS/Vibe-Trading/pull/819),感谢 @g0rdonL)。MCP dataclass 结果不再因误报的 `Circular reference detected` 崩溃([#849](https://github.com/HKUDS/Vibe-Trading/pull/849),感谢 @Echoandelementwebsites),`alpha bench` CLI/HTML 透传 `_meta` 幸存者偏差披露([#841](https://github.com/HKUDS/Vibe-Trading/pull/841),关闭 [#797](https://github.com/HKUDS/Vibe-Trading/issues/797),感谢 @AmirF194)。另有 12 个横跨日志、连接器与渠道的正确性修复([#799](https://github.com/HKUDS/Vibe-Trading/pull/799)–[#810](https://github.com/HKUDS/Vibe-Trading/pull/810),感谢 @santhreal),以及 CLI 余额视图显示真实账户标签([#843](https://github.com/HKUDS/Vibe-Trading/pull/843),关闭 [#846](https://github.com/HKUDS/Vibe-Trading/issues/846),感谢 @Robin1987China)。
|
||||
- **2026-07-24** 🔀 **记忆 Tier 2、可组合优化器权重约束 + 周期处理大扫荡**:持久化记忆获得 **Tier 2 结构化组织**([#815](https://github.com/HKUDS/Vibe-Trading/pull/815),感谢 @shadowinlife),回测优化器支持**可组合权重约束**([#818](https://github.com/HKUDS/Vibe-Trading/pull/818),感谢 @he-yufeng)。正确性:日线 bar 校验器可选**非正价格**——允许在负价格 bar 上开仓,但仍拒绝零价格([#816](https://github.com/HKUDS/Vibe-Trading/pull/816),关闭 [#571](https://github.com/HKUDS/Vibe-Trading/issues/571),感谢 @darkknight4563)。另有 19 个 loader **周期规范化修复**:全面接受小写 `1h/4h/1d/1w` 别名,不支持的周期现在直接快速报错,不再静默退回日线;Yahoo `4H` 映射为 `1h`,MT5 接受 `1W/1M`([#812](https://github.com/HKUDS/Vibe-Trading/pull/812)–[#838](https://github.com/HKUDS/Vibe-Trading/pull/838),感谢 @santhreal);交易日志修复东财 Excel 序列日期([#811](https://github.com/HKUDS/Vibe-Trading/pull/811),感谢 @santhreal);README 导航锚点修复([#840](https://github.com/HKUDS/Vibe-Trading/pull/840),感谢 @dvirarad)。
|
||||
- **2026-07-23** 🔧 **可靠性清扫 + 严格 alpha-bench 接上入口 + 可选记忆生命周期**:一批 22 个贡献者 PR。一轮覆盖面很广的**可靠性清扫**端到端修正了周期(timeframe)处理——yfinance `1M`→月线(而非分钟)、CCXT `1W`/`1M`、akshare/india-broker 对不支持的周期直接拒绝而非静默返回日线,Tiger/Alpaca/OKX/Shoonya/Longbridge 连接器把 `1H`/`4H` 保持为小时线——外加交易日志的 Excel 日期归一化(eastmoney 浮点 `YYYYMMDD`、富途/同花顺序列日期)、`report_audit` 有限数值 JSON、空 `holding_days` 校验,以及飞书/CLI 的 markdown 表格边列([#778](https://github.com/HKUDS/Vibe-Trading/pull/778)–[#794](https://github.com/HKUDS/Vibe-Trading/pull/794),感谢 @santhreal)。**MT5** `trading_history` 现在把 numpy 标量转成原生 Python 类型,JSON 序列化不再因 `int64` 失败([#776](https://github.com/HKUDS/Vibe-Trading/pull/776),关闭 [#774](https://github.com/HKUDS/Vibe-Trading/issues/774),感谢 @shadowinlife);**PIT 基本面**对复述行去重,并防止快照在迟到的重述公告时回退到更早的财季([#772](https://github.com/HKUDS/Vibe-Trading/pull/772),关闭 [#771](https://github.com/HKUDS/Vibe-Trading/issues/771),感谢 @klmtseng)。新增:**`alpha bench --strict`** 终于接上了自 0.1.9 起就存在却无入口的严格同 universe 随机对照 + OOS 闸([#796](https://github.com/HKUDS/Vibe-Trading/pull/796),关闭 [#773](https://github.com/HKUDS/Vibe-Trading/issues/773),感谢 @he-yufeng);一个可选的**记忆生命周期**(质量评分、艾宾浩斯衰减、仅归档的 GC——全部默认关闭)([#733](https://github.com/HKUDS/Vibe-Trading/pull/733),关闭 [#732](https://github.com/HKUDS/Vibe-Trading/issues/732),感谢 @shadowinlife);以及回测的**再平衡说明**产物 + 换手率指标([#795](https://github.com/HKUDS/Vibe-Trading/pull/795),感谢 @he-yufeng)。
|
||||
|
||||
<details>
|
||||
<summary>更早的更新</summary>
|
||||
|
||||
- **2026-07-23** 🔧 **可靠性清扫 + 严格 alpha-bench 接上入口 + 可选记忆生命周期**:一批 22 个贡献者 PR。一轮覆盖面很广的**可靠性清扫**端到端修正了周期(timeframe)处理——yfinance `1M`→月线(而非分钟)、CCXT `1W`/`1M`、akshare/india-broker 对不支持的周期直接拒绝而非静默返回日线,Tiger/Alpaca/OKX/Shoonya/Longbridge 连接器把 `1H`/`4H` 保持为小时线——外加交易日志的 Excel 日期归一化(eastmoney 浮点 `YYYYMMDD`、富途/同花顺序列日期)、`report_audit` 有限数值 JSON、空 `holding_days` 校验,以及飞书/CLI 的 markdown 表格边列([#778](https://github.com/HKUDS/Vibe-Trading/pull/778)–[#794](https://github.com/HKUDS/Vibe-Trading/pull/794),感谢 @santhreal)。**MT5** `trading_history` 现在把 numpy 标量转成原生 Python 类型,JSON 序列化不再因 `int64` 失败([#776](https://github.com/HKUDS/Vibe-Trading/pull/776),关闭 [#774](https://github.com/HKUDS/Vibe-Trading/issues/774),感谢 @shadowinlife);**PIT 基本面**对复述行去重,并防止快照在迟到的重述公告时回退到更早的财季([#772](https://github.com/HKUDS/Vibe-Trading/pull/772),关闭 [#771](https://github.com/HKUDS/Vibe-Trading/issues/771),感谢 @klmtseng)。新增:**`alpha bench --strict`** 终于接上了自 0.1.9 起就存在却无入口的严格同 universe 随机对照 + OOS 闸([#796](https://github.com/HKUDS/Vibe-Trading/pull/796),关闭 [#773](https://github.com/HKUDS/Vibe-Trading/issues/773),感谢 @he-yufeng);一个可选的**记忆生命周期**(质量评分、艾宾浩斯衰减、仅归档的 GC——全部默认关闭)([#733](https://github.com/HKUDS/Vibe-Trading/pull/733),关闭 [#732](https://github.com/HKUDS/Vibe-Trading/issues/732),感谢 @shadowinlife);以及回测的**再平衡说明**产物 + 换手率指标([#795](https://github.com/HKUDS/Vibe-Trading/pull/795),感谢 @he-yufeng)。
|
||||
- **2026-07-22** 🚀 **v0.1.12 发布**([Release notes](https://github.com/HKUDS/Vibe-Trading/releases/tag/v0.1.12),`pip install -U vibe-trading-ai`):**相关性状态(correlation regime)时间线**新增 `GET /correlation/regime` 端点 + 可选的 Correlation 页签条带——把边密度(edge density)送入一个因果迟滞状态机,标记市场「融合(FUSED)」的时段,属于描述性的风险上下文,而非交易信号([#756](https://github.com/HKUDS/Vibe-Trading/pull/756),关闭 [#719](https://github.com/HKUDS/Vibe-Trading/issues/719),感谢 @ebujinovch)。Provider 端点解析现在会回退到每个 provider 的规范 base URL,并优雅处理非 SSE 端点,修复了 glm-5.1 上的原生 **zai** provider([#758](https://github.com/HKUDS/Vibe-Trading/issues/758))。此外还有一轮贯穿 metrics、factors、pattern、session 与 journal 的 strict-JSON / 有限数值 **可靠性清理**([#761](https://github.com/HKUDS/Vibe-Trading/pull/761)–[#770](https://github.com/HKUDS/Vibe-Trading/pull/770),感谢 @santhreal),以及一次 Binance 维持保证金档位解耦,让 `-PERP` 回测保持零凭证([#757](https://github.com/HKUDS/Vibe-Trading/pull/757),感谢 @honginp)。汇总 0.1.11 以来约 90 项修复。
|
||||
- **2026-07-21** 🔧 **数据加载完整性 + 一轮可靠性修复**:部分行情结果现在会通过 fallback 链补齐缺失的标的,补不齐则快速失败,而不再静默缩小回测的标的范围([#689](https://github.com/HKUDS/Vibe-Trading/pull/689),关闭 [#681](https://github.com/HKUDS/Vibe-Trading/issues/681),感谢 @xkam7ar);OKX 行情改用 `history-candles` 端点并带限流重试,以支持深度历史回填([#644](https://github.com/HKUDS/Vibe-Trading/pull/644),感谢 @tyj147454413-cmd)。此外还有一批修复:MCP 网络守卫现在接受 IPv6 / 大小写不同的主机名([#750](https://github.com/HKUDS/Vibe-Trading/pull/750),感谢 @Robin1987China)、交易日志解析器跳过空白/NaN 的标的行([#749](https://github.com/HKUDS/Vibe-Trading/pull/749),感谢 @Robin1987China)、Shadow Account 在日线数据上跳过挖掘出的入场时段闸门([#748](https://github.com/HKUDS/Vibe-Trading/pull/748),感谢 @Robin1987China),以及 MiniMax 区域 API 端点可选([#731](https://github.com/HKUDS/Vibe-Trading/pull/731),感谢 @octo-patch)。
|
||||
- **2026-07-20** 🔀 **Provider、MetaTrader 5 与一轮可靠性清理**:原生 **Anthropic Messages API**(可选 `[anthropic]` extra,[#695](https://github.com/HKUDS/Vibe-Trading/pull/695),感谢 @jelech)、**SiliconFlow**([#565](https://github.com/HKUDS/Vibe-Trading/pull/565),感谢 @UNHNQ)与 **iFlytek 星火**([#537](https://github.com/HKUDS/Vibe-Trading/pull/537),感谢 @FenjuFu)加入 Provider 阵容;新增 **MetaTrader 5(Exness)** 券商连接器 + `mt5` 外汇/贵金属数据源(券商连接器 → **12**,[#481](https://github.com/HKUDS/Vibe-Trading/pull/481),感谢 @StaniellG)。此外还有与 Provider 无关的 **`llm-vision` OCR** 引擎([#548](https://github.com/HKUDS/Vibe-Trading/pull/548),感谢 @shadowinlife)、**80× 信号对齐向量化**([#698](https://github.com/HKUDS/Vibe-Trading/pull/698),感谢 @shadowinlife)、Binance **USD-M 资金费率/维持保证金档位** 历史数据([#716](https://github.com/HKUDS/Vibe-Trading/pull/716),感谢 @honginp)、swarm 的 MCP 发现缓存([#704](https://github.com/HKUDS/Vibe-Trading/pull/704)),以及一次可靠性整合,关闭 **13** 个 SSE/会话/CLI/swarm/调度器问题([#584](https://github.com/HKUDS/Vibe-Trading/pull/584),感谢 @xkam7ar)。正确性修复:期权 **部分平仓** 现在按请求数量平仓而不再清空整仓([#577](https://github.com/HKUDS/Vibe-Trading/issues/577))、集中化的 Provider 凭证解析([#563](https://github.com/HKUDS/Vibe-Trading/pull/563))、排队中取消的处理([#641](https://github.com/HKUDS/Vibe-Trading/pull/641))、前端流式 DOM 竞态([#717](https://github.com/HKUDS/Vibe-Trading/pull/717),感谢 @Marnie0415),以及连接器 CLI 渲染器([#726](https://github.com/HKUDS/Vibe-Trading/pull/726),感谢 @nareshkps)。
|
||||
@@ -949,7 +950,7 @@ vibe-trading serve --port 8899
|
||||
|
||||
对于 localhost 开发,`vibe-trading serve` 会保持浏览器工作流简单。对任何非本地客户端,敏感 API endpoints 都要求 `API_AUTH_KEY`;JSON/upload 请求请使用 `Authorization: Bearer <key>`。浏览器 EventSource streams 会在你于 Settings 中输入同一个 key 后由 Web UI 处理。
|
||||
|
||||
Shell-capable tools(`bash` / `background_run`)仅对交互式本地 CLI 启用。其他所有入口 —— HTTP/SSE API 以及 MCP server 的**所有** transport(含 stdio)—— 默认关闭,除非你显式设置 `VIBE_TRADING_ENABLE_SHELL_TOOLS=1`(或给 `vibe-trading-mcp` 传 `--enable-shell-tools`)。transport 类型永远不会隐式授予 shell 访问权限。文档和日志读取器默认限制在 upload/import roots 内;请将文件放在 `agent/uploads`、`agent/runs`、`./uploads`、`./data`、`~/.vibe-trading/uploads` 或 `~/.vibe-trading/imports` 下,或通过 `VIBE_TRADING_ALLOWED_FILE_ROOTS` 添加专用目录。
|
||||
Shell-capable process tools(`bash` / `background_run` / `cancel_background`)仅对交互式本地 CLI 启用。其他所有入口 —— HTTP/SSE API 以及 MCP server 的**所有** transport(含 stdio)—— 默认关闭,除非你显式设置 `VIBE_TRADING_ENABLE_SHELL_TOOLS=1`(或给 `vibe-trading-mcp` 传 `--enable-shell-tools`)。transport 类型永远不会隐式授予 shell 访问权限。`cancel_background` 只终止 `background_run` 返回的已跟踪 task ID;系统会拒绝按进程名广泛终止 Python,避免把 Vibe-Trading 自身一起终止。文档和日志读取器默认限制在 upload/import roots 内;请将文件放在 `agent/uploads`、`agent/runs`、`./uploads`、`./data`、`~/.vibe-trading/uploads` 或 `~/.vibe-trading/imports` 下,或通过 `VIBE_TRADING_ALLOWED_FILE_ROOTS` 添加专用目录。
|
||||
|
||||
### Web UI Settings
|
||||
|
||||
|
||||
@@ -142,7 +142,10 @@ def _rolling_correlation_matrix(
|
||||
# (e.g. crypto via OKX/CCXT at UTC midnight vs US equity via
|
||||
# yfinance at EDT midnight = 04:00 UTC) align correctly.
|
||||
ts.index = ts.index.normalize()
|
||||
rets = ts.pct_change().dropna()
|
||||
# ``fill_method=None`` is explicit because under the project's
|
||||
# pandas>=2,<3 pin the ``pct_change`` default forward-fills missing
|
||||
# prices, silently manufacturing 0% returns on halted sessions.
|
||||
rets = ts.pct_change(fill_method=None).dropna()
|
||||
rets.name = code
|
||||
returns_frames.append(rets)
|
||||
|
||||
|
||||
@@ -31,19 +31,42 @@ _BARS_PER_DAY = {
|
||||
"1D": {"tushare": 1, "okx": 1, "yfinance": 1, "akshare": 1, "ccxt": 1, "mootdx": 1, "futu": 1, "mt5": 1},
|
||||
}
|
||||
|
||||
# Runner/loaders also emit these aliases; map them onto the table keys above.
|
||||
_SOURCE_ALIASES = {"yahoo": "yfinance", "binance": "ccxt"}
|
||||
|
||||
|
||||
def _normalize_interval(interval: str) -> str:
|
||||
"""Map project interval tokens onto ``_BARS_PER_DAY`` keys.
|
||||
|
||||
Minute bars stay lowercase (``1m``); hour/day use the uppercase keys the
|
||||
table already stores (``1H`` / ``4H`` / ``1D``). Loaders accept both cases
|
||||
after the interval-map fixes; annualisation must too.
|
||||
"""
|
||||
token = str(interval or "1D").strip()
|
||||
lower = token.lower()
|
||||
if lower in ("1m", "5m", "15m", "30m"):
|
||||
return lower
|
||||
if lower in ("1h", "4h", "1d"):
|
||||
return lower.upper()
|
||||
return token
|
||||
|
||||
|
||||
def calc_bars_per_year(interval: str = "1D", source: str = "tushare") -> int:
|
||||
"""Number of bars per year for annualisation.
|
||||
|
||||
Args:
|
||||
interval: Bar size (1m / 5m / 15m / 30m / 1H / 4H / 1D).
|
||||
source: Data source (tushare / yfinance / okx).
|
||||
interval: Bar size (1m / 5m / 15m / 30m / 1H / 4H / 1D), case-insensitive
|
||||
for hour/day tokens.
|
||||
source: Data source (tushare / yfinance / okx). ``yahoo`` aliases to
|
||||
``yfinance``; ``binance`` aliases to ``ccxt``.
|
||||
|
||||
Returns:
|
||||
Bars per year.
|
||||
"""
|
||||
trading_days = _TRADING_DAYS.get(source, 252)
|
||||
bars_per_day = _BARS_PER_DAY.get(interval, {}).get(source, 1)
|
||||
source_key = _SOURCE_ALIASES.get(str(source or "").strip().lower(), source)
|
||||
interval_key = _normalize_interval(interval)
|
||||
trading_days = _TRADING_DAYS.get(source_key, 252)
|
||||
bars_per_day = _BARS_PER_DAY.get(interval_key, {}).get(source_key, 1)
|
||||
return trading_days * bars_per_day
|
||||
|
||||
|
||||
|
||||
+11
-10
@@ -81,8 +81,8 @@ logger = logging.getLogger(__name__)
|
||||
_skills_loader = None
|
||||
_registry = None
|
||||
_goal_store = None
|
||||
# Fail-closed default: the bash / background_run shell tools are a remote
|
||||
# code-execution surface once the MCP server is reachable by any client (stdio,
|
||||
# Fail-closed default: bash / background_run / cancel_background form a remote
|
||||
# process-control surface once the MCP server is reachable by any client (stdio,
|
||||
# SSE, or Streamable HTTP), so they stay OFF unless an operator explicitly opts
|
||||
# in. main() may flip this on via --enable-shell-tools or the
|
||||
# VIBE_TRADING_ENABLE_SHELL_TOOLS env var. Keeping the module-level default off
|
||||
@@ -100,11 +100,12 @@ def _env_shell_tools_enabled() -> bool:
|
||||
def _resolve_include_shell_tools(cli_opt_in: bool) -> bool:
|
||||
"""Resolve whether the MCP server should register shell tools.
|
||||
|
||||
Shell tools (``bash`` / ``background_run``) run arbitrary OS commands and are
|
||||
an RCE surface regardless of transport. They are therefore disabled for every
|
||||
transport unless the operator explicitly opts in. Transport type never
|
||||
implicitly grants shell access: previously ``stdio`` force-enabled these tools
|
||||
with no opt-out (GHSA-6wjh-cc6v-xfrx), which also widened the reachable
|
||||
Process-control tools (``bash`` / ``background_run`` /
|
||||
``cancel_background``) run commands or terminate tracked command trees and
|
||||
are an RCE surface regardless of transport. They are therefore disabled for
|
||||
every transport unless the operator explicitly opts in. Transport type never
|
||||
implicitly grants shell access: previously ``stdio`` force-enabled these
|
||||
tools with no opt-out (GHSA-6wjh-cc6v-xfrx), which also widened the reachable
|
||||
surface of the ``bash`` OS-command-injection issue (GHSA-m768-22r9-h4x7).
|
||||
|
||||
Args:
|
||||
@@ -2126,9 +2127,9 @@ def main():
|
||||
parser.add_argument(
|
||||
"--enable-shell-tools",
|
||||
action="store_true",
|
||||
help="Register the bash / background_run shell tools (arbitrary OS "
|
||||
"command execution — RCE surface). OFF by default for every transport; "
|
||||
"equivalent to setting VIBE_TRADING_ENABLE_SHELL_TOOLS=1.",
|
||||
help="Register bash / background_run / cancel_background (OS process "
|
||||
"control — RCE surface). OFF by default for every transport; equivalent "
|
||||
"to setting VIBE_TRADING_ENABLE_SHELL_TOOLS=1.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
_include_shell_tools = _resolve_include_shell_tools(args.enable_shell_tools)
|
||||
|
||||
@@ -78,7 +78,7 @@ See `scripts/cta_template.py` for a complete, runnable example (MA crossover).
|
||||
The template below is the canonical skeleton — replace the `# SIGNAL LOGIC` section:
|
||||
|
||||
```python
|
||||
from vnpy.app.cta_strategy import (
|
||||
from vnpy_ctastrategy import (
|
||||
CtaTemplate,
|
||||
StopOrder,
|
||||
TickData,
|
||||
@@ -270,7 +270,7 @@ To load in vnpy:
|
||||
To run the vnpy backtester:
|
||||
|
||||
```python
|
||||
from vnpy.app.cta_backtester import BacktestingEngine
|
||||
from vnpy_ctastrategy.backtesting import BacktestingEngine
|
||||
from vnpy.trader.constant import Interval
|
||||
|
||||
engine = BacktestingEngine()
|
||||
|
||||
@@ -5,14 +5,14 @@ This file is the reference implementation for the vnpy-export skill.
|
||||
Copy it to your vnpy project's strategies/ folder and adjust parameters.
|
||||
|
||||
Requirements:
|
||||
pip install vnpy vnpy_ctp (or vnpy_tts for paper trading)
|
||||
pip install vnpy vnpy_ctastrategy vnpy_ctp (or vnpy_tts for paper trading)
|
||||
|
||||
Usage in vnpy Trader:
|
||||
CTA Strategy App → Add Strategy → MaCrossStrategy
|
||||
Set vt_symbol, e.g. "IF2406.CFFEX" (futures) or "000001.SZSE" (A-share)
|
||||
"""
|
||||
|
||||
from vnpy.app.cta_strategy import (
|
||||
from vnpy_ctastrategy import (
|
||||
CtaTemplate,
|
||||
StopOrder,
|
||||
TickData,
|
||||
|
||||
@@ -27,7 +27,7 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_SUBCLASSES_CACHE: list[type[BaseTool]] | None = None
|
||||
_SHELL_TOOL_NAMES = {"bash", "background_run"}
|
||||
_SHELL_TOOL_NAMES = {"bash", "background_run", "cancel_background"}
|
||||
|
||||
|
||||
def _discover_subclasses() -> list[type[BaseTool]]:
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Shared safety checks for host shell commands."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
_PYTHON_PROCESS = r"python(?:w|\d+(?:\.\d+)*)?(?:\.exe)?\b"
|
||||
_TASKKILL_PYTHON = re.compile(
|
||||
rf"\btaskkill(?:\.exe)?\b"
|
||||
rf"[^;&\r\n]*(?:/im\s+{_PYTHON_PROCESS}|get-process\s+"
|
||||
rf"(?:-name\s+)?{_PYTHON_PROCESS})",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_UNIX_PYTHON_KILL = re.compile(
|
||||
rf"^(?:sudo\s+)?(?:pkill|killall)\b[^;&\r\n]*{_PYTHON_PROCESS}",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_POWERSHELL_PREFIX = re.compile(
|
||||
r"^(?:powershell|pwsh)(?:\.exe)?\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_POWERSHELL_PYTHON_BY_NAME = re.compile(
|
||||
rf"\bstop-process\b[^;&\r\n]*-name\s+{_PYTHON_PROCESS}",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_POWERSHELL_PYTHON_PIPE = re.compile(
|
||||
rf"\bget-process\s+(?:-name\s+)?{_PYTHON_PROCESS}"
|
||||
r"[^;&\r\n]*\|\s*stop-process\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_BROAD_PYTHON_KILL_ERROR = (
|
||||
"Broad Python process termination is blocked because it can terminate "
|
||||
"Vibe-Trading itself. Use cancel_background with the task_id returned "
|
||||
"by background_run."
|
||||
)
|
||||
|
||||
|
||||
def broad_python_kill_error(command: str) -> str | None:
|
||||
"""Return an error when a shell command broadly kills Python by name.
|
||||
|
||||
Args:
|
||||
command: Shell source submitted to a shell-capable tool.
|
||||
|
||||
Returns:
|
||||
An actionable error for unsafe broad termination, otherwise ``None``.
|
||||
"""
|
||||
for segment in re.split(r"[;\r\n]|(?<![<>])&(?![<>])", command):
|
||||
normalized = segment.strip().replace('"', " ").replace("'", " ")
|
||||
normalized = re.sub(r"\s+", " ", normalized)
|
||||
if _TASKKILL_PYTHON.search(normalized):
|
||||
return _BROAD_PYTHON_KILL_ERROR
|
||||
if _UNIX_PYTHON_KILL.search(normalized):
|
||||
return _BROAD_PYTHON_KILL_ERROR
|
||||
if _POWERSHELL_PREFIX.search(normalized) and (
|
||||
_POWERSHELL_PYTHON_BY_NAME.search(normalized)
|
||||
or _POWERSHELL_PYTHON_PIPE.search(normalized)
|
||||
):
|
||||
return _BROAD_PYTHON_KILL_ERROR
|
||||
return None
|
||||
@@ -323,12 +323,15 @@ def _load_csi300_panel(start: str, end: str) -> dict[str, pd.DataFrame]:
|
||||
ed = end.replace("-", "")
|
||||
|
||||
codes: list[str] = []
|
||||
constituent_source = "tushare index_weight"
|
||||
constituent_source_date: str | None = None
|
||||
try:
|
||||
weights = pro.index_weight(
|
||||
index_code="399300.SZ", start_date=sd, end_date=ed
|
||||
)
|
||||
if weights is not None and not weights.empty:
|
||||
latest_date = weights["trade_date"].max()
|
||||
constituent_source_date = str(latest_date)
|
||||
codes = (
|
||||
weights[weights["trade_date"] == latest_date]["con_code"]
|
||||
.drop_duplicates()
|
||||
@@ -340,6 +343,8 @@ def _load_csi300_panel(start: str, end: str) -> dict[str, pd.DataFrame]:
|
||||
|
||||
if not codes:
|
||||
codes = list(_CSI300_FALLBACK_CODES)
|
||||
constituent_source = "hand-picked fallback"
|
||||
constituent_source_date = None
|
||||
logger.warning("csi300: using %d-name fallback (degraded run)", len(codes))
|
||||
|
||||
# Fetch raw daily in parallel — we need ``amount`` which the standard
|
||||
@@ -381,6 +386,16 @@ def _load_csi300_panel(start: str, end: str) -> dict[str, pd.DataFrame]:
|
||||
panel["vwap"] = safe_div(
|
||||
panel["amount"] * 1000.0, panel["volume"] * 100.0 + 1.0
|
||||
)
|
||||
panel["_meta"] = {
|
||||
"universe": "csi300",
|
||||
# A terminal snapshot is forward-looking relative to the start of the
|
||||
# requested interval; the static fallback is survivor-selected too.
|
||||
"survivorship_bias": True,
|
||||
"degraded": constituent_source != "tushare index_weight",
|
||||
"constituent_source": constituent_source,
|
||||
"constituent_source_date": constituent_source_date,
|
||||
"constituent_count": len(codes),
|
||||
}
|
||||
return panel
|
||||
|
||||
|
||||
@@ -396,14 +411,17 @@ def _load_sp500_panel(start: str, end: str) -> dict[str, pd.DataFrame]:
|
||||
"""
|
||||
codes = _fetch_sp500_constituents()
|
||||
constituent_source = "wikipedia"
|
||||
constituent_source_date: str | None = _SP500_CONSTITUENT_SOURCE_DATE
|
||||
if not codes:
|
||||
codes = list(_SP500_FALLBACK_CODES)
|
||||
constituent_source = "hand-picked fallback"
|
||||
constituent_source_date = None
|
||||
logger.warning("sp500: using %d-name fallback (degraded run)", len(codes))
|
||||
|
||||
logger.warning(
|
||||
"SP500 universe uses current constituents (%s @ %s) → survivorship-biased",
|
||||
constituent_source, _SP500_CONSTITUENT_SOURCE_DATE,
|
||||
constituent_source,
|
||||
constituent_source_date,
|
||||
)
|
||||
|
||||
# yfinance loader expects project-style symbols (``AAPL.US``).
|
||||
@@ -423,8 +441,9 @@ def _load_sp500_panel(start: str, end: str) -> dict[str, pd.DataFrame]:
|
||||
panel["_meta"] = {
|
||||
"universe": "sp500",
|
||||
"survivorship_bias": True,
|
||||
"degraded": constituent_source == "hand-picked fallback",
|
||||
"constituent_source": constituent_source,
|
||||
"constituent_source_date": _SP500_CONSTITUENT_SOURCE_DATE,
|
||||
"constituent_source_date": constituent_source_date,
|
||||
"constituent_count": len(codes),
|
||||
}
|
||||
return panel
|
||||
|
||||
@@ -7,11 +7,13 @@ import os
|
||||
import signal
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from src.agent.tools import BaseTool
|
||||
from src.tools._shell_safety import broad_python_kill_error
|
||||
|
||||
WORKDIR = Path(__file__).resolve().parents[2]
|
||||
_COMMAND_TIMEOUT_SECONDS = 300.0
|
||||
@@ -58,6 +60,16 @@ def _taskkill_windows_process_tree(process: subprocess.Popen[str]) -> None:
|
||||
process.kill()
|
||||
|
||||
|
||||
def _force_stop_process_tree(process: subprocess.Popen[str]) -> None:
|
||||
"""Force-stop one tracked process tree without draining its pipes."""
|
||||
if process.poll() is not None:
|
||||
return
|
||||
if os.name == "nt":
|
||||
_taskkill_windows_process_tree(process)
|
||||
else:
|
||||
_signal_posix_process_group(process, signal.SIGKILL)
|
||||
|
||||
|
||||
def _terminate_process_tree(process: subprocess.Popen[str]) -> tuple[str, str]:
|
||||
"""Terminate a timed-out process tree, drain its pipes, and reap its root."""
|
||||
if os.name == "nt":
|
||||
@@ -104,21 +116,45 @@ class BackgroundManager:
|
||||
Returns:
|
||||
JSON string containing status and task_id.
|
||||
"""
|
||||
safety_error = broad_python_kill_error(command)
|
||||
if safety_error:
|
||||
return json.dumps(
|
||||
{"status": "error", "error": safety_error},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
task_id = uuid.uuid4().hex[:8]
|
||||
self.tasks[task_id] = {
|
||||
"status": "running",
|
||||
"result": None,
|
||||
"command": command,
|
||||
"exit_code": None,
|
||||
}
|
||||
started_at = time.monotonic()
|
||||
with self._lock:
|
||||
self.tasks[task_id] = {
|
||||
"status": "running",
|
||||
"result": None,
|
||||
"command": command,
|
||||
"exit_code": None,
|
||||
"process": None,
|
||||
"cancel_requested": False,
|
||||
"started_at": started_at,
|
||||
}
|
||||
threading.Thread(target=self._execute, args=(task_id, command), daemon=True).start()
|
||||
return json.dumps({"status": "ok", "task_id": task_id, "message": f"Started: {command[:80]}"})
|
||||
|
||||
def _execute(self, task_id: str, command: str) -> None:
|
||||
exit_code: int | None = None
|
||||
process: subprocess.Popen[str] | None = None
|
||||
output = ""
|
||||
status = "error"
|
||||
try:
|
||||
process = _start_process(command)
|
||||
with self._lock:
|
||||
task = self.tasks.get(task_id)
|
||||
if task is None:
|
||||
_force_stop_process_tree(process)
|
||||
return
|
||||
task["process"] = process
|
||||
cancel_requested = bool(task.get("cancel_requested"))
|
||||
if cancel_requested:
|
||||
_force_stop_process_tree(process)
|
||||
|
||||
try:
|
||||
stdout, stderr = process.communicate(timeout=_COMMAND_TIMEOUT_SECONDS)
|
||||
output = _combined_output(stdout, stderr)
|
||||
@@ -137,25 +173,135 @@ class BackgroundManager:
|
||||
status = "timeout"
|
||||
except Exception as e:
|
||||
output, status = str(e), "error"
|
||||
self.tasks[task_id]["status"] = status
|
||||
self.tasks[task_id]["result"] = output or "(no output)"
|
||||
self.tasks[task_id]["exit_code"] = exit_code
|
||||
|
||||
with self._lock:
|
||||
task = self.tasks.get(task_id)
|
||||
if task is None:
|
||||
return
|
||||
if task.get("cancel_requested"):
|
||||
status = "cancelled"
|
||||
cancel_message = "Cancelled by request"
|
||||
output = (
|
||||
f"{output}\n{cancel_message}"
|
||||
if output
|
||||
else cancel_message
|
||||
)
|
||||
output = output[:_MAX_OUTPUT_CHARS]
|
||||
task["status"] = status
|
||||
task["result"] = output or "(no output)"
|
||||
task["exit_code"] = exit_code
|
||||
task["process"] = None
|
||||
started_at = task.get("started_at")
|
||||
if isinstance(started_at, (int, float)):
|
||||
task["duration_seconds"] = round(
|
||||
max(0.0, time.monotonic() - started_at),
|
||||
3,
|
||||
)
|
||||
self._notifications.append({
|
||||
"task_id": task_id, "status": status,
|
||||
"command": command[:80], "result": (output or "")[:500],
|
||||
"exit_code": exit_code,
|
||||
})
|
||||
|
||||
def cancel(self, task_id: str) -> str:
|
||||
"""Cancel one task by its tracked process group.
|
||||
|
||||
Args:
|
||||
task_id: Identifier returned by :meth:`run`.
|
||||
|
||||
Returns:
|
||||
JSON string describing the cancellation request.
|
||||
"""
|
||||
with self._lock:
|
||||
task = self.tasks.get(task_id)
|
||||
if task is None:
|
||||
return json.dumps({
|
||||
"status": "error",
|
||||
"error": f"Unknown task {task_id}",
|
||||
})
|
||||
|
||||
task_status = task["status"]
|
||||
if task_status == "cancelling":
|
||||
return json.dumps({
|
||||
"status": "ok",
|
||||
"task_id": task_id,
|
||||
"task_status": "cancelling",
|
||||
"message": "Cancellation already requested",
|
||||
})
|
||||
if task_status != "running":
|
||||
return json.dumps({
|
||||
"status": "error",
|
||||
"error": f"Task {task_id} is already {task_status}",
|
||||
"task_status": task_status,
|
||||
})
|
||||
|
||||
process = task.get("process")
|
||||
if process is not None and process.poll() is not None:
|
||||
return json.dumps({
|
||||
"status": "error",
|
||||
"error": f"Task {task_id} is already finishing",
|
||||
"task_status": task_status,
|
||||
})
|
||||
|
||||
task["cancel_requested"] = True
|
||||
task["status"] = "cancelling"
|
||||
|
||||
if process is not None:
|
||||
_force_stop_process_tree(process)
|
||||
return json.dumps({
|
||||
"status": "ok",
|
||||
"task_id": task_id,
|
||||
"task_status": "cancelling",
|
||||
"message": "Cancellation requested",
|
||||
})
|
||||
|
||||
def check(self, task_id: Optional[str] = None) -> str:
|
||||
task: dict | None = None
|
||||
task_summaries: list[tuple[str, str, str]] = []
|
||||
with self._lock:
|
||||
if task_id:
|
||||
t = self.tasks.get(task_id)
|
||||
task = dict(t) if t is not None else None
|
||||
else:
|
||||
task_summaries = [
|
||||
(tid, t["status"], t["command"])
|
||||
for tid, t in self.tasks.items()
|
||||
]
|
||||
|
||||
if task_id:
|
||||
t = self.tasks.get(task_id)
|
||||
t = task
|
||||
if not t:
|
||||
return json.dumps({"status": "error", "error": f"Unknown task {task_id}"})
|
||||
return json.dumps({"status": t["status"], "command": t["command"][:60],
|
||||
"result": t.get("result") or "(running)",
|
||||
"exit_code": t.get("exit_code")}, ensure_ascii=False)
|
||||
lines = [f"{tid}: [{t['status']}] {t['command'][:60]}" for tid, t in self.tasks.items()]
|
||||
status = t["status"]
|
||||
result = t.get("result")
|
||||
payload = {
|
||||
"status": status,
|
||||
"command": t["command"][:60],
|
||||
"result": result or "(running)",
|
||||
"exit_code": t.get("exit_code"),
|
||||
}
|
||||
started_at = t.get("started_at")
|
||||
if isinstance(started_at, (int, float)):
|
||||
duration = t.get("duration_seconds")
|
||||
if not isinstance(duration, (int, float)):
|
||||
duration = max(0.0, time.monotonic() - started_at)
|
||||
elapsed = round(duration, 1)
|
||||
payload["elapsed_seconds"] = elapsed
|
||||
payload["timeout_seconds"] = _COMMAND_TIMEOUT_SECONDS
|
||||
if status in {"running", "cancelling"}:
|
||||
remaining = max(0.0, _COMMAND_TIMEOUT_SECONDS - duration)
|
||||
payload["timeout_remaining_seconds"] = round(remaining, 1)
|
||||
if not result:
|
||||
payload["result"] = (
|
||||
f"({status} for {elapsed:g}s; automatic timeout "
|
||||
f"after {_COMMAND_TIMEOUT_SECONDS:g}s; use "
|
||||
"cancel_background(task_id) to stop safely)"
|
||||
)
|
||||
return json.dumps(payload, ensure_ascii=False)
|
||||
lines = [
|
||||
f"{tid}: [{status}] {command[:60]}"
|
||||
for tid, status, command in task_summaries
|
||||
]
|
||||
return "\n".join(lines) if lines else "No background tasks."
|
||||
|
||||
def drain_notifications(self) -> List[dict]:
|
||||
@@ -175,7 +321,12 @@ def get_background_manager() -> BackgroundManager:
|
||||
|
||||
class BackgroundRunTool(BaseTool):
|
||||
name = "background_run"
|
||||
description = "Run command in background thread. Returns task_id immediately. Use for long-running operations (ML training, large data processing)."
|
||||
description = (
|
||||
"Run a shell command in a tracked background process group. Returns "
|
||||
"task_id immediately and times out after 300 seconds. On Windows the "
|
||||
"shell is cmd.exe, not PowerShell. Poll with check_background and stop "
|
||||
"only with cancel_background; never use taskkill/pkill/killall."
|
||||
)
|
||||
parameters = {"type": "object", "properties": {
|
||||
"command": {"type": "string", "description": "Shell command to run in background"},
|
||||
}, "required": ["command"]}
|
||||
@@ -187,7 +338,11 @@ class BackgroundRunTool(BaseTool):
|
||||
|
||||
class CheckBackgroundTool(BaseTool):
|
||||
name = "check_background"
|
||||
description = "Check background task status. Omit task_id to list all."
|
||||
description = (
|
||||
"Check background task status, elapsed time, and remaining time before "
|
||||
"the 300-second automatic timeout. Omit task_id to list all. To stop "
|
||||
"a running task, pass its task_id to cancel_background."
|
||||
)
|
||||
parameters = {"type": "object", "properties": {
|
||||
"task_id": {"type": "string"},
|
||||
}, "required": []}
|
||||
@@ -195,3 +350,27 @@ class CheckBackgroundTool(BaseTool):
|
||||
|
||||
def execute(self, **kw: Any) -> str:
|
||||
return _BG.check(kw.get("task_id"))
|
||||
|
||||
|
||||
class CancelBackgroundTool(BaseTool):
|
||||
"""Cancel one process group created by :class:`BackgroundRunTool`."""
|
||||
|
||||
name = "cancel_background"
|
||||
description = (
|
||||
"Cancel exactly one background_run task by task_id using its tracked "
|
||||
"process group/PID. Never kills processes by executable name."
|
||||
)
|
||||
parameters = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_id": {
|
||||
"type": "string",
|
||||
"description": "task_id returned by background_run",
|
||||
},
|
||||
},
|
||||
"required": ["task_id"],
|
||||
}
|
||||
is_readonly = False
|
||||
|
||||
def execute(self, **kw: Any) -> str:
|
||||
return _BG.cancel(kw["task_id"])
|
||||
@@ -7,6 +7,7 @@ import subprocess
|
||||
from typing import Any
|
||||
|
||||
from src.agent.tools import BaseTool
|
||||
from src.tools._shell_safety import broad_python_kill_error
|
||||
|
||||
_OUTPUT_LIMIT = 50_000
|
||||
_DEFAULT_TIMEOUT = 120
|
||||
@@ -16,7 +17,12 @@ class BashTool(BaseTool):
|
||||
"""Execute shell commands in the working directory."""
|
||||
|
||||
name = "bash"
|
||||
description = "Execute a shell command in the working directory. Use for installing packages, running scripts, or inspecting files."
|
||||
description = (
|
||||
"Execute a shell command in the working directory. On Windows the "
|
||||
"shell is cmd.exe, not PowerShell. Use for installing packages, "
|
||||
"running scripts, or inspecting files. Never kill Python processes "
|
||||
"by name; stop a background_run task with cancel_background."
|
||||
)
|
||||
parameters = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -38,6 +44,12 @@ class BashTool(BaseTool):
|
||||
"""
|
||||
command = kwargs["command"]
|
||||
cwd = kwargs.get("run_dir")
|
||||
safety_error = broad_python_kill_error(command)
|
||||
if safety_error:
|
||||
return json.dumps(
|
||||
{"status": "error", "error": safety_error},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
|
||||
@@ -353,6 +353,14 @@ def _read_image(path: Path) -> str:
|
||||
def _read_text(path: Path) -> str:
|
||||
"""Read a text-like file with encoding fallback."""
|
||||
data = path.read_bytes()
|
||||
# UTF-16 with BOM (Notepad "Unicode", Excel Unicode Text) before latin-1:
|
||||
# latin-1 never raises, so BOM+NUL bytes become mojibake without this.
|
||||
if data.startswith((b"\xff\xfe", b"\xfe\xff")):
|
||||
try:
|
||||
decoded = data.decode("utf-16")
|
||||
return _envelope(path, "text", decoded, encoding="utf-16", size=len(data))
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
last_err: Exception | None = None
|
||||
for enc in _ENCODING_FALLBACK:
|
||||
try:
|
||||
|
||||
@@ -11,6 +11,7 @@ User-created skills are stored separately from bundled skills:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
@@ -22,11 +23,27 @@ from src.agent.tools import BaseTool
|
||||
|
||||
_ALLOWED_SUBDIRS = {"references", "templates", "examples", "assets"}
|
||||
|
||||
# Keep CJK/other scripts in slugs (same ranges as persistent memory) so
|
||||
# distinct non-ASCII skill names do not all collapse to "--".
|
||||
_NON_LATIN_SCRIPT_RANGES = (
|
||||
"一-鿿" # CJK Unified Ideographs
|
||||
"㐀-䶿" # CJK Extension A
|
||||
"-" # Thai
|
||||
"ؠ-ي" # Arabic letters
|
||||
"א-ת" # Hebrew letters
|
||||
"Ѐ-ӿ" # Cyrillic
|
||||
)
|
||||
_SKILL_SLUG_DISALLOWED_RE = re.compile(rf"[^a-z0-9\-{_NON_LATIN_SCRIPT_RANGES}]")
|
||||
|
||||
|
||||
def _sanitize_skill_name(name: str) -> str:
|
||||
"""Sanitize skill name to a safe directory slug."""
|
||||
return re.sub(r"[^a-z0-9-]", "-", name.lower().strip())[:60]
|
||||
|
||||
slug = _SKILL_SLUG_DISALLOWED_RE.sub("-", name.lower().strip())[:60]
|
||||
if not name.strip():
|
||||
return ""
|
||||
if slug.strip("-") == "":
|
||||
return hashlib.sha256(name.strip().encode("utf-8")).hexdigest()[:8]
|
||||
return slug
|
||||
|
||||
class SaveSkillTool(BaseTool):
|
||||
"""Save a successful workflow as a reusable skill."""
|
||||
|
||||
@@ -112,12 +112,13 @@ def load_dataframe(path: str | Path) -> pd.DataFrame:
|
||||
raise ValueError(f"Unsupported extension: {ext}")
|
||||
|
||||
last_err: Exception | None = None
|
||||
for enc in ("utf-8", "utf-8-sig", "gbk", "gb2312"):
|
||||
# utf-16 covers Excel "CSV UTF-16" / Unicode exports (BOM required).
|
||||
for enc in ("utf-8-sig", "utf-8", "utf-16", "gbk", "gb2312"):
|
||||
try:
|
||||
return pd.read_csv(p, dtype=str, encoding=enc)
|
||||
except UnicodeDecodeError as exc:
|
||||
last_err = exc
|
||||
raise ValueError(f"Failed to decode CSV with utf-8/gbk/gb2312: {last_err}")
|
||||
raise ValueError(f"Failed to decode CSV with utf-8/utf-16/gbk/gb2312: {last_err}")
|
||||
|
||||
|
||||
# ---------------- Format detection ----------------
|
||||
@@ -526,6 +527,13 @@ def _infer_market_from_symbol(symbol: str) -> str:
|
||||
return "china_a"
|
||||
if "-" in s and any(quote in s for quote in ("USDT", "USDC", "BTC", "USD")):
|
||||
return "crypto"
|
||||
# Binance-style concatenated pairs (BTCUSDT) are purely alphabetic, so the
|
||||
# isalpha() US-equity branch below would mis-label them without this check.
|
||||
for quote in ("USDT", "USDC", "BUSD"):
|
||||
if len(s) > len(quote) and s.endswith(quote):
|
||||
base = s[: -len(quote)]
|
||||
if base.isalpha() and len(base) >= 2:
|
||||
return "crypto"
|
||||
if s.isalpha():
|
||||
return "us"
|
||||
return "other"
|
||||
|
||||
@@ -130,8 +130,8 @@ def test_html_context_includes_universe_meta_when_present(capsys, monkeypatch, _
|
||||
|
||||
|
||||
def test_meta_key_absent_from_envelope_and_context_when_loader_has_none(capsys, monkeypatch, _reg, _captured_html_context):
|
||||
"""csi300/other loaders return no _meta; final envelope/context must not gain a stray key."""
|
||||
rc, cap = _run(monkeypatch, capsys, _ns(universe="csi300"), _result())
|
||||
"""Loaders without _meta must not gain a stray downstream key."""
|
||||
rc, cap = _run(monkeypatch, capsys, _ns(universe="btc-usdt"), _result())
|
||||
assert rc == 0
|
||||
envelope = _envelope(cap.out)
|
||||
assert "meta" not in envelope
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
"""Universe loaders disclose constituent provenance and degradation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from src.tools import alpha_bench_tool as tool
|
||||
|
||||
|
||||
def _daily_frame() -> pd.DataFrame:
|
||||
"""Return one deterministic Tushare-style daily row."""
|
||||
return pd.DataFrame(
|
||||
{
|
||||
"trade_date": ["20240102"],
|
||||
"open": [10.0],
|
||||
"high": [11.0],
|
||||
"low": [9.0],
|
||||
"close": [10.5],
|
||||
"vol": [100.0],
|
||||
"amount": [105.0],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class _FakeTusharePro:
|
||||
"""Minimal Tushare Pro surface used by the CSI300 loader."""
|
||||
|
||||
def __init__(self, weights: pd.DataFrame | Exception) -> None:
|
||||
self.weights = weights
|
||||
|
||||
def index_weight(self, **_kwargs) -> pd.DataFrame:
|
||||
"""Return configured weights or simulate an upstream failure."""
|
||||
if isinstance(self.weights, Exception):
|
||||
raise self.weights
|
||||
return self.weights
|
||||
|
||||
def daily(self, **_kwargs) -> pd.DataFrame:
|
||||
"""Return deterministic OHLCV data for every requested code."""
|
||||
return _daily_frame()
|
||||
|
||||
|
||||
def _install_fake_tushare(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
pro: _FakeTusharePro,
|
||||
) -> None:
|
||||
"""Install deterministic credentials and an in-memory Tushare module."""
|
||||
monkeypatch.setattr(
|
||||
tool,
|
||||
"get_env_config",
|
||||
lambda: SimpleNamespace(data=SimpleNamespace(tushare_token="test-token")),
|
||||
)
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"tushare",
|
||||
SimpleNamespace(pro_api=lambda _token: pro),
|
||||
)
|
||||
|
||||
|
||||
def test_csi300_reports_terminal_index_weight_snapshot(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Healthy CSI300 loads disclose their terminal Tushare snapshot."""
|
||||
weights = pd.DataFrame(
|
||||
{
|
||||
"trade_date": ["20240102", "20240131", "20240131"],
|
||||
"con_code": ["600000.SH", "000001.SZ", "000002.SZ"],
|
||||
}
|
||||
)
|
||||
_install_fake_tushare(monkeypatch, _FakeTusharePro(weights))
|
||||
|
||||
panel = tool._load_csi300_panel("2024-01-01", "2024-01-31")
|
||||
|
||||
assert panel["_meta"] == {
|
||||
"universe": "csi300",
|
||||
"survivorship_bias": True,
|
||||
"degraded": False,
|
||||
"constituent_source": "tushare index_weight",
|
||||
"constituent_source_date": "20240131",
|
||||
"constituent_count": 2,
|
||||
}
|
||||
|
||||
|
||||
def test_csi300_reports_hand_picked_fallback(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Failed index lookup discloses the smaller survivor-selected roster."""
|
||||
monkeypatch.setattr(
|
||||
tool,
|
||||
"_CSI300_FALLBACK_CODES",
|
||||
["600519.SH", "000001.SZ"],
|
||||
)
|
||||
_install_fake_tushare(monkeypatch, _FakeTusharePro(RuntimeError("offline")))
|
||||
|
||||
panel = tool._load_csi300_panel("2024-01-01", "2024-01-31")
|
||||
|
||||
assert panel["_meta"] == {
|
||||
"universe": "csi300",
|
||||
"survivorship_bias": True,
|
||||
"degraded": True,
|
||||
"constituent_source": "hand-picked fallback",
|
||||
"constituent_source_date": None,
|
||||
"constituent_count": 2,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("codes", "source", "source_date", "degraded"),
|
||||
[
|
||||
(["AAPL"], "wikipedia", tool._SP500_CONSTITUENT_SOURCE_DATE, False),
|
||||
([], "hand-picked fallback", None, True),
|
||||
],
|
||||
)
|
||||
def test_sp500_source_date_matches_the_roster(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
codes: list[str],
|
||||
source: str,
|
||||
source_date: str | None,
|
||||
degraded: bool,
|
||||
) -> None:
|
||||
"""SP500 metadata never assigns Wikipedia's date to a fallback list."""
|
||||
|
||||
class _Loader:
|
||||
def fetch(self, *_args, **_kwargs) -> dict:
|
||||
return {}
|
||||
|
||||
import backtest.loaders.registry as registry
|
||||
|
||||
monkeypatch.setattr(tool, "_fetch_sp500_constituents", lambda: codes)
|
||||
monkeypatch.setattr(registry, "resolve_loader", lambda _market: _Loader())
|
||||
|
||||
panel = tool._load_sp500_panel("2024-01-01", "2024-01-31")
|
||||
|
||||
assert panel["_meta"]["constituent_source"] == source
|
||||
assert panel["_meta"]["constituent_source_date"] == source_date
|
||||
assert panel["_meta"]["degraded"] is degraded
|
||||
@@ -6,8 +6,10 @@ import json
|
||||
import os
|
||||
import shlex
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -49,6 +51,266 @@ def test_nonzero_exit_is_reported_as_error() -> None:
|
||||
]
|
||||
|
||||
|
||||
def test_cancel_unknown_task_returns_actionable_error() -> None:
|
||||
manager = BackgroundManager()
|
||||
|
||||
result = json.loads(manager.cancel("missing"))
|
||||
|
||||
assert result == {
|
||||
"status": "error",
|
||||
"error": "Unknown task missing",
|
||||
}
|
||||
|
||||
|
||||
def test_check_running_task_reports_timeout_context(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
manager = BackgroundManager()
|
||||
manager.tasks["slow"] = {
|
||||
"status": "running",
|
||||
"result": None,
|
||||
"command": "python slow.py",
|
||||
"exit_code": None,
|
||||
"process": None,
|
||||
"cancel_requested": False,
|
||||
"started_at": 100.0,
|
||||
}
|
||||
monkeypatch.setattr(background_tools.time, "monotonic", lambda: 297.0)
|
||||
|
||||
result = json.loads(manager.check("slow"))
|
||||
|
||||
assert result["status"] == "running"
|
||||
assert result["elapsed_seconds"] == 197.0
|
||||
assert result["timeout_seconds"] == 300.0
|
||||
assert result["timeout_remaining_seconds"] == 103.0
|
||||
assert "cancel_background" in result["result"]
|
||||
|
||||
|
||||
def test_cancel_targets_only_the_tracked_process(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
manager = BackgroundManager()
|
||||
target = MagicMock()
|
||||
target.poll.return_value = None
|
||||
target.pid = 4321
|
||||
sibling = MagicMock()
|
||||
sibling.poll.return_value = None
|
||||
sibling.pid = 8765
|
||||
manager.tasks = {
|
||||
"target": {
|
||||
"status": "running",
|
||||
"result": None,
|
||||
"command": "python target.py",
|
||||
"exit_code": None,
|
||||
"process": target,
|
||||
"cancel_requested": False,
|
||||
},
|
||||
"sibling": {
|
||||
"status": "running",
|
||||
"result": None,
|
||||
"command": "python sibling.py",
|
||||
"exit_code": None,
|
||||
"process": sibling,
|
||||
"cancel_requested": False,
|
||||
},
|
||||
}
|
||||
stopped: list[object] = []
|
||||
monkeypatch.setattr(
|
||||
background_tools,
|
||||
"_force_stop_process_tree",
|
||||
stopped.append,
|
||||
)
|
||||
|
||||
result = json.loads(manager.cancel("target"))
|
||||
|
||||
assert result == {
|
||||
"status": "ok",
|
||||
"task_id": "target",
|
||||
"task_status": "cancelling",
|
||||
"message": "Cancellation requested",
|
||||
}
|
||||
assert stopped == [target]
|
||||
assert manager.tasks["target"]["status"] == "cancelling"
|
||||
assert manager.tasks["target"]["cancel_requested"] is True
|
||||
assert manager.tasks["sibling"]["status"] == "running"
|
||||
assert manager.tasks["sibling"]["cancel_requested"] is False
|
||||
|
||||
|
||||
def test_cancel_completed_task_does_not_touch_process(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
manager = BackgroundManager()
|
||||
process = MagicMock()
|
||||
manager.tasks["done"] = {
|
||||
"status": "completed",
|
||||
"result": "finished",
|
||||
"command": "python done.py",
|
||||
"exit_code": 0,
|
||||
"process": process,
|
||||
"cancel_requested": False,
|
||||
}
|
||||
stop = MagicMock()
|
||||
monkeypatch.setattr(background_tools, "_force_stop_process_tree", stop)
|
||||
|
||||
result = json.loads(manager.cancel("done"))
|
||||
|
||||
assert result == {
|
||||
"status": "error",
|
||||
"error": "Task done is already completed",
|
||||
"task_status": "completed",
|
||||
}
|
||||
stop.assert_not_called()
|
||||
|
||||
|
||||
def test_cancel_background_tool_contract() -> None:
|
||||
tool = background_tools.CancelBackgroundTool()
|
||||
|
||||
assert tool.name == "cancel_background"
|
||||
assert tool.parameters["required"] == ["task_id"]
|
||||
assert tool.is_readonly is False
|
||||
|
||||
|
||||
def test_windows_process_tree_kill_uses_pid_never_image_name(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
process = MagicMock()
|
||||
process.pid = 4321
|
||||
run = MagicMock()
|
||||
monkeypatch.setattr(background_tools.subprocess, "run", run)
|
||||
|
||||
background_tools._taskkill_windows_process_tree(process)
|
||||
|
||||
args = run.call_args.args[0]
|
||||
assert args == ["taskkill", "/PID", "4321", "/T", "/F"]
|
||||
assert "/IM" not in args
|
||||
assert "python.exe" not in args
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"command",
|
||||
[
|
||||
'taskkill /F /IM python.exe 2>nul & echo "done"',
|
||||
(
|
||||
"taskkill /F /PID (Get-Process python | "
|
||||
"Where-Object {$_.MainWindowTitle -eq ''} | "
|
||||
"Select-Object -First 1 -ExpandProperty Id)"
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_background_run_rejects_broad_python_termination(command: str) -> None:
|
||||
manager = BackgroundManager()
|
||||
|
||||
result = json.loads(manager.run(command))
|
||||
|
||||
assert result["status"] == "error"
|
||||
assert "cancel_background" in result["error"]
|
||||
assert manager.tasks == {}
|
||||
|
||||
|
||||
def _wait_for_task_status(
|
||||
manager: BackgroundManager,
|
||||
task_id: str,
|
||||
expected: set[str],
|
||||
timeout: float = 5.0,
|
||||
) -> dict:
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
task = json.loads(manager.check(task_id))
|
||||
if task["status"] in expected:
|
||||
return task
|
||||
time.sleep(0.02)
|
||||
pytest.fail(
|
||||
f"task {task_id} did not reach {sorted(expected)}: "
|
||||
f"{manager.check(task_id)}"
|
||||
)
|
||||
|
||||
|
||||
def _wait_for_registered_process(
|
||||
manager: BackgroundManager,
|
||||
task_id: str,
|
||||
timeout: float = 5.0,
|
||||
) -> None:
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
with manager._lock:
|
||||
process = manager.tasks[task_id].get("process")
|
||||
if process is not None:
|
||||
return
|
||||
time.sleep(0.02)
|
||||
pytest.fail(f"task {task_id} did not register its process")
|
||||
|
||||
|
||||
def test_cancel_running_task_reaps_process() -> None:
|
||||
manager = BackgroundManager()
|
||||
command = f'"{sys.executable}" -c "import time; time.sleep(30)"'
|
||||
task_id = json.loads(manager.run(command))["task_id"]
|
||||
_wait_for_registered_process(manager, task_id)
|
||||
|
||||
result = json.loads(manager.cancel(task_id))
|
||||
task = _wait_for_task_status(manager, task_id, {"cancelled"})
|
||||
|
||||
assert result["status"] == "ok"
|
||||
assert task["status"] == "cancelled"
|
||||
assert task["exit_code"] not in (None, 0)
|
||||
assert "Cancelled by request" in task["result"]
|
||||
|
||||
|
||||
def test_cancel_running_task_leaves_sibling_alive() -> None:
|
||||
manager = BackgroundManager()
|
||||
command = f'"{sys.executable}" -c "import time; time.sleep(30)"'
|
||||
target_id = json.loads(manager.run(command))["task_id"]
|
||||
sibling_id = json.loads(manager.run(command))["task_id"]
|
||||
_wait_for_registered_process(manager, target_id)
|
||||
_wait_for_registered_process(manager, sibling_id)
|
||||
|
||||
try:
|
||||
result = json.loads(manager.cancel(target_id))
|
||||
target = _wait_for_task_status(manager, target_id, {"cancelled"})
|
||||
sibling = json.loads(manager.check(sibling_id))
|
||||
|
||||
assert result["status"] == "ok"
|
||||
assert target["status"] == "cancelled"
|
||||
assert sibling["status"] == "running"
|
||||
finally:
|
||||
sibling = json.loads(manager.check(sibling_id))
|
||||
if sibling["status"] in {"running", "cancelling"}:
|
||||
manager.cancel(sibling_id)
|
||||
_wait_for_task_status(manager, sibling_id, {"cancelled"})
|
||||
|
||||
|
||||
def test_cancel_before_process_registration_is_honored(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
manager = BackgroundManager()
|
||||
start_entered = threading.Event()
|
||||
allow_start = threading.Event()
|
||||
process = MagicMock()
|
||||
process.pid = 4321
|
||||
process.poll.return_value = None
|
||||
process.communicate.return_value = ("", "")
|
||||
process.returncode = -9
|
||||
stop = MagicMock()
|
||||
|
||||
def delayed_start(command: str) -> object:
|
||||
del command
|
||||
start_entered.set()
|
||||
assert allow_start.wait(timeout=2.0)
|
||||
return process
|
||||
|
||||
monkeypatch.setattr(background_tools, "_start_process", delayed_start)
|
||||
monkeypatch.setattr(background_tools, "_force_stop_process_tree", stop)
|
||||
|
||||
task_id = json.loads(manager.run("python slow.py"))["task_id"]
|
||||
assert start_entered.wait(timeout=2.0)
|
||||
result = json.loads(manager.cancel(task_id))
|
||||
allow_start.set()
|
||||
task = _wait_for_task_status(manager, task_id, {"cancelled"})
|
||||
|
||||
assert result["status"] == "ok"
|
||||
assert task["status"] == "cancelled"
|
||||
stop.assert_called_once_with(process)
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="POSIX process-group regression")
|
||||
def test_timeout_kills_descendant_and_reaps_shell(
|
||||
tmp_path: Path,
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Safety regression tests for host shell execution."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from src.tools import bash_tool
|
||||
from src.tools.bash_tool import BashTool
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"command",
|
||||
[
|
||||
'taskkill /F /IM python.exe 2>nul & echo "killed any python"',
|
||||
(
|
||||
"taskkill /F /PID (Get-Process python | "
|
||||
"Where-Object {$_.MainWindowTitle -eq ''} | "
|
||||
"Select-Object -First 1 -ExpandProperty Id)"
|
||||
),
|
||||
'powershell -Command "taskkill /F /IM python.exe"',
|
||||
'start "" taskkill /F /IM python.exe',
|
||||
"powershell -NoProfile -Command \"Stop-Process -Name python -Force\"",
|
||||
"powershell -Command \"Get-Process python | Stop-Process -Force\"",
|
||||
"pkill -9 -f python",
|
||||
"killall python3",
|
||||
],
|
||||
)
|
||||
def test_rejects_broad_python_process_termination(
|
||||
command: str,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
run = MagicMock(side_effect=AssertionError("unsafe command reached subprocess"))
|
||||
monkeypatch.setattr(bash_tool.subprocess, "run", run)
|
||||
|
||||
result = json.loads(BashTool().execute(command=command))
|
||||
|
||||
assert result["status"] == "error"
|
||||
assert "cancel_background" in result["error"]
|
||||
run.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"command",
|
||||
[
|
||||
"python --version",
|
||||
"taskkill /PID 4321 /T /F",
|
||||
"echo python.exe",
|
||||
],
|
||||
)
|
||||
def test_allows_non_broad_process_commands(
|
||||
command: str,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
run = MagicMock()
|
||||
run.return_value.returncode = 0
|
||||
run.return_value.stdout = "ok"
|
||||
run.return_value.stderr = ""
|
||||
monkeypatch.setattr(bash_tool.subprocess, "run", run)
|
||||
|
||||
result = json.loads(BashTool().execute(command=command))
|
||||
|
||||
assert result["status"] == "ok"
|
||||
run.assert_called_once()
|
||||
@@ -231,6 +231,23 @@ class TestRollingCorrelationMatrix:
|
||||
assert -1 <= p_matrix[0][1] <= 1
|
||||
assert -1 <= s_matrix[0][1] <= 1
|
||||
|
||||
def test_does_not_forward_fill_missing_closes(self):
|
||||
# Under pandas>=2,<3 a bare pct_change() forward-fills NaN closes,
|
||||
# manufacturing a 0% return on the halted session and pairing it
|
||||
# against the peer's real move. GAP tracks PEER exactly, so once the
|
||||
# halted session is dropped instead of filled the two are perfectly
|
||||
# correlated. Mirrors TestAlignedReturns in test_regime.py.
|
||||
peer_closes = [100.0, 110.0, 115.5, 112.035, 121.0, 123.42]
|
||||
gap_closes = [c / 2.0 for c in peer_closes]
|
||||
gap_closes[2] = np.nan # trading halt: no close printed
|
||||
price_series = {
|
||||
"GAP": self._make_price_df(gap_closes),
|
||||
"PEER": self._make_price_df(peer_closes),
|
||||
}
|
||||
labels, matrix = _rolling_correlation_matrix(price_series, window=30, method="pearson")
|
||||
i, j = labels.index("GAP"), labels.index("PEER")
|
||||
assert matrix[i][j] == pytest.approx(1.0)
|
||||
|
||||
def test_empty_dict_returns_empty(self):
|
||||
labels, matrix = _rolling_correlation_matrix({}, window=30, method="pearson")
|
||||
assert labels == []
|
||||
|
||||
@@ -63,6 +63,19 @@ def test_gbk_fallback(tmp_path: Path) -> None:
|
||||
assert "中文" in result["text"]
|
||||
|
||||
|
||||
def test_utf16_bom_not_latin1_mojibake(tmp_path: Path) -> None:
|
||||
# Notepad/Excel "Unicode text" writes UTF-16 with BOM. latin-1 never
|
||||
# raises, so without utf-16 earlier in the fallback the file was accepted
|
||||
# as mojibake (NUL bytes between ASCII letters).
|
||||
p = tmp_path / "unicode.txt"
|
||||
p.write_bytes("hello 动量\n".encode("utf-16"))
|
||||
result = _call(p)
|
||||
assert result["status"] == "ok"
|
||||
assert result["encoding"] == "utf-16"
|
||||
assert result["text"] == "hello 动量\n"
|
||||
assert "\x00" not in result["text"]
|
||||
|
||||
|
||||
def test_unknown_extension_treated_as_text(tmp_path: Path) -> None:
|
||||
p = tmp_path / "weird.xyzfmt"
|
||||
p.write_text("abc", encoding="utf-8")
|
||||
|
||||
@@ -185,10 +185,10 @@ def test_mcp_server_exposes_well_known_tool_names() -> None:
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shell-tool opt-in policy (GHSA-6wjh-cc6v-xfrx / GHSA-m768-22r9-h4x7)
|
||||
#
|
||||
# The bash / background_run tools run arbitrary OS commands. The MCP server must
|
||||
# NOT register them unless the operator explicitly opts in — for EVERY transport.
|
||||
# stdio previously force-enabled them with no opt-out; that is the regression
|
||||
# these tests guard against.
|
||||
# The bash / background_run / cancel_background tools control OS processes. The
|
||||
# MCP server must NOT register them unless the operator explicitly opts in — for
|
||||
# EVERY transport. stdio previously force-enabled shell tools with no opt-out;
|
||||
# that is the regression these tests guard against.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -234,6 +234,7 @@ def test_mcp_registry_omits_shell_tools_under_default_policy(monkeypatch: pytest
|
||||
|
||||
assert "bash" not in registry.tool_names
|
||||
assert "background_run" not in registry.tool_names
|
||||
assert "cancel_background" not in registry.tool_names
|
||||
|
||||
|
||||
class _RecordingRegistry:
|
||||
|
||||
@@ -92,6 +92,20 @@ class TestBarsPerYear:
|
||||
# Falls back to 1 bar/day
|
||||
assert calc_bars_per_year("2H", "tushare") == 252
|
||||
|
||||
def test_lowercase_hour_matches_uppercase(self) -> None:
|
||||
# Loaders accept 1h/4h after interval-map fixes; annualisation must too.
|
||||
assert calc_bars_per_year("1h", "okx") == calc_bars_per_year("1H", "okx")
|
||||
assert calc_bars_per_year("4h", "ccxt") == calc_bars_per_year("4H", "ccxt")
|
||||
assert calc_bars_per_year("1h", "okx") == 365 * 24
|
||||
|
||||
def test_lowercase_day_matches_uppercase(self) -> None:
|
||||
assert calc_bars_per_year("1d", "tushare") == calc_bars_per_year("1D", "tushare")
|
||||
|
||||
def test_yahoo_source_aliases_yfinance(self) -> None:
|
||||
# Runner primary source for US equity is often "yahoo".
|
||||
assert calc_bars_per_year("1H", "yahoo") == calc_bars_per_year("1H", "yfinance")
|
||||
assert calc_bars_per_year("1H", "yahoo") == 252 * 7
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# win_rate_and_stats
|
||||
|
||||
@@ -37,6 +37,20 @@ class TestSanitizeSkillName:
|
||||
def test_empty(self) -> None:
|
||||
assert _sanitize_skill_name("") == ""
|
||||
|
||||
def test_cjk_name_keeps_distinct_slug(self) -> None:
|
||||
a = _sanitize_skill_name("动量策略")
|
||||
b = _sanitize_skill_name("均值回归")
|
||||
assert a == "动量策略"
|
||||
assert b == "均值回归"
|
||||
assert a != b
|
||||
|
||||
def test_punctuation_only_uses_stable_digest(self) -> None:
|
||||
slug = _sanitize_skill_name("???")
|
||||
assert len(slug) == 8
|
||||
assert slug.isalnum()
|
||||
assert slug == _sanitize_skill_name("???")
|
||||
assert slug != _sanitize_skill_name("!!!")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SaveSkillTool
|
||||
|
||||
@@ -14,6 +14,7 @@ def test_shell_tools_absent_from_default_registry(monkeypatch: pytest.MonkeyPatc
|
||||
|
||||
assert "bash" not in registry.tool_names
|
||||
assert "background_run" not in registry.tool_names
|
||||
assert "cancel_background" not in registry.tool_names
|
||||
|
||||
|
||||
def test_shell_tools_require_registry_opt_in(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
@@ -23,3 +24,4 @@ def test_shell_tools_require_registry_opt_in(monkeypatch: pytest.MonkeyPatch) ->
|
||||
|
||||
assert "bash" in registry.tool_names
|
||||
assert "background_run" in registry.tool_names
|
||||
assert "cancel_background" in registry.tool_names
|
||||
@@ -20,6 +20,7 @@ from src.tools.trade_journal_parsers import (
|
||||
_normalize_side,
|
||||
_qualify_a_share,
|
||||
_to_float,
|
||||
load_dataframe,
|
||||
parse_tonghuashun,
|
||||
parse_eastmoney,
|
||||
parse_futu,
|
||||
@@ -121,6 +122,9 @@ def test_normalize_side_rejects_missing_or_unknown(raw: object) -> None:
|
||||
("AAPL", "us"),
|
||||
("BTC-USDT", "crypto"),
|
||||
("ETH-USD", "crypto"),
|
||||
("BTCUSDT", "crypto"),
|
||||
("ethusdc", "crypto"),
|
||||
("SOLBUSD", "crypto"),
|
||||
("123456", "other"),
|
||||
],
|
||||
)
|
||||
@@ -205,6 +209,18 @@ def test_parse_file_unknown_raises(tmp_path: Path) -> None:
|
||||
parse_file(csv)
|
||||
|
||||
|
||||
def test_load_dataframe_accepts_utf16_bom_csv(tmp_path: Path) -> None:
|
||||
# Excel "CSV UTF-16" / Unicode CSV exports a BOM; previously every
|
||||
# encoding attempt raised UnicodeDecodeError and the load failed.
|
||||
csv = tmp_path / "futu_utf16.csv"
|
||||
body = "Date,Symbol,Side,Quantity,Price\n2024-01-02,AAPL,Buy,10,100\n"
|
||||
csv.write_bytes(body.encode("utf-16"))
|
||||
df = load_dataframe(csv)
|
||||
assert list(df.columns) == ["Date", "Symbol", "Side", "Quantity", "Price"]
|
||||
assert detect_format(df) == "futu"
|
||||
assert df.iloc[0]["Symbol"] == "AAPL"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"header,value,error",
|
||||
[
|
||||
|
||||
Generated
+27
-36
@@ -16,10 +16,10 @@
|
||||
"i18next": "^26.3.6",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"katex": "^0.18.1",
|
||||
"lucide-react": "^1.25.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.2.7",
|
||||
"react-i18next": "^17.0.10",
|
||||
"lucide-react": "^1.26.0",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
"react-i18next": "^17.0.11",
|
||||
"react-markdown": "^9.0.0",
|
||||
"react-router-dom": "^7.18.1",
|
||||
"rehype-highlight": "^7.0.0",
|
||||
@@ -41,7 +41,7 @@
|
||||
"@vitest/coverage-v8": "^4.1.10",
|
||||
"autoprefixer": "^10.5.4",
|
||||
"jsdom": "^29.1.1",
|
||||
"postcss": "^8.5.20",
|
||||
"postcss": "^8.5.23",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vite": "^6.4.3",
|
||||
@@ -2959,12 +2959,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-4.0.1.tgz",
|
||||
"integrity": "sha512-0zHsZJrK7S3K2aucXWL6ycoYJ/iNtIcFHC/nYQgFklPtrv5LpJctIiSCroWZWeuoXvuyFdzp6KzjJQ+OT5MfFw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"void-elements": "3.1.0"
|
||||
"funding": {
|
||||
"url": "https://locize.com"
|
||||
}
|
||||
},
|
||||
"node_modules/html-url-attributes": {
|
||||
@@ -3369,9 +3369,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "1.25.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.25.0.tgz",
|
||||
"integrity": "sha512-/mdJTRbiwcLOQ1NZZK1amZF9rIZyvO18D6r9TngE6TG1NmqHgFuT4eE7Xrkm9UsXMbBJD1NlfwHVltCDWHrOTw==",
|
||||
"version": "1.26.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.26.0.tgz",
|
||||
"integrity": "sha512-raglYVR2+VkMfJL158krjVmE+rV5ST2lzA/KQm1FRSjMHT4MnWaegHxoVEpmc2So3nOEhp9oGejJwAPX8MoAjg==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
@@ -4570,9 +4570,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.20",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz",
|
||||
"integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==",
|
||||
"version": "8.5.23",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz",
|
||||
"integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -4804,34 +4804,34 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.7",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
|
||||
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
|
||||
"version": "19.2.8",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
||||
"integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.2.7",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
|
||||
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
|
||||
"version": "19.2.8",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
|
||||
"integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.2.7"
|
||||
"react": "^19.2.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "17.0.10",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.10.tgz",
|
||||
"integrity": "sha512-XneHftyYA774MJkkccSkZ5oKrUpCnXIPmxio3wemqrVzCRLWiGXOMbIzObrer03fNDEnm8g8R5yYls4HcE+esg==",
|
||||
"version": "17.0.11",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.11.tgz",
|
||||
"integrity": "sha512-cDtkXgxjuFTWUH6V+aQn1Ve5vDiUztCNPWW5GtSHDccsgRXO1nE6QFWCEmc1KAutrb3OUv87wFShJL5RhUwPXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"html-parse-stringify": "^3.0.1",
|
||||
"html-parse-stringify": "^4.0.1",
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -6101,15 +6101,6 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
"i18next": "^26.3.6",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"katex": "^0.18.1",
|
||||
"lucide-react": "^1.25.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.2.7",
|
||||
"react-i18next": "^17.0.10",
|
||||
"lucide-react": "^1.26.0",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
"react-i18next": "^17.0.11",
|
||||
"react-markdown": "^9.0.0",
|
||||
"react-router-dom": "^7.18.1",
|
||||
"rehype-highlight": "^7.0.0",
|
||||
@@ -45,7 +45,7 @@
|
||||
"@vitest/coverage-v8": "^4.1.10",
|
||||
"autoprefixer": "^10.5.4",
|
||||
"jsdom": "^29.1.1",
|
||||
"postcss": "^8.5.20",
|
||||
"postcss": "^8.5.23",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vite": "^6.4.3",
|
||||
|
||||
+621
-689
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user