원클릭으로
maoquant
A-share quantitative backtesting skill system.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
A-share quantitative backtesting skill system.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
| name | maoquant |
| version | 0.3.0 |
| spec | skill-manifest/0.1 |
| description | A-share quantitative backtesting skill system. |
| argument-hint | [backtest|scan] [args...] |
| allowed-tools | Read, Write, Edit, Bash, Glob, Grep |
| environment | {"python":">=3.9","packages":["numpy>=1.24","pandas>=2.0","matplotlib>=3.5","python-dotenv>=1.0"],"env_vars":[{"name":"FaceCat_URL","required":true,"default":"https://www.jjmfc.com:9969"},{"name":"TDX_DIR","required":false},{"name":"CACHE_DIR","required":false,"default":"./cache"}]} |
| selftest | cd $SKILL_DIR && python -m catquant.selftest |
catquant.resolve.resolve(query) to map names to codes. NEVER ask the user "请问茅台的代码是什么".check_available(code) and if it returns False, STOP immediately and tell the user. Do NOT assume data exists. Do NOT create a script without checking first. Most stocks are NOT covered by the free API.The catquant package lives inside this skill directory ($SKILL_DIR). Every generated script must:
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "$SKILL_DIR"))
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())
First run: pip install -r $SKILL_DIR/requirements.txt if dependencies are missing.
STOP. You MUST complete BOTH steps below BEFORE writing any backtest or scan script. If you skip check_available, the script WILL fail and the user gets nothing.
from catquant.resolve import resolve, check_available
# Step 0a: Resolve name to code
code, name = resolve("茅台") # -> ("600519.SH", "茅台")
# Step 0b: Check data — YOU MUST DO THIS, DO NOT ASSUME DATA EXISTS
ok, source, hint = check_available(code)
if not ok:
print(hint) # Tell the user WHY it failed and HOW to fix it
sys.exit(1) # STOP HERE. Do NOT create a backtest script.
CRITICAL: If check_available returns (False, ...) you MUST:
The free FaceCat API only covers ~84 stocks. Most stocks (including 茅台、比亚迪、宁德时代 etc.) are NOT covered. You MUST verify before proceeding.
If ok is True: use the returned source value in get_history():
source="facecat" (default) or source="tdx", tdx_dir=os.environ["TDX_DIR"]Parse $ARGUMENTS: backtest [strategy] [symbol] [interval]
Steps:
backtesting/{strategy_name}/{symbol}_{strategy}_backtest.pyAlways render charts with the strategy's indicator lines. Use overlays for lines on the price chart and panels for indicator subplots:
| Strategy | overlays (on price) | panels (subplots) |
|---|---|---|
| EMA Cross | EMA fast + EMA slow lines | MACD panel (DIF/DEA + MACD bars + zero_line) |
| MACD | EMA(12) + EMA(26) lines | MACD panel (DIF/DEA + MACD bars + zero_line) |
| RSI | EMA(14) line | RSI panel (RSI line) |
| KDJ | -- | KDJ panel (K/D/J lines) |
| BOLL | Upper + Mid + Lower lines + fill | -- |
| Custom | Indicator lines used in signals | Relevant oscillator |
Example:
render(result, bars, outdir, "kline",
overlays=[
{"data": ema5, "label": "EMA5", "color": "#ff9800"},
{"data": ema20, "label": "EMA20", "color": "#2196f3"},
],
panels=[{
"title": "MACD",
"lines": [
{"data": dif, "label": "DIF", "color": "#2962ff"},
{"data": dea, "label": "DEA", "color": "#ff6d00"},
],
"bars": [{"data": macd, "label": "MACD"}],
"zero_line": True,
}])
render(result, bars, outdir, "equity")
Parse $ARGUMENTS: scan [screening criteria]
Natural language criteria. Default: scan all stocks (do NOT ask for clarification if criteria is clear enough).
Steps:
scanning/{name}_scan.pyquick_scan(pre_filter) — instant, no K-line downloadscan(filter_fn, pre_filter) — two-layer, downloads K-lineslambda p: p.volume > 0 and p.close > 3 and "ST" not in p.name
Fields: p.code, p.name, p.close, p.open, p.high, p.low, p.volume, p.amount, p.lastClose, p.pe, p.totalShares, p.flowShares, p.upperLimit, p.lowerLimit
def filter_fn(code, name, bars):
if len(bars) < 30: return None
close, high, low, vol = bars_to_arrays(bars)
# compute indicators...
if condition: return {"score": value, "reason": "..."}
return None
scan(filter_fn, pre_filter=None, universe=None, count=250,
cycle=1440, source="facecat", max_results=20,
sort_key="score", ascending=False, verbose=True, refresh=False)
quick_scan(pre_filter=None, max_results=50, sort_key="close", ascending=False)
When the user requests a PDF report, generate a complete, high-quality report — not a minimal one. The report must include AI commentary that interprets results in plain language, not just numbers.
Page 1 — Cover
Page 2 — Executive Summary (metrics + AI verdict)
Page 3 — K-line Chart (embed kline.png full-width)
Page 4 — Equity Curve (embed equity.png full-width)
Page 5 — Trade Records (table, paginated if > 30 trades)
Page 6 — Risk Analysis (risk ratios + AI risk commentary)
Page 7 — Conclusion & Suggestions
import sys
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
def _register_cjk_font():
candidates = []
if sys.platform == "win32":
candidates = [
r"C:\Windows\Fonts\simsun.ttc",
r"C:\Windows\Fonts\msyh.ttc",
r"C:\Windows\Fonts\simhei.ttf",
]
elif sys.platform == "darwin":
candidates = [
"/System/Library/Fonts/PingFang.ttc",
"/Library/Fonts/Arial Unicode MS.ttf",
]
else:
candidates = [
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
]
for path in candidates:
try:
pdfmetrics.registerFont(TTFont("CJK", path))
return "CJK"
except Exception:
continue
raise RuntimeError("No CJK font found. Windows: simsun.ttc; Linux: sudo apt install fonts-wqy-microhei")
CJK_FONT = _register_cjk_font()
Never use default Helvetica or Times-Roman for Chinese — they produce garbled output.
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib import colors
styles = getSampleStyleSheet()
def cn(size=10, bold=False, color=colors.black):
return ParagraphStyle("cn", fontName=CJK_FONT, fontSize=size,
leading=size * 1.5, textColor=color,
fontWeight="Bold" if bold else "Normal")
title_style = cn(22, bold=True)
h2_style = cn(13, bold=True, color=colors.HexColor("#1a237e"))
body_style = cn(10)
caption_style = cn(9, color=colors.HexColor("#555555"))
verdict_style = cn(11, bold=True)
[股票名称 + 代码] (28pt, bold, centered)
[策略名称] (16pt, centered)
[回测区间: YYYY-MM-DD ~ YYYY-MM-DD]
[生成时间: YYYY-MM-DD HH:MM]
[MaoQuant · 量化回测报告] (footer)
Metrics table — use a 2-column grid layout:
| 指标 | 值 |
|---|---|
| 初始资金 | 100,000 元 |
| 最终资产 | X,XXX,XXX 元 |
| 总收益率 | +XX.XX% |
| 年化收益率 | +XX.XX% |
| 最大回撤 | -XX.XX% |
| 夏普比率 | X.XX |
| 索提诺比率 | X.XX |
| 卡玛比率 | X.XX |
| 总交易次数 | XX 次 |
| 胜率 | XX.XX% |
| 盈亏比 | X.XX |
| 平均持仓 | XX 天 |
| 最长连续亏损 | X 次 |
AI 综合评语 — generate from metrics using the rules below.
Generate natural-language commentary based on these thresholds. Write in Chinese, conversational tone, 3-5 sentences per section.
Overall Verdict (one of four tones):
def overall_verdict(m):
tr = m["total_return"]
md = abs(m["max_drawdown"])
sr = m["sharpe_ratio"]
if tr > 0.3 and md < 0.2 and sr > 1.0:
return "strong" # "策略表现出色,..."
elif tr > 0 and sr > 0.5:
return "decent" # "策略整体有效,..."
elif tr > 0:
return "marginal" # "策略勉强盈利,但风险控制有待加强..."
else:
return "losing" # "策略在此区间亏损,..."
Commentary templates (customize with actual numbers):
strong: "策略在{years}年回测中表现出色,累计收益{tr:.1%},年化{ar:.1%}。
最大回撤控制在{md:.1%}以内,夏普比率{sr:.2f}显示风险调整后收益优秀。
适合作为核心策略参考。"
decent: "策略整体有效,累计盈利{tr:.1%},但存在一定波动。
最大回撤{md:.1%}在可接受范围内,夏普比率{sr:.2f}处于合理水平。
建议配合止损策略使用。"
marginal: "策略勉强盈利{tr:.1%},但胜率仅{wr:.1%},盈亏比{pf:.2f}偏低。
最大回撤{md:.1%}较大,持仓期间心理压力较高。
建议优化参数或换用其他策略。"
losing: "策略在此区间亏损{tr:.1%},共{total}笔交易,胜率{wr:.1%}。
最大回撤达{md:.1%},说明策略与该股票当前趋势不匹配。
建议尝试其他策略或等待趋势确认。"
Win rate interpretation:
win_rate > 0.6 and profit_factor < 1.2 → "胜率较高但盈亏比偏低,可能存在小赚大亏的问题,注意设置止盈。"win_rate < 0.4 and profit_factor > 2.0 → "胜率较低但盈亏比优秀,属于趋势跟踪型策略,需要心理承受能力。"max_consecutive_losses > 5 → "最长连续亏损{n}次,实盘时需做好心态管理。"Risk rating (show as colored badge):
def risk_rating(m):
md = abs(m["max_drawdown"])
if md < 0.15: return ("低风险", "#4caf50")
elif md < 0.25: return ("中等风险", "#ff9800")
elif md < 0.40: return ("较高风险", "#f44336")
else: return ("高风险", "#b71c1c")
Columns: 序号 | 买入日期 | 买入价 | 卖出日期 | 卖出价 | 股数 | 手续费 | 盈亏(元) | 持仓天数
#e8f5e9)#ffebee)#fffde7) + "持仓中" in 卖出日期 columnfrom reportlab.platypus import TableStyle
from reportlab.lib import colors
def trade_row_style(trades):
styles = []
for i, t in enumerate(trades):
row = i + 1 # header is row 0
if t["exit_date"] == "":
bg = colors.HexColor("#fffde7")
elif t["pnl"] > 0:
bg = colors.HexColor("#e8f5e9")
else:
bg = colors.HexColor("#ffebee")
styles.append(("BACKGROUND", (0, row), (-1, row), bg))
return styles
Display these ratios with plain-language explanation next to each:
| 指标 | 值 | 含义 |
|---|---|---|
| 夏普比率 | X.XX | > 1.0 表示风险调整后收益良好 |
| 索提诺比率 | X.XX | 仅考虑下行波动,> 1.0 为佳 |
| 卡玛比率 | X.XX | 年化收益/最大回撤,> 0.5 可接受 |
| 最大回撤 | -XX% | 最大亏损幅度 |
| 最长回撤持续 | XX 天 | 深度亏损持续时间 |
AI 风险评语: 2-3 sentences interpreting the risk profile:
"该策略最大回撤{md:.1%},最长持续{dur}个交易日。
卡玛比率{calmar:.2f},说明每承受1%的最大回撤,
可获得{calmar:.2f}%的年化收益。{rating}。"
Always include these 4 sections:
avg_holding_bars < 5: "持仓过短,频繁交易产生较高手续费,建议提高信号阈值"max_drawdown < -0.30: "最大回撤偏大,建议加入止损 stop_loss=0.08"total_trades < 10: "样本量不足,建议延长回测区间或换用更频繁触发的策略"win_rate < 0.35: "胜率较低,建议配合趋势过滤条件(如均线方向)"Save to: backtesting/{strategy}/{symbol}_report.pdf
When the user pastes a TDX/通达信 formula and asks to backtest or scan it, translate it to Python using these rules. Do NOT run the formula text directly — always convert to Python first.
| TDX | Python (bars = List[SecurityData], arrays from bars_to_arrays) |
|---|---|
C / O / H / L / V | close[i] / open_[i] / high[i] / low[i] / vol[i] |
REF(X, N) | x[i - N] |
HHV(X, N) | np.max(x[i-N+1:i+1]) |
LLV(X, N) | np.min(x[i-N+1:i+1]) |
MA(X, N) | np.mean(x[i-N+1:i+1]) |
EMA(X, N) | ema_series(x, N)[i] |
CROSS(A, B) | cross_above(a, b)[i] |
AND / OR / NOT | and / or / not (Python booleans) |
:= (intermediate variable) | var = expr |
XG:A AND B AND C; (final signal) | signal = a and b and c |
For a formula that checks the last N bars at each point in history:
def check_pattern(o, h, l, c, v, i):
"""Check if pattern triggers at bar index i."""
if i < 5: # guard: need enough lookback
return False
# translate each TDX condition directly:
# A1:=REF(C,3)>REF(O,3) ->
a1 = c[i-3] > o[i-3]
# A2:=REF(V,3)>REF(V,4)*1.8 ->
a2 = v[i-3] > v[i-4] * 1.8
# ...
return a1 and a2 # and all other conditions
# Scan all bars
signals = [i for i in range(5, len(bars)) if check_pattern(o, h, l, c, v, i)]
backtest.run() with buy_signal / sell_signal arrays derived from the formula.# Build universe from TDX vipdoc
def get_tdx_universe(tdx_dir):
universe = []
for mkt, pfx in [("sh","SH"), ("sz","SZ")]:
lday = os.path.join(tdx_dir, "vipdoc", mkt, "lday")
for f in os.listdir(lday):
if not f.endswith(".day"): continue
num = f[2:-4]
# A-shares only: skip indices/funds/bonds
if mkt == "sh" and not (num.startswith("6") or num.startswith("688")): continue
if mkt == "sz" and not (num.startswith("0") or num.startswith("3")): continue
universe.append(f"{num}.{pfx}")
return universe
# Per-stock: slide window over full history
for code in universe:
bars = get_history(code, count=2000, cycle=1440, source="tdx", tdx_dir=TDX_DIR)
o, h, l, c, v = ... # arrays
for i in range(lookback, len(bars)):
if check_pattern(o, h, l, c, v, i):
entry = c[i]
for hold in [5, 10, 20]:
if i + hold < len(bars):
ret = (c[i+hold] - entry) / entry * 100
record(code, date, entry, ret, hold)
def is_limit_up(code, close, prev_close):
from catquant.backtest import get_limit_pct
limit = get_limit_pct(code)
return close >= round(prev_close * (1 + limit) - 0.01, 2)
Always report:
Matplotlib 默认字体不支持中文,所有含中文标签/标题的图表必须先注册字体,否则显示方块乱码。
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.font_manager import fontManager
def _setup_mpl_cjk():
candidates = []
import sys, os
if sys.platform == "win32":
candidates = [
r"C:\Windows\Fonts\simsun.ttc",
r"C:\Windows\Fonts\msyh.ttc",
r"C:\Windows\Fonts\simhei.ttf",
]
elif sys.platform == "darwin":
candidates = ["/System/Library/Fonts/PingFang.ttc"]
else:
candidates = ["/usr/share/fonts/truetype/wqy/wqy-microhei.ttc"]
for path in candidates:
if os.path.exists(path):
fontManager.addfont(path)
from matplotlib.font_manager import FontProperties
name = FontProperties(fname=path).get_name()
matplotlib.rcParams["font.sans-serif"] = [name, "DejaVu Sans"]
matplotlib.rcParams["axes.unicode_minus"] = False
return
# fallback: suppress warnings and use English labels
import warnings
warnings.filterwarnings("ignore", "Glyph.*missing from font")
_setup_mpl_cjk() # call once before any plt usage
Rules:
_setup_mpl_cjk() at module level, before any plt. callaxes.unicode_minus = False prevents minus sign rendering as a boxcatquant.data_engine inside scripts onlybacktest.run():
backtesting/ or scanning/catquant.chart.render()