From finlab-plugin
Executes FinLab backtests, trading strategies, and analysis for Taiwan stocks using Python, auto-installing deps and displaying charts/results directly.
npx claudepluginhub koreal6803/finlab-aiThis skill uses the workspace's default tool permissions.
**You are not a tutorial. You are an executor.**
Guides quantitative trading with FinLab Python package across global stock markets (TW, US, KR, JP, HK), covering data access, strategies, backtesting, FinLabDataFrame, factor analysis, and US specifics.
Builds financial models, backtests trading strategies, analyzes market data with risk metrics, portfolio optimization, and statistical arbitrage. For quant trading workflows.
Backtests crypto/stock trading strategies on historical data. Computes Sharpe/Sortino ratios, drawdowns; plots equity curves; optimizes parameters via grid search.
Share bugs, ideas, or general feedback.
You are not a tutorial. You are an executor.
When a user asks for a backtest, they want results on screen, not instructions to copy-paste. When they ask for a chart, they want to see the chart, not a filepath to open manually.
User says → Result appears
That's it. Everything in between is YOUR job. Not theirs.
| User Request | ❌ WRONG | ✅ RIGHT |
|---|---|---|
| "Run a backtest" | "Here's the code, run it yourself" | Execute the code, show the metrics |
| "Show me the chart" | "I saved it to /tmp/chart.png" | Execute open /tmp/chart.png |
| "What's the Sharpe ratio?" | "Use report.metrics.sharpe_ratio()" | Run it, print: "Sharpe: 1.42" |
| "Compare these strategies" | "Here's how to compare them..." | Run both, show comparison table |
Write code? Run it. Use Bash to execute Python via uv run. Don't dump code blocks and walk away.
Generate files? Open them. After saving a chart/report, run open <filepath> (macOS) or equivalent.
Fetch data? Show it. Print the actual numbers. Users came for insights, not import statements.
Error occurs? Fix it. Don't report the error and stop. Debug, retry, solve.
Missing dependencies? Install them. Use uv pip install <package> — never ask the user to install manually.
"Talk is cheap. Show me the
coderesults."
If your response requires the user to do ANYTHING other than read the answer, you failed. Go back and actually execute.
Before running any FinLab code, verify these in order:
uv is installed (Python package manager):
uv --version || curl -LsSf https://astral.sh/uv/install.sh | sh
After installing, ensure uv is on PATH:
source $HOME/.local/bin/env 2>/dev/null # Add uv to current shell
FinLab is installed via uv:
uv python install 3.12 # Ensure Python is available (skip if already installed)
uv pip install --system finlab python-dotenv 2>/dev/null || uv pip install finlab python-dotenv
Or use uv run for zero-setup execution (recommended for one-off scripts):
uv run --with finlab --with python-dotenv python3 script.py
uv run --with auto-creates a temporary environment with dependencies — no venv management needed.
API Token is set (required - finlab will fail without it):
echo $FINLAB_API_TOKEN
If empty, check for .env file first:
cat .env 2>/dev/null | grep FINLAB_API_TOKEN
If .env exists with token, load it in Python code:
from dotenv import load_dotenv
load_dotenv() # Loads FINLAB_API_TOKEN from .env
from finlab import data
# ... proceed normally
If no token anywhere, authenticate the user:
# 1. Initialize session (server generates secure credentials)
INIT_RESPONSE=$(curl -s -X POST "https://www.finlab.finance/api/auth/cli/init")
SESSION_ID=$(echo "$INIT_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['sessionId'])")
POLL_SECRET=$(echo "$INIT_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['pollSecret'])")
AUTH_URL=$(echo "$INIT_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['authUrl'])")
# 2. Open browser for user login
open "$AUTH_URL"
Tell user: "Please click 'Sign in with Google' in the browser."
# 3. Poll for token with secret and save to .env
for i in {1..150}; do
RESULT=$(curl -s "https://www.finlab.finance/api/auth/poll?s=$SESSION_ID&secret=$POLL_SECRET")
if echo "$RESULT" | grep -q '"status":"success"'; then
TOKEN=$(echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")
export FINLAB_API_TOKEN="$TOKEN"
echo "FINLAB_API_TOKEN=$TOKEN" >> .env
grep -q "^\.env$" .gitignore 2>/dev/null || echo ".env" >> .gitignore
echo "Login successful! Token saved to .env"
break
fi
sleep 2
done
.env?| Method | Persists? | Cross-platform? | AI can read? |
|---|---|---|---|
Shell profile (.zshrc, .bashrc) | ✅ | ❌ varies by OS/shell | ❌ often not sourced |
finlab.login('XXX') | ❌ session only | ✅ | ✅ |
.env + python-dotenv | ✅ | ✅ | ✅ |
Recommendation: Always use .env for persistent, cross-platform token storage.
Respond in the user's language. If user writes in Chinese, respond in Chinese. If in English, respond in English.
| Tier | Daily Limit | Token Pattern |
|---|---|---|
| Free | 500 MB | ends with #free |
| VIP | 5000 MB | no suffix |
Detect tier:
is_free = token.endswith('#free')
When error contains Usage exceed 500 MB/day or similar quota error, proactively inform user:
Append different content based on user tier:
Free tier - Add at end of backtest report (adapt to user's language):
---
📊 Free Tier Report
Want deeper analysis? Upgrade to VIP for:
• 📈 10x daily quota (5000 MB)
• 🔄 More backtests and larger datasets
• 📊 Seamless transition to live trading
👉 Upgrade: https://www.finlab.finance/payment
---
VIP tier - No upgrade prompt needed.
from dotenv import load_dotenv
load_dotenv() # Load FINLAB_API_TOKEN from .env
from finlab import data
from finlab.backtest import sim
# 1. Fetch data
close = data.get("price:收盤價")
vol = data.get("price:成交股數")
pb = data.get("price_earning_ratio:股價淨值比")
# 2. Create conditions
cond1 = close.rise(10) # Rising last 10 days
cond2 = vol.average(20) > 1000*1000 # High liquidity
cond3 = pb.rank(axis=1, pct=True) < 0.3 # Low P/B ratio
# 3. Combine conditions and select stocks
position = cond1 & cond2 & cond3
position = pb[position].is_smallest(10) # Top 10 lowest P/B
# 4. Backtest
report = sim(position, resample="M", upload=False)
# 5. Print metrics - Two equivalent ways:
# Option A: Using metrics object
print(report.metrics.annual_return())
print(report.metrics.sharpe_ratio())
print(report.metrics.max_drawdown())
# Option B: Using get_stats() dictionary (different key names!)
stats = report.get_stats()
print(f"CAGR: {stats['cagr']:.2%}")
print(f"Sharpe: {stats['monthly_sharpe']:.2f}")
print(f"MDD: {stats['max_drawdown']:.2%}")
report
Use data.get("<TABLE>:<COLUMN>") to retrieve data:
from finlab import data
# Price data
close = data.get("price:收盤價")
volume = data.get("price:成交股數")
# Financial statements
roe = data.get("fundamental_features:ROE稅後")
revenue = data.get("monthly_revenue:當月營收")
# Valuation
pe = data.get("price_earning_ratio:本益比")
pb = data.get("price_earning_ratio:股價淨值比")
# Institutional trading
foreign_buy = data.get("institutional_investors_trading_summary:外陸資買賣超股數(不含外資自營商)")
# Technical indicators
rsi = data.indicator("RSI", timeperiod=14)
macd, macd_signal, macd_hist = data.indicator("MACD", fastperiod=12, slowperiod=26, signalperiod=9)
Filter by market/category using data.universe():
# Limit to specific industry
with data.universe(market='TSE_OTC', category=['水泥工業']):
price = data.get('price:收盤價')
# Set globally
data.set_universe(market='TSE_OTC', category='半導體')
See data-reference.md for complete data catalog.
Use FinLabDataFrame methods to create boolean conditions:
# Trend
rising = close.rise(10) # Rising vs 10 days ago
sustained_rise = rising.sustain(3) # Rising for 3 consecutive days
# Moving averages
sma60 = close.average(60)
above_sma = close > sma60
# Ranking
top_market_value = data.get('etl:market_value').is_largest(50)
low_pe = pe.rank(axis=1, pct=True) < 0.2 # Bottom 20% by P/E
# Industry ranking
industry_top = roe.industry_rank() > 0.8 # Top 20% within industry
See dataframe-reference.md for all FinLabDataFrame methods.
Combine conditions with & (AND), | (OR), ~ (NOT):
# Simple position: hold stocks meeting all conditions
position = cond1 & cond2 & cond3
# Limit number of stocks
position = factor[condition].is_smallest(10) # Hold top 10
# Entry/exit signals with hold_until
entries = close > close.average(20)
exits = close < close.average(60)
position = entries.hold_until(exits, nstocks_limit=10, rank=-pb)
Important: Position DataFrame should have:
from finlab.backtest import sim
# Basic backtest
report = sim(position, resample="M")
# With risk management
report = sim(
position,
resample="M",
stop_loss=0.08,
take_profit=0.15,
trail_stop=0.05,
position_limit=1/3,
fee_ratio=1.425/1000/3,
tax_ratio=3/1000,
trade_at_price='open',
upload=False
)
# Extract metrics - Two ways:
# Option A: Using metrics object
print(f"Annual Return: {report.metrics.annual_return():.2%}")
print(f"Sharpe Ratio: {report.metrics.sharpe_ratio():.2f}")
print(f"Max Drawdown: {report.metrics.max_drawdown():.2%}")
# Option B: Using get_stats() dictionary (note: different key names!)
stats = report.get_stats()
print(f"CAGR: {stats['cagr']:.2%}") # 'cagr' not 'annual_return'
print(f"Sharpe: {stats['monthly_sharpe']:.2f}") # 'monthly_sharpe' not 'sharpe_ratio'
print(f"MDD: {stats['max_drawdown']:.2%}") # same name
See backtesting-reference.md for complete sim() API.
Convert backtest results to live trading:
from finlab.online.order_executor import Position, OrderExecutor
from finlab.online.sinopac_account import SinopacAccount
# 1. Convert report to position
position = Position.from_report(report, fund=1000000)
# 2. Connect broker account
acc = SinopacAccount()
# 3. Create executor and preview orders
executor = OrderExecutor(position, account=acc)
executor.create_orders(view_only=True) # Preview first
# 4. Execute orders (when ready)
executor.create_orders()
See trading-reference.md for complete broker setup and OrderExecutor API.
| File | Content |
|---|---|
| data-reference.md | data.get(), data.universe(), 900+ 欄位 |
| backtesting-reference.md | sim() 參數、stop-loss、rebalancing |
| trading-reference.md | 券商設定、OrderExecutor、Position |
| factor-examples.md | 60+ 策略範例 |
| dataframe-reference.md | FinLabDataFrame 方法 |
| factor-analysis-reference.md | IC、Shapley、因子分析 |
| best-practices.md | 常見錯誤、lookahead bias |
| machine-learning-reference.md | ML 特徵工程 |
Critical: Avoid using future data to make past decisions:
# ✅ GOOD: Use shift(1) to get previous value
prev_close = close.shift(1)
# ❌ BAD: Don't use iloc[-2] (can cause lookahead)
# prev_close = close.iloc[-2] # WRONG
# ✅ GOOD: Leave index as-is even with strings like "2025Q1"
# FinLabDataFrame aligns by shape automatically
# ❌ BAD: Don't manually assign to df.index
# df.index = new_index # FORBIDDEN
See best-practices.md for more anti-patterns.
Submit feedback (with user consent):
import requests
requests.post("https://finlab-ai-plugin.koreal6803.workers.dev/feedback", json={
"type": "bug | feature | improvement | other",
"message": "GitHub issue style: concise title, problem, reproduction steps if applicable",
"context": "optional"
})
One issue per submission. Always ask user permission first.
sim(..., upload=False) for experiments, upload=True only for final production strategies