forked from lab/TPM
Compare commits
No commits in common. '82d71ca64205e85eec6f56035058f6940c3aac2e' and '4e3246e4228274e80adc98573ceb1914bf48ff22' have entirely different histories.
82d71ca642
...
4e3246e422
32 changed files with 64 additions and 3053 deletions
@ -1,88 +0,0 @@ |
|||||||
# Git |
|
||||||
.git |
|
||||||
.gitignore |
|
||||||
.gitattributes |
|
||||||
|
|
||||||
# Documentation |
|
||||||
*.md |
|
||||||
README.md |
|
||||||
LICENSE |
|
||||||
CHANGELOG.md |
|
||||||
|
|
||||||
# Development files |
|
||||||
.vscode |
|
||||||
.idea |
|
||||||
.DS_Store |
|
||||||
*.swp |
|
||||||
*.swo |
|
||||||
*~ |
|
||||||
|
|
||||||
# Python |
|
||||||
__pycache__ |
|
||||||
*.pyc |
|
||||||
*.pyo |
|
||||||
*.pyd |
|
||||||
.Python |
|
||||||
*.so |
|
||||||
*.egg |
|
||||||
*.egg-info |
|
||||||
dist |
|
||||||
build |
|
||||||
.pytest_cache |
|
||||||
.coverage |
|
||||||
htmlcov |
|
||||||
|
|
||||||
# Node.js |
|
||||||
node_modules |
|
||||||
npm-debug.log |
|
||||||
yarn-error.log |
|
||||||
package-lock.json |
|
||||||
package.json |
|
||||||
|
|
||||||
# Test files |
|
||||||
test_*.py |
|
||||||
*_test.py |
|
||||||
tests/ |
|
||||||
.pytest_cache/ |
|
||||||
|
|
||||||
# Logs |
|
||||||
*.log |
|
||||||
cleanup_duplicates.log |
|
||||||
sync_missing_stocks.log |
|
||||||
|
|
||||||
# Temporary files |
|
||||||
*.tmp |
|
||||||
*.bak |
|
||||||
*.swp |
|
||||||
tmp/ |
|
||||||
temp/ |
|
||||||
|
|
||||||
# IDE |
|
||||||
.claude/ |
|
||||||
|
|
||||||
# Documentation (all the MD files you created) |
|
||||||
AGENTIC_RAG_DEPLOYMENT.md |
|
||||||
ARCHITECTURE.md |
|
||||||
ARCHITECTURE_CLARIFICATION.md |
|
||||||
CLONE_TO_GITHUB.md |
|
||||||
CONTEXT_PROMPT_ENGINEERING.md |
|
||||||
COT_COMPARISON.md |
|
||||||
COT_IMPLEMENTATION.md |
|
||||||
DEPLOYMENT_GUIDE.md |
|
||||||
DEPLOYMENT_STATUS.md |
|
||||||
DEPLOY_ON_EXISTING_SERVER.md |
|
||||||
DEPLOY_VIA_GITHUB.md |
|
||||||
FINAL_DESIGN_SUMMARY.md |
|
||||||
FIXES_2025-10-19.md |
|
||||||
QUICK_START_V2.md |
|
||||||
TESTING_CHECKLIST.md |
|
||||||
TESTING_QUICK_REFERENCE.md |
|
||||||
TROUBLESHOOTING.md |
|
||||||
V2_PROMPT_UPDATES.md |
|
||||||
|
|
||||||
# Config for other platforms |
|
||||||
render.yaml |
|
||||||
nixpacks.toml |
|
||||||
|
|
||||||
# Status files |
|
||||||
update_status.json |
|
||||||
@ -1,2 +0,0 @@ |
|||||||
# Auto detect text files and perform LF normalization |
|
||||||
* text=auto |
|
||||||
@ -1,116 +0,0 @@ |
|||||||
# LLM 投資建議功能設置指南 |
|
||||||
|
|
||||||
## 功能介紹 |
|
||||||
|
|
||||||
LLM 投資建議功能使用 OpenAI GPT 模型為投資組合提供專業的投資分析和建議,包括: |
|
||||||
- 策略績效評估 |
|
||||||
- 風險分析 |
|
||||||
- 市場適配性評估 |
|
||||||
- 具體的改進建議 |
|
||||||
|
|
||||||
## 設置步驟 |
|
||||||
|
|
||||||
### 1. 獲取 OpenAI API 金鑰 |
|
||||||
|
|
||||||
1. 前往 [OpenAI API](https://platform.openai.com/api-keys) 頁面 |
|
||||||
2. 點擊 "Create new secret key" |
|
||||||
3. 複製生成的 API 金鑰 |
|
||||||
|
|
||||||
### 2. 配置 API 金鑰 |
|
||||||
|
|
||||||
您可以通過以下兩種方式之一設置 API 金鑰: |
|
||||||
|
|
||||||
#### 方法一:環境變數(推薦) |
|
||||||
```bash |
|
||||||
export OPENAI_API_KEY="your-api-key-here" |
|
||||||
``` |
|
||||||
|
|
||||||
#### 方法二:修改配置檔案 |
|
||||||
編輯 `config_openai.py` 檔案: |
|
||||||
```python |
|
||||||
OPENAI_CONFIG = { |
|
||||||
'api_key': 'your-actual-api-key-here', |
|
||||||
# ... 其他配置 |
|
||||||
} |
|
||||||
``` |
|
||||||
|
|
||||||
### 3. 安裝依賴 |
|
||||||
|
|
||||||
```bash |
|
||||||
pip install -r requirements.txt |
|
||||||
``` |
|
||||||
|
|
||||||
### 4. 啟動服務 |
|
||||||
|
|
||||||
```bash |
|
||||||
docker compose up -d flask |
|
||||||
``` |
|
||||||
|
|
||||||
### 5. 測試功能 |
|
||||||
|
|
||||||
1. 訪問任意策略詳情頁面 |
|
||||||
2. 查看「🤖 LLM 投資建議」區塊 |
|
||||||
3. 系統會自動生成 AI 投資建議 |
|
||||||
|
|
||||||
## 配置選項 |
|
||||||
|
|
||||||
### OpenAI 模型選擇 |
|
||||||
|
|
||||||
在 `config_openai.py` 中可以調整: |
|
||||||
|
|
||||||
```python |
|
||||||
OPENAI_CONFIG = { |
|
||||||
'model': 'gpt-4', # 選擇 gpt-3.5-turbo 可降低成本 |
|
||||||
'max_tokens': 2000, # 最大回應長度 |
|
||||||
'temperature': 0.7, # 創意程度 (0-1) |
|
||||||
} |
|
||||||
``` |
|
||||||
|
|
||||||
### 快取設定 |
|
||||||
|
|
||||||
```python |
|
||||||
CACHE_CONFIG = { |
|
||||||
'enabled': True, # 啟用快取 |
|
||||||
'ttl': 3600, # 快取時間(秒) |
|
||||||
'max_size': 100, # 最大快取項目數 |
|
||||||
} |
|
||||||
``` |
|
||||||
|
|
||||||
## 費用估算 |
|
||||||
|
|
||||||
- **GPT-4**: 約 $0.03/1K tokens |
|
||||||
- **GPT-3.5-turbo**: 約 $0.002/1K tokens |
|
||||||
|
|
||||||
每個投資建議約使用 1000-2000 tokens,建議選擇合適的模型以控制成本。 |
|
||||||
|
|
||||||
## 故障排除 |
|
||||||
|
|
||||||
### 常見問題 |
|
||||||
|
|
||||||
1. **API 金鑰錯誤** |
|
||||||
- 檢查 API 金鑰是否正確設置 |
|
||||||
- 確認金鑰沒有額外的空格 |
|
||||||
|
|
||||||
2. **連接到服務失敗** |
|
||||||
- 檢查 Flask 服務是否正常運行 |
|
||||||
- 確認防火牆設定允許內部通訊 |
|
||||||
|
|
||||||
3. **生成建議失敗** |
|
||||||
- 查看後端日誌確認錯誤原因 |
|
||||||
- 檢查 OpenAI API 額度是否充足 |
|
||||||
|
|
||||||
### 檢查日誌 |
|
||||||
|
|
||||||
```bash |
|
||||||
docker compose logs flask |
|
||||||
``` |
|
||||||
|
|
||||||
## 進階配置 |
|
||||||
|
|
||||||
如需自訂 Prompt 模板,請編輯 `prompts/investment_advice.py` 檔案中的模板函數。 |
|
||||||
|
|
||||||
## 安全性注意事項 |
|
||||||
|
|
||||||
- 請勿將 API 金鑰提交到版本控制系統 |
|
||||||
- 考慮使用環境變數而非硬編碼 |
|
||||||
- 定期輪換 API 金鑰 |
|
||||||
@ -1 +1 @@ |
|||||||
web: gunicorn -w 2 -b 0.0.0.0:$PORT --timeout 120 --chdir /flask main:app |
web: gunicorn main:app --preload --workers 28 --timeout 120 |
||||||
@ -1,74 +0,0 @@ |
|||||||
### TPM – 投資組合大擂台 |
|
||||||
|
|
||||||
#### 1) 內容概要 |
|
||||||
- Flask + PostgreSQL + Redis 的投資策略平台,內含回測、圖表與 LLM 投資建議。 |
|
||||||
- 前端採 Jinja SSR + Bootstrap;LLM 透過 `llm_service.py` 封裝,可切換 OpenAI/OpenRouter/Mock。 |
|
||||||
|
|
||||||
#### 2) 技術棧(現況,請勿任意更換) |
|
||||||
- Backend: Flask 2.2, psycopg2, Flask-Caching, Plotly |
|
||||||
- DB/Cache: PostgreSQL, Redis |
|
||||||
- Frontend: Jinja, Bootstrap 5, Bootstrap Icons(避免新增其他 CSS 框架) |
|
||||||
- LLM: OpenAI SDK(可接 OpenRouter),支援 Mock |
|
||||||
- Container: Docker, docker-compose |
|
||||||
|
|
||||||
#### 3) 目錄重點 |
|
||||||
- `main.py`: 路由與頁面組裝;禁止塞商業邏輯 |
|
||||||
- `llm_service.py`: LLM 供應商、Prompt、重試、快取 |
|
||||||
- `portfolio_builder.py`: 投組演算法 |
|
||||||
- `templates/`: Jinja 模板(僅結構與少量初始化) |
|
||||||
- `static/js/{components,pages}/`: 前端 JS 組件與頁面邏輯 |
|
||||||
- `sql_script/`: DB 初始化 |
|
||||||
- `data_init/`: 資料初始化與更新腳本 |
|
||||||
|
|
||||||
#### 4) 快速開始 |
|
||||||
1) 準備 `.env`(置於專案根目錄) |
|
||||||
``` |
|
||||||
LLM_PROVIDER=openrouter |
|
||||||
OPENROUTER_API_KEY=your_key |
|
||||||
OPENROUTER_MODEL=google/gemini-2.0-flash-exp:free |
|
||||||
LLM_TIMEOUT=60 |
|
||||||
LLM_MAX_TOKENS=1500 |
|
||||||
LLM_TEMPERATURE=0.6 |
|
||||||
MOCK_LLM=false |
|
||||||
``` |
|
||||||
2) 啟動容器 |
|
||||||
``` |
|
||||||
docker compose up -d --build --force-recreate |
|
||||||
``` |
|
||||||
3) 服務連線 |
|
||||||
- Web: http://localhost:8007 |
|
||||||
- Postgres: container 內部名稱 `db` |
|
||||||
- Redis: container 內部名稱 `redis` |
|
||||||
|
|
||||||
#### 5) 開發規範(避免技術債) |
|
||||||
- 不動既有架構、Docker 設定與不相關功能。 |
|
||||||
- 僅在確定「已使用」時才把套件寫入 `requirements.txt`;未用到的要移除。 |
|
||||||
- 完成一個環節、測試通過才 commit;不要在同一個 commit 混雜多項變更。 |
|
||||||
- 前端:避免大型 inline JS;新邏輯放 `static/js/pages/*.js` 或 `static/js/components/*.js`。 |
|
||||||
- 後端:商業邏輯放在服務檔案(如 `llm_service.py`),`main.py` 保持輕薄。 |
|
||||||
- LLM:僅經 `get_llm_advisor().generate_advice(strategy_id, strategy_dict)`;參數由 `.env` 控制。 |
|
||||||
|
|
||||||
#### 6) 測試 |
|
||||||
- 後端:可使用離線腳本(`MOCK_LLM=true`)進行測試。 |
|
||||||
- 任何變更建議附最小可重現測試或腳本(避免手動點擊測試)。 |
|
||||||
|
|
||||||
#### 7) 常見問題 |
|
||||||
- 500 + LLM 失敗:確認 `.env` 已注入容器;離線測試可先設 `MOCK_LLM=true` |
|
||||||
- DB 連線錯誤:程式內部連線 host 應為 `db` |
|
||||||
- KeyError(TSLA/AAPL):確認 `data_init` 成功寫入對應市場資料 |
|
||||||
|
|
||||||
#### 8) Git / PR 規範 |
|
||||||
- 分支命名:`feature/<area>-<short>`、`fix/<area>-<short>` |
|
||||||
- PR 標題:`[TPM] <Title>`;內容包含「動機 / 變更 / 風險 / 測試方式」 |
|
||||||
- 小步提交、保持向後相容;前端改動請將 JS 抽出至 `static/js/` |
|
||||||
|
|
||||||
#### 9) 設計原則 |
|
||||||
- 關注點分離:路由薄、服務厚;模板薄、JS 組件化 |
|
||||||
- 僅在既有層擴展功能;避免跨層耦合 |
|
||||||
- 可回退:大改以 feature flag 包裝,保持 simple 模式可用 |
|
||||||
|
|
||||||
> 更詳細的協作規範請見 `cursor.md`。 |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,30 +0,0 @@ |
|||||||
""" |
|
||||||
OpenAI API 配置 |
|
||||||
|
|
||||||
請設置您的OpenAI API金鑰 |
|
||||||
""" |
|
||||||
|
|
||||||
import os |
|
||||||
|
|
||||||
# OpenAI API 配置 |
|
||||||
OPENAI_CONFIG = { |
|
||||||
'api_key': os.getenv('OPENAI_API_KEY', 'your-api-key-here'), |
|
||||||
'model': 'gpt-4', # 可選擇 gpt-3.5-turbo 以降低成本 |
|
||||||
'max_tokens': 2000, |
|
||||||
'temperature': 0.7, |
|
||||||
'timeout': 30, # 請求超時時間(秒) |
|
||||||
} |
|
||||||
|
|
||||||
# API 調用限制 |
|
||||||
RATE_LIMITS = { |
|
||||||
'requests_per_minute': 60, # 每分鐘最大請求數 |
|
||||||
'max_retries': 3, # 最大重試次數 |
|
||||||
'retry_delay': 2, # 重試間隔(秒) |
|
||||||
} |
|
||||||
|
|
||||||
# 快取設定 |
|
||||||
CACHE_CONFIG = { |
|
||||||
'enabled': True, |
|
||||||
'ttl': 3600, # 快取時間(秒) |
|
||||||
'max_size': 100, # 最大快取項目數 |
|
||||||
} |
|
||||||
@ -1,60 +0,0 @@ |
|||||||
### Purpose |
|
||||||
- 定義 AI 與人類協作的開發規範,避免分散、重複與難以維護的代碼。 |
|
||||||
- 嚴格限制可修改範圍與方式,降低技術債。 |
|
||||||
|
|
||||||
### Architecture Guardrails |
|
||||||
- Backend: 維持 Flask(單應用)+ PostgreSQL + Redis + Plotly SSR。不得引入新後端框架(如 FastAPI、Django)。 |
|
||||||
- Frontend: 維持 Jinja SSR + Bootstrap 5。避免新增其他 CSS 框架;僅在必要時使用 jQuery(若頁面已依賴)。 |
|
||||||
- LLM: 只能透過 `llm_service.py` 的封裝介面呼叫;前端僅呼叫既有 API。 |
|
||||||
- DB: 目前採 `psycopg2` 直連 SQL。若要導入 ORM,需先提 RFC 並分層重構(Repository/Service 分離)。 |
|
||||||
- Realtime: 若需 Agentic 進度,優先 SSE;如需 WebSocket,需提 RFC。禁止同時混用兩者。 |
|
||||||
|
|
||||||
### Files Ownership |
|
||||||
- `main.py`: 僅負責路由、參數與 Response 序列化;禁止寫演算法或外部 API 邏輯。 |
|
||||||
- `llm_service.py`: 唯一 LLM 入口;新增供應商、Prompt、重試、快取皆在此擴展。 |
|
||||||
- `portfolio_builder.py`: 僅放投組相關演算法。 |
|
||||||
- `templates/*.html`: 僅做結構與插值;避免大型 inline JS;頁面邏輯放 `static/js/pages/*.js`。 |
|
||||||
- `static/js/components/*`: 前端可複用組件;不得引用非必要的全域物件。 |
|
||||||
- `docker-compose.yml`: 僅調整現有服務設定;新增服務需提 RFC。 |
|
||||||
- `requirements.txt`: 僅加入「已使用」的最小依賴;未使用須移除。 |
|
||||||
|
|
||||||
### Frontend Rules |
|
||||||
- 禁止在模板中新增大型 inline JS。新邏輯放 `static/js/pages/<page>.js` 或 `static/js/components/<comp>.js`,模板只保留初始化。 |
|
||||||
- 樣式統一 Bootstrap 5;避免再引入其他 CSS 框架;圖標統一 Bootstrap Icons。 |
|
||||||
- Markdown:優先在後端使用 `markdown` 轉 HTML;不要寫自製 Markdown 解析器。 |
|
||||||
|
|
||||||
### LLM Rules |
|
||||||
- 僅用 `get_llm_advisor().generate_advice(strategy_id, strategy_dict)` 封裝介面。 |
|
||||||
- Provider/Model/Token/Timeout 由 `.env` 或 `config.py` 管理;程式內不得硬編碼。 |
|
||||||
- 慢任務需提供 `mode=simple|agentic|auto`,預設 simple。 |
|
||||||
- Mock 測試:支援 `MOCK_LLM=true` 的離線路徑。 |
|
||||||
|
|
||||||
### Testing & Quality |
|
||||||
- 後端:新增/修改路由與服務需附基本單元測試或離線測試腳本(可用 `MOCK_LLM`)。 |
|
||||||
- 前端:邏輯拆為可測的純函數;避免深層 DOM 操作耦合。 |
|
||||||
- 風格:PEP8;命名語義化;函式不超過 ~100 行;避免深巢狀。 |
|
||||||
- 禁留 TODO/死碼;必要時加簡短註解說明「為何存在」。 |
|
||||||
|
|
||||||
### Git / PR Workflow |
|
||||||
- 分支:`feature/<area>-<short>`、`fix/<area>-<short>`。 |
|
||||||
- PR 標題:`[TPM] <Title>`;內容包含「動機 / 變更 / 雙向風險 / 測試」。 |
|
||||||
- 提交頻率:完成一個環節且測試通過才 commit;不要在同一 commit 混雜多項變更。 |
|
||||||
- 禁止無關變更(例如排版清理與邏輯修改混在一起)。 |
|
||||||
|
|
||||||
### Change Checklist (DoD) |
|
||||||
- 有對應文件或內嵌註解簡述「為何」。 |
|
||||||
- 測試或腳本可重跑;離線可跑(如 LLM mock)。 |
|
||||||
- API 相容,不破壞現有 flow。 |
|
||||||
- 不新增全域副作用或跨層耦合。 |
|
||||||
|
|
||||||
### Out-of-Scope(需先提 RFC) |
|
||||||
- 更換 Web 框架或引入大型基礎設施(如微服務、消息隊列)。 |
|
||||||
- 引入新前端框架或 CSS 系統。 |
|
||||||
- 重構資料層成 ORM。 |
|
||||||
|
|
||||||
--- |
|
||||||
本文件供 AI/協作工具與開發者遵循,若有必要變更,請先起草 RFC 並在 PR 中說明動機與回滾方案。 |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,8 +1,8 @@ |
|||||||
pandas==1.5.3 |
pandas==1.5.3 |
||||||
psycopg2==2.9.5 |
psycopg2==2.9.5 |
||||||
requests==2.31 |
requests==2.28.2 |
||||||
SQLAlchemy==2.0.4 |
SQLAlchemy==2.0.4 |
||||||
yfinance==0.2.66 |
yfinance==0.2.22 |
||||||
tqdm==4.62.3 |
tqdm==4.62.3 |
||||||
schedule==1.2.1 |
schedule==1.2.1 |
||||||
numpy==1.23.5 |
numpy==1.23.5 |
||||||
|
|||||||
@ -1,51 +0,0 @@ |
|||||||
#!/usr/bin/env python3 |
|
||||||
""" |
|
||||||
One-time script to initialize Railway database from local machine. |
|
||||||
Run this ONCE after deploying to Railway. |
|
||||||
|
|
||||||
Usage: |
|
||||||
export DATABASE_URL="postgresql://user:password@host:port/database" |
|
||||||
python3 init_railway_db.py |
|
||||||
""" |
|
||||||
import os |
|
||||||
import sys |
|
||||||
|
|
||||||
# Check DATABASE_URL |
|
||||||
DATABASE_URL = os.environ.get('DATABASE_URL') |
|
||||||
if not DATABASE_URL: |
|
||||||
print("❌ ERROR: DATABASE_URL environment variable not set") |
|
||||||
print("\nGet your DATABASE_URL from Railway:") |
|
||||||
print("1. Go to Railway dashboard") |
|
||||||
print("2. Click on PostgreSQL service") |
|
||||||
print("3. Go to 'Connect' tab") |
|
||||||
print("4. Copy the 'Postgres Connection URL'") |
|
||||||
print("\nThen run:") |
|
||||||
print(' export DATABASE_URL="postgresql://..."') |
|
||||||
print(" python3 init_railway_db.py") |
|
||||||
sys.exit(1) |
|
||||||
|
|
||||||
print(f"✅ DATABASE_URL is set") |
|
||||||
print(f"📊 Connecting to: {DATABASE_URL.split('@')[1] if '@' in DATABASE_URL else '***'}") |
|
||||||
|
|
||||||
# Import after checking DATABASE_URL |
|
||||||
os.chdir('/Users/chiuyiting/Documents/GitHub/TPM') |
|
||||||
sys.path.insert(0, '/Users/chiuyiting/Documents/GitHub/TPM/data_init') |
|
||||||
|
|
||||||
# Now run the initialization scripts |
|
||||||
print("\n" + "="*60) |
|
||||||
print("🚀 Starting Railway Database Initialization") |
|
||||||
print("="*60 + "\n") |
|
||||||
|
|
||||||
print("📋 Step 1: Initializing Taiwan stock data (this will create schema)...") |
|
||||||
import data_init.data_init_tw_v0 |
|
||||||
print("\n✅ Taiwan data initialized\n") |
|
||||||
|
|
||||||
print("📋 Step 2: Initializing US stock data...") |
|
||||||
import data_init.data_init_us_v0 |
|
||||||
print("\n✅ US data initialized\n") |
|
||||||
|
|
||||||
print("\n" + "="*60) |
|
||||||
print("🎉 Railway Database Initialization Complete!") |
|
||||||
print("="*60) |
|
||||||
print("\nYour Railway app should now work properly.") |
|
||||||
print("Check https://nthutpm.up.railway.app") |
|
||||||
@ -1,259 +0,0 @@ |
|||||||
""" |
|
||||||
市場基準資料模組 |
|
||||||
|
|
||||||
從資料庫取得實際的市場基準資料(台股加權指數、S&P 500) |
|
||||||
用於 Context Engineering 的市場環境背景 |
|
||||||
""" |
|
||||||
|
|
||||||
import psycopg2 |
|
||||||
import pandas as pd |
|
||||||
import numpy as np |
|
||||||
from datetime import datetime, timedelta |
|
||||||
from typing import Dict, Any, Optional |
|
||||||
import logging |
|
||||||
|
|
||||||
logger = logging.getLogger(__name__) |
|
||||||
|
|
||||||
# 從 config 匯入資料庫設定 |
|
||||||
try: |
|
||||||
from config import SQL_CONFIG |
|
||||||
except ImportError: |
|
||||||
# Fallback 設定 |
|
||||||
SQL_CONFIG = { |
|
||||||
"database": "portfolio_platform", |
|
||||||
"user": "postgres", |
|
||||||
"host": "db", |
|
||||||
"port": "5432", |
|
||||||
"password": "thiispassword1qaz!QAZ" |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
class MarketBenchmark: |
|
||||||
"""市場基準資料類別""" |
|
||||||
|
|
||||||
def __init__(self): |
|
||||||
"""初始化市場基準資料""" |
|
||||||
self.cache = {} |
|
||||||
self.cache_timeout = 3600 # 1小時快取 |
|
||||||
self.cache_time = {} |
|
||||||
|
|
||||||
def _is_cache_valid(self, key: str) -> bool: |
|
||||||
"""檢查快取是否有效""" |
|
||||||
if key not in self.cache_time: |
|
||||||
return False |
|
||||||
return (datetime.now().timestamp() - self.cache_time[key]) < self.cache_timeout |
|
||||||
|
|
||||||
def get_market_context(self, tw: bool = True, force_refresh: bool = False) -> Dict[str, Any]: |
|
||||||
""" |
|
||||||
獲取市場環境背景(從資料庫計算實際數據) |
|
||||||
|
|
||||||
Args: |
|
||||||
tw: True=台灣市場,False=美國市場 |
|
||||||
force_refresh: 強制重新計算(不使用快取) |
|
||||||
|
|
||||||
Returns: |
|
||||||
市場環境背景字典 |
|
||||||
""" |
|
||||||
cache_key = f"market_{'tw' if tw else 'us'}" |
|
||||||
|
|
||||||
# 檢查快取 |
|
||||||
if not force_refresh and self._is_cache_valid(cache_key): |
|
||||||
logger.info(f"Using cached market context for {'TW' if tw else 'US'}") |
|
||||||
return self.cache[cache_key] |
|
||||||
|
|
||||||
try: |
|
||||||
if tw: |
|
||||||
context = self._get_tw_market_context() |
|
||||||
else: |
|
||||||
context = self._get_us_market_context() |
|
||||||
|
|
||||||
# 更新快取 |
|
||||||
self.cache[cache_key] = context |
|
||||||
self.cache_time[cache_key] = datetime.now().timestamp() |
|
||||||
|
|
||||||
logger.info(f"Calculated market context for {'TW' if tw else 'US'}: {context}") |
|
||||||
return context |
|
||||||
|
|
||||||
except Exception as e: |
|
||||||
logger.error(f"Error getting market context: {e}") |
|
||||||
# Fallback 到靜態資料 |
|
||||||
return self._get_fallback_context(tw) |
|
||||||
|
|
||||||
def _get_tw_market_context(self) -> Dict[str, Any]: |
|
||||||
"""取得台灣市場基準資料(從資料庫計算)""" |
|
||||||
conn = psycopg2.connect(**SQL_CONFIG) |
|
||||||
|
|
||||||
try: |
|
||||||
# 取得 0050.TW 近期資料 |
|
||||||
query = """ |
|
||||||
SELECT date, price |
|
||||||
FROM stock_price_tw |
|
||||||
WHERE ticker = '0050.TW' |
|
||||||
ORDER BY date DESC |
|
||||||
LIMIT 1260 -- 約5年交易日 |
|
||||||
""" |
|
||||||
|
|
||||||
df = pd.read_sql(query, conn) |
|
||||||
df = df.sort_values('date') |
|
||||||
df['return'] = df['price'].pct_change() |
|
||||||
|
|
||||||
# 計算各項指標 |
|
||||||
latest_price = df['price'].iloc[-1] |
|
||||||
year_start_idx = max(0, len(df) - 252) # 今年開始(約252交易日) |
|
||||||
ytd_return = (latest_price / df['price'].iloc[year_start_idx]) - 1 |
|
||||||
|
|
||||||
# 近5年年化報酬 |
|
||||||
total_return = (latest_price / df['price'].iloc[0]) - 1 |
|
||||||
years = len(df) / 252 |
|
||||||
avg_5y_return = (1 + total_return) ** (1 / years) - 1 |
|
||||||
|
|
||||||
# 年化波動率 |
|
||||||
volatility = df['return'].std() * np.sqrt(252) |
|
||||||
|
|
||||||
# 市場情緒判斷(基於近期趨勢) |
|
||||||
recent_returns = df['return'].iloc[-63:].sum() # 最近3個月 |
|
||||||
if recent_returns > 0.05: |
|
||||||
sentiment = "bull" |
|
||||||
elif recent_returns < -0.05: |
|
||||||
sentiment = "bear" |
|
||||||
else: |
|
||||||
sentiment = "neutral" |
|
||||||
|
|
||||||
return { |
|
||||||
"market_name": "台灣加權指數(0050.TW)", |
|
||||||
"ytd_return": float(ytd_return), |
|
||||||
"avg_5y_return": float(avg_5y_return), |
|
||||||
"current_price": float(latest_price), |
|
||||||
"volatility": float(volatility), |
|
||||||
"sentiment": sentiment, |
|
||||||
"last_update": df['date'].iloc[-1].strftime("%Y-%m-%d"), |
|
||||||
"data_points": len(df) |
|
||||||
} |
|
||||||
|
|
||||||
finally: |
|
||||||
conn.close() |
|
||||||
|
|
||||||
def _get_us_market_context(self) -> Dict[str, Any]: |
|
||||||
"""取得美國市場基準資料(從資料庫計算)""" |
|
||||||
conn = psycopg2.connect(**SQL_CONFIG) |
|
||||||
|
|
||||||
try: |
|
||||||
# 取得 SPY 近期資料 |
|
||||||
query = """ |
|
||||||
SELECT date, price |
|
||||||
FROM stock_price |
|
||||||
WHERE ticker = 'SPY' |
|
||||||
ORDER BY date DESC |
|
||||||
LIMIT 1260 -- 約5年交易日 |
|
||||||
""" |
|
||||||
|
|
||||||
df = pd.read_sql(query, conn) |
|
||||||
df = df.sort_values('date') |
|
||||||
df['return'] = df['price'].pct_change() |
|
||||||
|
|
||||||
# 計算各項指標 |
|
||||||
latest_price = df['price'].iloc[-1] |
|
||||||
year_start_idx = max(0, len(df) - 252) |
|
||||||
ytd_return = (latest_price / df['price'].iloc[year_start_idx]) - 1 |
|
||||||
|
|
||||||
# 近5年年化報酬 |
|
||||||
total_return = (latest_price / df['price'].iloc[0]) - 1 |
|
||||||
years = len(df) / 252 |
|
||||||
avg_5y_return = (1 + total_return) ** (1 / years) - 1 |
|
||||||
|
|
||||||
# 年化波動率 |
|
||||||
volatility = df['return'].std() * np.sqrt(252) |
|
||||||
|
|
||||||
# 市場情緒判斷 |
|
||||||
recent_returns = df['return'].iloc[-63:].sum() |
|
||||||
if recent_returns > 0.05: |
|
||||||
sentiment = "bull" |
|
||||||
elif recent_returns < -0.05: |
|
||||||
sentiment = "bear" |
|
||||||
else: |
|
||||||
sentiment = "neutral" |
|
||||||
|
|
||||||
return { |
|
||||||
"market_name": "S&P 500(SPY)", |
|
||||||
"ytd_return": float(ytd_return), |
|
||||||
"avg_5y_return": float(avg_5y_return), |
|
||||||
"current_price": float(latest_price), |
|
||||||
"volatility": float(volatility), |
|
||||||
"sentiment": sentiment, |
|
||||||
"last_update": df['date'].iloc[-1].strftime("%Y-%m-%d"), |
|
||||||
"data_points": len(df) |
|
||||||
} |
|
||||||
|
|
||||||
finally: |
|
||||||
conn.close() |
|
||||||
|
|
||||||
def _get_fallback_context(self, tw: bool) -> Dict[str, Any]: |
|
||||||
"""Fallback 靜態資料(資料庫查詢失敗時使用)""" |
|
||||||
if tw: |
|
||||||
return { |
|
||||||
"market_name": "台灣加權指數", |
|
||||||
"ytd_return": 0.18, |
|
||||||
"avg_5y_return": 0.09, |
|
||||||
"volatility": 0.15, |
|
||||||
"sentiment": "neutral", |
|
||||||
"last_update": "static", |
|
||||||
"is_fallback": True |
|
||||||
} |
|
||||||
else: |
|
||||||
return { |
|
||||||
"market_name": "S&P 500", |
|
||||||
"ytd_return": 0.22, |
|
||||||
"avg_5y_return": 0.12, |
|
||||||
"volatility": 0.14, |
|
||||||
"sentiment": "bull", |
|
||||||
"last_update": "static", |
|
||||||
"is_fallback": True |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
# 單例模式 |
|
||||||
_market_benchmark_instance = None |
|
||||||
|
|
||||||
def get_market_benchmark() -> MarketBenchmark: |
|
||||||
"""獲取市場基準實例(單例)""" |
|
||||||
global _market_benchmark_instance |
|
||||||
if _market_benchmark_instance is None: |
|
||||||
_market_benchmark_instance = MarketBenchmark() |
|
||||||
return _market_benchmark_instance |
|
||||||
|
|
||||||
|
|
||||||
# 便利函數(向後兼容) |
|
||||||
def get_market_context(tw: bool = True) -> Dict[str, Any]: |
|
||||||
""" |
|
||||||
獲取市場環境背景 |
|
||||||
|
|
||||||
此函數與 prompts/investment_advice_v2.py 中的函數簽名相同 |
|
||||||
可直接替換使用 |
|
||||||
""" |
|
||||||
benchmark = get_market_benchmark() |
|
||||||
return benchmark.get_market_context(tw) |
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": |
|
||||||
# 測試腳本 |
|
||||||
import json |
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO) |
|
||||||
|
|
||||||
print("="*80) |
|
||||||
print("測試市場基準資料模組") |
|
||||||
print("="*80) |
|
||||||
|
|
||||||
# 測試台灣市場 |
|
||||||
print("\n台灣市場基準:") |
|
||||||
tw_context = get_market_context(tw=True) |
|
||||||
print(json.dumps(tw_context, indent=2, ensure_ascii=False)) |
|
||||||
|
|
||||||
# 測試美國市場 |
|
||||||
print("\n美國市場基準:") |
|
||||||
us_context = get_market_context(tw=False) |
|
||||||
print(json.dumps(us_context, indent=2, ensure_ascii=False)) |
|
||||||
|
|
||||||
print("\n" + "="*80) |
|
||||||
print("測試完成!") |
|
||||||
print("="*80) |
|
||||||
@ -1,8 +0,0 @@ |
|||||||
[phases.setup] |
|
||||||
nixPkgs = ['python39'] |
|
||||||
|
|
||||||
[phases.install] |
|
||||||
cmds = ['pip install -r requirements.txt'] |
|
||||||
|
|
||||||
[start] |
|
||||||
cmd = 'gunicorn -w 2 -b 0.0.0.0:$PORT --timeout 120 main:app' |
|
||||||
@ -1,160 +0,0 @@ |
|||||||
""" |
|
||||||
投資建議相關的Prompt模板 |
|
||||||
|
|
||||||
包含: |
|
||||||
- 基本投資建議模板 |
|
||||||
- 進階分析模板 |
|
||||||
- 不同市場環境的模板 |
|
||||||
""" |
|
||||||
|
|
||||||
from typing import Dict, Any |
|
||||||
|
|
||||||
|
|
||||||
def get_basic_investment_prompt(strategy_data: Dict[str, Any]) -> str: |
|
||||||
"""基本投資建議Prompt""" |
|
||||||
return f""" |
|
||||||
請基於以下投資策略數據,提供專業的投資建議: |
|
||||||
|
|
||||||
策略概況: |
|
||||||
- 年化報酬率:{strategy_data.get('annual_ret', 0):.2%} |
|
||||||
- 年化波動率:{strategy_data.get('vol', 0):.2%} |
|
||||||
- 夏普比率:{strategy_data.get('annual_sr', 0):.2f} |
|
||||||
- 最大回落:{strategy_data.get('mdd', 0):.2%} |
|
||||||
- 投資目標:{strategy_data.get('role', 'N/A')} |
|
||||||
|
|
||||||
投資組合包含:{', '.join(strategy_data.get('assets', []))} |
|
||||||
|
|
||||||
請提供: |
|
||||||
1. 整體表現評估 |
|
||||||
2. 風險收益分析 |
|
||||||
3. 改進建議 |
|
||||||
""" |
|
||||||
|
|
||||||
|
|
||||||
def get_comprehensive_analysis_prompt(strategy_data: Dict[str, Any]) -> str: |
|
||||||
"""全面分析Prompt - 現代化結構""" |
|
||||||
return f""" |
|
||||||
作為專業投資顧問,請對以下投資策略進行全面分析並提供建議: |
|
||||||
|
|
||||||
【策略基本資訊】 |
|
||||||
- 策略名稱:{strategy_data.get('name', 'N/A')} |
|
||||||
- 投資目標:{strategy_data.get('role', 'N/A')} |
|
||||||
- 市場類型:{'台灣市場' if strategy_data.get('tw', True) else '美國市場'} |
|
||||||
|
|
||||||
【關鍵績效指標】 |
|
||||||
- 年化報酬率:{strategy_data.get('annual_ret', 0):.2%} |
|
||||||
- 年化波動率:{strategy_data.get('vol', 0):.2%} |
|
||||||
- 年化夏普比率:{strategy_data.get('annual_sr', 0):.2f} |
|
||||||
- 最大回落(MDD):{strategy_data.get('mdd', 0):.2%} |
|
||||||
- Alpha值:{strategy_data.get('alpha', 0):.4f} |
|
||||||
- Beta值:{strategy_data.get('beta', 0):.4f} |
|
||||||
- VaR (95%):{strategy_data.get('var10', 0):.2%} |
|
||||||
- R-squared:{strategy_data.get('r2', 0):.4f} |
|
||||||
|
|
||||||
【投資組合配置】 |
|
||||||
持有資產:{', '.join(strategy_data.get('assets', []))} |
|
||||||
權重配置:{strategy_data.get('weight', {}).get('columns', [])} |
|
||||||
|
|
||||||
【分析要求】 |
|
||||||
請從以下面向提供詳細分析和建議: |
|
||||||
|
|
||||||
1. **績效評估**: |
|
||||||
- 與市場基準比較(台灣加權指數或S&P 500) |
|
||||||
- 歷史表現趨勢分析 |
|
||||||
- 相對表現評價 |
|
||||||
|
|
||||||
2. **風險分析**: |
|
||||||
- 整體風險水平評估 |
|
||||||
- 主要風險來源識別 |
|
||||||
- 風險調整後報酬分析 |
|
||||||
|
|
||||||
3. **市場適配性**: |
|
||||||
- 當前市場環境適配程度 |
|
||||||
- 經濟週期位置評估 |
|
||||||
- 市場波動性影響分析 |
|
||||||
|
|
||||||
4. **投資建議**: |
|
||||||
- 資產配置優化建議 |
|
||||||
- 風險管理改進方案 |
|
||||||
- 定期調整和再平衡建議 |
|
||||||
|
|
||||||
5. **未來展望**: |
|
||||||
- 未來3-6個月投資展望 |
|
||||||
- 潛在風險預警 |
|
||||||
- 操作建議和時機點 |
|
||||||
|
|
||||||
--- |
|
||||||
請用繁體中文回答,確保報告結構完整、語氣專業且易於理解。使用和格式化讓內容更生動。 |
|
||||||
""" |
|
||||||
|
|
||||||
|
|
||||||
def get_risk_focused_prompt(strategy_data: Dict[str, Any]) -> str: |
|
||||||
"""風險導向分析Prompt""" |
|
||||||
return f""" |
|
||||||
請重點分析以下投資策略的風險特性,並提供風險管理建議: |
|
||||||
|
|
||||||
【風險指標】 |
|
||||||
- 年化波動率:{strategy_data.get('vol', 0):.2%} |
|
||||||
- 最大回落:{strategy_data.get('mdd', 0):.2%} |
|
||||||
- Beta值:{strategy_data.get('beta', 0):.4f} |
|
||||||
- VaR (10%):{strategy_data.get('var10', 0):.2%} |
|
||||||
- 夏普比率:{strategy_data.get('annual_sr', 0):.2f} |
|
||||||
|
|
||||||
【風險分析要求】 |
|
||||||
1. 評估當前風險水平(低/中/高) |
|
||||||
2. 識別主要風險來源 |
|
||||||
3. 分析風險與報酬的配比 |
|
||||||
4. 提供風險降低策略 |
|
||||||
5. 建議風險監控指標 |
|
||||||
|
|
||||||
請提供具體、可操作的風險管理建議。 |
|
||||||
""" |
|
||||||
|
|
||||||
|
|
||||||
def get_market_context_prompt(strategy_data: Dict[str, Any], market_condition: str = "normal") -> str: |
|
||||||
"""市場環境特定Prompt""" |
|
||||||
market_contexts = { |
|
||||||
"bull": "目前市場處於牛市環境", |
|
||||||
"bear": "目前市場處於熊市環境", |
|
||||||
"volatile": "目前市場波動劇烈", |
|
||||||
"normal": "目前市場環境正常" |
|
||||||
} |
|
||||||
|
|
||||||
context = market_contexts.get(market_condition, market_contexts["normal"]) |
|
||||||
|
|
||||||
return f""" |
|
||||||
{context},請針對以下投資策略提供相應的投資建議: |
|
||||||
|
|
||||||
【策略資訊】 |
|
||||||
- 年化報酬率:{strategy_data.get('annual_ret', 0):.2%} |
|
||||||
- 年化波動率:{strategy_data.get('vol', 0):.2%} |
|
||||||
- 投資目標:{strategy_data.get('role', 'N/A')} |
|
||||||
|
|
||||||
【市場適配建議】 |
|
||||||
根據當前市場環境,請分析: |
|
||||||
1. 該策略在當前環境下的適配程度 |
|
||||||
2. 可能的調整建議 |
|
||||||
3. 風險控制措施 |
|
||||||
4. 時機選擇建議 |
|
||||||
|
|
||||||
請提供針對當前市場環境的具體操作建議。 |
|
||||||
""" |
|
||||||
|
|
||||||
|
|
||||||
def build_custom_prompt(strategy_data: Dict[str, Any], analysis_type: str, **kwargs) -> str: |
|
||||||
"""自訂Prompt生成器 |
|
||||||
|
|
||||||
Args: |
|
||||||
strategy_data: 策略資料 |
|
||||||
analysis_type: 分析類型 ('basic', 'comprehensive', 'risk', 'market') |
|
||||||
**kwargs: 額外參數 |
|
||||||
""" |
|
||||||
templates = { |
|
||||||
'basic': get_basic_investment_prompt, |
|
||||||
'comprehensive': get_comprehensive_analysis_prompt, |
|
||||||
'risk': get_risk_focused_prompt, |
|
||||||
'market': lambda data: get_market_context_prompt(data, kwargs.get('condition', 'normal')) |
|
||||||
} |
|
||||||
|
|
||||||
template_func = templates.get(analysis_type, get_basic_investment_prompt) |
|
||||||
return template_func(strategy_data) |
|
||||||
@ -1,12 +0,0 @@ |
|||||||
{ |
|
||||||
"$schema": "https://railway.app/railway.schema.json", |
|
||||||
"build": { |
|
||||||
"builder": "DOCKERFILE", |
|
||||||
"dockerfilePath": "Dockerfile" |
|
||||||
}, |
|
||||||
"deploy": { |
|
||||||
"startCommand": "/flask/start.sh", |
|
||||||
"restartPolicyType": "ON_FAILURE", |
|
||||||
"restartPolicyMaxRetries": 10 |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,46 +0,0 @@ |
|||||||
services: |
|
||||||
# Flask Web Service |
|
||||||
- type: web |
|
||||||
name: tpm-flask |
|
||||||
env: docker |
|
||||||
dockerfilePath: ./Dockerfile |
|
||||||
plan: free |
|
||||||
healthCheckPath: / |
|
||||||
envVars: |
|
||||||
- key: DATABASE_URL |
|
||||||
fromDatabase: |
|
||||||
name: tpm-db |
|
||||||
property: connectionString |
|
||||||
- key: REDIS_URL |
|
||||||
fromService: |
|
||||||
name: tpm-redis |
|
||||||
type: redis |
|
||||||
property: connectionString |
|
||||||
- key: OPENROUTER_API_KEY |
|
||||||
sync: false |
|
||||||
- key: OPENROUTER_MODEL |
|
||||||
value: google/gemini-2.0-flash-exp:free |
|
||||||
- key: MOCK_LLM |
|
||||||
value: false |
|
||||||
- key: LLM_TIMEOUT |
|
||||||
value: 60 |
|
||||||
- key: LLM_MAX_TOKENS |
|
||||||
value: 1500 |
|
||||||
- key: LLM_TEMPERATURE |
|
||||||
value: 0.6 |
|
||||||
|
|
||||||
# PostgreSQL Database |
|
||||||
- type: pserv |
|
||||||
name: tpm-db |
|
||||||
env: docker |
|
||||||
plan: free |
|
||||||
disk: |
|
||||||
name: postgres-data |
|
||||||
mountPath: /var/lib/postgresql/data |
|
||||||
sizeGB: 1 |
|
||||||
|
|
||||||
# Redis Cache |
|
||||||
- type: redis |
|
||||||
name: tpm-redis |
|
||||||
plan: free |
|
||||||
maxmemoryPolicy: allkeys-lru |
|
||||||
@ -1,10 +0,0 @@ |
|||||||
#!/bin/bash |
|
||||||
cd /flask |
|
||||||
echo "Working directory: $(pwd)" |
|
||||||
echo "Checking assets files:" |
|
||||||
ls -la assets*.json 2>&1 || echo "No assets files found" |
|
||||||
|
|
||||||
# Use Railway's PORT env var if available, otherwise default to 8000 |
|
||||||
PORT=${PORT:-8000} |
|
||||||
echo "Starting gunicorn on port $PORT..." |
|
||||||
exec gunicorn -w 2 -b 0.0.0.0:$PORT --timeout 120 main:app |
|
||||||
Loading…
Reference in new issue