From f280b91d930367c556f23748512e0f8a3dbb784e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 03:08:55 +0000 Subject: [PATCH 1/2] Initial plan From d3d82ba5bf33c46a47dbbf99be449441e64c5801 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 03:21:06 +0000 Subject: [PATCH 2/2] Phase 5: Agent integration - skills update, e2e demo, mock pipeline support Agent-Logs-Url: https://github.com/JetSquirrel/longbridge-fs/sessions/52104d82-1545-48db-9e77-76f9309ed3b7 Co-authored-by: JetSquirrel <20291255+JetSquirrel@users.noreply.github.com> --- .claude/skills/longbridge-trading/SKILL.md | 793 ++++++++++++++------- cmd/longbridge-fs/main.go | 19 +- demo_e2e.sh | 258 +++++++ internal/research/mock.go | 195 +++++ task.md | 6 +- 5 files changed, 1011 insertions(+), 260 deletions(-) create mode 100755 demo_e2e.sh create mode 100644 internal/research/mock.go diff --git a/.claude/skills/longbridge-trading/SKILL.md b/.claude/skills/longbridge-trading/SKILL.md index 429759e..0821bfe 100644 --- a/.claude/skills/longbridge-trading/SKILL.md +++ b/.claude/skills/longbridge-trading/SKILL.md @@ -7,6 +7,14 @@ description: Execute stock trading operations using Longbridge FS file-based tra 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. +The system implements a **five-layer Harness architecture**: + +``` +L1 Research → L2 Signal → L3 Portfolio → L4 Risk → L5 Execution +``` + +Each layer communicates through files, so you can read, write, or inject data at any layer without modifying code. + ## When to Use This Skill Use this skill when the user wants to: @@ -14,7 +22,10 @@ Use this skill when the user wants to: - 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) +- Set up signal definitions (SMA_CROSS, RSI, PRICE_CHANGE) +- Configure portfolio targets and trigger rebalancing +- Set up risk control rules (stop-loss/take-profit, pre-trade limits) +- Run end-to-end pipeline: research → signal → portfolio → execution - Query trading history ## Prerequisites @@ -35,188 +46,329 @@ Before using this skill, verify: If the controller is not running, start it: ```bash -# Mock mode (for testing, no real API calls) +# Mock mode (for testing, no real API calls — enables full pipeline simulation) ./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 +## L1 Research Layer -To submit an order, append a new ORDER entry to `fs/trade/beancount.txt`: +The research layer aggregates news, topics and custom data feeds for the symbols in your watchlist. -**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 +### Configure Watchlist +```bash +cat > fs/research/watchlist.json << 'EOF' +{ + "symbols": ["AAPL.US", "TSLA.US", "700.HK"], + "refresh_interval": "5m", + "feeds": ["news", "topics"] +} +EOF ``` -**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 +Controller behavior: +- In real mode: fetches live news/topics from Content API for each symbol. +- In mock mode: generates synthetic feeds automatically to enable full pipeline testing. -``` +### Read Research Feeds -**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 +```bash +# Latest news for a symbol +cat fs/research/feeds/news/AAPL.US/latest.json + +# Community topics for a symbol +cat fs/research/feeds/topics/AAPL.US/latest.json -**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 +# Aggregated research summary across all symbols +cat fs/research/summary.json +``` -### 2. Check Order Results +### Inject Custom Research Data -After submitting an order, wait 2-3 seconds and read `fs/trade/beancount.txt` to check results: +AI agents can write custom research data directly: ```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 +mkdir -p fs/research/feeds/custom +cat > fs/research/feeds/custom/my_analysis.json << 'EOF' +{ + "name": "sector_rotation_analysis", + "created_at": "2026-04-01T07:00:00Z", + "author": "claude-agent", + "data": { + "recommendation": "overweight_tech", + "confidence": 0.82 + } +} +EOF ``` -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 -``` +## L2 Signal Layer -**REJECTION Example:** -``` -2026-02-12 * "REJECTION" "BUY AAPL.US" - ; intent_id: 20260212-001 - ; reason: Insufficient funds -``` +The signal layer converts market data into actionable trading signals. -### 3. Get Stock Quotes +### Create Signal Definitions -To get real-time market data, create a track file: +Signal definitions are JSON files in `fs/signal/definitions/`. The controller evaluates builtin signals every cycle. +**Built-in Signal: SMA Crossover** ```bash -# Request quote for AAPL.US -touch fs/quote/track/AAPL.US - -# Wait 2-3 seconds for controller to process -sleep 3 +cat > fs/signal/definitions/sma_cross.json << 'EOF' +{ + "name": "sma_crossover", + "type": "builtin", + "enabled": true, + "symbols": ["AAPL.US", "TSLA.US"], + "params": { + "indicator": "SMA_CROSS", + "fast_period": 5, + "slow_period": 20 + } +} +EOF +``` -# Read quote data -cat fs/quote/hold/AAPL.US/overview.json -cat fs/quote/hold/AAPL.US/overview.txt +**Built-in Signal: RSI** +```bash +cat > fs/signal/definitions/rsi.json << 'EOF' +{ + "name": "rsi_signal", + "type": "builtin", + "enabled": true, + "symbols": ["AAPL.US"], + "params": { + "indicator": "RSI", + "period": 14, + "overbought": 70, + "oversold": 30 + } +} +EOF ``` -**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 +**Built-in Signal: Price Change** +```bash +cat > fs/signal/definitions/price_change.json << 'EOF' +{ + "name": "price_momentum", + "type": "builtin", + "enabled": true, + "symbols": ["TSLA.US"], + "params": { + "indicator": "PRICE_CHANGE", + "threshold_pct": 5.0, + "window": 5 + } +} +EOF +``` -**Quote JSON Format:** -```json +**External Signal (Agent-computed)** +```bash +# Agent writes signal output directly +mkdir -p fs/signal/output/AAPL.US +cat > fs/signal/output/AAPL.US/latest.json << 'EOF' { "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" + "updated_at": "2026-04-01T08:00:00Z", + "signals": [ + { + "name": "llm_sentiment", + "value": "BULLISH", + "strength": 0.78, + "detail": "Positive earnings sentiment detected", + "computed_at": "2026-04-01T08:00:00Z" + } + ] } +EOF ``` -### 4. Check Account Status +### Read Signal Output ```bash -# View account balance and summary -cat fs/account/state.json +# All active signals across all symbols +cat fs/signal/active.json + +# Per-symbol signal output +cat fs/signal/output/AAPL.US/latest.json + +# Signal history (append-only JSONL) +cat fs/signal/output/AAPL.US/history.jsonl ``` -**state.json Format:** +**active.json example:** ```json { - "cash": 10000.00, - "market_value": 18050.00, - "total_value": 28050.00, - "available": 10000.00, - "updated_at": "2026-02-12T10:30:00Z" + "updated_at": "2026-04-01T08:05:00Z", + "signals": [ + { "symbol": "AAPL.US", "name": "sma_crossover", "value": "BULLISH", "strength": 0.72 }, + { "symbol": "AAPL.US", "name": "rsi_signal", "value": "NEUTRAL", "strength": 0.45 }, + { "symbol": "TSLA.US", "name": "sma_crossover", "value": "BEARISH", "strength": 0.61 } + ] } ``` -### 5. View Positions and P&L +Signal values: `BULLISH`, `BEARISH`, `NEUTRAL`, `OVERBOUGHT`, `OVERSOLD`, `SURGE`, `DROP` + +--- + +## L3 Portfolio Layer + +The portfolio layer manages target allocations and rebalancing. + +### Set Portfolio Target ```bash -# View position-level P&L -cat fs/account/pnl.json +cat > fs/portfolio/target.json << 'EOF' +{ + "version": 1, + "updated_at": "2026-04-01T00:00:00Z", + "total_capital_pct": 0.90, + "cash_reserve_pct": 0.10, + "positions": { + "AAPL.US": { "weight": 0.40 }, + "TSLA.US": { "weight": 0.35 }, + "700.HK": { "weight": 0.15 }, + "NVDA.US": { "weight": 0.10 } + } +} +EOF +``` + +### Read Portfolio State + +```bash +# Current portfolio positions and weights +cat fs/portfolio/current.json + +# Target vs current comparison +cat fs/portfolio/diff.json -# View portfolio with current quotes -cat fs/quote/portfolio.json +# Historical snapshots +ls fs/portfolio/history/ ``` -**pnl.json Format:** +**diff.json example:** ```json { - "positions": [ + "updated_at": "2026-04-01T08:10:00Z", + "target_version": 1, + "requires_rebalance": true, + "adjustments": [ + { + "symbol": "AAPL.US", + "current_weight": 0.28, + "target_weight": 0.40, + "drift": -0.12, + "action": "BUY", + "estimated_value": 12000 + } + ] +} +``` + +### Trigger Rebalance + +**Manual rebalance** (write pending orders): +```bash +cat > fs/portfolio/rebalance/pending.json << 'EOF' +{ + "rebalance_id": "rebal-20260401-001", + "created_at": "2026-04-01T08:10:00Z", + "orders": [ { "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 + "side": "BUY", + "qty": 50, + "order_type": "MARKET", + "tif": "DAY" } - ], - "total_unrealized_pnl": 500.00, - "updated_at": "2026-02-12T10:30:00Z" + ] } +EOF ``` -### 6. Set Risk Control Rules +**Auto-rebalance mode** (controller creates pending orders automatically when drift exceeds threshold): +```bash +./build/longbridge-fs controller --root ./fs --mock --auto-rebalance & +``` + +--- + +## L4 Risk Control Layer + +The risk layer enforces pre-trade checks and monitors trading limits. + +### Configure Risk Policy + +```bash +cat > fs/trade/risk/policy.json << 'EOF' +{ + "version": 1, + "enabled": true, + "mode": "ENFORCE", + "pre_trade_checks": true, + "post_trade_monitoring": true, + "daily_loss_limit": { + "enabled": true, + "max_loss_pct": 0.03, + "action": "HALT" + }, + "order_frequency": { + "enabled": true, + "max_orders_per_hour": 20, + "max_orders_per_day": 100 + } +} +EOF +``` + +Risk modes: +- `ENFORCE` (default): reject orders that violate rules +- `WARN`: log violations but allow orders through +- `DISABLED`: skip all pre-trade checks + +### Configure Pre-Trade Rules + +```bash +cat > fs/trade/risk/pre_trade.json << 'EOF' +{ + "max_single_order_pct": 0.10, + "max_single_order_value": 50000, + "allowed_symbols": [], + "blocked_symbols": ["MEME.US"], + "allowed_sides": ["BUY", "SELL"], + "require_limit_price": false, + "max_deviation_from_market_pct": 0.05 +} +EOF +``` + +### Configure Position Limits + +```bash +cat > fs/trade/risk/position_limits.json << 'EOF' +{ + "max_position_pct": 0.25, + "max_positions_count": 15, + "sector_limits": {}, + "per_symbol_limits": { + "TSLA.US": { "max_pct": 0.10 } + } +} +EOF +``` -Configure automatic stop-loss and take-profit by editing `fs/trade/risk_control.json`: +### Configure Stop-Loss / Take-Profit (Legacy Risk Control) ```bash -# Create or update risk control configuration cat > fs/trade/risk_control.json << 'EOF' { "AAPL.US": { @@ -224,45 +376,40 @@ cat > fs/trade/risk_control.json << 'EOF' "take_profit": 200.00, "qty": "100" }, - "9988.HK": { - "stop_loss": 150.00, - "take_profit": 180.00 + "TSLA.US": { + "stop_loss": 200.00, + "take_profit": 350.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 +### Read Risk Status -### 7. Stop Controller Safely +```bash +# Current risk state and counters +cat fs/trade/risk/status.json -To safely stop the controller daemon: +# Today's order/loss counters +cat fs/trade/risk/daily_limits.json -```bash -touch fs/.kill +# Violations log (append-only) +cat fs/trade/risk/violations.jsonl ``` -The controller will detect this file in the next polling cycle and exit gracefully without affecting pending orders. +--- -## Common Workflows +## L5 Execution Layer -### Workflow 1: Buy Stock at Market Price +### Submit Standard Orders -```bash -# Step 1: Check current price -touch fs/quote/track/AAPL.US -sleep 3 -cat fs/quote/hold/AAPL.US/overview.json +Append ORDER entries to `fs/trade/beancount.txt`: -# Step 2: Submit market buy order +**Market Order:** +```bash cat >> fs/trade/beancount.txt << 'EOF' -2026-02-12 * "ORDER" "BUY AAPL.US" - ; intent_id: 20260212-001 +2026-04-01 * "ORDER" "BUY AAPL.US" + ; intent_id: 20260401-001 ; side: BUY ; symbol: AAPL.US ; qty: 100 @@ -270,159 +417,293 @@ cat >> fs/trade/beancount.txt << 'EOF' ; tif: DAY EOF +``` -# Step 3: Wait and check result -sleep 3 -tail -20 fs/trade/beancount.txt +**Limit Order with traceability:** +```bash +cat >> fs/trade/beancount.txt << 'EOF' +2026-04-01 * "ORDER" "BUY AAPL.US from signal" + ; intent_id: 20260401-002 + ; side: BUY + ; symbol: AAPL.US + ; qty: 100 + ; type: LIMIT + ; price: 180.50 + ; tif: DAY + ; source: rebalance + ; rebalance_id: rebal-20260401-001 + ; signal_refs: sma_crossover,rsi_signal -# Step 4: Verify position -cat fs/account/pnl.json +EOF ``` -### Workflow 2: Set Stop-Loss for Existing Position +### Submit Algorithmic Orders +**TWAP (Time-Weighted Average Price):** ```bash -# Step 1: Check current positions -cat fs/account/pnl.json +cat >> fs/trade/beancount.txt << 'EOF' +2026-04-01 * "ORDER" "BUY AAPL.US via TWAP" + ; intent_id: 20260401-003 + ; side: BUY + ; symbol: AAPL.US + ; qty: 500 + ; type: LIMIT + ; price: 182.00 + ; tif: DAY + ; algo: TWAP + ; algo_duration: 30m + ; algo_slices: 5 -# Step 2: Get current price -touch fs/quote/track/AAPL.US +EOF +``` + +**ICEBERG (hidden quantity):** +```bash +cat >> fs/trade/beancount.txt << 'EOF' +2026-04-01 * "ORDER" "BUY AAPL.US via ICEBERG" + ; intent_id: 20260401-004 + ; side: BUY + ; symbol: AAPL.US + ; qty: 1000 + ; type: LIMIT + ; price: 182.00 + ; tif: GTC + ; algo: ICEBERG + ; algo_slices: 10 + +EOF +``` + +### Check Order Results + +```bash +# Wait for controller to process 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 +# Check for EXECUTION or REJECTION +grep -A 10 "intent_id: 20260401-001" fs/trade/beancount.txt +``` + +**EXECUTION example:** +``` +2026-04-01 * "EXECUTION" "BUY AAPL.US @ 180.25" + ; intent_id: 20260401-001 + ; order_id: 1234567890 + ; side: BUY + ; symbol: AAPL.US + ; filled_qty: 100 + ; avg_price: 180.25 + ; status: FILLED + ; executed_at: 2026-04-01T10:30:15Z +``` + +**REJECTION example:** +``` +2026-04-01 * "REJECTION" "BUY AAPL.US" + ; intent_id: 20260401-001 + ; reason: max_single_order_pct exceeded ``` -### Workflow 3: Monitor and Trade Based on Price +--- + +## Common Workflows + +### Workflow 1: Full Harness Pipeline (Research → Signal → Portfolio → Execution) ```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) +# Step 1: Initialize FS +./build/longbridge-fs init --root ./fs + +# Step 2: Configure watchlist (L1) +cat > fs/research/watchlist.json << 'EOF' +{"symbols": ["AAPL.US", "TSLA.US"], "refresh_interval": "5m", "feeds": ["news", "topics"]} +EOF + +# Step 3: Define signals (L2) +cat > fs/signal/definitions/sma.json << 'EOF' +{"name": "sma_crossover", "type": "builtin", "enabled": true, + "symbols": ["AAPL.US"], "params": {"indicator": "SMA_CROSS", "fast_period": 5, "slow_period": 20}} +EOF + +# Step 4: Set portfolio target (L3) +cat > fs/portfolio/target.json << 'EOF' +{"version": 1, "total_capital_pct": 0.90, "cash_reserve_pct": 0.10, + "positions": {"AAPL.US": {"weight": 0.40}}} +EOF + +# Step 5: Start controller in mock mode (enables full pipeline simulation) +./build/longbridge-fs controller --root ./fs --mock --interval 2s & + +# Step 6: Wait and inspect pipeline output +sleep 5 +cat fs/research/summary.json +cat fs/signal/active.json +cat fs/portfolio/diff.json + +# Step 7: Submit orders (L5) +cat >> fs/trade/beancount.txt << 'EOF' +2026-04-01 * "ORDER" "BUY AAPL.US" + ; intent_id: 20260401-100 ; side: BUY ; symbol: AAPL.US - ; qty: 100 + ; qty: 50 ; type: MARKET ; tif: DAY EOF - echo "Buy order submitted at $PRICE" - break - fi +sleep 3 +tail -20 fs/trade/beancount.txt - sleep 10 -done +# Step 8: Stop controller +touch fs/.kill ``` -## 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) +### Workflow 2: Signal-Driven Order Submission -**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) +```bash +# Read active signals and submit orders for BULLISH signals +SIGNALS=$(cat fs/signal/active.json) +echo "$SIGNALS" | python3 -c " +import json, sys +active = json.load(sys.stdin) +for s in active.get('signals', []): + if s['value'] == 'BULLISH' and s['strength'] > 0.6: + print(f\"Buy signal: {s['symbol']} ({s['name']}, strength={s['strength']:.2f})\") +" +``` -## Error Handling +### Workflow 3: Buy Stock at Market Price -### Order Rejected +```bash +# Step 1: Check current price +touch fs/quote/track/AAPL.US +sleep 3 +cat fs/quote/hold/AAPL.US/overview.json -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) +# Step 2: Submit market buy order +cat >> fs/trade/beancount.txt << 'EOF' +2026-04-01 * "ORDER" "BUY AAPL.US" + ; intent_id: 20260401-001 + ; side: BUY + ; symbol: AAPL.US + ; qty: 100 + ; type: MARKET + ; tif: DAY -**Solution:** Check the rejection reason and fix the order parameters. +EOF -### Controller Not Responding +# Step 3: Wait and check result +sleep 3 +tail -20 fs/trade/beancount.txt -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 +# Step 4: Verify position +cat fs/account/pnl.json +``` -### File Format Errors +### Workflow 4: Set Stop-Loss for Existing Position -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 +```bash +# Set stop-loss at 5% below current price for AAPL.US +touch fs/quote/track/AAPL.US +sleep 3 +CURRENT_PRICE=$(jq -r '.last' fs/quote/hold/AAPL.US/overview.json) +STOP_PRICE=$(echo "$CURRENT_PRICE * 0.95" | bc) -## Tips for AI Agents +jq --arg symbol "AAPL.US" --argjson stop "$STOP_PRICE" \ + '.[$symbol] = {"stop_loss": $stop}' \ + fs/trade/risk_control.json > /tmp/risk.json && \ + mv /tmp/risk.json fs/trade/risk_control.json +``` -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 +## Quick Reference: All Layer Files ``` fs/ +├── research/ # L1 Research +│ ├── watchlist.json # <- WRITE: symbols to track +│ ├── summary.json # -> READ: aggregated feed status +│ └── feeds/ +│ ├── news/{SYMBOL}/latest.json # -> READ: news articles +│ ├── topics/{SYMBOL}/latest.json# -> READ: community topics +│ └── custom/{name}.json # <- WRITE: agent custom data +│ +├── signal/ # L2 Signal +│ ├── definitions/{name}.json # <- WRITE: signal configs +│ ├── active.json # -> READ: current signals +│ └── output/{SYMBOL}/ +│ ├── latest.json # -> READ: per-symbol output +│ └── history.jsonl # -> READ: signal history +│ +├── portfolio/ # L3 Portfolio +│ ├── target.json # <- WRITE: target weights +│ ├── current.json # -> READ: actual weights +│ ├── diff.json # -> READ: drift / actions +│ └── rebalance/pending.json # <- WRITE: pending orders +│ ├── account/ -│ ├── state.json # Account balance and summary -│ └── pnl.json # Position-level P&L +│ ├── state.json # -> READ: balances and orders +│ └── pnl.json # -> READ: per-position 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 +│ ├── beancount.txt # <- WRITE ORDER / -> READ EXECUTION +│ ├── risk_control.json # <- WRITE: stop-loss/take-profit +│ ├── blocks/ # -> READ: archived orders +│ └── risk/ # L4 Risk +│ ├── policy.json # <- WRITE: risk policy +│ ├── pre_trade.json # <- WRITE: order limits +│ ├── position_limits.json # <- WRITE: position caps +│ ├── daily_limits.json # -> READ: daily counters +│ ├── status.json # -> READ: risk gate status +│ └── violations.jsonl # -> READ: violation log +│ └── 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 + ├── track/ # <- CREATE: request a quote + ├── hold/{SYMBOL}/ + │ ├── overview.json # -> READ: current price + │ ├── D.json # -> READ: daily kline (120d) + │ └── intraday.json # -> READ: intraday ticks + └── portfolio.json # -> READ: portfolio with quotes ``` -## Additional Resources +## Controller Options -- [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 +| Flag | Default | Description | +|---------------------|--------------|------------------------------------------| +| `--root` | `.` | FS root directory | +| `--interval` | `2s` | Poll interval | +| `--mock` | `false` | Mock mode — no API calls, full pipeline | +| `--auto-rebalance` | `false` | Auto-create rebalance orders on drift | +| `--compact-after` | `10` | Compact ledger after N executions | +| `--credential` | `credential` | Credential file (real mode) | -## Summary +In **mock mode** (`--mock`): +- All five layers run without API calls +- Research feeds are populated with synthetic data +- Kline data is generated automatically for signal computation +- Orders are simulated with realistic mock fills +- Fully self-contained for testing and development -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 +## Stock Symbol Format -All operations are file-based, making them natural for AI agents and easy to audit. +**US Stocks:** `AAPL.US`, `MSFT.US`, `TSLA.US`, `NVDA.US` +**HK Stocks:** `700.HK`, `9988.HK`, `0001.HK` +**CN Stocks:** `600519.SH`, `000001.SZ` + +## Tips for AI Agents + +1. **Always wait after operations**: Controller needs 2-3 seconds per cycle to process files +2. **Mock mode for development**: Use `--mock` to test the full pipeline without credentials +3. **Read before acting**: Check `signal/active.json` and `portfolio/diff.json` before submitting orders +4. **Append, don't overwrite**: Always append to `beancount.txt`, never overwrite +5. **Use unique intent_ids**: Use timestamp-based IDs to avoid conflicts +6. **Signal to order traceability**: Always include `signal_refs` in orders for audit trail +7. **Stop gracefully**: Use `touch fs/.kill` to stop the controller cleanly + +## Additional Resources + +- [README](../README.md) - Project overview and quick start +- [End-to-End Demo](../demo_e2e.sh) - Full pipeline demo script (mock mode) +- [Basic Demo](../demo.sh) - Basic execution demo +- [Spec](../spec.md) - Full architecture specification diff --git a/cmd/longbridge-fs/main.go b/cmd/longbridge-fs/main.go index 6ff9cf5..a1fb2ff 100644 --- a/cmd/longbridge-fs/main.go +++ b/cmd/longbridge-fs/main.go @@ -542,7 +542,24 @@ func runController(root string, interval time.Duration, credFile string, mock bo } // Phase 3: Refresh research feeds (news/topics from Content API) - if !useMock { + if useMock { + // Mock mode: generate synthetic research data to enable full pipeline simulation + if err := research.RefreshFeedsMock(root); err != nil { + if verbose { + log.Printf("⚠ Mock research refresh failed: %v", err) + } + } else if verbose { + log.Printf("✓ Mock research feeds generated") + } + // Generate mock kline data for symbols in watchlist (enables signal computation) + if wl, err := research.ParseWatchlist(root); err == nil { + for _, sym := range wl.Symbols { + if err := research.GenerateMockKlineData(root, sym, 120); err != nil && verbose { + log.Printf("⚠ Mock kline data for %s failed: %v", sym, err) + } + } + } + } else { if err := research.RefreshFeeds(ctx, root, credFile); err != nil { // Don't fail the entire cycle for research refresh errors if verbose { diff --git a/demo_e2e.sh b/demo_e2e.sh new file mode 100755 index 0000000..fa75b18 --- /dev/null +++ b/demo_e2e.sh @@ -0,0 +1,258 @@ +#!/bin/bash +# demo_e2e.sh — End-to-end Harness Pipeline Demo +# Demonstrates the full five-layer pipeline in mock mode: +# L1 Research → L2 Signal → L3 Portfolio → L4 Risk → L5 Execution +# +# Usage: +# ./demo_e2e.sh # run full demo +# ./demo_e2e.sh --clean # remove fs/ directory first, then run +set -e + +BIN="go run ./cmd/longbridge-fs" +FS_ROOT="./fs_e2e_demo" + +# Optional cleanup +if [ "${1}" = "--clean" ]; then + echo "==> Removing previous demo FS..." + rm -rf "${FS_ROOT}" +fi + +echo "================================================================" +echo " Longbridge-FS | End-to-End Harness Pipeline Demo (Mock)" +echo "================================================================" +echo "" + +# ------------------------------------------------------------------ # +# STEP 1: Initialize the five-layer FS +# ------------------------------------------------------------------ # +echo "==> [1/9] Initialize file system (five-layer structure)" +$BIN init --root "${FS_ROOT}" +echo "" + +# ------------------------------------------------------------------ # +# STEP 2: L1 Research — configure watchlist +# ------------------------------------------------------------------ # +echo "==> [2/9] L1 Research: configure watchlist" +cat > "${FS_ROOT}/research/watchlist.json" << 'EOF' +{ + "symbols": ["AAPL.US", "TSLA.US", "700.HK"], + "refresh_interval": "5m", + "feeds": ["news", "topics"] +} +EOF +echo " watchlist.json written with symbols: AAPL.US, TSLA.US, 700.HK" +echo "" + +# ------------------------------------------------------------------ # +# STEP 3: L2 Signal — configure signal definitions +# ------------------------------------------------------------------ # +echo "==> [3/9] L2 Signal: create signal definitions" + +cat > "${FS_ROOT}/signal/definitions/sma_cross.json" << 'EOF' +{ + "name": "sma_crossover", + "type": "builtin", + "enabled": true, + "symbols": ["AAPL.US", "TSLA.US", "700.HK"], + "params": { + "indicator": "SMA_CROSS", + "fast_period": 5, + "slow_period": 20 + } +} +EOF + +cat > "${FS_ROOT}/signal/definitions/rsi.json" << 'EOF' +{ + "name": "rsi_signal", + "type": "builtin", + "enabled": true, + "symbols": ["AAPL.US", "TSLA.US"], + "params": { + "indicator": "RSI", + "period": 14, + "overbought": 70, + "oversold": 30 + } +} +EOF + +echo " Signal definitions created: sma_crossover, rsi_signal" +echo "" + +# ------------------------------------------------------------------ # +# STEP 4: L3 Portfolio — configure target allocation +# ------------------------------------------------------------------ # +echo "==> [4/9] L3 Portfolio: set target allocation" +cat > "${FS_ROOT}/portfolio/target.json" << 'EOF' +{ + "version": 1, + "updated_at": "2026-04-01T00:00:00Z", + "total_capital_pct": 0.90, + "cash_reserve_pct": 0.10, + "positions": { + "AAPL.US": { "weight": 0.40 }, + "TSLA.US": { "weight": 0.35 }, + "700.HK": { "weight": 0.15 }, + "NVDA.US": { "weight": 0.10 } + } +} +EOF +echo " portfolio/target.json written (AAPL 40%, TSLA 35%, 700.HK 15%, NVDA 10%)" +echo "" + +# ------------------------------------------------------------------ # +# STEP 5: L4 Risk — enable risk gate +# ------------------------------------------------------------------ # +echo "==> [5/9] L4 Risk: configure risk policy" +cat > "${FS_ROOT}/trade/risk/policy.json" << 'EOF' +{ + "version": 1, + "enabled": true, + "mode": "WARN", + "pre_trade_checks": true, + "post_trade_monitoring": false, + "daily_loss_limit": { + "enabled": false, + "max_loss_pct": 0.05, + "action": "HALT" + }, + "order_frequency": { + "enabled": false, + "max_orders_per_hour": 50, + "max_orders_per_day": 200 + } +} +EOF +cat > "${FS_ROOT}/trade/risk/pre_trade.json" << 'EOF' +{ + "max_single_order_pct": 0.20, + "max_single_order_value": 100000, + "allowed_symbols": [], + "blocked_symbols": [], + "allowed_sides": ["BUY", "SELL"], + "require_limit_price": false, + "max_deviation_from_market_pct": 0.10 +} +EOF +echo " Risk policy set to WARN mode with pre-trade checks enabled" +echo "" + +# ------------------------------------------------------------------ # +# STEP 6: Start controller in mock + harness mode +# ------------------------------------------------------------------ # +echo "==> [6/9] Start controller in mock mode (background)" +$BIN controller --root "${FS_ROOT}" --interval 2s --mock --compact-after 10 & +CONTROLLER_PID=$! +echo " Controller PID: ${CONTROLLER_PID}" +echo " Waiting for initial cycle (research feeds + signals)..." +sleep 5 +echo "" + +# ------------------------------------------------------------------ # +# STEP 7: Inspect pipeline output (L1-L3) +# ------------------------------------------------------------------ # +echo "==> [7/9] Inspect pipeline state" + +echo "" +echo " --- L1 Research Summary ---" +if [ -f "${FS_ROOT}/research/summary.json" ]; then + cat "${FS_ROOT}/research/summary.json" +else + echo " (no summary yet)" +fi + +echo "" +echo " --- L2 Active Signals ---" +if [ -f "${FS_ROOT}/signal/active.json" ]; then + cat "${FS_ROOT}/signal/active.json" +else + echo " (no signals yet)" +fi + +echo "" +echo " --- L3 Portfolio Diff ---" +if [ -f "${FS_ROOT}/portfolio/diff.json" ]; then + cat "${FS_ROOT}/portfolio/diff.json" +else + echo " (no diff yet)" +fi +echo "" + +# ------------------------------------------------------------------ # +# STEP 8: L5 Execution — inject orders into ledger +# ------------------------------------------------------------------ # +echo "==> [8/9] L5 Execution: submit orders through beancount ledger" + +TODAY=$(date +%Y-%m-%d) +TS=$(date +%Y%m%d-%H%M%S) + +cat >> "${FS_ROOT}/trade/beancount.txt" << EOF +${TODAY} * "ORDER" "BUY AAPL.US via harness demo" + ; intent_id: ${TS}-001 + ; side: BUY + ; symbol: AAPL.US + ; qty: 10 + ; type: MARKET + ; tif: DAY + ; source: rebalance + +EOF + +cat >> "${FS_ROOT}/trade/beancount.txt" << EOF +${TODAY} * "ORDER" "BUY TSLA.US via harness demo" + ; intent_id: ${TS}-002 + ; side: BUY + ; symbol: TSLA.US + ; qty: 5 + ; type: MARKET + ; tif: DAY + ; source: rebalance + +EOF + +echo " Orders appended. Waiting for controller to execute..." +sleep 4 + +echo "" +echo " --- Beancount Ledger (last 40 lines) ---" +tail -n 40 "${FS_ROOT}/trade/beancount.txt" +echo "" + +# ------------------------------------------------------------------ # +# STEP 9: Stop controller and print summary +# ------------------------------------------------------------------ # +echo "==> [9/9] Stop controller" +touch "${FS_ROOT}/.kill" +sleep 3 +# Fallback: kill by PID if still running +if kill -0 "${CONTROLLER_PID}" 2>/dev/null; then + kill "${CONTROLLER_PID}" 2>/dev/null || true +fi + +echo "" +echo "================================================================" +echo " Pipeline Summary" +echo "================================================================" +echo "" +echo " File System Root : ${FS_ROOT}/" +echo "" +echo " Layer | Output File" +echo " ------------|---------------------------------------------------" +echo " L1 Research | ${FS_ROOT}/research/summary.json" +echo " | ${FS_ROOT}/research/feeds/news/AAPL.US/latest.json" +echo " L2 Signal | ${FS_ROOT}/signal/active.json" +echo " | ${FS_ROOT}/signal/output/AAPL.US/latest.json" +echo " L3 Portfolio| ${FS_ROOT}/portfolio/current.json" +echo " | ${FS_ROOT}/portfolio/diff.json" +echo " L4 Risk | ${FS_ROOT}/trade/risk/status.json" +echo " L5 Execution| ${FS_ROOT}/trade/beancount.txt" +echo "" +echo " To inspect any layer:" +echo " cat ${FS_ROOT}/signal/active.json" +echo " cat ${FS_ROOT}/portfolio/diff.json" +echo " cat ${FS_ROOT}/trade/beancount.txt" +echo "" +echo "================================================================" +echo " Demo complete! Run './demo_e2e.sh --clean' to start fresh." +echo "================================================================" diff --git a/internal/research/mock.go b/internal/research/mock.go new file mode 100644 index 0000000..f9c716f --- /dev/null +++ b/internal/research/mock.go @@ -0,0 +1,195 @@ +package research + +import ( + "encoding/json" + "fmt" + "math/rand" + "os" + "path/filepath" + "time" + + "longbridge-fs/internal/model" +) + +// mockNewsTitles contains sample news headlines for mock mode +var mockNewsTitles = []string{ + "Strong earnings beat analyst expectations", + "New product launch drives analyst upgrades", + "Market volatility creates buying opportunity", + "Institutional investors increase stake", + "Revenue growth accelerates in latest quarter", + "Strategic partnership announced with major player", + "Cost-cutting measures improve margins", + "Expansion into new markets on track", +} + +// mockTopicTitles contains sample discussion topics for mock mode +var mockTopicTitles = []string{ + "Is now a good time to buy?", + "Earnings preview: what to expect", + "Technical analysis shows bullish signal", + "Comparing sector peers performance", + "Long-term growth thesis intact", + "Short-term headwinds vs long-term opportunity", +} + +// RefreshFeedsMock generates synthetic research feeds without real API calls. +// Used in mock mode to enable full pipeline simulation. +func RefreshFeedsMock(root string) error { + watchlist, err := ParseWatchlist(root) + if err != nil { + if os.IsNotExist(err) { + return nil // No watchlist, skip + } + return fmt.Errorf("parse watchlist: %w", err) + } + + if len(watchlist.Symbols) == 0 { + return nil + } + + for _, symbol := range watchlist.Symbols { + for _, feed := range watchlist.Feeds { + switch feed { + case "news": + if err := writeMockNews(root, symbol); err != nil { + fmt.Printf("Warning: failed to write mock news for %s: %v\n", symbol, err) + } + case "topics": + if err := writeMockTopics(root, symbol); err != nil { + fmt.Printf("Warning: failed to write mock topics for %s: %v\n", symbol, err) + } + } + } + } + + return GenerateSummary(root) +} + +// writeMockNews writes synthetic news feed data for a symbol +func writeMockNews(root, symbol string) error { + now := time.Now().UTC() + // Pick a couple of headlines deterministically based on symbol + idx := len(symbol) % len(mockNewsTitles) + items := []model.NewsItem{ + { + ID: fmt.Sprintf("mock-news-%s-001", symbol), + Title: fmt.Sprintf("[%s] %s", symbol, mockNewsTitles[idx]), + Source: "mock", + PublishedAt: now.Add(-2 * time.Hour).Format(time.RFC3339), + Summary: fmt.Sprintf("Mock news item for %s generated at %s", symbol, now.Format(time.RFC3339)), + }, + { + ID: fmt.Sprintf("mock-news-%s-002", symbol), + Title: fmt.Sprintf("[%s] %s", symbol, mockNewsTitles[(idx+1)%len(mockNewsTitles)]), + Source: "mock", + PublishedAt: now.Add(-5 * time.Hour).Format(time.RFC3339), + Summary: fmt.Sprintf("Additional mock news item for %s", symbol), + }, + } + + feed := model.NewsFeed{ + Symbol: symbol, + FetchedAt: now.Format(time.RFC3339), + Items: items, + } + + newsDir := filepath.Join(root, "research", "feeds", "news", symbol) + if err := os.MkdirAll(newsDir, 0755); err != nil { + return err + } + + data, err := json.MarshalIndent(feed, "", " ") + if err != nil { + return err + } + + return os.WriteFile(filepath.Join(newsDir, "latest.json"), append(data, '\n'), 0644) +} + +// writeMockTopics writes synthetic topics feed data for a symbol +func writeMockTopics(root, symbol string) error { + now := time.Now().UTC() + idx := (len(symbol) + 1) % len(mockTopicTitles) + items := []model.TopicItem{ + { + ID: fmt.Sprintf("mock-topic-%s-001", symbol), + Title: fmt.Sprintf("[%s] %s", symbol, mockTopicTitles[idx]), + Source: "mock", + PublishedAt: now.Add(-1 * time.Hour).Format(time.RFC3339), + Summary: fmt.Sprintf("Mock community discussion for %s", symbol), + }, + } + + feed := model.TopicsFeed{ + Symbol: symbol, + FetchedAt: now.Format(time.RFC3339), + Items: items, + } + + topicsDir := filepath.Join(root, "research", "feeds", "topics", symbol) + if err := os.MkdirAll(topicsDir, 0755); err != nil { + return err + } + + data, err := json.MarshalIndent(feed, "", " ") + if err != nil { + return err + } + + return os.WriteFile(filepath.Join(topicsDir, "latest.json"), append(data, '\n'), 0644) +} + +// GenerateMockKlineData writes synthetic daily kline data for a symbol. +// Used in mock mode to enable signal computation without real quote data. +func GenerateMockKlineData(root, symbol string, days int) error { + holdDir := filepath.Join(root, "quote", "hold", symbol) + if err := os.MkdirAll(holdDir, 0755); err != nil { + return err + } + + klinePath := filepath.Join(holdDir, "D.json") + // Only generate if file doesn't exist yet + if _, err := os.Stat(klinePath); err == nil { + return nil + } + + bars := make([]model.Candlestick, days) + basePrice := 100.0 + float64(rand.Intn(400)) // random base 100–500 + now := time.Now().UTC() + + for i := 0; i < days; i++ { + day := now.AddDate(0, 0, -(days - 1 - i)) + // simple random walk with a slight upward drift (bias of 0.02% per day) + change := (rand.Float64() - 0.48) * 2.0 + basePrice = basePrice * (1 + change/100) + if basePrice < 1 { + basePrice = 1 + } + open := basePrice * (1 + (rand.Float64()-0.5)*0.01) + high := basePrice * (1 + rand.Float64()*0.02) + low := basePrice * (1 - rand.Float64()*0.02) + close_ := basePrice * (1 + (rand.Float64()-0.5)*0.01) + volume := int64(100000 + rand.Intn(900000)) + bars[i] = model.Candlestick{ + Date: day.Format("2006-01-02"), + Open: round2(open), + Close: round2(close_), + High: round2(high), + Low: round2(low), + Volume: volume, + Turnover: round2(float64(volume) * close_), + } + } + + data, err := json.MarshalIndent(bars, "", " ") + if err != nil { + return err + } + + return os.WriteFile(klinePath, append(data, '\n'), 0644) +} + +func round2(v float64) float64 { + return float64(int(v*100+0.5)) / 100 +} diff --git a/task.md b/task.md index f02859b..5be9bf7 100644 --- a/task.md +++ b/task.md @@ -48,6 +48,6 @@ 目标:降低 Agent 接入门槛 -- [ ] `.claude/skills/` 更新,覆盖五层操作 -- [ ] 端到端 demo 脚本 (研究 → 信号 → 组合 → 执行) -- [ ] Mock 模式支持全管道模拟 +- [x] `.claude/skills/` 更新,覆盖五层操作 +- [x] 端到端 demo 脚本 (研究 → 信号 → 组合 → 执行) +- [x] Mock 模式支持全管道模拟