Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 94 additions & 13 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# ==============================================================================
# 🟢 General / OS / IDE
# ==============================================================================
# Mac OS
.DS_Store

# Windows
Thumbs.db
ehthumbs.db
Desktop.ini

# IDEs (VS Code, JetBrains)
.idea/
.vscode/
*.swp
*.swo

# ==============================================================================
# 🟡 Node.js / Next.js (Frontend)
# ==============================================================================
# Dependencies
node_modules
.pnp
.pnp.js

# Local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# Testing
coverage

# Turbo
# Turbo Repo
.turbo

# Vercel
Expand All @@ -27,12 +38,82 @@ out/
build
dist


# Debug
# Debug Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# Misc
.DS_Store
# ==============================================================================
# 🔵 Python / FastAPI (Backend)
# ==============================================================================
# Byte-compiled / Optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# Distribution / Packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# Virtual Environments (非常重要,不要上傳 venv)
.venv/
venv/
ENV/
env/

# Unit Test / Coverage
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Type Checking
.mypy_cache/
.dmypy.json
dmypy.json

# ==============================================================================
# 🔴 Secrets & Local Data (Privacy Protection)
# ==============================================================================
# Local env files (金鑰與設定)
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.env*.local

# SSL Certificates
*.pem
*.key

# User Uploads (模塊一:交易紀錄)
# 忽略 server 下的 uploads 資料夾,保護用戶隱私
apps/server/uploads/
**/uploads/
47 changes: 45 additions & 2 deletions apps/server/main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,46 @@
from spoonos_server.server.app import app
import os
import shutil
from pathlib import Path
from fastapi import UploadFile, File
from spoonos_server.server.app import app # 保持原本的導入,這是主程式

__all__ = ["app"]
# --- [模塊一核心] 檔案上傳基礎設施 ---

# 1. 設定上傳目錄
# 這會在 apps/server/ 下自動建立一個 'uploads' 資料夾
UPLOAD_DIR = Path(__file__).parent / "uploads"
UPLOAD_DIR.mkdir(exist_ok=True)

@app.post("/api/upload")
async def upload_file(file: UploadFile = File(...)):
"""
[Investment Profiler 專用接口]
接收前端上傳的交易文件 (CSV/Excel),存到伺服器本地,並返回絕對路徑。
"""
try:
# 為了防止檔名重複,你也可以在這裡加上時間戳記 (uuid)
# 目前保持原檔名,方便你測試
safe_filename = os.path.basename(file.filename)
file_location = UPLOAD_DIR / safe_filename

# 2. 將上傳的文件寫入磁碟 (Stream 寫入,節省記憶體)
with open(file_location, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)

# 3. 獲取絕對路徑 (這是 Python Tool 讀取所需的關鍵參數)
absolute_path = str(file_location.resolve())

print(f"✅ [Upload] 文件已保存至: {absolute_path}")

return {
"status": "success",
"file_path": absolute_path, # <--- 前端拿到這個路徑後,要塞進 Prompt 裡
"filename": safe_filename,
"message": "上傳成功!請將 file_path 提供給 Agent 進行分析。"
}
except Exception as e:
print(f"❌ [Upload Error] {str(e)}")
return {"status": "error", "message": str(e)}

# 導出 app 供 uvicorn 啟動
__all__ = ["app"]
7 changes: 7 additions & 0 deletions apps/server/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,12 @@ x402>=0.2.1,<1.0

# Extra dependencies merged from the separate Crypto PowerData requirements.txt
pandas>=1.0.0
openpyxl>=3.1.5

# Crypto PowerData dependencies
ccxt>=4.0.0
numpy>=1.20.0
quantstats>=0.0.81
TA-Lib>=0.4.25
asyncio-throttle>=1.0.0
pydantic-settings>=2.0.0
Expand All @@ -159,5 +161,10 @@ gql>=3.5.0
# Development dependencies
pytest>=7.0.0
pytest-cov>=4.0.0
pytest-asyncio>=0.21.0
bump2version>=1.0.0
pycares>=4.9,<5.0

# FastAPI file upload support
python-multipart>=0.0.9

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Investment Profiler

## Description
This skill analyzes user transaction history (CSV) and psychological questionnaire results to generate a personalized MBTI investment personality profile.

## Tools
- `analyze_investment_personality`: Takes a CSV file string and a questionnaire JSON, returns a comprehensive analysis report.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# 這裡必須跟 tools.py 裡的函數名稱完全一致
from .tools import analyze_user_profile

# 導出工具列表
tools = [analyze_user_profile]
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import pandas as pd
import quantstats as qs
import numpy as np
import os

class PerformanceEngine:
def process_trade_history(self, file_path: str) -> dict:
"""
全能型交易文件讀取器:支持 Excel, 標準 CSV, 以及帶雜訊 Header 的 CSV
"""
df = pd.DataFrame()
try:
# --- 階段 1: 智能讀取 (Smart Ingestion) ---
if file_path.endswith(('.xlsx', '.xls')):
try:
df = pd.read_excel(file_path)
except Exception as e:
return {"error": f"Excel 讀取失敗: {str(e)}"}
else:
# CSV 處理邏輯
encodings = ['utf-8', 'utf-8-sig', 'big5', 'gbk']
success = False

for enc in encodings:
try:
# 策略 A: 先讀第一行看看長什麼樣
# header=None 代表先不要把第一行當標題
preview = pd.read_csv(file_path, encoding=enc, nrows=2, header=None)
first_cell = str(preview.iloc[0, 0])

