Merge branch 'brokermr810:main' into main

This commit is contained in:
PengfaGuo
2026-01-25 11:32:44 +08:00
committed by GitHub
27 changed files with 5122 additions and 119 deletions
+60 -3
View File
@@ -106,6 +106,14 @@ QuantDinger includes a built-in **LLM-based multi-agent research system** that g
## 📸 Visual Tour
<div align="center">
<h3>🗺️ System Architecture Overview</h3>
<p>A comprehensive view of QuantDinger's AI-powered research, backtesting, and automated trading capabilities.</p>
<img src="docs/screenshots/tuopu.png" alt="QuantDinger System Topology" width="100%" style="border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); max-width: 800px;">
</div>
<br/>
<div align="center">
<h3>📊 Professional Quant Dashboard</h3>
<p>Real-time monitoring of market dynamics, assets, and strategy status.</p>
@@ -288,7 +296,29 @@ Config lives in `.env` (see `backend_api_python/env.example`): `ENABLE_AGENT_MEM
- **Auto-Restore**: Resumes running strategies after system restarts
- **Order Queue**: Background worker for order execution
### 7. Tech Stack
### 7. Multi-LLM Provider Support
QuantDinger supports multiple AI providers with auto-detection:
| Provider | Features |
|----------|----------|
| **OpenRouter** | Multi-model gateway (default), 100+ models |
| **OpenAI** | GPT-4o, GPT-4o-mini |
| **Google Gemini** | Gemini 1.5 Flash/Pro |
| **DeepSeek** | DeepSeek Chat (cost-effective) |
| **xAI Grok** | Grok Beta |
Simply configure your preferred provider's API key in `.env`. The system auto-detects available providers.
### 8. User Management & Security
- **Multi-User Support**: PostgreSQL-backed user accounts with role-based permissions
- **OAuth Login**: Google and GitHub OAuth integration
- **Email Verification**: Registration and password reset via email codes
- **Security Features**: Cloudflare Turnstile captcha, IP/account rate limiting
- **Demo Mode**: Read-only mode for public demonstrations
### 9. Tech Stack
- **Backend**: Python (Flask) + PostgreSQL + Redis (optional)
- **Frontend**: Vue 2 + Ant Design Vue + KlineCharts/ECharts
@@ -708,10 +738,13 @@ Use `backend_api_python/env.example` as a template. Common settings include:
- **Auth**: `SECRET_KEY`, `ADMIN_USER`, `ADMIN_PASSWORD`
- **Server**: `PYTHON_API_HOST`, `PYTHON_API_PORT`, `PYTHON_API_DEBUG`
- **Database**: `DATABASE_URL` (PostgreSQL connection string)
- **AI / LLM**: `OPENROUTER_API_KEY`, `OPENROUTER_MODEL`, timeouts
- **AI / LLM**: `LLM_PROVIDER` (openrouter/openai/google/deepseek/grok), provider-specific API keys
- **OAuth**: `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`
- **Security**: `TURNSTILE_SITE_KEY`, `TURNSTILE_SECRET_KEY`, `ENABLE_REGISTRATION`
- **Web search**: `SEARCH_PROVIDER`, `SEARCH_GOOGLE_*`, `SEARCH_BING_API_KEY`
- **Order Execution**: `ORDER_MODE` (maker/market), `MAKER_WAIT_SEC`, `MAKER_OFFSET_BPS`
- **Proxy (optional)**: `PROXY_PORT` or `PROXY_URL`
- **Workers**: `ENABLE_PENDING_ORDER_WORKER`, `DISABLE_RESTORE_RUNNING_STRATEGIES`
- **Workers**: `ENABLE_PENDING_ORDER_WORKER`, `ENABLE_PORTFOLIO_MONITOR`
---
@@ -823,6 +856,30 @@ Your contributions help us maintain and improve QuantDinger.
---
### 🎓 Supporting Partners
We are proud to be supported by academic institutions and organizations advancing quantitative finance education and research.
<div align="center">
<table>
<tr>
<td align="center" width="50%">
<a href="https://beinvolved.indiana.edu/organization/quantfiniu" target="_blank">
<img src="docs/screenshots/qfs_logo.png" alt="Indiana University Quantitative Finance Society" width="280" style="border-radius: 8px;">
</a>
<br/><br/>
<strong>Quantitative Finance Society (QFS)</strong><br/>
<small>Indiana University Bloomington</small><br/>
<small>Fostering the next generation of quantitative finance professionals</small>
</td>
</tr>
</table>
</div>
> 💡 **Interested in becoming a supporting partner?** We welcome collaborations with universities, research institutions, and organizations. Contact us at [brokermr810@gmail.com](mailto:brokermr810@gmail.com) or via [Telegram](https://t.me/worldinbroker).
---
### Acknowledgements
QuantDinger stands on the shoulders of great open-source projects:
+61 -2
View File
@@ -106,6 +106,14 @@ QuantDinger 包含一个内置的**基于 LLM 的多智能体研究系统**,
## 📸 功能预览
<div align="center">
<h3>🗺️ 系统架构总览</h3>
<p>QuantDinger AI 驱动的研究、回测和自动化交易功能全景图。</p>
<img src="docs/screenshots/tuopu.png" alt="QuantDinger 系统拓扑图" width="100%" style="border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); max-width: 800px;">
</div>
<br/>
<div align="center">
<h3>📊 专业量化仪表盘</h3>
<p>实时监控市场动态、资产状况和策略状态。</p>
@@ -326,7 +334,29 @@ score = w_{sim}\cdot sim + w_{recency}\cdot recency + w_{returns}\cdot returns\_
- **自动恢复**:系统重启后恢复运行中的策略
- **订单队列**:后台工作线程用于订单执行
### 7. 技术栈
### 7. 多LLM提供商支持
QuantDinger 支持多个 AI 提供商,具备自动检测功能:
| 提供商 | 特点 |
|--------|------|
| **OpenRouter** | 多模型网关(默认),100+ 模型 |
| **OpenAI** | GPT-4o, GPT-4o-mini |
| **Google Gemini** | Gemini 1.5 Flash/Pro |
| **DeepSeek** | DeepSeek Chat(性价比高) |
| **xAI Grok** | Grok Beta |
只需在 `.env` 中配置您首选提供商的 API 密钥,系统会自动检测可用提供商。
### 8. 用户管理与安全
- **多用户支持**:基于 PostgreSQL 的用户账户,支持基于角色的权限管理
- **OAuth 登录**Google 和 GitHub OAuth 集成
- **邮箱验证**:通过邮箱验证码进行注册和密码重置
- **安全功能**Cloudflare Turnstile 人机验证、IP/账户速率限制
- **演示模式**:用于公开演示的只读模式
### 9. 技术栈
- **后端**Python (Flask) + PostgreSQL + Redis(可选)
- **前端**Vue 2 + Ant Design Vue + KlineCharts/ECharts
@@ -535,9 +565,14 @@ npm run serve
- **认证**: `SECRET_KEY`, `ADMIN_USER`, `ADMIN_PASSWORD`
- **服务器**: `PYTHON_API_HOST`, `PYTHON_API_PORT`, `PYTHON_API_DEBUG`
- **AI / LLM**: `OPENROUTER_API_KEY`, `OPENROUTER_MODEL`
- **数据库**: `DATABASE_URL` (PostgreSQL 连接字符串)
- **AI / LLM**: `LLM_PROVIDER` (openrouter/openai/google/deepseek/grok), 各提供商 API 密钥
- **OAuth**: `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`
- **安全**: `TURNSTILE_SITE_KEY`, `TURNSTILE_SECRET_KEY`, `ENABLE_REGISTRATION`
- **网络搜索**: `SEARCH_PROVIDER`, `SEARCH_GOOGLE_*`, `SEARCH_BING_API_KEY`
- **订单执行**: `ORDER_MODE` (maker/market), `MAKER_WAIT_SEC`, `MAKER_OFFSET_BPS`
- **代理 (可选)**: `PROXY_PORT``PROXY_URL`
- **后台服务**: `ENABLE_PENDING_ORDER_WORKER`, `ENABLE_PORTFOLIO_MONITOR`
---
@@ -632,6 +667,30 @@ QuantDinger 的代码使用 **Apache License 2.0** 授权。但需要注意:**
---
### 🎓 支持伙伴
我们很荣幸获得推动量化金融教育和研究的学术机构和组织的支持。
<div align="center">
<table>
<tr>
<td align="center" width="50%">
<a href="https://beinvolved.indiana.edu/organization/quantfiniu" target="_blank">
<img src="docs/screenshots/qfs_logo.png" alt="印第安纳大学量化金融学会" width="280" style="border-radius: 8px;">
</a>
<br/><br/>
<strong>量化金融学会 (QFS)</strong><br/>
<small>印第安纳大学布卢明顿分校</small><br/>
<small>培养下一代量化金融专业人才</small>
</td>
</tr>
</table>
</div>
> 💡 **有兴趣成为支持伙伴吗?** 我们欢迎与大学、研究机构和组织合作。请通过 [brokermr810@gmail.com](mailto:brokermr810@gmail.com) 或 [Telegram](https://t.me/worldinbroker) 联系我们。
---
### 致谢
QuantDinger 站在这些伟大的开源项目肩膀之上:Flask, Pandas, CCXT, Vue.js, Ant Design Vue, KlineCharts 等。
+62 -3
View File
@@ -106,6 +106,14 @@ QuantDingerには、ウェブから金融情報を収集し、ローカル市場
## 📸 ビジュアルツアー
<div align="center">
<h3>🗺️ システムアーキテクチャ概要</h3>
<p>QuantDinger の AI 駆動型リサーチ、バックテスト、自動取引機能の全体像。</p>
<img src="docs/screenshots/tuopu.png" alt="QuantDinger システムトポロジー" width="100%" style="border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); max-width: 800px;">
</div>
<br/>
<div align="center">
<h3>📊 プロフェッショナル・クオンツダッシュボード</h3>
<p>市場の動向、資産、戦略ステータスをリアルタイムで監視。</p>
@@ -296,9 +304,31 @@ score = w_{sim}\cdot sim + w_{recency}\cdot recency + w_{returns}\cdot returns\_
- **自動復元**: システム再起動後に実行中の戦略を再開
- **注文キュー**: 注文実行のためのバックグラウンドワーカー
### 7. 技術スタック
### 7. マルチLLMプロバイダーサポート
- **バックエンド**: Python (Flask) + SQLite + Redis(オプション)
QuantDinger は複数の AI プロバイダーをサポートし、自動検出機能を備えています:
| プロバイダー | 特徴 |
|------------|------|
| **OpenRouter** | マルチモデルゲートウェイ(デフォルト)、100+ モデル |
| **OpenAI** | GPT-4o, GPT-4o-mini |
| **Google Gemini** | Gemini 1.5 Flash/Pro |
| **DeepSeek** | DeepSeek Chat(コスパ良好) |
| **xAI Grok** | Grok Beta |
`.env` でお好みのプロバイダーの API キーを設定するだけで、システムが利用可能なプロバイダーを自動検出します。
### 8. ユーザー管理とセキュリティ
- **マルチユーザーサポート**:PostgreSQL ベースのユーザーアカウント、ロールベースの権限管理
- **OAuth ログイン**Google および GitHub OAuth 統合
- **メール認証**:メール認証コードによる登録とパスワードリセット
- **セキュリティ機能**Cloudflare Turnstile キャプチャ、IP/アカウントレート制限
- **デモモード**:公開デモ用の読み取り専用モード
### 9. 技術スタック
- **バックエンド**: Python (Flask) + PostgreSQL + Redis(オプション)
- **フロントエンド**: Vue 2 + Ant Design Vue + KlineCharts/ECharts
- **デプロイ**: Docker Compose
@@ -491,9 +521,14 @@ npm run serve
- **認証**: `SECRET_KEY`, `ADMIN_USER`, `ADMIN_PASSWORD`
- **サーバー**: `PYTHON_API_HOST`, `PYTHON_API_PORT`, `PYTHON_API_DEBUG`
- **AI / LLM**: `OPENROUTER_API_KEY`, `OPENROUTER_MODEL`
- **データベース**: `DATABASE_URL` (PostgreSQL 接続文字列)
- **AI / LLM**: `LLM_PROVIDER` (openrouter/openai/google/deepseek/grok), 各プロバイダー API キー
- **OAuth**: `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`
- **セキュリティ**: `TURNSTILE_SITE_KEY`, `TURNSTILE_SECRET_KEY`, `ENABLE_REGISTRATION`
- **ウェブ検索**: `SEARCH_PROVIDER`, `SEARCH_GOOGLE_*`, `SEARCH_BING_API_KEY`
- **注文執行**: `ORDER_MODE` (maker/market), `MAKER_WAIT_SEC`, `MAKER_OFFSET_BPS`
- **プロキシ (オプション)**: `PROXY_PORT` または `PROXY_URL`
- **バックグラウンドサービス**: `ENABLE_PENDING_ORDER_WORKER`, `ENABLE_PORTFOLIO_MONITOR`
---
@@ -588,6 +623,30 @@ QuantDinger のコードは **Apache License 2.0** で提供されています
---
### 🎓 サポートパートナー
量的金融の教育と研究を推進する学術機関や組織からのサポートを光栄に思います。
<div align="center">
<table>
<tr>
<td align="center" width="50%">
<a href="https://beinvolved.indiana.edu/organization/quantfiniu" target="_blank">
<img src="docs/screenshots/qfs_logo.png" alt="インディアナ大学量的金融学会" width="280" style="border-radius: 8px;">
</a>
<br/><br/>
<strong>量的金融学会 (QFS)</strong><br/>
<small>インディアナ大学ブルーミントン校</small><br/>
<small>次世代の量的金融専門家を育成</small>
</td>
</tr>
</table>
</div>
> 💡 **サポートパートナーになりませんか?** 大学、研究機関、組織との協力を歓迎します。[brokermr810@gmail.com](mailto:brokermr810@gmail.com) または [Telegram](https://t.me/worldinbroker) でお問い合わせください。
---
### 謝辞
QuantDingerは、Flask, Pandas, CCXT, Vue.js, Ant Design Vue, KlineChartsなどの素晴らしいオープンソースプロジェクトの上に成り立っています。
+62 -3
View File
@@ -106,6 +106,14 @@ QuantDinger는 웹에서 금융 정보를 수집하고, 로컬 시장 데이터
## 📸 비주얼 투어
<div align="center">
<h3>🗺️ 시스템 아키텍처 개요</h3>
<p>QuantDinger의 AI 기반 리서치, 백테스팅 및 자동화 거래 기능 전체 개요.</p>
<img src="docs/screenshots/tuopu.png" alt="QuantDinger 시스템 토폴로지" width="100%" style="border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); max-width: 800px;">
</div>
<br/>
<div align="center">
<h3>📊 전문 퀀트 대시보드</h3>
<p>시장 역학, 자산 및 전략 상태에 대한 실시간 모니터링.</p>
@@ -296,9 +304,31 @@ score = w_{sim}\cdot sim + w_{recency}\cdot recency + w_{returns}\cdot returns\_
- **자동 복구**: 시스템 재시작 후 실행 중인 전략 재개
- **주문 대기열**: 주문 실행을 위한 백그라운드 워커
### 7. 기술 스택
### 7. 멀티 LLM 제공자 지원
- **백엔드**: Python (Flask) + SQLite + Redis(옵션)
QuantDinger는 자동 감지 기능을 갖춘 여러 AI 제공자를 지원합니다:
| 제공자 | 특징 |
|--------|------|
| **OpenRouter** | 멀티모델 게이트웨이(기본값), 100+ 모델 |
| **OpenAI** | GPT-4o, GPT-4o-mini |
| **Google Gemini** | Gemini 1.5 Flash/Pro |
| **DeepSeek** | DeepSeek Chat(가성비 좋음) |
| **xAI Grok** | Grok Beta |
`.env`에서 선호하는 제공자의 API 키만 설정하면 시스템이 사용 가능한 제공자를 자동 감지합니다.
### 8. 사용자 관리 및 보안
- **멀티유저 지원**: PostgreSQL 기반 사용자 계정, 역할 기반 권한 관리
- **OAuth 로그인**: Google 및 GitHub OAuth 통합
- **이메일 인증**: 이메일 인증 코드를 통한 등록 및 비밀번호 재설정
- **보안 기능**: Cloudflare Turnstile 캡차, IP/계정 속도 제한
- **데모 모드**: 공개 데모를 위한 읽기 전용 모드
### 9. 기술 스택
- **백엔드**: Python (Flask) + PostgreSQL + Redis(옵션)
- **프론트엔드**: Vue 2 + Ant Design Vue + KlineCharts/ECharts
- **배포**: Docker Compose
@@ -504,9 +534,14 @@ npm run serve
- **인증**: `SECRET_KEY`, `ADMIN_USER`, `ADMIN_PASSWORD`
- **서버**: `PYTHON_API_HOST`, `PYTHON_API_PORT`, `PYTHON_API_DEBUG`
- **AI / LLM**: `OPENROUTER_API_KEY`, `OPENROUTER_MODEL`
- **데이터베이스**: `DATABASE_URL` (PostgreSQL 연결 문자열)
- **AI / LLM**: `LLM_PROVIDER` (openrouter/openai/google/deepseek/grok), 각 제공자 API 키
- **OAuth**: `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`
- **보안**: `TURNSTILE_SITE_KEY`, `TURNSTILE_SECRET_KEY`, `ENABLE_REGISTRATION`
- **웹 검색**: `SEARCH_PROVIDER`, `SEARCH_GOOGLE_*`, `SEARCH_BING_API_KEY`
- **주문 실행**: `ORDER_MODE` (maker/market), `MAKER_WAIT_SEC`, `MAKER_OFFSET_BPS`
- **프록시 (옵션)**: `PROXY_PORT` 또는 `PROXY_URL`
- **백그라운드 서비스**: `ENABLE_PENDING_ORDER_WORKER`, `ENABLE_PORTFOLIO_MONITOR`
---
@@ -601,6 +636,30 @@ QuantDinger의 코드는 **Apache License 2.0**으로 제공됩니다. 다만 **
---
### 🎓 지원 파트너
양적 금융 교육 및 연구를 발전시키는 학술 기관과 조직의 지원을 받게 되어 자랑스럽습니다.
<div align="center">
<table>
<tr>
<td align="center" width="50%">
<a href="https://beinvolved.indiana.edu/organization/quantfiniu" target="_blank">
<img src="docs/screenshots/qfs_logo.png" alt="인디애나 대학교 양적금융학회" width="280" style="border-radius: 8px;">
</a>
<br/><br/>
<strong>양적금융학회 (QFS)</strong><br/>
<small>인디애나 대학교 블루밍턴</small><br/>
<small>차세대 양적 금융 전문가 양성</small>
</td>
</tr>
</table>
</div>
> 💡 **지원 파트너가 되는 데 관심이 있으신가요?** 대학, 연구 기관 및 조직과의 협력을 환영합니다. [brokermr810@gmail.com](mailto:brokermr810@gmail.com) 또는 [Telegram](https://t.me/worldinbroker)으로 연락주세요.
---
### 감사의 말
QuantDinger는 Flask, Pandas, CCXT, Vue.js, Ant Design Vue, KlineCharts 등과 같은 훌륭한 오픈 소스 프로젝트의 어깨 위에 서 있습니다.
+64 -5
View File
@@ -106,6 +106,14 @@ QuantDinger 包含一個內置的**基於 LLM 的多智能體研究系統**,
## 📸 功能預覽
<div align="center">
<h3>🗺️ 系統架構總覽</h3>
<p>QuantDinger AI 驅動的研究、回測和自動化交易功能全景圖。</p>
<img src="docs/screenshots/tuopu.png" alt="QuantDinger 系統拓撲圖" width="100%" style="border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); max-width: 800px;">
</div>
<br/>
<div align="center">
<h3>📊 專業量化儀表盤</h3>
<p>實時監控市場動態、資產狀況和策略狀態。</p>
@@ -296,9 +304,31 @@ score = w_{sim}\cdot sim + w_{recency}\cdot recency + w_{returns}\cdot returns\_
- **自動恢復**:系統重啟後恢復運行中的策略
- **訂單隊列**:後台工作線程用於訂單執行
### 7. 技術棧
### 7. 多LLM提供商支援
- **後端**Python (Flask) + SQLite + Redis(可選)
QuantDinger 支援多個 AI 提供商,具備自動檢測功能:
| 提供商 | 特點 |
|--------|------|
| **OpenRouter** | 多模型閘道(預設),100+ 模型 |
| **OpenAI** | GPT-4o, GPT-4o-mini |
| **Google Gemini** | Gemini 1.5 Flash/Pro |
| **DeepSeek** | DeepSeek Chat(高性價比) |
| **xAI Grok** | Grok Beta |
只需在 `.env` 中配置您首選提供商的 API 金鑰,系統會自動檢測可用提供商。
### 8. 使用者管理與安全
- **多使用者支援**:基於 PostgreSQL 的使用者帳戶,支援基於角色的權限管理
- **OAuth 登入**Google 和 GitHub OAuth 整合
- **郵箱驗證**:透過郵箱驗證碼進行註冊和密碼重設
- **安全功能**Cloudflare Turnstile 人機驗證、IP/帳戶速率限制
- **示範模式**:用於公開示範的唯讀模式
### 9. 技術棧
- **後端**Python (Flask) + PostgreSQL + Redis(可選)
- **前端**Vue 2 + Ant Design Vue + KlineCharts/ECharts
- **部署**Docker Compose
@@ -504,10 +534,15 @@ npm run serve
使用 `backend_api_python/env.example` 作為模板。常用設置包括:
- **認證**: `SECRET_KEY`, `ADMIN_USER`, `ADMIN_PASSWORD`
- **服器**: `PYTHON_API_HOST`, `PYTHON_API_PORT`, `PYTHON_API_DEBUG`
- **AI / LLM**: `OPENROUTER_API_KEY`, `OPENROUTER_MODEL`
- **網絡搜索**: `SEARCH_PROVIDER`, `SEARCH_GOOGLE_*`, `SEARCH_BING_API_KEY`
- **服器**: `PYTHON_API_HOST`, `PYTHON_API_PORT`, `PYTHON_API_DEBUG`
- **資料庫**: `DATABASE_URL` (PostgreSQL 連接字串)
- **AI / LLM**: `LLM_PROVIDER` (openrouter/openai/google/deepseek/grok), 各提供商 API 金鑰
- **OAuth**: `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`
- **安全**: `TURNSTILE_SITE_KEY`, `TURNSTILE_SECRET_KEY`, `ENABLE_REGISTRATION`
- **網路搜尋**: `SEARCH_PROVIDER`, `SEARCH_GOOGLE_*`, `SEARCH_BING_API_KEY`
- **訂單執行**: `ORDER_MODE` (maker/market), `MAKER_WAIT_SEC`, `MAKER_OFFSET_BPS`
- **代理 (可選)**: `PROXY_PORT``PROXY_URL`
- **後台服務**: `ENABLE_PENDING_ORDER_WORKER`, `ENABLE_PORTFOLIO_MONITOR`
---
@@ -602,6 +637,30 @@ QuantDinger 的代碼使用 **Apache License 2.0** 授權。但請注意:**Apa
---
### 🎓 支持夥伴
我們很榮幸獲得推動量化金融教育和研究的學術機構和組織的支持。
<div align="center">
<table>
<tr>
<td align="center" width="50%">
<a href="https://beinvolved.indiana.edu/organization/quantfiniu" target="_blank">
<img src="docs/screenshots/qfs_logo.png" alt="印第安納大學量化金融學會" width="280" style="border-radius: 8px;">
</a>
<br/><br/>
<strong>量化金融學會 (QFS)</strong><br/>
<small>印第安納大學布盧明頓分校</small><br/>
<small>培養下一代量化金融專業人才</small>
</td>
</tr>
</table>
</div>
> 💡 **有興趣成為支持夥伴嗎?** 我們歡迎與大學、研究機構和組織合作。請透過 [brokermr810@gmail.com](mailto:brokermr810@gmail.com) 或 [Telegram](https://t.me/worldinbroker) 聯繫我們。
---
### 致謝
QuantDinger 站在這些偉大的開源項目肩膀之上:Flask, Pandas, CCXT, Vue.js, Ant Design Vue, KlineCharts 等。
@@ -22,6 +22,7 @@ def register_routes(app: Flask):
from app.routes.ibkr import ibkr_bp
from app.routes.mt5 import mt5_bp
from app.routes.user import user_bp
from app.routes.global_market import global_market_bp
app.register_blueprint(health_bp)
app.register_blueprint(auth_bp, url_prefix='/api/auth') # Auth routes
@@ -39,3 +40,4 @@ def register_routes(app: Flask):
app.register_blueprint(portfolio_bp, url_prefix='/api/portfolio')
app.register_blueprint(ibkr_bp, url_prefix='/api/ibkr')
app.register_blueprint(mt5_bp, url_prefix='/api/mt5')
app.register_blueprint(global_market_bp, url_prefix='/api/global-market')
+6 -1
View File
@@ -379,9 +379,14 @@ def login_with_code():
(user['id'],)
)
db.commit()
affected = cur.rowcount
cur.close()
if affected == 0:
logger.error(f"Failed to update last_login_at: no rows affected for user_id={user['id']}")
else:
logger.info(f"Updated last_login_at for user_id={user['id']}")
except Exception as e:
logger.warning(f"Failed to update last_login_at: {e}")
logger.error(f"Failed to update last_login_at for user_id={user.get('id')}: {e}")
# Log login
security.log_security_event('login_via_code', user['id'], ip_address, user_agent)
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -66,7 +66,7 @@ def get_public_config():
'google/gemini-2.5-pro': 'Google: Gemini 2.5 Pro',
'openai/gpt-4o-mini': 'OpenAI: GPT-4o-mini',
'openai/gpt-5-mini': 'OpenAI: GPT-5 Mini',
'openai/gpt-oss-120b': 'OpenAI: gpt-oss-120b',
'openai/gpt-4.1-mini': 'OpenAI: GPT-4.1 Mini',
'deepseek/deepseek-v3.2': 'DeepSeek: DeepSeek V3.2',
'minimax/minimax-m2': 'MiniMax: MiniMax M2',
'anthropic/claude-sonnet-4': 'Anthropic: Claude Sonnet 4',
+19 -11
View File
@@ -816,9 +816,6 @@ def get_strategy_notifications():
user_strategy_ids = [r.get('id') for r in rows if r.get('id')]
cur.close()
if not user_strategy_ids:
return jsonify({'code': 1, 'msg': 'success', 'data': {'items': []}})
where = []
args = []
@@ -830,9 +827,15 @@ def get_strategy_notifications():
else:
return jsonify({'code': 1, 'msg': 'success', 'data': {'items': []}})
else:
placeholders = ",".join(["?"] * len(user_strategy_ids))
where.append(f"strategy_id IN ({placeholders})")
args.extend(user_strategy_ids)
if user_strategy_ids:
placeholders = ",".join(["?"] * len(user_strategy_ids))
where.append(f"(strategy_id IN ({placeholders}) OR (strategy_id IS NULL AND user_id = ?))")
args.extend(user_strategy_ids)
args.append(user_id)
else:
# Only portfolio monitor notifications (strategy_id is NULL)
where.append("strategy_id IS NULL AND user_id = ?")
args.append(user_id)
if since_id:
where.append("id > ?")
@@ -889,15 +892,18 @@ def mark_notification_read():
if not notification_id:
return jsonify({'code': 0, 'msg': 'Missing id'}), 400
# Only update notifications for user's strategies
# Update notifications for user's strategies OR portfolio monitor notifications
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"""
UPDATE qd_strategy_notifications SET is_read = 1
WHERE id = ? AND strategy_id IN (SELECT id FROM qd_strategies_trading WHERE user_id = ?)
WHERE id = ? AND (
strategy_id IN (SELECT id FROM qd_strategies_trading WHERE user_id = ?)
OR (strategy_id IS NULL AND user_id = ?)
)
""",
(int(notification_id), user_id)
(int(notification_id), user_id, user_id)
)
db.commit()
cur.close()
@@ -920,8 +926,9 @@ def mark_all_notifications_read():
"""
UPDATE qd_strategy_notifications SET is_read = 1
WHERE strategy_id IN (SELECT id FROM qd_strategies_trading WHERE user_id = ?)
OR (strategy_id IS NULL AND user_id = ?)
""",
(user_id,)
(user_id, user_id)
)
db.commit()
cur.close()
@@ -944,8 +951,9 @@ def clear_notifications():
"""
DELETE FROM qd_strategy_notifications
WHERE strategy_id IN (SELECT id FROM qd_strategies_trading WHERE user_id = ?)
OR (strategy_id IS NULL AND user_id = ?)
""",
(user_id,)
(user_id, user_id)
)
db.commit()
cur.close()
+3 -2
View File
@@ -387,8 +387,9 @@ class LLMService:
logger.error(f"{p.value} API HTTP error ({current_model}): {error_detail}")
last_error = str(e)
# Check for payment/quota errors
if e.response and e.response.status_code in (402, 429):
# Check for recoverable errors - try fallback model
# 402: Payment required, 403: Forbidden (invalid key), 404: Model not found, 429: Rate limit
if e.response and e.response.status_code in (402, 403, 404, 429):
logger.warning(f"{p.value} returned {e.response.status_code} for model {current_model}; trying fallback...")
continue
@@ -768,8 +768,9 @@ def _send_monitor_notification(
cur.close()
elif ch == 'telegram':
chat_id = targets.get('telegram', '')
token_override = targets.get('telegram_bot_token', '')
if chat_id:
notifier._notify_telegram(chat_id=chat_id, text=f"<b>{error_title}</b>\n\n{error_msg}", parse_mode="HTML")
notifier._notify_telegram(chat_id=chat_id, text=f"<b>{error_title}</b>\n\n{error_msg}", token_override=token_override, parse_mode="HTML")
elif ch == 'email':
to_email = targets.get('email', '')
if to_email:
@@ -815,11 +816,13 @@ def _send_monitor_notification(
elif ch == 'telegram':
chat_id = targets.get('telegram', '')
token_override = targets.get('telegram_bot_token', '')
if chat_id:
# Use Telegram-optimized format
notifier._notify_telegram(
chat_id=chat_id,
text=telegram_report,
token_override=token_override,
parse_mode="HTML"
)
@@ -1099,8 +1102,9 @@ def _check_position_alerts():
cur.close()
elif ch == 'telegram':
chat_id = targets.get('telegram', '')
token_override = targets.get('telegram_bot_token', '')
if chat_id:
notifier._notify_telegram(chat_id=chat_id, text=alert_message, parse_mode="HTML")
notifier._notify_telegram(chat_id=chat_id, text=alert_message, token_override=token_override, parse_mode="HTML")
elif ch == 'email':
to_email = targets.get('email', '')
if to_email:
+177 -12
View File
@@ -1,19 +1,24 @@
"""
Search service.
Integrates Google Custom Search (CSE) and Bing Search API.
Integrates Google Custom Search (CSE), Bing Search API, and DuckDuckGo (free fallback).
Configuration is provided via environment variables (see env.example) through config_loader.
"""
import requests
import json
import time
from typing import List, Dict, Any, Optional
from app.utils.logger import get_logger
from app.utils.config_loader import load_addon_config
logger = get_logger(__name__)
# Track Google API quota status
_google_quota_exhausted = False
_google_quota_reset_time = 0
class SearchService:
"""Search service."""
"""Search service with automatic fallback."""
def __init__(self):
self._config = {}
@@ -28,7 +33,7 @@ class SearchService:
def search(self, query: str, num_results: int = None, date_restrict: str = None) -> List[Dict[str, Any]]:
"""
Execute a web search.
Execute a web search with automatic fallback.
Args:
query: Search query
@@ -38,18 +43,45 @@ class SearchService:
Returns:
List of search results
"""
global _google_quota_exhausted, _google_quota_reset_time
# 重新加载配置以支持热更新
self._load_config()
limit = num_results if num_results else self.max_results
# Check if Google quota has reset (after midnight UTC typically)
if _google_quota_exhausted and time.time() > _google_quota_reset_time:
_google_quota_exhausted = False
logger.info("Google API quota reset, re-enabling Google search")
results = []
if self.provider == 'bing':
return self._search_bing(query, limit)
results = self._search_bing(query, limit)
elif self.provider == 'duckduckgo':
results = self._search_duckduckgo(query, limit)
else:
return self._search_google(query, limit, date_restrict)
# Google with fallback
if not _google_quota_exhausted:
results = self._search_google(query, limit, date_restrict)
# If Google failed or returned empty, try fallbacks
if not results:
logger.info("Google search failed or empty, trying fallback search engines...")
# Try Bing first if configured
results = self._search_bing(query, limit)
# If Bing also failed, try DuckDuckGo (free, no API key needed)
if not results:
results = self._search_duckduckgo(query, limit)
return results
def _search_google(self, query: str, num_results: int, date_restrict: str = None) -> List[Dict[str, Any]]:
"""Google Custom Search (CSE)."""
global _google_quota_exhausted, _google_quota_reset_time
api_key = self._config.get('google', {}).get('api_key')
cx = self._config.get('google', {}).get('cx')
@@ -71,17 +103,23 @@ class SearchService:
params['dateRestrict'] = date_restrict
try:
# logger.info(f"正在调用 Google Search API: q={query}, params={params}")
response = requests.get(url, params=params, timeout=10)
# Check for quota exceeded (429)
if response.status_code == 429:
logger.warning("Google Search API quota exceeded (429). Switching to fallback search engines.")
_google_quota_exhausted = True
# Set reset time to next day midnight UTC
import datetime
tomorrow = datetime.datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) + datetime.timedelta(days=1)
_google_quota_reset_time = tomorrow.timestamp()
return []
response.raise_for_status()
data = response.json()
# logger.info(f"Google Search 原始响应: {json.dumps(data, ensure_ascii=False)}") # 打印全部字符
# logger.info(f"Google Search 原始响应: {json.dumps(data, ensure_ascii=False)[:500]}...") # 打印前500字符避免日志过大
results = []
if 'items' in data:
# logger.info(f"Google Search 返回了 {len(data['items'])} 条结果")
for item in data['items']:
logger.debug(f"Search Item: {item.get('title')} - {item.get('link')}")
results.append({
@@ -96,10 +134,20 @@ class SearchService:
return results
except requests.exceptions.HTTPError as e:
if hasattr(e, 'response') and e.response is not None and e.response.status_code == 429:
logger.warning("Google Search API quota exceeded. Switching to fallback.")
_google_quota_exhausted = True
import datetime
tomorrow = datetime.datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) + datetime.timedelta(days=1)
_google_quota_reset_time = tomorrow.timestamp()
else:
logger.error(f"Google search failed: {e}")
if hasattr(e, 'response') and e.response is not None:
logger.error(f"Response: {e.response.text}")
return []
except Exception as e:
logger.error(f"Google search failed: {e}")
if 'response' in locals():
logger.error(f"Response: {response.text}")
return []
def _search_bing(self, query: str, num_results: int) -> List[Dict[str, Any]]:
@@ -140,3 +188,120 @@ class SearchService:
logger.error(f"Bing search failed: {e}")
return []
def _search_duckduckgo(self, query: str, num_results: int) -> List[Dict[str, Any]]:
"""
DuckDuckGo search (free, no API key required).
Uses the DuckDuckGo HTML search endpoint.
"""
try:
# Use DuckDuckGo Instant Answer API
url = "https://api.duckduckgo.com/"
params = {
'q': query,
'format': 'json',
'no_html': 1,
'skip_disambig': 1
}
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
results = []
# Get results from RelatedTopics
related_topics = data.get('RelatedTopics', [])
for topic in related_topics[:num_results]:
if isinstance(topic, dict):
if 'FirstURL' in topic:
results.append({
'title': topic.get('Text', '')[:100],
'link': topic.get('FirstURL', ''),
'snippet': topic.get('Text', ''),
'source': 'DuckDuckGo',
'published': ''
})
# Handle nested topics
elif 'Topics' in topic:
for sub_topic in topic['Topics']:
if len(results) >= num_results:
break
if 'FirstURL' in sub_topic:
results.append({
'title': sub_topic.get('Text', '')[:100],
'link': sub_topic.get('FirstURL', ''),
'snippet': sub_topic.get('Text', ''),
'source': 'DuckDuckGo',
'published': ''
})
# Also check AbstractURL and AbstractText
if data.get('AbstractURL') and len(results) < num_results:
results.insert(0, {
'title': data.get('Heading', query),
'link': data.get('AbstractURL', ''),
'snippet': data.get('AbstractText', ''),
'source': 'DuckDuckGo',
'published': ''
})
# If no results from Instant Answer, try HTML scraping as fallback
if not results:
results = self._search_duckduckgo_html(query, num_results)
if results:
logger.info(f"DuckDuckGo search returned {len(results)} results")
return results[:num_results]
except Exception as e:
logger.error(f"DuckDuckGo search failed: {e}")
# Try HTML fallback
return self._search_duckduckgo_html(query, num_results)
def _search_duckduckgo_html(self, query: str, num_results: int) -> List[Dict[str, Any]]:
"""
DuckDuckGo HTML search fallback.
Scrapes the lite HTML version for better results.
"""
try:
url = "https://lite.duckduckgo.com/lite/"
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
data = {'q': query}
response = requests.post(url, headers=headers, data=data, timeout=10)
response.raise_for_status()
results = []
# Simple HTML parsing without BeautifulSoup
html = response.text
# Find all result links (they have class="result-link")
import re
# Pattern to find result entries
link_pattern = r'<a[^>]*class="result-link"[^>]*href="([^"]*)"[^>]*>([^<]*)</a>'
snippet_pattern = r'<td[^>]*class="result-snippet"[^>]*>([^<]*)</td>'
links = re.findall(link_pattern, html)
snippets = re.findall(snippet_pattern, html)
for i, (link, title) in enumerate(links[:num_results]):
snippet = snippets[i] if i < len(snippets) else ''
if link and title:
results.append({
'title': title.strip(),
'link': link,
'snippet': snippet.strip(),
'source': 'DuckDuckGo',
'published': ''
})
return results
except Exception as e:
logger.error(f"DuckDuckGo HTML search failed: {e}")
return []
@@ -52,29 +52,48 @@ class TradingExecutor:
self._ensure_db_columns()
def _ensure_db_columns(self):
"""确保必要的数据库字段存在"""
"""确保必要的数据库字段存在(支持 SQLite 和 PostgreSQL"""
try:
db_type = os.getenv('DB_TYPE', 'sqlite').lower()
with get_db_connection() as db:
cursor = db.cursor()
col_names = set()
# SQLite 兼容:使用 PRAGMA table_info 检查列是否存在
try:
cursor.execute("PRAGMA table_info(qd_strategy_positions)")
cols = cursor.fetchall() or []
col_names = {c.get('name') for c in cols if isinstance(c, dict)}
except Exception:
col_names = set()
if db_type == 'postgresql':
# PostgreSQL: 使用 information_schema 查询列
try:
cursor.execute("""
SELECT column_name FROM information_schema.columns
WHERE table_name = 'qd_strategy_positions'
""")
cols = cursor.fetchall() or []
col_names = {c.get('column_name') or c.get('COLUMN_NAME') for c in cols if isinstance(c, dict)}
except Exception:
col_names = set()
else:
# SQLite: 使用 PRAGMA table_info
try:
cursor.execute("PRAGMA table_info(qd_strategy_positions)")
cols = cursor.fetchall() or []
col_names = {c.get('name') for c in cols if isinstance(c, dict)}
except Exception:
col_names = set()
if 'highest_price' not in col_names:
logger.info("Adding highest_price column to qd_strategy_positions (SQLite)...")
# SQLite 不支持 AFTER 子句,类型用 REAL 即可
cursor.execute("ALTER TABLE qd_strategy_positions ADD COLUMN highest_price REAL DEFAULT 0")
logger.info(f"Adding highest_price column to qd_strategy_positions ({db_type})...")
if db_type == 'postgresql':
cursor.execute("ALTER TABLE qd_strategy_positions ADD COLUMN IF NOT EXISTS highest_price DOUBLE PRECISION DEFAULT 0")
else:
cursor.execute("ALTER TABLE qd_strategy_positions ADD COLUMN highest_price REAL DEFAULT 0")
db.commit()
logger.info("highest_price column added")
if 'lowest_price' not in col_names:
logger.info("Adding lowest_price column to qd_strategy_positions (SQLite)...")
cursor.execute("ALTER TABLE qd_strategy_positions ADD COLUMN lowest_price REAL DEFAULT 0")
logger.info(f"Adding lowest_price column to qd_strategy_positions ({db_type})...")
if db_type == 'postgresql':
cursor.execute("ALTER TABLE qd_strategy_positions ADD COLUMN IF NOT EXISTS lowest_price DOUBLE PRECISION DEFAULT 0")
else:
cursor.execute("ALTER TABLE qd_strategy_positions ADD COLUMN lowest_price REAL DEFAULT 0")
db.commit()
logger.info("lowest_price column added")
@@ -170,9 +170,14 @@ class UserService:
(user['id'],)
)
db.commit()
affected = cur.rowcount
cur.close()
if affected == 0:
logger.error(f"Failed to update last_login_at: no rows affected for user_id={user['id']}")
else:
logger.info(f"Updated last_login_at for user_id={user['id']}")
except Exception as e:
logger.warning(f"Failed to update last_login_at: {e}")
logger.error(f"Failed to update last_login_at for user_id={user.get('id')}: {e}")
# Remove password_hash from return value
user.pop('password_hash', None)
@@ -1,16 +0,0 @@
-- Migration: Add notification_settings column to qd_users table
-- Run this if you see error: column "notification_settings" does not exist
-- Add notification_settings column (if not exists)
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'qd_users' AND column_name = 'notification_settings'
) THEN
ALTER TABLE qd_users ADD COLUMN notification_settings TEXT DEFAULT '';
RAISE NOTICE 'Column notification_settings added to qd_users';
ELSE
RAISE NOTICE 'Column notification_settings already exists';
END IF;
END $$;
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+79
View File
@@ -0,0 +1,79 @@
/**
* Global Market Dashboard API
*/
import request from '@/utils/request'
const BASE_URL = '/api/global-market'
/**
* Get global market overview (indices, forex, crypto, commodities)
* Includes geo coordinates for world map display
*/
export function getMarketOverview () {
return request({
url: `${BASE_URL}/overview`,
method: 'get'
})
}
/**
* Get market heatmap data (crypto, stock sectors, forex)
*/
export function getMarketHeatmap () {
return request({
url: `${BASE_URL}/heatmap`,
method: 'get'
})
}
/**
* Get financial news - separated by language (cn/en)
* @param {string} lang - Language filter: 'cn', 'en', or 'all' (default)
*/
export function getMarketNews (lang = 'all') {
return request({
url: `${BASE_URL}/news`,
method: 'get',
params: { lang }
})
}
/**
* Get economic calendar with impact indicators
*/
export function getEconomicCalendar () {
return request({
url: `${BASE_URL}/calendar`,
method: 'get'
})
}
/**
* Get market sentiment (Fear & Greed Index, VIX)
*/
export function getMarketSentiment () {
return request({
url: `${BASE_URL}/sentiment`,
method: 'get'
})
}
/**
* Get trading opportunities based on technical analysis
*/
export function getTradingOpportunities () {
return request({
url: `${BASE_URL}/opportunities`,
method: 'get'
})
}
/**
* Force refresh all market data (clears cache)
*/
export function refreshMarketData () {
return request({
url: `${BASE_URL}/refresh`,
method: 'post'
})
}
+14 -7
View File
@@ -16,6 +16,13 @@ export const asyncRouterMap = [
component: () => import('@/views/dashboard'),
meta: { title: 'menu.dashboard', keepAlive: true, icon: 'dashboard', permission: ['dashboard'] }
},
// 全球金融面板
{
path: '/global-market',
name: 'GlobalMarket',
component: () => import('@/views/global-market'),
meta: { title: 'menu.dashboard.globalMarket', keepAlive: true, icon: 'global', permission: ['dashboard'] }
},
// AI 分析
{
path: '/ai-analysis/:pageNo([1-9]\\d*)?',
@@ -44,13 +51,6 @@ export const asyncRouterMap = [
component: () => import('@/views/portfolio'),
meta: { title: 'menu.dashboard.portfolio', keepAlive: true, icon: 'fund', permission: ['dashboard'] }
},
// 指标社区(keepAlive disabled intentionally for iframe page)
{
path: '/indicator-community',
name: 'IndicatorCommunity',
component: () => import('@/views/indicator-community'),
meta: { title: 'menu.dashboard.community', keepAlive: false, icon: 'shop', permission: ['dashboard'] }
},
// 系统设置 (admin only)
{
path: '/settings',
@@ -71,6 +71,13 @@ export const asyncRouterMap = [
name: 'Profile',
component: () => import('@/views/profile'),
meta: { title: 'menu.myProfile', keepAlive: false, icon: 'user', permission: ['dashboard'] }
},
// 官方社区(keepAlive disabled intentionally for iframe page
{
path: '/indicator-community',
name: 'IndicatorCommunity',
component: () => import('@/views/indicator-community'),
meta: { title: 'menu.dashboard.community', keepAlive: false, icon: 'shop', permission: ['dashboard'] }
}
// other
+62 -2
View File
@@ -26,9 +26,10 @@ const locale = {
'menu.dashboard': 'Dashboard',
'menu.dashboard.analysis': 'AI Analysis',
'menu.dashboard.indicator': 'Indicator Analysis',
'menu.dashboard.community': 'Indicator Community',
'menu.dashboard.community': 'Official Community',
'menu.dashboard.tradingAssistant': 'Trading Assistant',
'menu.dashboard.portfolio': 'Portfolio',
'menu.dashboard.globalMarket': 'Global Market',
'menu.settings': 'Settings',
'menu.dashboard.aiTradingAssistant': 'AI Trading Assistant',
'menu.dashboard.signalRobot': 'Signal Robot',
@@ -2353,6 +2354,8 @@ const locale = {
'portfolio.form.deselectAll': 'Deselect All',
'portfolio.form.selectedCount': '{count} of {total} selected',
'portfolio.form.pleaseSelectPositions': 'Please select at least one position to monitor',
'portfolio.form.notificationFromProfile': 'Notifications will be sent to addresses configured in your profile',
'portfolio.form.goToProfile': 'Go to settings',
'portfolio.message.loadFailed': 'Failed to load data',
'portfolio.message.saveSuccess': 'Saved successfully',
'portfolio.message.saveFailed': 'Failed to save',
@@ -2581,7 +2584,64 @@ const locale = {
'settings.desc.CREDITS_REFERRAL_BONUS': 'Credits awarded to referrer when someone signs up with their referral code',
'settings.desc.VERIFICATION_CODE_MAX_ATTEMPTS': 'Maximum attempts to verify a code before lockout',
'settings.desc.VERIFICATION_CODE_LOCK_MINUTES': 'Lockout duration after exceeding max attempts',
'settings.desc.RECHARGE_TELEGRAM_URL': 'Telegram customer service URL for recharge inquiries'
'settings.desc.RECHARGE_TELEGRAM_URL': 'Telegram customer service URL for recharge inquiries',
// Global Market
'globalMarket.title': 'Global Market Dashboard',
'globalMarket.lastUpdate': 'Last Update',
'globalMarket.refresh': 'Refresh',
'globalMarket.name': 'Name',
'globalMarket.price': 'Price',
'globalMarket.change': 'Change',
'globalMarket.trend': 'Trend',
'globalMarket.pair': 'Pair',
'globalMarket.unit': 'Unit',
'globalMarket.refreshSuccess': 'Data refreshed successfully',
'globalMarket.refreshError': 'Failed to refresh data',
'globalMarket.fetchError': 'Failed to fetch data',
'globalMarket.loading': 'Loading...',
'globalMarket.fearGreedIndex': 'Fear & Greed Index',
'globalMarket.fearGreedTip': 'Fear & Greed Index measures market sentiment, 0 = Extreme Fear, 100 = Extreme Greed',
'globalMarket.volatilityIndex': 'Volatility Index',
'globalMarket.vixTitle': 'VIX Volatility Index',
'globalMarket.vixTip': 'VIX measures expected market volatility, higher values indicate more market fear',
'globalMarket.extremeFear': 'Extreme Fear',
'globalMarket.fear': 'Fear',
'globalMarket.neutral': 'Neutral',
'globalMarket.greed': 'Greed',
'globalMarket.extremeGreed': 'Extreme Greed',
'globalMarket.marketOverview': 'Market Overview',
'globalMarket.indices': 'Indices',
'globalMarket.forex': 'Forex',
'globalMarket.crypto': 'Crypto',
'globalMarket.commodities': 'Commodities',
'globalMarket.heatmap': 'Market Heatmap',
'globalMarket.cryptoHeatmap': 'Crypto Heatmap',
'globalMarket.sectorHeatmap': 'Sector Heatmap',
'globalMarket.forexHeatmap': 'Forex Heatmap',
'globalMarket.opportunities': 'Trading Opportunities',
'globalMarket.noOpportunities': 'No significant opportunities detected',
'globalMarket.financialNews': 'Financial News',
'globalMarket.noNews': 'No news available',
'globalMarket.economicCalendar': 'Economic Calendar',
'globalMarket.noEvents': 'No events scheduled',
'globalMarket.actual': 'Actual',
'globalMarket.forecast': 'Forecast',
'globalMarket.previous': 'Previous',
'globalMarket.signal.bullish': 'Bullish Momentum',
'globalMarket.signal.bearish': 'Bearish Momentum',
'globalMarket.signal.overbought': 'Overbought Warning',
'globalMarket.signal.oversold': 'Oversold Opportunity',
'globalMarket.worldMap': 'Global Markets Map',
'globalMarket.rising': 'Rising',
'globalMarket.falling': 'Falling',
'globalMarket.bullish': 'Bullish',
'globalMarket.bearish': 'Bearish',
'globalMarket.expectedImpact': 'Expected Impact',
'globalMarket.aboveForecast': 'Above Forecast',
'globalMarket.belowForecast': 'Below Forecast',
'globalMarket.upcomingEvents': 'Upcoming Events',
'globalMarket.releasedEvents': 'Released Data'
}
export default {
+62 -2
View File
@@ -25,10 +25,11 @@ const locale = {
'menu.home': '主页',
'menu.dashboard': '仪表盘',
'menu.dashboard.indicator': '指标分析',
'menu.dashboard.community': '指标社区',
'menu.dashboard.community': '官方社区',
'menu.dashboard.analysis': 'AI 分析',
'menu.dashboard.tradingAssistant': '交易助手',
'menu.dashboard.portfolio': '资产监测',
'menu.dashboard.globalMarket': '全球金融',
'menu.settings': '系统设置',
'menu.dashboard.aiTradingAssistant': 'AI交易助手',
'menu.dashboard.signalRobot': '信号机器人',
@@ -2163,6 +2164,8 @@ const locale = {
'portfolio.form.deselectAll': '全不选',
'portfolio.form.selectedCount': '已选 {count}/{total}',
'portfolio.form.pleaseSelectPositions': '请至少选择一个持仓进行监控',
'portfolio.form.notificationFromProfile': '通知将发送到您在个人中心配置的地址',
'portfolio.form.goToProfile': '前往配置',
'portfolio.message.loadFailed': '加载数据失败',
'portfolio.message.saveSuccess': '保存成功',
'portfolio.message.saveFailed': '保存失败',
@@ -2391,7 +2394,64 @@ const locale = {
'settings.desc.CREDITS_REFERRAL_BONUS': '用户通过邀请链接成功邀请新用户时,邀请人获得的积分奖励',
'settings.desc.VERIFICATION_CODE_MAX_ATTEMPTS': '验证码验证失败的最大尝试次数,超过后将锁定',
'settings.desc.VERIFICATION_CODE_LOCK_MINUTES': '验证码验证失败次数超过限制后的锁定时长(分钟)',
'settings.desc.RECHARGE_TELEGRAM_URL': '用户点击充值时跳转的Telegram客服链接'
'settings.desc.RECHARGE_TELEGRAM_URL': '用户点击充值时跳转的Telegram客服链接',
// Global Market
'globalMarket.title': '全球金融面板',
'globalMarket.lastUpdate': '最后更新',
'globalMarket.refresh': '刷新数据',
'globalMarket.name': '名称',
'globalMarket.price': '价格',
'globalMarket.change': '涨跌',
'globalMarket.trend': '走势',
'globalMarket.pair': '货币对',
'globalMarket.unit': '单位',
'globalMarket.refreshSuccess': '数据刷新成功',
'globalMarket.refreshError': '数据刷新失败',
'globalMarket.fetchError': '获取数据失败',
'globalMarket.loading': '加载中...',
'globalMarket.fearGreedIndex': '恐惧贪婪指数',
'globalMarket.fearGreedTip': '恐惧贪婪指数衡量市场情绪,0表示极度恐惧,100表示极度贪婪',
'globalMarket.volatilityIndex': '波动率指数',
'globalMarket.vixTitle': 'VIX 波动率指数',
'globalMarket.vixTip': 'VIX指数衡量市场预期波动率,数值越高表示市场越恐慌',
'globalMarket.extremeFear': '极度恐惧',
'globalMarket.fear': '恐惧',
'globalMarket.neutral': '中性',
'globalMarket.greed': '贪婪',
'globalMarket.extremeGreed': '极度贪婪',
'globalMarket.marketOverview': '全球市场概览',
'globalMarket.indices': '主要指数',
'globalMarket.forex': '外汇市场',
'globalMarket.crypto': '加密货币',
'globalMarket.commodities': '大宗商品',
'globalMarket.heatmap': '市场热力图',
'globalMarket.cryptoHeatmap': '加密货币热力图',
'globalMarket.sectorHeatmap': '板块热力图',
'globalMarket.forexHeatmap': '外汇热力图',
'globalMarket.opportunities': '交易机会',
'globalMarket.noOpportunities': '暂无明显交易机会',
'globalMarket.financialNews': '财经资讯',
'globalMarket.noNews': '暂无新闻',
'globalMarket.economicCalendar': '财经日历',
'globalMarket.noEvents': '暂无事件',
'globalMarket.actual': '实际',
'globalMarket.forecast': '预期',
'globalMarket.previous': '前值',
'globalMarket.signal.bullish': '看涨趋势',
'globalMarket.signal.bearish': '看跌趋势',
'globalMarket.signal.overbought': '超买警告',
'globalMarket.signal.oversold': '超卖机会',
'globalMarket.worldMap': '全球市场地图',
'globalMarket.rising': '上涨',
'globalMarket.falling': '下跌',
'globalMarket.bullish': '利多',
'globalMarket.bearish': '利空',
'globalMarket.expectedImpact': '预期影响',
'globalMarket.aboveForecast': '高于预期',
'globalMarket.belowForecast': '低于预期',
'globalMarket.upcomingEvents': '即将公布',
'globalMarket.releasedEvents': '已公布数据'
}
export default {
File diff suppressed because it is too large Load Diff
+67 -31
View File
@@ -727,9 +727,9 @@
>
<template #message>
<span>
{{ $t('portfolio.form.notificationFromProfile') || '通知将发送到您在个人中心配置的地址' }}
{{ $t('portfolio.form.notificationFromProfile') }}
<router-link to="/profile" style="margin-left: 8px">
<a-icon type="setting" /> {{ $t('portfolio.form.goToProfile') || '前往配置' }}
<a-icon type="setting" /> {{ $t('portfolio.form.goToProfile') }}
</router-link>
</span>
</template>
@@ -806,9 +806,9 @@
>
<template #message>
<span>
{{ $t('portfolio.form.notificationFromProfile') || '通知将发送到您在个人中心配置的地址' }}
{{ $t('portfolio.form.notificationFromProfile') }}
<router-link to="/profile" style="margin-left: 8px">
<a-icon type="setting" /> {{ $t('portfolio.form.goToProfile') || '前往配置' }}
<a-icon type="setting" /> {{ $t('portfolio.form.goToProfile') }}
</router-link>
</span>
</template>
@@ -836,15 +836,21 @@
:key="pos.id"
class="position-checkbox-item"
>
<a-checkbox :value="pos.id">
<span class="position-checkbox-label">
<a-tag :color="getMarketColor(pos.market)" size="small">{{ pos.market }}</a-tag>
<span class="symbol">{{ pos.symbol }}</span>
<span class="name">{{ pos.name }}</span>
<span :class="['pnl', pos.pnl >= 0 ? 'positive' : 'negative']">
{{ pos.pnl >= 0 ? '+' : '' }}{{ formatNumber(pos.pnl_percent) }}%
</span>
</span>
<a-checkbox :value="pos.id" class="position-checkbox">
<div class="position-checkbox-label">
<div class="position-left">
<a-tag :color="getMarketColor(pos.market)" size="small">{{ pos.market }}</a-tag>
<span class="symbol">{{ pos.symbol }}</span>
</div>
<div class="position-middle">
<span class="name" :title="pos.name">{{ pos.name }}</span>
</div>
<div class="position-right">
<span :class="['pnl', pos.pnl >= 0 ? 'positive' : 'negative']">
{{ pos.pnl >= 0 ? '+' : '' }}{{ formatNumber(pos.pnl_percent) }}%
</span>
</div>
</div>
</a-checkbox>
</div>
</a-checkbox-group>
@@ -2615,34 +2621,64 @@ export default {
}
}
.position-checkbox {
width: 100%;
// Override Ant Design checkbox label width
::v-deep .ant-checkbox + span {
width: calc(100% - 24px);
padding-left: 8px;
padding-right: 0;
}
}
.position-checkbox-label {
display: inline-flex;
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
width: 100%;
.symbol {
font-weight: 600;
color: #262626;
min-width: 60px;
.position-left {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
.symbol {
font-weight: 600;
color: #262626;
min-width: 50px;
}
}
.name {
color: #8c8c8c;
font-size: 12px;
.position-middle {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 120px;
.name {
color: #8c8c8c;
font-size: 12px;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.pnl {
font-weight: 500;
font-size: 12px;
.position-right {
flex-shrink: 0;
margin-left: auto;
&.positive { color: @green; }
&.negative { color: @red; }
.pnl {
font-weight: 500;
font-size: 12px;
white-space: nowrap;
&.positive { color: @green; }
&.negative { color: @red; }
}
}
}
@@ -2676,8 +2712,8 @@ export default {
}
.position-checkbox-label {
.symbol { color: #d1d4dc; }
.name { color: #868993; }
.position-left .symbol { color: #d1d4dc; }
.position-middle .name { color: #868993; }
}
.position-select-actions {
+21 -2
View File
@@ -171,8 +171,12 @@
v-decorator="[item.key, { initialValue: getFieldValue(groupKey, item.key) || item.default }]"
:placeholder="item.default ? `${$t('settings.default')}: ${item.default}` : $t('settings.pleaseSelect')"
>
<a-select-option v-for="opt in item.options" :key="opt" :value="opt">
{{ opt }}
<a-select-option
v-for="opt in getSelectOptions(item.options)"
:key="opt.value"
:value="opt.value"
>
{{ opt.label }}
</a-select-option>
</a-select>
</template>
@@ -249,6 +253,21 @@ export default {
this.loadSettings()
},
methods: {
// schema options
// - string[]: ['openrouter','openai', ...]
// - {value,label}[]: [{value:'openrouter',label:'OpenRouter'}, ...]
getSelectOptions (options) {
const arr = Array.isArray(options) ? options : []
return arr.map(opt => {
if (opt && typeof opt === 'object') {
return {
value: opt.value != null ? String(opt.value) : '',
label: opt.label != null ? String(opt.label) : String(opt.value || '')
}
}
return { value: String(opt), label: String(opt) }
}).filter(o => o.value !== '')
},
async loadSettings () {
this.loading = true
try {