From f6341f718e54a079bf6a21070232b9712c12d9c2 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Thu, 12 Feb 2026 03:57:48 +0000 Subject: [PATCH 1/2] Initial plan From fe058c4ba5bbae6dfc18427e738683e986c8ca0f Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Thu, 12 Feb 2026 04:04:08 +0000 Subject: [PATCH 2/2] docs: enhance README, add comprehensive docs and Claude skill Co-authored-by: JetSquirrel <20291255+JetSquirrel@users.noreply.github.com> --- .claude/skills/longbridge-trading/SKILL.md | 428 +++++++++++++++++++++ README.md | 160 +++++++- docs/README.md | 23 ++ docs/ai-agent-guide.md | 427 ++++++++++++++++++++ docs/architecture.md | 208 ++++++++++ docs/order-format.md | 390 +++++++++++++++++++ 6 files changed, 1632 insertions(+), 4 deletions(-) create mode 100644 .claude/skills/longbridge-trading/SKILL.md create mode 100644 docs/README.md create mode 100644 docs/ai-agent-guide.md create mode 100644 docs/architecture.md create mode 100644 docs/order-format.md diff --git a/.claude/skills/longbridge-trading/SKILL.md b/.claude/skills/longbridge-trading/SKILL.md new file mode 100644 index 0000000..429759e --- /dev/null +++ b/.claude/skills/longbridge-trading/SKILL.md @@ -0,0 +1,428 @@ +--- +name: longbridge-trading +description: Execute stock trading operations using Longbridge FS file-based trading system for HK/US stocks +--- + +# Longbridge Trading Skill + +This skill enables you to perform stock trading operations through the Longbridge FS file-based trading system. All operations are performed by reading and writing files, making it natural for AI agents. + +## When to Use This Skill + +Use this skill when the user wants to: +- Buy or sell stocks (HK/US markets) +- Check stock quotes and market data +- View account balance and positions +- Monitor portfolio performance and P&L +- Set up risk control rules (stop-loss/take-profit) +- Query trading history + +## Prerequisites + +Before using this skill, verify: + +1. **Controller is running**: Check if the Longbridge FS controller daemon is active + ```bash + ps aux | grep longbridge-fs + ``` + +2. **File system is initialized**: The `fs/` directory should exist with proper structure + ```bash + ls -la fs/ + ``` + +3. **Permissions**: Ensure you have read/write access to the `fs/` directory + +If the controller is not running, start it: +```bash +# Mock mode (for testing, no real API calls) +./build/longbridge-fs controller --root ./fs --mock --interval 2s & + +# Real mode (requires API credentials) +./build/longbridge-fs controller --root ./fs --credential ./configs/credential --interval 2s & +``` + +## Core Operations + +### 1. Submit Buy/Sell Orders + +To submit an order, append a new ORDER entry to `fs/trade/beancount.txt`: + +**Market Order Format:** +``` +2026-02-12 * "ORDER" "BUY AAPL.US" + ; intent_id: 20260212-001 + ; side: BUY + ; symbol: AAPL.US + ; qty: 100 + ; type: MARKET + ; tif: DAY + +``` + +**Limit Order Format:** +``` +2026-02-12 * "ORDER" "BUY AAPL.US" + ; intent_id: 20260212-002 + ; side: BUY + ; symbol: AAPL.US + ; qty: 100 + ; type: LIMIT + ; price: 180.50 + ; tif: DAY + +``` + +**Required Fields:** +- `intent_id`: Unique identifier (use timestamp format: YYYYMMDD-HHMMSS or YYYYMMDD-NNN) +- `side`: BUY or SELL +- `symbol`: Stock symbol with market suffix (e.g., AAPL.US, 9988.HK, 700.HK) +- `qty`: Quantity to trade +- `type`: MARKET or LIMIT +- `tif`: DAY (expires end of day) or GTC (good till canceled) +- `price`: Required for LIMIT orders only + +**Important Notes:** +- Always APPEND to the file, never overwrite +- Include an empty line at the end +- Controller polls every 2 seconds (default), so wait 2-3 seconds after submission +- Each field MUST start with ` ;` (two spaces + semicolon) +- Use proper date format: YYYY-MM-DD + +### 2. Check Order Results + +After submitting an order, wait 2-3 seconds and read `fs/trade/beancount.txt` to check results: + +```bash +# Read the entire beancount file +cat fs/trade/beancount.txt + +# Search for specific intent_id +grep "intent_id: 20260212-001" fs/trade/beancount.txt -A 10 +``` + +The controller will append either: +- **EXECUTION** record if order was filled +- **REJECTION** record if order was rejected + +**EXECUTION Example:** +``` +2026-02-12 * "EXECUTION" "BUY AAPL.US @ 180.25" + ; intent_id: 20260212-001 + ; order_id: 1234567890 + ; side: BUY + ; symbol: AAPL.US + ; filled_qty: 100 + ; avg_price: 180.25 + ; status: FILLED + ; executed_at: 2026-02-12T10:30:15Z +``` + +**REJECTION Example:** +``` +2026-02-12 * "REJECTION" "BUY AAPL.US" + ; intent_id: 20260212-001 + ; reason: Insufficient funds +``` + +### 3. Get Stock Quotes + +To get real-time market data, create a track file: + +```bash +# Request quote for AAPL.US +touch fs/quote/track/AAPL.US + +# Wait 2-3 seconds for controller to process +sleep 3 + +# Read quote data +cat fs/quote/hold/AAPL.US/overview.json +cat fs/quote/hold/AAPL.US/overview.txt +``` + +**Available Quote Files:** +- `overview.json` / `overview.txt`: Real-time price, volume, change +- `D.json`: Daily K-line (120 days) +- `W.json`: Weekly K-line (52 weeks) +- `5D.json`: 5-minute K-line +- `intraday.json`: Intraday tick data + +**Quote JSON Format:** +```json +{ + "symbol": "AAPL.US", + "last_done": 180.50, + "prev_close": 179.00, + "open": 179.50, + "high": 181.00, + "low": 178.50, + "volume": 45000000, + "turnover": 8100000000, + "timestamp": "2026-02-12T16:00:00Z" +} +``` + +### 4. Check Account Status + +```bash +# View account balance and summary +cat fs/account/state.json +``` + +**state.json Format:** +```json +{ + "cash": 10000.00, + "market_value": 18050.00, + "total_value": 28050.00, + "available": 10000.00, + "updated_at": "2026-02-12T10:30:00Z" +} +``` + +### 5. View Positions and P&L + +```bash +# View position-level P&L +cat fs/account/pnl.json + +# View portfolio with current quotes +cat fs/quote/portfolio.json +``` + +**pnl.json Format:** +```json +{ + "positions": [ + { + "symbol": "AAPL.US", + "qty": 100, + "avg_cost": 175.50, + "current_price": 180.50, + "market_value": 18050.00, + "cost_basis": 17550.00, + "unrealized_pnl": 500.00, + "unrealized_pnl_percent": 2.85 + } + ], + "total_unrealized_pnl": 500.00, + "updated_at": "2026-02-12T10:30:00Z" +} +``` + +### 6. Set Risk Control Rules + +Configure automatic stop-loss and take-profit by editing `fs/trade/risk_control.json`: + +```bash +# Create or update risk control configuration +cat > fs/trade/risk_control.json << 'EOF' +{ + "AAPL.US": { + "stop_loss": 170.00, + "take_profit": 200.00, + "qty": "100" + }, + "9988.HK": { + "stop_loss": 150.00, + "take_profit": 180.00 + } +} +EOF +``` + +**How it works:** +- Controller monitors prices for configured symbols +- When price hits `stop_loss`, automatically submits SELL order +- When price hits `take_profit`, automatically submits SELL order +- Rule is removed after triggering to prevent duplicate orders +- If `qty` is specified, sells that quantity; otherwise sells entire position + +### 7. Stop Controller Safely + +To safely stop the controller daemon: + +```bash +touch fs/.kill +``` + +The controller will detect this file in the next polling cycle and exit gracefully without affecting pending orders. + +## Common Workflows + +### Workflow 1: Buy Stock at Market Price + +```bash +# Step 1: Check current price +touch fs/quote/track/AAPL.US +sleep 3 +cat fs/quote/hold/AAPL.US/overview.json + +# Step 2: Submit market buy order +cat >> fs/trade/beancount.txt << 'EOF' +2026-02-12 * "ORDER" "BUY AAPL.US" + ; intent_id: 20260212-001 + ; side: BUY + ; symbol: AAPL.US + ; qty: 100 + ; type: MARKET + ; tif: DAY + +EOF + +# Step 3: Wait and check result +sleep 3 +tail -20 fs/trade/beancount.txt + +# Step 4: Verify position +cat fs/account/pnl.json +``` + +### Workflow 2: Set Stop-Loss for Existing Position + +```bash +# Step 1: Check current positions +cat fs/account/pnl.json + +# Step 2: Get current price +touch fs/quote/track/AAPL.US +sleep 3 +CURRENT_PRICE=$(jq -r '.last_done' fs/quote/hold/AAPL.US/overview.json) + +# Step 3: Set stop-loss at 5% below current price +STOP_PRICE=$(echo "$CURRENT_PRICE * 0.95" | bc) +jq --arg symbol "AAPL.US" --arg stop "$STOP_PRICE" \ + '.[$symbol] = {"stop_loss": ($stop | tonumber)}' \ + fs/trade/risk_control.json > /tmp/risk.json && \ + mv /tmp/risk.json fs/trade/risk_control.json +``` + +### Workflow 3: Monitor and Trade Based on Price + +```bash +# Monitor AAPL, buy when price drops below 175 +while true; do + touch fs/quote/track/AAPL.US + sleep 3 + PRICE=$(jq -r '.last_done' fs/quote/hold/AAPL.US/overview.json) + echo "Current price: $PRICE" + + if (( $(echo "$PRICE < 175" | bc -l) )); then + # Submit buy order + cat >> fs/trade/beancount.txt << EOF +2026-02-12 * "ORDER" "BUY AAPL.US" + ; intent_id: $(date +%Y%m%d-%H%M%S) + ; side: BUY + ; symbol: AAPL.US + ; qty: 100 + ; type: MARKET + ; tif: DAY + +EOF + echo "Buy order submitted at $PRICE" + break + fi + + sleep 10 +done +``` + +## Stock Symbol Format + +Always use the correct symbol format with market suffix: + +**US Stocks:** +- AAPL.US (Apple) +- MSFT.US (Microsoft) +- TSLA.US (Tesla) +- NVDA.US (NVIDIA) + +**HK Stocks:** +- 700.HK (Tencent) +- 9988.HK (Alibaba) +- 0001.HK (CKH Holdings) + +**CN Stocks:** +- 600519.SH (Kweichow Moutai - Shanghai) +- 000001.SZ (Ping An Bank - Shenzhen) + +## Error Handling + +### Order Rejected + +If you see a REJECTION record, common reasons include: +- Insufficient funds +- Market closed +- Invalid symbol +- Invalid price (outside allowable range) +- Invalid quantity (less than minimum lot size) + +**Solution:** Check the rejection reason and fix the order parameters. + +### Controller Not Responding + +If orders are not being processed: +1. Check if controller is running: `ps aux | grep longbridge-fs` +2. Check controller logs for errors +3. Restart controller if needed +4. Use `--mock` mode for testing + +### File Format Errors + +If controller logs show parsing errors: +- Verify Beancount format (indentation with 2 spaces, semicolon prefix) +- Check date format (YYYY-MM-DD) +- Ensure all required fields are present +- Verify no extra characters or wrong encoding + +## Tips for AI Agents + +1. **Always wait after operations**: File operations need 2-3 seconds for controller to process +2. **Use unique intent_ids**: Use timestamp-based IDs to avoid conflicts +3. **Append, don't overwrite**: Always append to beancount.txt, never overwrite +4. **Check results**: Always verify order execution by reading the beancount file +5. **Handle errors gracefully**: Orders can be rejected; check for REJECTION records +6. **Use Mock mode for testing**: Start controller with `--mock` flag during development +7. **Read before acting**: Check current state (positions, prices) before submitting orders + +## File System Reference + +``` +fs/ +├── account/ +│ ├── state.json # Account balance and summary +│ └── pnl.json # Position-level P&L +├── trade/ +│ ├── beancount.txt # Main order ledger (read/write) +│ ├── risk_control.json # Risk control rules (read/write) +│ └── blocks/ # Archived orders (read-only) +│ └── block_NNNN.txt +└── quote/ + ├── track/ # Create files here to request quotes + ├── hold/ # Quote data stored here + │ └── SYMBOL/ + │ ├── overview.json + │ ├── overview.txt + │ ├── D.json + │ └── intraday.json + └── portfolio.json # Full portfolio with quotes +``` + +## Additional Resources + +- [README](../README.md) - Project overview and quick start +- [AI Agent Guide](../docs/ai-agent-guide.md) - Detailed programming guide +- [Architecture](../docs/architecture.md) - System design and internals +- [Longbridge API](https://github.com/longportapp/openapi-go) - Official SDK documentation + +## Summary + +This skill allows you to trade stocks through simple file operations: +- **Write** to `fs/trade/beancount.txt` to submit orders +- **Create** files in `fs/quote/track/` to request quotes +- **Read** from `fs/account/` and `fs/quote/hold/` to check status +- **Edit** `fs/trade/risk_control.json` to configure risk rules +- **Create** `fs/.kill` to stop controller + +All operations are file-based, making them natural for AI agents and easy to audit. diff --git a/README.md b/README.md index 611164d..3b86f71 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,33 @@ > AI 驱动的港股/美股交易文件系统 +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![Go Version](https://img.shields.io/badge/Go-1.23+-00ADD8?logo=go)](go.mod) + ## 简介 Longbridge FS 是一个基于文件系统的股票交易框架,通过读写文件完成行情查询与交易操作。天然适配 AI Agent 的文件操作能力。 +**核心理念**:将股票交易抽象为文件系统操作,让 AI Agent 可以像操作文件一样进行交易。无需学习复杂的 API,只需读写文本文件即可完成行情查询、订单提交、风控管理等所有操作。 + ### 核心特性 -- **文件驱动** — 通过文件读写完成所有交易操作 +- **文件驱动** — 通过文件读写完成所有交易操作,AI Agent 无需学习 API - **AI 友好** — JSON 输出,天然适配 AI Agent 的文件操作能力 - **审计追踪** — 所有交易记录在 beancount 格式的 append-only 账本中 -- **盈亏追踪** — 自动生成 `pnl.json` 和 `portfolio.json` +- **盈亏追踪** — 自动生成 `pnl.json` 和 `portfolio.json`,实时追踪持仓盈亏 - **风控止损** — 配置 `risk_control.json` 实现自动止损/止盈 -- **容错降级** — 网络故障时自动切换到 Mock 模式 +- **容错降级** — 网络故障时自动切换到 Mock 模式,保证开发测试不中断 - **Kill Switch** — 创建 `.kill` 文件即可安全停止 controller +### 适用场景 + +- 使用 AI Agent 进行自动化交易 +- 构建基于规则的交易系统 +- 交易策略回测与模拟 +- 学习股票交易 API +- 交易记录审计与分析 + ## 项目结构 ``` @@ -180,8 +193,147 @@ make clean # 清理构建 make deps # 下载依赖 ``` +## 进阶功能 + +### 批量订单处理 + +Controller 会批量处理所有未执行的 ORDER,每个订单处理完成后追加结果: + +```bash +# 一次性提交多个订单 +cat >> fs/trade/beancount.txt << 'EOF' +2026-02-11 * "ORDER" "BUY AAPL" + ; intent_id: 20260211-001 + ; side: BUY + ; symbol: AAPL.US + ; qty: 100 + ; type: LIMIT + ; price: 180.00 + ; tif: DAY + +2026-02-11 * "ORDER" "BUY MSFT" + ; intent_id: 20260211-002 + ; side: BUY + ; symbol: MSFT.US + ; qty: 50 + ; type: MARKET + ; tif: DAY +EOF +``` + +### 订单状态追踪 + +每个订单执行后,controller 会追加 EXECUTION 或 REJECTION 记录: + +``` +2026-02-11 * "EXECUTION" "BUY AAPL.US @ 179.50" + ; intent_id: 20260211-001 + ; order_id: 1234567890 + ; side: BUY + ; symbol: AAPL.US + ; filled_qty: 100 + ; avg_price: 179.50 + ; status: FILLED + ; executed_at: 2026-02-11T10:30:00Z +``` + +### 账本归档与压缩 + +当执行订单数量达到 `--compact-after` 阈值时,controller 会自动将已执行订单归档到 `fs/trade/blocks/` 目录: + +``` +fs/trade/blocks/ +├── block_0001.txt # 前 10 笔订单 +├── block_0002.txt # 第 11-20 笔订单 +└── ... +``` + +主账本文件 `beancount.txt` 只保留未执行的订单,保持文件精简。 + +### 行情数据格式 + +行情数据同时提供 JSON 和文本两种格式: + +**JSON 格式**(适合程序解析): +```json +{ + "symbol": "AAPL.US", + "last_done": 180.50, + "prev_close": 179.00, + "open": 179.50, + "high": 181.00, + "low": 178.50, + "volume": 45000000, + "turnover": 8100000000, + "timestamp": "2026-02-11T16:00:00Z" +} +``` + +**文本格式**(适合人类阅读): +``` +Symbol: AAPL.US +Price: $180.50 Change: +1.50 (+0.84%) +Open: $179.50 High: $181.00 Low: $178.50 +Volume: 45,000,000 Turnover: $8.1B +Updated: 2026-02-11 16:00:00 EST +``` + +## 故障排查 + +### Controller 无法启动 + +1. 检查凭证文件是否存在且格式正确 +2. 使用 `--mock` 模式测试基本功能 +3. 查看日志输出,检查具体错误信息 + +### 订单未执行 + +1. 检查 beancount.txt 格式是否正确(每个字段必须以 `;` 开头) +2. 确认 controller 正在运行(`ps aux | grep longbridge-fs`) +3. 查看是否有 REJECTION 记录,了解拒绝原因 + +### 行情数据未更新 + +1. 确认 track 文件已创建 +2. 等待下一个轮询周期(默认 2 秒) +3. 检查网络连接和 API 配额 + +## 安全建议 + +- 不要将 `configs/credential` 文件提交到版本控制系统 +- 使用 Mock 模式进行开发和测试 +- 在生产环境设置合理的风控规则 +- 定期备份 beancount.txt 账本文件 +- 使用只读 API token 进行行情查询 + +## 常见问题 + +### Q: 为什么使用 Beancount 格式? + +A: Beancount 是一种成熟的复式记账格式,具有良好的可读性和可审计性。每笔交易都是 append-only,便于追溯历史记录。 + +### Q: 如何实现定时交易? + +A: 可以配合 cron 或其他调度工具,在指定时间写入 ORDER 到 beancount.txt 文件。 + +### Q: 支持哪些市场? + +A: 支持所有 Longbridge API 支持的市场,包括港股(HK)、美股(US)、A股(CN)等。 + +### Q: 可以同时运行多个 controller 吗? + +A: 不建议。多个 controller 可能导致订单重复执行。如需分布式部署,请使用文件锁机制。 + ## 相关链接 - [Longbridge OpenAPI Go SDK](https://github.com/longportapp/openapi-go) -- [Beancount](https://beancount.github.io/docs/) +- [Beancount 文档](https://beancount.github.io/docs/) +- [项目文档](./docs/) + +## 贡献指南 + +欢迎提交 Issue 和 Pull Request! + +## License +MIT License - 详见 [LICENSE](LICENSE) 文件 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..5f3c58c --- /dev/null +++ b/docs/README.md @@ -0,0 +1,23 @@ +# Longbridge FS 文档 + +欢迎使用 Longbridge FS 文档! + +## 目录 + +- [架构设计](./architecture.md) - 系统架构和设计理念 +- [文件系统结构](./filesystem.md) - 详细的文件系统布局说明 +- [订单格式](./order-format.md) - Beancount 订单格式详解 +- [行情数据](./market-data.md) - 行情数据获取和格式 +- [风控配置](./risk-control.md) - 风控规则配置指南 +- [API 参考](./api-reference.md) - Controller 参数和配置 +- [AI Agent 使用指南](./ai-agent-guide.md) - 如何用 AI Agent 操作 Longbridge FS + +## 快速链接 + +- [主 README](../README.md) +- [示例脚本](../demo.sh) +- [配置示例](../configs/) + +## 获取帮助 + +如有问题,请提交 [GitHub Issue](https://github.com/JetSquirrel/longbridge-fs/issues)。 diff --git a/docs/ai-agent-guide.md b/docs/ai-agent-guide.md new file mode 100644 index 0000000..2bf9ddb --- /dev/null +++ b/docs/ai-agent-guide.md @@ -0,0 +1,427 @@ +# AI Agent 使用指南 + +本指南面向 AI Agent(如 Claude、ChatGPT 等)和自动化脚本开发者,介绍如何使用 Longbridge FS 进行股票交易。 + +## 核心概念 + +Longbridge FS 将股票交易抽象为**文件操作**: +- **写入文件** = 提交订单/配置 +- **读取文件** = 查询状态/行情 +- **创建文件** = 触发操作 +- **删除文件** = 取消/停止 + +## 基本工作流 + +### 1. 初始化环境 + +确保 Controller 正在运行: + +```bash +# 检查 Controller 进程 +ps aux | grep longbridge-fs + +# 如果未运行,启动 Mock 模式 +./build/longbridge-fs controller --root ./fs --mock --interval 2s & +``` + +### 2. 提交订单 + +向 `fs/trade/beancount.txt` 追加订单记录: + +```python +# Python 示例 +import datetime + +def submit_order(symbol, side, qty, order_type="MARKET", price=None): + intent_id = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + + order = f""" +{datetime.date.today()} * "ORDER" "{side} {symbol}" + ; intent_id: {intent_id} + ; side: {side} + ; symbol: {symbol} + ; qty: {qty} + ; type: {order_type} + ; tif: DAY +""" + + if order_type == "LIMIT" and price: + order += f" ; price: {price}\n" + + with open("fs/trade/beancount.txt", "a") as f: + f.write(order) + + return intent_id + +# 提交买入订单 +intent_id = submit_order("AAPL.US", "BUY", 100, "LIMIT", 180.00) +print(f"Order submitted: {intent_id}") +``` + +**订单字段说明**: +- `intent_id`:唯一标识符,建议使用时间戳 +- `side`:BUY 或 SELL +- `symbol`:股票代码(如 AAPL.US, 9988.HK) +- `qty`:数量 +- `type`:MARKET(市价)或 LIMIT(限价) +- `tif`:Time in Force,DAY(当日有效)或 GTC(撤单前有效) +- `price`:限价单必须,市价单忽略 + +### 3. 检查订单结果 + +等待 Controller 处理(默认 2 秒轮询),然后读取账本: + +```python +def get_order_result(intent_id): + with open("fs/trade/beancount.txt", "r") as f: + lines = f.readlines() + + # 查找包含 intent_id 的 EXECUTION 或 REJECTION + for i, line in enumerate(lines): + if intent_id in line: + # 读取后续行直到找到状态 + for j in range(i, min(i+20, len(lines))): + if "EXECUTION" in lines[j]: + return "FILLED", extract_execution_details(lines[j:j+10]) + elif "REJECTION" in lines[j]: + return "REJECTED", extract_rejection_reason(lines[j:j+10]) + + return "PENDING", None + +# 轮询等待结果 +import time +for _ in range(10): # 最多等待 20 秒 + status, details = get_order_result(intent_id) + if status != "PENDING": + print(f"Order {status}: {details}") + break + time.sleep(2) +``` + +### 4. 查询行情 + +创建 track 文件触发行情获取: + +```python +def get_quote(symbol): + # 创建 track 文件 + track_file = f"fs/quote/track/{symbol}" + open(track_file, "w").close() + + # 等待 Controller 处理 + time.sleep(3) + + # 读取行情数据 + import json + quote_file = f"fs/quote/hold/{symbol}/overview.json" + + if os.path.exists(quote_file): + with open(quote_file, "r") as f: + return json.load(f) + else: + return None + +# 获取 AAPL 行情 +quote = get_quote("AAPL.US") +if quote: + print(f"AAPL Price: ${quote['last_done']}") + print(f"Change: {quote['last_done'] - quote['prev_close']}") +``` + +### 5. 查看持仓和盈亏 + +```python +def get_account_state(): + with open("fs/account/state.json", "r") as f: + return json.load(f) + +def get_pnl(): + with open("fs/account/pnl.json", "r") as f: + return json.load(f) + +def get_portfolio(): + with open("fs/quote/portfolio.json", "r") as f: + return json.load(f) + +# 查看账户 +state = get_account_state() +print(f"Cash: ${state['cash']}") +print(f"Total Value: ${state['total_value']}") + +# 查看盈亏 +pnl = get_pnl() +for position in pnl['positions']: + print(f"{position['symbol']}: {position['unrealized_pnl']:+.2f}") + +# 查看组合 +portfolio = get_portfolio() +for item in portfolio: + print(f"{item['symbol']}: {item['qty']} shares @ ${item['current_price']}") +``` + +### 6. 配置风控 + +```python +def set_risk_control(symbol, stop_loss=None, take_profit=None, qty=None): + import json + + # 读取现有配置 + risk_file = "fs/trade/risk_control.json" + try: + with open(risk_file, "r") as f: + risk_config = json.load(f) + except FileNotFoundError: + risk_config = {} + + # 更新配置 + risk_config[symbol] = {} + if stop_loss: + risk_config[symbol]["stop_loss"] = stop_loss + if take_profit: + risk_config[symbol]["take_profit"] = take_profit + if qty: + risk_config[symbol]["qty"] = str(qty) + + # 写回文件 + with open(risk_file, "w") as f: + json.dump(risk_config, f, indent=2) + +# 设置 AAPL 止损止盈 +set_risk_control("AAPL.US", stop_loss=170.0, take_profit=200.0, qty=100) +``` + +## 高级用法 + +### 批量订单 + +```python +def submit_batch_orders(orders): + """ + orders: [{"symbol": "AAPL.US", "side": "BUY", "qty": 100, ...}, ...] + """ + batch_text = "" + intent_ids = [] + + for order in orders: + intent_id = datetime.datetime.now().strftime("%Y%m%d-%H%M%S%f") + intent_ids.append(intent_id) + + batch_text += f""" +{datetime.date.today()} * "ORDER" "{order['side']} {order['symbol']}" + ; intent_id: {intent_id} + ; side: {order['side']} + ; symbol: {order['symbol']} + ; qty: {order['qty']} + ; type: {order.get('type', 'MARKET')} + ; tif: {order.get('tif', 'DAY')} +""" + if 'price' in order: + batch_text += f" ; price: {order['price']}\n" + + with open("fs/trade/beancount.txt", "a") as f: + f.write(batch_text) + + return intent_ids +``` + +### 监控价格并触发交易 + +```python +def monitor_price_and_trade(symbol, target_price, side, qty): + """ + 监控价格,达到目标时自动下单 + """ + while True: + quote = get_quote(symbol) + if quote and quote['last_done'] >= target_price: + intent_id = submit_order(symbol, side, qty) + print(f"Price reached ${target_price}, order submitted: {intent_id}") + break + time.sleep(5) + +# 当 AAPL 达到 185 时买入 +monitor_price_and_trade("AAPL.US", 185.0, "BUY", 100) +``` + +### 自动止损策略 + +```python +def auto_stop_loss(symbol, loss_percent=0.05): + """ + 自动为所有持仓设置止损(基于当前价格) + """ + pnl = get_pnl() + + for position in pnl['positions']: + if position['symbol'] == symbol: + current_price = position['current_price'] + stop_price = current_price * (1 - loss_percent) + + set_risk_control( + symbol=symbol, + stop_loss=stop_price, + qty=position['qty'] + ) + + print(f"Set stop loss for {symbol} at ${stop_price:.2f}") + return + + print(f"No position found for {symbol}") + +# 为 AAPL 持仓设置 5% 止损 +auto_stop_loss("AAPL.US", 0.05) +``` + +## 完整示例:AI 交易助手 + +```python +class LongbridgeAgent: + def __init__(self, fs_root="./fs"): + self.fs_root = fs_root + + def buy_stock(self, symbol, qty, price=None): + """买入股票""" + order_type = "LIMIT" if price else "MARKET" + intent_id = submit_order(symbol, "BUY", qty, order_type, price) + + # 等待结果 + for _ in range(10): + status, details = get_order_result(intent_id) + if status != "PENDING": + return {"status": status, "details": details} + time.sleep(2) + + return {"status": "TIMEOUT"} + + def sell_stock(self, symbol, qty, price=None): + """卖出股票""" + order_type = "LIMIT" if price else "MARKET" + intent_id = submit_order(symbol, "SELL", qty, order_type, price) + + for _ in range(10): + status, details = get_order_result(intent_id) + if status != "PENDING": + return {"status": status, "details": details} + time.sleep(2) + + return {"status": "TIMEOUT"} + + def get_price(self, symbol): + """获取实时价格""" + quote = get_quote(symbol) + return quote['last_done'] if quote else None + + def get_positions(self): + """获取所有持仓""" + pnl = get_pnl() + return pnl['positions'] + + def set_stop_loss(self, symbol, price, qty=None): + """设置止损""" + set_risk_control(symbol, stop_loss=price, qty=qty) + + def stop_controller(self): + """停止 Controller""" + open(f"{self.fs_root}/.kill", "w").close() + +# 使用示例 +agent = LongbridgeAgent() + +# 查询价格 +price = agent.get_price("AAPL.US") +print(f"AAPL: ${price}") + +# 买入 +result = agent.buy_stock("AAPL.US", 100, price=180.0) +print(f"Buy result: {result}") + +# 设置止损 +agent.set_stop_loss("AAPL.US", 170.0, qty=100) + +# 查看持仓 +positions = agent.get_positions() +for pos in positions: + print(f"{pos['symbol']}: {pos['qty']} shares, PnL: ${pos['unrealized_pnl']:.2f}") +``` + +## 注意事项 + +1. **等待处理时间**:订单提交后需等待 Controller 轮询处理(默认 2 秒) +2. **intent_id 唯一性**:确保每个订单的 intent_id 唯一,避免冲突 +3. **文件追加**:订单必须追加到 beancount.txt,不要覆盖 +4. **格式正确**:注意 Beancount 格式的缩进和分号 +5. **错误处理**:订单可能被拒绝(资金不足、市场关闭等),需检查 REJECTION +6. **并发控制**:避免同时运行多个写入进程 +7. **Mock 模式**:开发时使用 `--mock` 模式,避免真实交易 + +## 调试技巧 + +### 查看 Controller 日志 + +Controller 会输出详细日志,帮助诊断问题: + +```bash +# 查看实时日志 +tail -f /path/to/controller.log +``` + +### 手动检查账本 + +```bash +# 查看最近的订单 +tail -50 fs/trade/beancount.txt + +# 搜索特定订单 +grep "intent_id: 20260211-001" fs/trade/beancount.txt -A 10 +``` + +### 验证 JSON 格式 + +```bash +# 验证 JSON 文件格式 +jq . fs/account/state.json +jq . fs/trade/risk_control.json +``` + +## 常见错误 + +### 订单格式错误 + +``` +# 错误:缺少必需字段 +2026-02-11 * "ORDER" "BUY AAPL" + ; intent_id: 001 + ; side: BUY + # 缺少 symbol, qty, type, tif + +# 正确:包含所有必需字段 +2026-02-11 * "ORDER" "BUY AAPL" + ; intent_id: 20260211-001 + ; side: BUY + ; symbol: AAPL.US + ; qty: 100 + ; type: MARKET + ; tif: DAY +``` + +### 文件权限错误 + +确保 Controller 和脚本都有读写权限: + +```bash +chmod -R 755 fs/ +``` + +### Controller 未运行 + +提交订单前确保 Controller 在运行: + +```bash +ps aux | grep longbridge-fs | grep -v grep +``` + +## 下一步 + +- 阅读[订单格式文档](./order-format.md)了解完整的字段定义 +- 阅读[风控配置文档](./risk-control.md)了解高级风控策略 +- 查看[架构设计文档](./architecture.md)了解系统内部原理 diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..66aa937 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,208 @@ +# 架构设计 + +## 设计理念 + +Longbridge FS 的核心设计理念是**将股票交易抽象为文件系统操作**,使得 AI Agent 和自动化脚本可以通过简单的文件读写完成复杂的交易任务。 + +### 为什么选择文件系统? + +1. **通用性**:所有编程语言和工具都支持文件操作 +2. **可审计性**:所有操作都留有文件记录,便于审计和回溯 +3. **AI 友好**:AI Agent 擅长文件操作,无需学习复杂的 API +4. **人机可读**:文本格式既可被程序解析,也可被人类直接阅读 +5. **解耦合**:交易逻辑与执行引擎分离,便于测试和维护 + +## 系统架构 + +``` +┌─────────────┐ +│ AI Agent │ +│ / Script │ +└──────┬──────┘ + │ Read/Write Files + ▼ +┌─────────────────────────────────────┐ +│ File System (fs/) │ +│ ┌─────────┬─────────┬───────────┐ │ +│ │ account │ trade │ quote │ │ +│ └─────────┴─────────┴───────────┘ │ +└──────────────┬──────────────────────┘ + │ Watch & Process + ▼ +┌───────────────────────────────────────┐ +│ Controller (守护进程) │ +│ ┌──────────────────────────────┐ │ +│ │ Polling Loop (每 2 秒) │ │ +│ │ 1. 解析新 ORDER │ │ +│ │ 2. 执行订单 (SDK/Mock) │ │ +│ │ 3. 追加 EXECUTION/REJECTION │ │ +│ │ 4. 更新账户状态 │ │ +│ │ 5. 获取行情数据 │ │ +│ │ 6. 检查风控规则 │ │ +│ │ 7. 检查 kill switch │ │ +│ └──────────────────────────────┘ │ +└──────────┬────────────────────────────┘ + │ API Calls + ▼ +┌───────────────────────┐ +│ Longbridge API │ +│ (或 Mock 模式) │ +└───────────────────────┘ +``` + +## 核心组件 + +### 1. Controller (cmd/longbridge-fs/main.go) + +守护进程,负责监听文件系统变化并执行相应操作。 + +**主要职责**: +- 轮询检测新的 ORDER 指令 +- 调用 Broker 执行订单 +- 更新账户状态和持仓 +- 获取行情数据 +- 执行风控规则 +- 账本归档压缩 + +**工作流程**: +``` +初始化 → 轮询循环 → 解析订单 → 执行订单 → 追加结果 → 更新状态 → 检查风控 → 检查 kill switch + ↑ │ + └────────────────────────────────────────────────────────────────────┘ +``` + +### 2. Ledger (internal/ledger/) + +账本管理模块,负责解析和管理 Beancount 格式的交易记录。 + +**核心功能**: +- **parser.go**:解析 beancount.txt,识别 ORDER/EXECUTION/REJECTION +- **compact.go**:归档已执行订单到 blocks/ 目录 + +**账本格式**: +- 采用 Beancount 复式记账格式 +- Append-only,所有记录不可修改 +- 使用注释字段(`;`)存储交易元数据 + +### 3. Broker (internal/broker/) + +订单执行引擎,负责与 Longbridge API 交互。 + +**执行模式**: +- **真实模式**:调用 Longbridge SDK 执行真实订单 +- **Mock 模式**:模拟订单执行,用于开发和测试 + +**订单类型支持**: +- MARKET:市价单 +- LIMIT:限价单 +- 其他类型可扩展 + +### 4. Market (internal/market/) + +行情数据获取模块。 + +**数据类型**: +- Overview:实时报价 +- Kline:K线数据(日K、周K、5分钟K等) +- Intraday:分时数据 + +**输出格式**: +- JSON:程序解析 +- TXT:人类阅读 + +### 5. Account (internal/account/) + +账户管理模块,负责计算账户状态和盈亏。 + +**输出文件**: +- `state.json`:账户余额、可用资金 +- `pnl.json`:持仓盈亏(未实现) + +### 6. Risk (internal/risk/) + +风控引擎,负责监控价格并触发止损/止盈。 + +**工作原理**: +1. 读取 `risk_control.json` 配置 +2. 获取实时价格 +3. 判断是否触及阈值 +4. 自动生成 SELL ORDER +5. 移除已触发的规则 + +## 数据流 + +### 订单执行流程 + +``` +1. AI/用户写入 ORDER 到 beancount.txt + ↓ +2. Controller 解析新 ORDER + ↓ +3. Broker 执行订单 (调用 API 或 Mock) + ↓ +4. 追加 EXECUTION 或 REJECTION 到 beancount.txt + ↓ +5. 更新 account/state.json 和 pnl.json + ↓ +6. 达到归档阈值时,压缩到 blocks/ 目录 +``` + +### 行情获取流程 + +``` +1. AI/用户创建 quote/track/SYMBOL 文件 + ↓ +2. Controller 检测到 track 文件 + ↓ +3. Market 模块获取行情数据 + ↓ +4. 写入 quote/hold/SYMBOL/{overview,kline,...}.json + ↓ +5. 删除 track 文件 +``` + +### 风控触发流程 + +``` +1. 用户配置 risk_control.json + ↓ +2. Controller 定期获取持仓价格 + ↓ +3. Risk 模块检查是否触及阈值 + ↓ +4. 触发时自动追加 SELL ORDER + ↓ +5. 移除已触发规则,避免重复 +``` + +## 扩展性 + +### 添加新的订单类型 + +1. 在 `internal/model/types.go` 添加新类型定义 +2. 在 `internal/ledger/parser.go` 添加解析逻辑 +3. 在 `internal/broker/broker.go` 添加执行逻辑 + +### 添加新的行情数据源 + +1. 在 `internal/market/market.go` 添加新的获取函数 +2. 定义新的文件命名规则 +3. 更新 Controller 轮询逻辑 + +### 集成其他 Broker + +替换 `internal/broker/broker.go` 的 SDK 调用即可,接口保持不变。 + +## 容错与降级 + +- **网络故障**:自动切换到 Mock 模式(如果启用) +- **API 限流**:返回 REJECTION,不影响后续订单 +- **文件损坏**:Controller 记录错误日志,继续运行 +- **Kill Switch**:创建 `.kill` 文件安全退出,不影响已提交订单 + +## 性能考虑 + +- **轮询间隔**:默认 2 秒,可通过 `--interval` 调整 +- **账本压缩**:定期归档减少主文件大小,加快解析速度 +- **行情缓存**:重复请求相同 Symbol 时可复用数据(未实现) +- **并发控制**:单个 Controller 进程,避免订单重复执行 diff --git a/docs/order-format.md b/docs/order-format.md new file mode 100644 index 0000000..0c28c3c --- /dev/null +++ b/docs/order-format.md @@ -0,0 +1,390 @@ +# 订单格式详解 + +本文档详细说明 Longbridge FS 的 Beancount 订单格式。 + +## Beancount 基础 + +Longbridge FS 使用 [Beancount](https://beancount.github.io/) 复式记账格式记录所有交易。每笔交易都是不可修改的(append-only),确保审计追踪。 + +## 订单类型 + +### ORDER - 交易指令 + +用户提交的交易意图,等待 Controller 执行。 + +**基本格式:** +``` +YYYY-MM-DD * "ORDER" "描述文本" + ; field1: value1 + ; field2: value2 + ... +``` + +**完整示例:** +``` +2026-02-12 * "ORDER" "BUY AAPL.US 100 shares" + ; intent_id: 20260212-001 + ; side: BUY + ; symbol: AAPL.US + ; qty: 100 + ; type: LIMIT + ; price: 180.50 + ; tif: DAY +``` + +### EXECUTION - 执行记录 + +订单成功执行后,Controller 追加的执行记录。 + +**格式:** +``` +YYYY-MM-DD * "EXECUTION" "执行描述" + ; intent_id: 原订单ID + ; order_id: 交易所订单ID + ; side: BUY/SELL + ; symbol: 股票代码 + ; filled_qty: 成交数量 + ; avg_price: 成交均价 + ; status: FILLED + ; executed_at: 执行时间戳 +``` + +**示例:** +``` +2026-02-12 * "EXECUTION" "BUY AAPL.US @ 180.25" + ; intent_id: 20260212-001 + ; order_id: 9876543210 + ; side: BUY + ; symbol: AAPL.US + ; filled_qty: 100 + ; avg_price: 180.25 + ; status: FILLED + ; executed_at: 2026-02-12T10:30:15Z +``` + +### REJECTION - 拒绝记录 + +订单被拒绝时,Controller 追加的拒绝记录。 + +**格式:** +``` +YYYY-MM-DD * "REJECTION" "拒绝描述" + ; intent_id: 原订单ID + ; reason: 拒绝原因 + ; rejected_at: 拒绝时间戳 +``` + +**示例:** +``` +2026-02-12 * "REJECTION" "BUY AAPL.US - Insufficient funds" + ; intent_id: 20260212-001 + ; reason: Insufficient funds. Available: $5000, Required: $18050 + ; rejected_at: 2026-02-12T10:30:15Z +``` + +## ORDER 字段详解 + +### 必需字段 + +#### intent_id +- **类型**:字符串 +- **说明**:订单唯一标识符,用于追踪订单状态 +- **格式建议**:YYYYMMDD-NNN 或 YYYYMMDD-HHMMSS +- **示例**:`20260212-001`, `20260212-103015` + +#### side +- **类型**:枚举 +- **值**:BUY 或 SELL +- **说明**:交易方向 + +#### symbol +- **类型**:字符串 +- **格式**:SYMBOL.MARKET +- **示例**: + - 美股:AAPL.US, MSFT.US, TSLA.US + - 港股:700.HK, 9988.HK, 0001.HK + - A股:600519.SH, 000001.SZ + +#### qty +- **类型**:整数 +- **说明**:交易数量 +- **注意**:必须符合交易所最小交易单位(如港股 1 手 = 100 股) + +#### type +- **类型**:枚举 +- **值**:MARKET 或 LIMIT +- **说明**: + - MARKET:市价单,以当前市场价格立即成交 + - LIMIT:限价单,以指定价格或更好价格成交 + +#### tif +- **类型**:枚举 +- **值**:DAY 或 GTC +- **说明**: + - DAY:当日有效,收盘前未成交则自动取消 + - GTC:撤单前有效,除非手动取消 + +### 条件字段 + +#### price +- **类型**:浮点数 +- **必需条件**:type = LIMIT 时必须提供 +- **说明**:限价单的目标价格 +- **示例**:`180.50`, `9988.00` + +### 可选字段 + +#### market (未实现) +- **类型**:枚举 +- **值**:US, HK, CN +- **说明**:市场代码,可从 symbol 推断,因此可选 + +## 格式规范 + +### 缩进规则 + +1. 日期行顶格 +2. 字段行必须以两个空格 + 分号开头:` ;` +3. 字段行示例:` ; field: value` + +**正确:** +``` +2026-02-12 * "ORDER" "BUY AAPL" + ; intent_id: 001 + ; side: BUY +``` + +**错误:** +``` +2026-02-12 * "ORDER" "BUY AAPL" +; intent_id: 001 # 缺少缩进 + ; side: BUY # 只有一个空格 + ; qty: 100 # 缩进过多 +``` + +### 空行规范 + +每个订单记录之后必须有一个空行,以便 Parser 正确分隔。 + +**正确:** +``` +2026-02-12 * "ORDER" "BUY AAPL" + ; intent_id: 001 + ; side: BUY + ; symbol: AAPL.US + ; qty: 100 + ; type: MARKET + ; tif: DAY + +2026-02-12 * "ORDER" "BUY MSFT" + ; intent_id: 002 + ; side: BUY +``` + +**错误(缺少空行):** +``` +2026-02-12 * "ORDER" "BUY AAPL" + ; intent_id: 001 + ; side: BUY +2026-02-12 * "ORDER" "BUY MSFT" # 两个订单粘在一起 + ; intent_id: 002 +``` + +### 日期格式 + +- **格式**:YYYY-MM-DD +- **示例**:`2026-02-12`, `2026-12-31` +- **不要使用**:`2026/02/12`, `02-12-2026`, `20260212` + +### 字段值格式 + +- **字符串**:直接写,不需要引号(除非描述文本) +- **数字**:整数或小数,不要加单位或逗号 +- **布尔值**:true 或 false +- **时间戳**:ISO 8601 格式(YYYY-MM-DDTHH:MM:SSZ) + +**正确:** +``` + ; qty: 100 + ; price: 180.50 + ; executed_at: 2026-02-12T10:30:15Z +``` + +**错误:** +``` + ; qty: "100" # 数字不需要引号 + ; price: $180.50 # 不要加货币符号 + ; price: 180.50 USD # 不要加单位 + ; qty: 1,000 # 不要加逗号分隔符 +``` + +## 完整示例 + +### 市价单买入 + +``` +2026-02-12 * "ORDER" "BUY 100 AAPL at market price" + ; intent_id: 20260212-100001 + ; side: BUY + ; symbol: AAPL.US + ; qty: 100 + ; type: MARKET + ; tif: DAY + +``` + +### 限价单卖出 + +``` +2026-02-12 * "ORDER" "SELL 50 MSFT at $420.00" + ; intent_id: 20260212-100002 + ; side: SELL + ; symbol: MSFT.US + ; qty: 50 + ; type: LIMIT + ; price: 420.00 + ; tif: GTC + +``` + +### 港股交易 + +``` +2026-02-12 * "ORDER" "BUY 1000 Tencent" + ; intent_id: 20260212-100003 + ; side: BUY + ; symbol: 700.HK + ; qty: 1000 + ; type: LIMIT + ; price: 350.00 + ; tif: DAY + +``` + +### 执行成功案例 + +``` +2026-02-12 * "ORDER" "BUY 100 NVDA" + ; intent_id: 20260212-100004 + ; side: BUY + ; symbol: NVDA.US + ; qty: 100 + ; type: MARKET + ; tif: DAY + +2026-02-12 * "EXECUTION" "BUY NVDA.US @ 875.50" + ; intent_id: 20260212-100004 + ; order_id: 123456789 + ; side: BUY + ; symbol: NVDA.US + ; filled_qty: 100 + ; avg_price: 875.50 + ; status: FILLED + ; executed_at: 2026-02-12T10:31:22Z + +``` + +### 执行失败案例 + +``` +2026-02-12 * "ORDER" "BUY 10000 AAPL" + ; intent_id: 20260212-100005 + ; side: BUY + ; symbol: AAPL.US + ; qty: 10000 + ; type: MARKET + ; tif: DAY + +2026-02-12 * "REJECTION" "BUY AAPL.US - Insufficient funds" + ; intent_id: 20260212-100005 + ; reason: Insufficient funds. Available: $50000, Required: $1805000 + ; rejected_at: 2026-02-12T10:31:25Z + +``` + +## 常见错误 + +### 1. 缺少必需字段 + +``` +# 错误:缺少 type 和 tif +2026-02-12 * "ORDER" "BUY AAPL" + ; intent_id: 001 + ; side: BUY + ; symbol: AAPL.US + ; qty: 100 + +``` + +**解决**:补全所有必需字段。 + +### 2. 限价单缺少 price + +``` +# 错误:LIMIT 类型必须有 price +2026-02-12 * "ORDER" "BUY AAPL" + ; intent_id: 001 + ; side: BUY + ; symbol: AAPL.US + ; qty: 100 + ; type: LIMIT + ; tif: DAY + +``` + +**解决**:添加 price 字段。 + +### 3. intent_id 重复 + +如果两个订单使用相同的 intent_id,后续查询时无法区分。 + +**解决**:确保每个订单的 intent_id 唯一,建议使用时间戳。 + +### 4. 符号格式错误 + +``` +# 错误:缺少市场后缀 + ; symbol: AAPL + +# 错误:使用错误的分隔符 + ; symbol: AAPL:US + ; symbol: AAPL-US +``` + +**解决**:使用正确格式 `SYMBOL.MARKET`,如 `AAPL.US`。 + +## 扩展字段 + +系统支持自定义扩展字段,但不会被 Controller 使用。可用于记录额外信息: + +``` +2026-02-12 * "ORDER" "BUY AAPL" + ; intent_id: 20260212-001 + ; side: BUY + ; symbol: AAPL.US + ; qty: 100 + ; type: MARKET + ; tif: DAY + ; strategy: momentum_buying + ; note: Technical breakout at $180 + ; trader: alice + +``` + +这些扩展字段会被保留在账本中,但不影响订单执行。 + +## 账本归档 + +当执行订单数量达到阈值(默认 10 笔),Controller 会自动归档已执行订单到 `fs/trade/blocks/` 目录,主账本只保留待执行订单。 + +归档后的区块文件格式相同,可以串联所有区块重建完整交易历史: + +```bash +cat fs/trade/blocks/*.txt fs/trade/beancount.txt > full_history.txt +``` + +## 参考资料 + +- [Beancount 官方文档](https://beancount.github.io/docs/) +- [Beancount 语法](https://beancount.github.io/docs/beancount_language_syntax.html) +- [示例脚本](../demo.sh)