forked from lab/TPM
Compare commits
32 Commits
4e3246e422
...
82d71ca642
| Author | SHA1 | Date |
|---|---|---|
|
|
82d71ca642 | 2 days ago |
|
|
1ad01649f2 | 2 months ago |
|
|
4956bdca5f | 2 months ago |
|
|
e75773733c | 2 months ago |
|
|
efbb22494e | 2 months ago |
|
|
ca52a05f19 | 2 months ago |
|
|
32ef5e1978 | 2 months ago |
|
|
51fdceb163 | 2 months ago |
|
|
d3dd4efc53 | 2 months ago |
|
|
b50b9ee92d | 2 months ago |
|
|
f076054d6a | 2 months ago |
|
|
4d45bca548 | 2 months ago |
|
|
e592b9893e | 2 months ago |
|
|
d43ea4c7b9 | 2 months ago |
|
|
0eabfaa906 | 2 months ago |
|
|
ca66ea71af | 2 months ago |
|
|
9c613cf4b7 | 2 months ago |
|
|
dd3270675a | 2 months ago |
|
|
3193ca1daf | 2 months ago |
|
|
f6f2eb7552 | 2 months ago |
|
|
cbd580bb12 | 2 months ago |
|
|
1c16cb4321 | 2 months ago |
|
|
14407e04d2 | 2 months ago |
|
|
bfe88e4b80 | 2 months ago |
|
|
3290b69d72 | 2 months ago |
|
|
c35d0cd8aa | 3 months ago |
|
|
5a93aa5fb4 | 3 months ago |
|
|
45e9ea7ec2 | 3 months ago |
|
|
fd39ea0287 | 3 months ago |
|
|
086961bed7 | 3 months ago |
|
|
9c6db84e15 | 3 months ago |
|
|
a811f29f84 | 3 months ago |
32 changed files with 3053 additions and 64 deletions
@ -0,0 +1,88 @@ |
|||||||
|
# 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 |
||||||
@ -0,0 +1,2 @@ |
|||||||
|
# Auto detect text files and perform LF normalization |
||||||
|
* text=auto |
||||||
@ -0,0 +1,116 @@ |
|||||||
|
# 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 main:app --preload --workers 28 --timeout 120 |
web: gunicorn -w 2 -b 0.0.0.0:$PORT --timeout 120 --chdir /flask main:app |
||||||
|
|||||||
@ -0,0 +1,74 @@ |
|||||||
|
### 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`。 |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,30 @@ |
|||||||
|
""" |
||||||
|
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, # 最大快取項目數 |
||||||
|
} |
||||||
@ -0,0 +1,60 @@ |
|||||||
|
### 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.28.2 |
requests==2.31 |
||||||
SQLAlchemy==2.0.4 |
SQLAlchemy==2.0.4 |
||||||
yfinance==0.2.22 |
yfinance==0.2.66 |
||||||
tqdm==4.62.3 |
tqdm==4.62.3 |
||||||
schedule==1.2.1 |
schedule==1.2.1 |
||||||
numpy==1.23.5 |
numpy==1.23.5 |
||||||
|
|||||||
@ -0,0 +1,51 @@ |
|||||||
|
#!/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") |
||||||
@ -0,0 +1,259 @@ |
|||||||
|
""" |
||||||
|
市場基準資料模組 |
||||||
|
|
||||||
|
從資料庫取得實際的市場基準資料(台股加權指數、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) |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
[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' |
||||||
@ -0,0 +1,160 @@ |
|||||||
|
""" |
||||||
|
投資建議相關的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) |
||||||
@ -0,0 +1,12 @@ |
|||||||
|
{ |
||||||
|
"$schema": "https://railway.app/railway.schema.json", |
||||||
|
"build": { |
||||||
|
"builder": "DOCKERFILE", |
||||||
|
"dockerfilePath": "Dockerfile" |
||||||
|
}, |
||||||
|
"deploy": { |
||||||
|
"startCommand": "/flask/start.sh", |
||||||
|
"restartPolicyType": "ON_FAILURE", |
||||||
|
"restartPolicyMaxRetries": 10 |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,46 @@ |
|||||||
|
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 |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
#!/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