# 檢測 AssetChangeDetails 格式 (特徵: 第一行開頭是 UID:)
if "UID:" in first_cell or "Company Name" in first_cell:
# 策略 B: 跳過雜訊行 (skiprows=1)
df = pd.read_csv(file_path, encoding=enc, skiprows=1)
else:
# 策略 C: 標準讀取
df = pd.read_csv(file_path, encoding=enc)

if not df.empty:
success = True
break
except:
continue

if not success:
return {"error": "無法識別文件編碼,請嘗試轉存為 UTF-8 CSV"}

# --- 階段 2: 欄位識別 (Column Mapping) ---
if df.empty:
return {"error": "文件為空"}

# 清洗欄位名稱 (去除空格)
df.columns = [str(c).strip() for c in df.columns]

# 定義我們的「字典」,讓程式能看懂不同交易所的語言
col_map = {
'time': ['Time(UTC)', 'Time', 'Date', '開倉時間', 'Created Time', '交易時間', 'datetime'],
'profit': ['Change', 'Profit', 'Realized PNL', '已實現收益', 'Amount', 'PNL', 'Cash Flow']
}

target_cols = {}
for key, candidates in col_map.items():
for col in candidates:
if col in df.columns:
target_cols[key] = col
break

if 'time' not in target_cols or 'profit' not in target_cols:
return {"error": f"欄位識別失敗。檢測到的欄位: {list(df.columns)}"}

# --- 階段 3: 數據計算 (Quant Analysis) ---
# 轉換時間
df['Time'] = pd.to_datetime(df[target_cols['time']])
df.set_index('Time', inplace=True)
df.sort_index(inplace=True)

# 提取損益序列
pnl_series = df[target_cols['profit']]

# 數據清洗:把非數字轉為 0 (例如 'TRANSFER' 這種文字)
pnl_series = pd.to_numeric(pnl_series, errors='coerce').fillna(0)

# 估算本金 (用於計算收益率 %)
# 邏輯:如果總流水很大,假設本金是流水的 10%;否則假設固定 1000U
total_flow = pnl_series.abs().sum()
initial_capital = total_flow * 0.1 if total_flow > 0 else 1000
returns = pnl_series / initial_capital

# 使用 QuantStats
metrics = {}
try:
# 只有當數據點足夠多時才算夏普率
if len(returns) > 5:
metrics = {
"sharpe_ratio": qs.stats.sharpe(returns),
"win_rate": qs.stats.win_rate(returns),
"max_drawdown": qs.stats.max_drawdown(returns),
"profit_factor": qs.stats.profit_factor(returns),
"total_trades": len(df),
"total_pnl_value": pnl_series.sum()
}
else:
metrics = {"total_trades": len(df), "note": "數據過少"}
except:
metrics = {"error": "QuantStats 計算異常", "total_trades": len(df)}

# 轉為 Python 原生類型 (JSON Friendly)
return {k: float(v) if isinstance(v, (np.float64, np.float32)) else v for k, v in metrics.items()}

except Exception as e:
import traceback
traceback.print_exc()
return {"error": f"分析崩潰: {str(e)}"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#算問卷的
import re

class QuestionnaireEngine:
def process_answers(self, answers: dict) -> dict:
"""
對用戶的 MBTI 問卷回答進行計分與分析。
兼容格式:
- {"EI_01": "A"}
- {"EI_01": "A: 選項內容..."}
- {"EI_01": "選項A"}
"""
# 初始化計分板
scores = {"E": 0, "I": 0, "S": 0, "N": 0, "T": 0, "F": 0, "J": 0, "P": 0}

raw_text_list = []

for q_id, answer_text in answers.items():
ans_str = str(answer_text).strip().upper()
raw_text_list.append(f"- {q_id}: {ans_str}")

# --- 核心容錯邏輯 ---
# 只要字串開頭是 B,或是包含 "選項B",就算 B
# 否則默認算 A (因為題目結構是二選一,A通常在前面)
choice = "A"
if ans_str.startswith("B") or "選項B" in ans_str or "(B)" in ans_str:
choice = "B"

# 解析維度 (從 ID "EI_01" 解析出 "EI")
if "_" in q_id:
dim_key = q_id.split("_")[0] # 拿到 "EI", "SN"...

if len(dim_key) == 2:
left_char = dim_key[0] # E, S, T, J
right_char = dim_key[1] # I, N, F, P

if choice == "A":
scores[left_char] += 1
else:
scores[right_char] += 1

# 結算與生成報告 (保持不變)
mbti_result = ""
mbti_result += "E" if scores["E"] >= scores["I"] else "I"
mbti_result += "S" if scores["S"] >= scores["N"] else "N"
mbti_result += "T" if scores["T"] >= scores["F"] else "F"
mbti_result += "J" if scores["J"] >= scores["P"] else "P"

analysis_report = (
f"【問卷計分結果】\n"
f"- E/I: {scores['E']}/{scores['I']}\n"
f"- S/N: {scores['S']}/{scores['N']}\n"
f"- T/F: {scores['T']}/{scores['F']}\n"
f"- J/P: {scores['J']}/{scores['P']}\n"
f"👉 綜合自述類型: {mbti_result}"
)

return {
"mbti_type": mbti_result,
"scores": scores,
"analysis_text": analysis_report,
"raw_text": "\n".join(raw_text_list)
}
Loading