بنقرة واحدة
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()