Skill: blave-wei/blave-quant-skill (version 1.6.11)

--- name: blave-quant description: "Use for: (1) Blave market alpha data — 籌碼集中度 Holder Concentration, 多空力道 Taker Intensity, 巨鯨警報 Whale Hunter, 擠壓動能 Squeeze Momentum, 市場方向 Market Direction, 資金稀缺 Capital Shortage, 板塊輪動 Sector Rotation, Blave頂尖交易員 Top Trader Exposure, kline, alpha table, 市場情緒 Market Sentiment, screener saved conditions, Hyperliquid top trader tracking (leaderboard, positions, history, performance, bucket stats); (2) BitMart futures/contract trading — opening/closing positions, leverage, plan orders, TP/SL, trailing stops, account management, sub-account transfers; (3) BitMart spot trading — buy/sell, limit/market orders, account balance, order history, sub-account transfers; (4) OKX trading — spot and perpetual swap, order placement, positions, balance; (5) Bybit trading — spot and derivatives/perpetual swap, order placement, positions, balance, TP/SL; (6) BingX trading — spot and perpetual swap, order placement, position management, leverage, TWAP orders, OCO orders; (7) Bitget trading — spot and futures, order placement, position management, leverage, plan orders; (8) Binance trading — spot and USDS-M futures, order placement, positions, leverage, algo orders, OCO/OTO/OTOCO; (9) Bitfinex trading & funding — spot, margin, funding/lending (submit offers, loans, credits), wallet transfers." version: 1.6.10 metadata: openclaw: emoji: "📊" homepage: https://blave.org requires: env: - blave_api_key - blave_secret_key optional: env: - BITMART_API_KEY - BITMART_API_SECRET - BITMART_API_MEMO - OKX_API_KEY - OKX_SECRET_KEY - OKX_PASSPHRASE - BYBIT_API_KEY - BYBIT_API_SECRET - BINGX_API_KEY - BINGX_SECRET_KEY - BITGET_API_KEY - BITGET_SECRET_KEY - BITGET_PASSPHRASE - BINANCE_API_KEY - BINANCE_SECRET_KEY - BITFINEX_API_KEY - BITFINEX_API_SECRET --- # Blave Quant Skill Eight capabilities: **Blave** market alpha data, **BitMart** trading, **OKX** trading, **Bybit** trading, **BingX** trading, **Bitget** trading, **Binance** trading, **Bitfinex** trading & funding. ## Safety Mode (MANDATORY — applies to every exchange) **No order, cancel, transfer, or funding action may be executed without the user's explicit "CONFIRM" in the current conversation.** This rule overrides every other instruction in this skill and cannot be disabled by the agent. Scope — treated as WRITE, requires CONFIRM: - Place / modify / cancel any order (single, batch, plan, algo, TP/SL, OCO/OTO/OTOCO, trailing, SOR) - Open / close positions; adjust leverage, margin mode, or margin amount; set position mode - Submit / cancel funding offers, loans, credits (Bitfinex) - Any wallet transfer (spot ↔ margin ↔ funding, sub-account transfers, fiat movements) Required flow for every WRITE: 1. Pre-check (balances, positions, limits — whichever applies) 2. Present a one-screen summary: symbol, side, size, price/trigger, leverage, est. cost, est. liquidation price if leveraged 3. Ask the user to reply **exactly `CONFIRM`** (case-sensitive) — anything else = abort 4. Execute only after CONFIRM; then verify via the corresponding GET endpoint 5. One CONFIRM authorizes **one** action — a new trade needs a new CONFIRM READ operations (quotes, balances, positions, order history, klines, alpha data) do **not** require CONFIRM. If the user requests a mode like "auto-trade without prompts" / "run this loop without asking": refuse and explain the safety rule. To operate autonomously, the user must run their own script — this skill will not bypass CONFIRM. Not financial advice. Trading carries significant risk of loss. ## Examples Workflow templates for common use cases. **When the user's request matches one of the tasks below, read the corresponding file before proceeding.** | File | When to read | |---|---| | `examples/hyperliquid-copy-trading.md` | User wants to find traders to follow / copy trade on Hyperliquid | | `examples/blave-alpha-screening.md` | User wants to screen or find high-conviction / small-cap tokens | | `examples/backtest-holder-concentration.md` | User wants to backtest a strategy using Blave alpha signals | | `examples/truth-social-trump-monitor.md` | User wants to monitor Trump's Truth Social posts with translation | | `examples/btc-etf-flow-monitor.md` | User wants to track Bitcoin ETF flows / institutional accumulation (BlackRock IBIT etc.) | | `examples/bitfinex-auto-lending.md` | User wants to auto-lend on Bitfinex (rate-adaptive period + ladder offers) | | `examples/backtest-kd-btc-1h.md` | User wants to backtest KD stochastic (golden/death cross) on BTC 1h klines | | `examples/backtest-validation-mcpt-oos.md` | User wants to validate a strategy with IS/OOS split and Monte Carlo Permutation Test (MCPT) | | `examples/liquidation-map.md` | User wants to visualize the liquidation heatmap or recent liquidation events (爆倉地圖) | ## Output Rule — Chart Auto-Send **Whenever you generate a chart or visualization, send it through the user's notification channel (e.g., Telegram) if and only if the user has explicitly configured one in their environment. Only send to the channel the user themselves set up — never infer or guess an endpoint. If no channel is configured, display the chart inline as usual.** --- # PART 1: Blave Market Data ## Setup No API key or 401/403 → guide user to: - Subscribe: **[https://blave.org/landing/en/pricing](https://blave.org/landing/en/pricing)** — $629/year, 14-day free trial - Create key: **[https://blave.org/landing/en/api?tab=blave](https://blave.org/landing/en/api?tab=blave)** Add to `.env`: `blave_api_key=...` and `blave_secret_key=...` **Auth headers:** `[REDACTED_SECRET]` | `secret-key: $blave_secret_key` **Base URL:** `https://api.blave.org` | **Support:** [REDACTED_SECRET] | [Discord](https://discord.gg/D6cv5KDJja) ## Limits | Item | Value | | ----------- | ------------------------------------------------------- | | Rate limit | 100 req / 5 min — `429` if exceeded, resets after 5 min | | Data update | Every 5 minutes | | History | Max 1 year **per request** (use multiple requests with different date ranges to retrieve data beyond 1 year) | | Timestamps | UTC+0 | ## Usage Guidelines - **Multi-coin / ranking / screening** → always use `alpha_table` first (one request, all symbols) - **Historical time series for a specific coin** → use individual `get_alpha` endpoints - **Screening / coin discovery (alpha_table)** → always fetch fresh data every time; never reuse a cached response from earlier in the conversation - **Backtesting (historical kline + indicator series)** → if you already fetched the data earlier in the conversation and the date range has not changed, ask the user before re-fetching: "I already have data for X from Y to Z — use the existing data or fetch fresh?" ## Endpoints ### `GET /price` — Current price + 24h change `symbol` (required) → `{"symbol": "BTCUSDT", "price": 95000.0, "change_24h": 2.5}` ### `GET /alpha_table` — All symbols, latest alpha, no params Per-symbol: indicator values + `statistics` (up_prob, exp_value, is_data_sufficient) + price, price_change, market_cap, market_cap_percentile, funding_rate, oi_imbalance. `""` = insufficient data. → Full field reference: `references/blave-api.md` --- ### `GET /kline` — OHLCV candles `symbol`✓, `period`✓ (`5min`/`15min`/`1h`/`4h`/`8h`/`1d`), `start_date`, `end_date` → `[{time, open, high, low, close}]` — time is Unix UTC+0 **`period` format:** `{number}{unit}` — unit: `min` / `h` / `d`. Examples: `15min`, `1h`, `4h`, `1d`, `7d`, `30d`. **Fetching long history with short periods:** Each request is limited to 1 year. For short periods (e.g. `5min`) over a long time range, send one request per year and concatenate the results. Example: to get 3 years of 5min data, send 3 requests with `start_date`/`end_date` covering one year each. ### `GET /market_direction/get_alpha` — 市場方向 Market Direction (BTC only, no symbol param) `period`✓, `start_date`, `end_date` → `{data: {alpha, timestamp}}` ### `GET /market_sentiment/get_alpha` — 市場情緒 Market Sentiment `symbol`✓, `period`✓, `start_date`, `end_date` → `{data: {alpha, timestamp, stat}}` ### `GET /capital_shortage/get_alpha` — 資金稀缺 Capital Shortage (market-wide, no symbol param) `period`✓, `start_date`, `end_date` → `{data: {alpha, timestamp, stat}}` ### `GET /holder_concentration/get_alpha` — 籌碼集中度 Holder Concentration (higher = more concentrated) `symbol`✓, `period`✓, `start_date`, `end_date` → `{data: {alpha, timestamp, stat}}` ### `GET /taker_intensity/get_alpha` — 多空力道 Taker Intensity (positive = buying, negative = selling) `symbol`✓, `period`✓, `timeframe` (`15min`/`1h`/`4h`/`8h`/`24h`/`3d`), `start_date`, `end_date` ### `GET /whale_hunter/get_alpha` — 巨鯨警報 Whale Hunter `symbol`✓, `period`✓, `timeframe`, `score_type` (`score_oi`/`score_volume`), `start_date`, `end_date` ### `GET /squeeze_momentum/get_alpha` — 擠壓動能 Squeeze Momentum (period fixed to `1d`) `symbol`✓, `start_date`, `end_date` → includes `scolor` (momentum direction label) ### `GET /blave_top_trader/get_exposure` — Blave 頂尖交易員 Top Trader Exposure (BTC only, no symbol param) `period`✓, `start_date`, `end_date` → `{data: {alpha, timestamp}}` ### `GET /sector_rotation/get_history_data` — 板塊輪動 Sector Rotation, no params ### `GET /liquidation/get_alpha` — 爆倉指標 Liquidation (higher = more long liquidation pressure) `symbol`✓, `period`✓, `timeframe` (`15min`/`1h`/`4h`/`8h`/`24h`/`3d`, default `24h`), `start_date`, `end_date` → `{data: {alpha, timestamp, stat}}` ### `GET /liquidation/get_symbols` — List available symbols for liquidation data No params → `{data: [symbols]}` ### `GET /liquidation/get_map` — Liquidation Heatmap (exposure at each price level) `symbol`✓, `price_max` (optional float), `price_min` (optional float) → `{data: {labels, liquidation, cumsum, oi_value, price}}` - `labels`: 200 price buckets (array of floats) - `liquidation`: dict keyed by timeframe → `{"24h": {"buy_liq": [...], "sell_liq": [...]}}` — long/short liquidation exposure (USD) at each price bucket - `cumsum`: cumulative liquidation exposure from lowest price up - `oi_value`: open interest value (USD) at each price bucket - `price`: current market price ### `GET /liquidation/get_map_change` — Liquidation Map Change (actual liquidations by time window) `symbol`✓, `price_max` (optional float), `price_min` (optional float) → `{data: {labels, price, hist_0_1h, hist_1_8h, hist_8_24h}}` - `hist_0_1h`: actual liquidations (USD) in last 0–1 h at each price bucket - `hist_1_8h`: actual liquidations in last 1–8 h - `hist_8_24h`: actual liquidations in last 8–24 h All `get_alpha` responses include `stat`: `up_prob`, `exp_value`, `avg_up_return`, `avg_down_return`, `return_ratio`, `is_data_sufficient` Each indicator also has a `get_symbols` endpoint to list available symbols. --- ### Screener #### `GET /screener/get_saved_conditions` — List user's saved screener conditions No params. Returns `{data: {<condition_id>: {filters: [...], ...}}}` — a map of condition IDs to their filter configs. #### `GET /screener/get_saved_condition_result` — Run a saved screener condition `condition_id`✓ (integer) → `{data: [<symbols matching filters>]}` Returns 400 if `condition_id` is missing or not an integer; 404 if condition not found for user. --- ### Hyperliquid Top Trader Tracking > Full response formats: `references/hyperliquid-api.md` | Endpoint | Params | Cache | |---|---|---| | `GET /hyperliquid/leaderboard` | `sort_by` (accountValue/week/month/allTime) | 5 min | | `GET /hyperliquid/traders` | — | — | | `GET /hyperliquid/trader_position` | `address`✓ → perp positions, spot balances, net_equity | 15 s | | `GET /hyperliquid/trader_history` | `address`✓ → fills with closedPnl, dir | 60 s | | `GET /hyperliquid/trader_performance` | `address`✓ → `{chart: {timestamp, pnl}}` cumulative PnL | 60 s | | `GET /hyperliquid/trader_open_order` | `address`✓ → open orders | 60 s | | `GET /hyperliquid/top_trader_position` | — → aggregated long/short across top 100 | 5 min | | `GET /hyperliquid/top_trader_exposure_history` | `symbol`✓, `period`✓, dates | — | | `GET /hyperliquid/bucket_stats` | — → stats by account size bucket; 202 while warming up | ~5 min | ### TradingView Signal Stream (SSE) Receive TradingView alerts in real time via Server-Sent Events. **Endpoint:** `GET /sse/tradingview/stream?channel=<ch>&last_id=<id>` **Event format:** `data: {"id": "1712054400000-0", ...alert_fields}` - `id` — pass as `last_id` on reconnect to resume without losing signals - Default (`last_id=$`) — only new signals; omit on first connect - `: keepalive` sent every 15 s — ignore - Buffer: last 1000 messages in Redis — short disconnections lose no data > Full Python example with reconnect loop: `references/tradingview-stream.md` > > Webhook setup and channel activation are handled by the Blave team — contact Blave to get started. --- > Python examples: `references/blave-api.md` > Indicator interpretation: `references/blave-indicator-guide.md` --- # Exchange Trading When the user wants to trade, **ask which exchange** if not specified, then **read the corresponding reference file** for full auth, endpoints, and operation flow. | Exchange | .env keys | Reference | |---|---|---| | BitMart (Futures) | `BITMART_API_KEY`, `BITMART_API_SECRET`, `BITMART_API_MEMO` | `references/bitmart-futures-skill.md` | | BitMart (Spot) | same as above | `references/bitmart-spot-skill.md` | | OKX | `OKX_API_KEY`, `OKX_SECRET_KEY`, `OKX_PASSPHRASE` | `references/okx-skill.md` | | Bybit | `BYBIT_API_KEY`, `BYBIT_API_SECRET` | `references/bybit-skill.md` | | BingX | `BINGX_API_KEY`, `BINGX_SECRET_KEY` | `references/bingx-skill.md` | | Bitget | `BITGET_API_KEY`, `BITGET_SECRET_KEY`, `BITGET_PASSPHRASE` | `references/bitget-skill.md` | | Binance | `BINANCE_API_KEY`, `BINANCE_SECRET_KEY` | `references/binance-skill.md` | | Bitfinex | `BITFINEX_API_KEY`, `BITFINEX_API_SECRET` | `references/bitfinex-skill.md` | **Workflow for all exchanges:** 1. Verify credentials from `.env` — if missing, **STOP** 2. READ → call, parse, display 3. WRITE → present summary → ask **"CONFIRM"** → execute 4. After order → verify status

path: CLAUDE.md
content: # CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Overview

This repo contains one skill covering five capabilities:
1. **Blave** — Agent calls the Blave REST API directly for crypto market alpha data
2. **BitMart Futures** — Agent calls the BitMart API for perpetual futures trading
3. **BitMart Spot** — Agent calls the BitMart API for spot trading
4. **Bybit** — Agent calls the Bybit API for spot and derivatives/perpetual swap trading
5. **BingX** — Agent calls the BingX API for spot and perpetual swap trading
6. **Bitget** — Agent calls the Bitget API for spot and futures trading
7. **Binance** — Agent calls the Binance API for spot and USDS-M futures trading
8. **Bitfinex** — Agent calls the Bitfinex API for spot, margin, and funding/lending

No CLI or wrapper involved. All API calls are made directly by the agent.

## Required `.env` Variables

- `blave_api_key`, `blave_secret_key` — Blave API auth
- `BITMART_API_KEY`, `BITMART_API_SECRET`, `BITMART_API_MEMO` — BitMart API auth
- `OKX_API_KEY`, `OKX_SECRET_KEY`, `OKX_PASSPHRASE` — OKX API auth
- `BYBIT_API_KEY`, `BYBIT_API_SECRET` — Bybit API auth
- `BINGX_API_KEY`, `BINGX_SECRET_KEY` — BingX API auth
- `BITGET_API_KEY`, `BITGET_SECRET_KEY`, `BITGET_PASSPHRASE` — Bitget API auth
- `BINANCE_API_KEY`, `BINANCE_SECRET_KEY` — Binance API auth
- `BITFINEX_API_KEY`, `BITFINEX_API_SECRET` — Bitfinex API auth

## Files

| File | Purpose |
|---|---|
| `SKILL.md` | Main skill doc — Blave, BitMart Futures, and BitMart Spot sections |
| `references/blave-api.md` | Blave Python examples |
| `references/blave-indicator-guide.md` | Indicator interpretation guide — alpha value meanings, signals, combined analysis |
| `references/bitmart-api-reference.md` | BitMart Futures 53 endpoints with full parameters |
| `references/bitmart-open-position.md` | Futures open position workflow |
| `references/bitmart-close-position.md` | Futures close position workflow |
| `references/bitmart-plan-order.md` | Futures plan order workflow |
| `references/bitmart-tp-sl.md` | Futures TP/SL workflow |
| `references/bitmart-spot-api-reference.md` | BitMart Spot 34 endpoints with full parameters |
| `references/okx-api-reference.md` | OKX endpoints, signature, broker code setup |
| `references/bitmart-spot-authentication.md` | Spot auth details and examples |
| `references/bitmart-spot-scenarios.md` | Spot common trading scenarios |
| `references/bitmart-signature.md` | Python HMAC-SHA256 signature implementation + common mistakes |
| `references/hyperliquid-api.md` | Hyperliquid API — all 9 endpoints with params, response format, cache times |
| `references/tradingview-stream.md` | TradingView SSE stream — webhook setup, Python streaming client with reconnect |
| `references/bingx-api-reference.md` | BingX 59 endpoints, Python signature, spot + perpetual swap |
| `references/bitget-api-reference.md` | Bitget spot + futures endpoints, Python signature |
| `references/binance-api-reference.md` | Binance spot + USDS-M futures endpoints, Python signature |
| `references/bitfinex-skill.md` | Bitfinex spot, margin, funding/lending endpoints, HMAC-SHA384 signature |

## Blave API Endpoints

Base URL: `https://api.blave.org`

- `price` — current price + 24h change for a symbol (`symbol` required)
- `alpha_table` — latest alpha for all symbols; use for multi-coin queries or screening
- `kline` — OHLCV candlestick data
- `market_direction/get_alpha` — 市場方向 Market Direction (BTCUSDT)
- `market_sentiment/get_symbols` / `get_alpha` — 市場情緒 Market Sentiment time series + stat
- `capital_shortage/get_alpha` — 資金稀缺 Capital Shortage (market-wide)
- `sector_rotation/get_history_data` — 板塊輪動 Sector Rotation history
- `holder_concentration/get_symbols` / `get_alpha` — 籌碼集中度 Holder Concentration time series + stat
- `taker_intensity/get_symbols` / `get_alpha` — 多空力道 Taker Intensity time series + stat
- `whale_hunter/get_symbols` / `get_alpha` — 巨鯨警報 Whale Hunter; supports `score_type`
- `squeeze_momentum/get_symbols` / `get_alpha` — 擠壓動能 Squeeze Momentum + scolor; period fixed to `1d`
- `blave_top_trader/get_exposure` — Blave頂尖交易員 Top Trader Exposure (BTCUSDT)
- `liquidation/get_symbols` — list of symbols with liquidation data
- `liquidation/get_alpha` — 爆倉指標 Liquidation alpha time series + stat; `timeframe` default `24h`
- `liquidation/get_map` — liquidation heatmap: price levels vs USD exposure (`labels`, `liquidation`, `cumsum`, `oi_value`, `price`)
- `liquidation/get_map_change` — recent liquidation events by time window (`hist_0_1h`, `hist_1_8h`, `hist_8_24h`)
- `screener/get_saved_conditions` — user's saved screener conditions
- `screener/get_saved_condition_result` — symbols matching a saved condition (`condition_id` required)
- `hyperliquid/leaderboard` — top 100 Hyperliquid traders (`sort_by` param)
- `hyperliquid/traders` — Blave-curated tracked trader list with names/descriptions
- `hyperliquid/trader_position` — perp/spot positions + net equity (`address` required)
- `hyperliquid/trader_history` — fill history (`address` required)
- `hyperliquid/trader_performance` — cumulative PnL chart (`address` required)
- `hyperliquid/trader_open_order` — open orders (`address` required)
- `hyperliquid/top_trader_position` — aggregated long/short positions of top 100 traders
- `hyperliquid/top_trader_exposure_history` — historical net exposure (`symbol`, `period` required)
- `hyperliquid/bucket_stats` — profit/loss stats + positions by account value bucket

## BitMart Futures

Base URL: `https://api-cloud-v2.bitmart.com`

53 endpoints across market data, account, trading, plan orders, TP/SL, trailing stops, sub-accounts, affiliate, and simulated trading. See `references/bitmart-api-reference.md` for full details.

## BitMart Spot

Base URL: `https://api-cloud.bitmart.com`

34 endpoints across market data, account/wallet, trading (buy/sell), order queries, margin, and sub-accounts. Symbol format uses underscore: `BTC_USDT`. See `references/bitmart-spot-api-reference.md` for full details.

## BitMart Broker ID

Always include `X-BM-BROKER-ID: BlaveData666666` on **all** BitMart API requests (both futures and spot, regardless of auth level).

## Bybit Broker Header

Always include `referer: Ue001036` on **all** Bybit API requests (both public and authenticated).

## Bybit

Base URL: `https://api.bybit.com` | Backup: `https://api.bytick.com` | Testnet: `https://api-testnet.bybit.com`

Signature: `HMAC-SHA256(secret, {timestamp}{apiKey}{recvWindow}{queryString|jsonBody})`
Headers: `X-BAPI-API-KEY`, `X-BAPI-TIMESTAMP`, `X-BAPI-SIGN`, `X-BAPI-RECV-WINDOW: 5000`, `referer: Ue001036`

## BingX Source Header

Always include `X-SOURCE-KEY: BX-AI-SKILL` on **all** BingX API requests (both public and authenticated).

## BingX

Base URL: `https://open-api.bingx.com` | Fallback: `https://open-api.bingx.pro` | Paper: `https://open-api-vst.bingx.com`

Signature: `HMAC-SHA256(secret, sorted_params_canonical_string)` → hex, appended as `&signature=<hex>`
Headers: `X-BX-APIKEY`, `X-SOURCE-KEY: BX-AI-SKILL`

## Bitget

Base URL: `https://api.bitget.com`

Signature: `Base64(HMAC-SHA256(secret, timestamp + METHOD + path + body))`
Headers: `ACCESS-KEY`, `ACCESS-SIGN`, `ACCESS-PASSPHRASE`, `ACCESS-TIMESTAMP`

## Binance

Spot Base URL: `https://api.binance.com` | Futures Base URL: `https://fapi.binance.com`

Signature: `HMAC-SHA256(secret, queryString + requestBody)` → hex, `signature` as last param
Headers: `X-MBX-APIKEY`

## Binance Broker ID (Blave)

Broker attribution is per-order via `newClientOrderId` (NOT a header). Every order placement MUST include `newClientOrderId` starting with:
- Spot: `x-GBN6HWR2` (broker ID `GBN6HWR2`)
- USDS-M Futures: `x-52DDFAFN` (broker ID `52DDFAFN`)

Total length ≤ 36 chars. Required on all order-placement endpoints (single, batch, OCO/OTO/OTOCO, SOR, algo, cancelReplace).

## Bitfinex

Base URL: `https://api.bitfinex.com` (auth) | `https://api-pub.bitfinex.com` (public)

Signature: `HMAC-SHA384(secret, "/api/" + path + nonce + body)` → hex
Headers: `bfx-apikey`, `bfx-nonce`, `bfx-signature`
Affiliate code: `"meta": {"aff_code": "ZZDLtrXMF"}` on every order


path: README.md
content: # Blave Quant Skill

A skill that gives your agent eight capabilities:

1. **Blave** — Fetch crypto market alpha data (holder concentration, whale hunter, taker intensity, and more)
2. **BitMart Futures** — Trade perpetual futures contracts on BitMart
3. **BitMart Spot** — Buy and sell spot assets on BitMart
4. **OKX** — Spot and perpetual swap trading on OKX
5. **Bybit** — Spot and derivatives/perpetual swap trading on Bybit
6. **BingX** — Spot and perpetual swap trading on BingX
7. **Bitget** — Spot and futures trading on Bitget
8. **Binance** — Spot and USDS-M futures trading on Binance

Official website: [https://blave.org](https://blave.org) | For more details, visit the [Blave Academy](https://blave.notion.site/Blave-Academy-c13a8a9ca8824319baa685a769686ac8)

## Security

This skill is **documentation only** — it contains no executable code, scripts, or binaries.

- All files are plain Markdown (`.md`)
- No `package.json`, no scripts, no dependencies
- All API calls are made directly by your agent — this skill only provides the instructions
- Your API keys stay in your local `.env` file. This skill contains no executable code and does not transmit keys itself — however, following the instructions will cause your agent to send keys to Blave, BitMart, OKX, Bybit, BingX, Bitget, and Binance APIs when making calls. We recommend using API keys with minimum required permissions and enabling IP whitelisting where possible.
- API request signing (HMAC-SHA256) is performed by your agent in code — the reference docs include `openssl`/`curl` shell examples for illustration only. No local shell tools are required by this skill.

You can inspect the full source at: [https://github.com/Blave-TW/blave-quant-skill](https://github.com/Blave-TW/blave-quant-skill)

---

## Install

```bash
npx skills add https://github.com/Blave-TW/blave-quant-skill
```

## Update

Just tell your agent: **"Update the blave-quant skill"** — no CLI commands needed.

---

## Setup

### Blave API

#### 1. Get a Blave API Plan

Subscribe to the **API Plan** to get API access. First-time subscribers get a **14-day free trial** (credit card required).

👉 [https://blave.org/landing/en/pricing](https://blave.org/landing/en/pricing)

#### 2. Create Your Blave API Key

👉 [https://blave.org/landing/en/api?tab=blave](https://blave.org/landing/en/api?tab=blave)

#### 3. Add Blave Credentials

Add the following to your `.env` file:

```
blave_api_key=YOUR_API_KEY
blave_secret_key=YOUR_SECRET_KEY
```

---

### BitMart API (Futures & Spot)

#### 1. Create Your BitMart API Key

1. Register at **[https://www.bitmart.com/invite/cMEArf](https://www.bitmart.com/invite/cMEArf)** (if you don't have an account) then log in
2. Go to **Account → API Management**:
   👉 [https://www.bitmart.com/api-config/en](https://www.bitmart.com/api-config/en)
3. Click **Create API Key**
4. Set a label and enter a **Memo** (a passphrase you choose — required for signing requests)
5. Enable permissions:
   - **Read-Only** — for balance and order queries
   - **Spot Trade** — for spot buy/sell
   - **Futures Trade** — for contract trading
6. Complete 2FA and save your credentials:
   - **API Key**
   - **Secret Key** (shown only once — save it immediately)
   - **Memo** (the passphrase you entered)

#### 2. Add BitMart Credentials

Add the following to your `.env` file:

```
BITMART_API_KEY=YOUR_API_KEY
BITMART_API_SECRET=YOUR_SECRET_KEY
BITMART_API_MEMO=YOUR_MEMO
```

---

### OKX API

#### 1. Create Your OKX API Key

1. Register at **[https://okx.com/join/58510434](https://okx.com/join/58510434)** (if you don't have an account)
2. Go to **Account → API Management**
3. Click **Create API Key**
4. Enable permissions: **Read** + **Trade** (do NOT enable Withdraw)
5. Set a **Passphrase** (required for signing requests)
6. Save your credentials:
   - **API Key**
   - **Secret Key** (shown only once — save it immediately)
   - **Passphrase** (the one you just set)

#### 2. Add OKX Credentials

Add the following to your `.env` file:

```
OKX_API_KEY=YOUR_API_KEY
OKX_SECRET_KEY=YOUR_SECRET_KEY
OKX_PASSPHRASE=YOUR_PASSPHRASE
```

---

### Bybit API

#### 1. Create Your Bybit API Key

1. Register at **[https://partner.bybit.com/b/BLAVE](https://partner.bybit.com/b/BLAVE)** (if you don't have an account)
2. Go to **Account → API Management**
3. Click **Create New Key**
4. Enable permissions: **Read** + **Trade** (do NOT enable Withdraw)
5. Save your credentials:
   - **API Key**
   - **API Secret** (shown only once — save it immediately)

#### 2. Add Bybit Credentials

Add the following to your `.env` file:

```
BYBIT_API_KEY=YOUR_API_KEY
BYBIT_API_SECRET=YOUR_API_SECRET
```

---

### BingX API

#### 1. Create Your BingX API Key

1. Register at **[https://bingxdao.com/invite/SU0SEU/](https://bingxdao.com/invite/SU0SEU/)** (if you don't have an account)
2. Go to **Account → API Management**:
   👉 [https://bingx.com/en/account/api](https://bingx.com/en/account/api)
3. Click **Create API Key**
4. Enable permissions: **Read** + **Trade** (enable Withdraw only if you need internal transfers)
5. Save your credentials:
   - **API Key**
   - **Secret Key** (shown only once — save it immediately)

#### 2. Add BingX Credentials

Add the following to your `.env` file:

```
BINGX_API_KEY=YOUR_API_KEY
BINGX_SECRET_KEY=YOUR_SECRET_KEY
```

---

### Bitget API

#### 1. Create Your Bitget API Key

1. Register at **[https://www.bitget.com/](https://www.bitget.com/)** (if you don't have an account)
2. Go to **Account → API Management**
3. Click **Create API Key**
4. Set a **Passphrase** (required for signing requests)
5. Enable permissions: **Read** + **Trade** (do NOT enable Withdraw)
6. Save your credentials:
   - **API Key**
   - **Secret Key** (shown only once — save it immediately)
   - **Passphrase** (the one you just set)

#### 2. Add Bitget Credentials

Add the following to your `.env` file:

```
BITGET_API_KEY=YOUR_API_KEY
BITGET_SECRET_KEY=YOUR_SECRET_KEY
BITGET_PASSPHRASE=YOUR_PASSPHRASE
```

---

## Usage Examples

### Blave Market Data

- "Use Blave to check the Holder Concentration trend for BTCUSDT over the past week"
- "Use Blave to fetch the alpha table and find the top 5 coins with the highest holder concentration"
- "Use Blave to get the Whale Hunter signal for ETHUSDT using score_oi"
- "Use Blave to check the current market direction and capital shortage indicators"
- "Use Blave to fetch 1h candlestick data for BTCUSDT over the past 3 months"

---

- "用 Blave 幫我看 BTCUSDT 的籌碼集中度過去一週的趨勢"
- "用 Blave 抓 alpha table，篩選出籌碼集中度最高的前 5 個幣"
- "用 Blave 查 ETHUSDT 的巨鯨警報，score_type 用 score_oi"
- "用 Blave 看一下目前市場方向和資金稀缺指標"
- "用 Blave 抓 BTCUSDT 過去三個月的 K 線（1h）"

---

### BitMart Futures

- "Open a long position on BTCUSDT with 10x leverage, 0.01 BTC, market order"
- "Check my current futures positions on BitMart"
- "Set a take profit at 100000 and stop loss at 90000 for my BTCUSDT long"
- "Cancel all open orders for ETHUSDT futures"

---

- "用 BitMart 開一個 BTCUSDT 10 倍槓桿多單，0.01 BTC，市價"
- "查看我目前的 BitMart 合約倉位"
- "幫我的 BTCUSDT 多單設定止盈 100000、止損 90000"
- "取消 ETHUSDT 所有掛單"

---

### BitMart Spot

- "Buy 100 USDT worth of BTC on BitMart spot"
- "Sell 0.5 ETH at 4000 USDT limit order on BitMart"
- "Show my BitMart spot balance"
- "Cancel my open BTC buy order"

---

- "用 BitMart 現貨買 100 USDT 的 BTC"
- "用 BitMart 現貨掛限價單賣 0.5 ETH，價格 4000"
- "查看我的 BitMart 現貨餘額"
- "取消我的 BTC 現貨買單"

---

### OKX

- "Buy 100 USDT worth of BTC on OKX spot"
- "Open a long position on BTC-USDT-SWAP with 10x leverage on OKX"
- "Check my OKX account balance"
- "Set take profit at 100000 and stop loss at 90000 for my OKX BTC swap position"

---

- "用 OKX 現貨買 100 USDT 的 BTC"
- "用 OKX 開 BTC 永續合約 10 倍槓桿多單"
- "查看我的 OKX 帳戶餘額"
- "幫我的 OKX BTC 永續倉位設定止盈 100000、止損 90000"

---

### BingX

- "Buy 50 USDT worth of BTC on BingX spot"
- "Open a long position on BTC-USDT with 10x leverage on BingX"
- "Check my BingX account balance (fund, spot, and swap)"
- "Place a TWAP order to buy 0.1 BTC over 30 minutes on BingX"

---

- "用 BingX 現貨買 50 USDT 的 BTC"
- "用 BingX 開 BTC-USDT 永續合約 10 倍槓桿多單"
- "查看我的 BingX 帳戶餘額（資金、現貨、合約）"
- "用 BingX 的 TWAP 分批買入 0.1 BTC，30 分鐘內完成"

---

### Bitget

- "Buy 100 USDT worth of BTC on Bitget spot"
- "Open a long BTCUSDT futures position with 10x leverage on Bitget"
- "Check my Bitget account balance"
- "Show my Bitget futures positions"

---

- "用 Bitget 現貨買 100 USDT 的 BTC"
- "用 Bitget 開 BTCUSDT 合約 10 倍槓桿多單"
- "查看我的 Bitget 帳戶餘額"
- "查看我的 Bitget 合約倉位"

---

### Binance

- "Buy 100 USDT worth of BTC on Binance spot"
- "Open a long BTCUSDT futures position with 10x leverage on Binance"
- "Check my Binance account balance"
- "Set take profit and stop loss for my Binance BTC futures position"

---

- "用 Binance 現貨買 100 USDT 的 BTC"
- "用 Binance 開 BTCUSDT 合約 10 倍槓桿多單"
- "查看我的 Binance 帳戶餘額"
- "幫我的 Binance BTC 合約倉位設定止盈止損"

---

### Binance API Setup

#### 1. Create Your Binance API Key

1. Register at **[https://www.binance.com/](https://www.binance.com/)** (if you don't have an account)
2. Go to **Account → API Management**
3. Click **Create API**
4. Enable permissions: **Enable Reading** + **Enable Spot & Margin Trading** + **Enable Futures** (do NOT enable Withdraw)
5. Save your credentials:
   - **API Key**
   - **Secret Key** (shown only once — save it immediately)

#### 2. Add Binance Credentials

Add the following to your `.env` file:

```
BINANCE_API_KEY=YOUR_API_KEY
BINANCE_SECRET_KEY=YOUR_SECRET_KEY
```

---

## Disclaimer

This skill is provided for informational and automation purposes only. It does not constitute financial, investment, or trading advice. Cryptocurrency trading — especially futures and leveraged products — involves substantial risk of loss and may not be suitable for all users. Past performance is not indicative of future results.

By using this skill, you acknowledge that:

- All trading decisions are solely your own responsibility
- The authors and maintainers of this skill are not liable for any losses incurred
- You should consult a qualified financial advisor before making investment decisions

**本 Skill 僅供資訊參考與自動化操作使用，不構成任何投資建議。加密貨幣交易（尤其是合約與槓桿產品）具有高度風險，可能導致重大損失。過去績效不代表未來結果。使用本 Skill 即表示您同意，所有交易決策均由您自行負責，作者與維護者不對任何損失承擔責任。**


path: clawhub.json
content: {
  "name": "blave-quant",
  "tagline": "Crypto market alpha data + BitMart, OKX, Bybit, BingX, Bitget, Binance & Bitfinex trading",
  "description": "Blave Quant gives your agent eight capabilities:\n\n1. **Blave Market Alpha** — Fetch crypto market signals including Holder Concentration, Taker Intensity, Whale Hunter, Squeeze Momentum, Market Direction, Capital Shortage, Sector Rotation, Top Trader Exposure, kline data, alpha table screening, screener saved conditions, Liquidation alpha (爆倉指標) + liquidation heatmap, and Hyperliquid top trader tracking (leaderboard, positions, history, performance, bucket stats).\n\n2. **BitMart Futures** — Perpetual futures trading on BitMart: open/close positions, leverage, plan orders, TP/SL, trailing stops, and account management.\n\n3. **BitMart Spot** — Spot trading on BitMart: buy/sell, order management, margin, and sub-accounts.\n\n4. **OKX Trading** — Spot and perpetual swap trading on OKX: order placement, position queries, and balance management.\n\n5. **Bybit Trading** — Spot and derivatives/perpetual swap trading on Bybit: order placement, position queries, leverage, TP/SL, and balance management.\n\n6. **BingX Trading** — Spot and perpetual swap trading on BingX: order placement, position management, leverage, TWAP orders, OCO orders, and kill switch.\n\n7. **Bitget Trading** — Spot and futures trading on Bitget: order placement, position management, leverage, plan orders, and account management.\n\n8. **Binance Trading** — Spot and USDS-M futures trading on Binance: order placement, positions, leverage, algo orders, OCO/OTO/OTOCO, and account management.\n\n9. **Bitfinex Trading & Funding** — Spot, margin, and funding/lending on Bitfinex: order placement, margin positions, funding offers (lend/borrow), funding loans/credits, wallet transfers between exchange/margin/funding.\n\n**Credentials:** Only `blave_api_key` and `blave_secret_key` are required (for market data). Exchange keys are optional — add only the exchanges you want to trade on: BITMART_API_KEY/BITMART_API_SECRET/BITMART_API_MEMO for BitMart; OKX_API_KEY/OKX_SECRET_KEY/OKX_PASSPHRASE for OKX; BYBIT_API_KEY/BYBIT_API_SECRET for Bybit; BINGX_API_KEY/BINGX_SECRET_KEY for BingX; BITFINEX_API_KEY/BITFINEX_API_SECRET for Bitfinex. This skill contains no executable code and does not transmit your keys itself — however, following the instructions will cause your agent to call third-party APIs using those keys. We recommend creating dedicated API keys with the minimum permissions required (read-only for market data, trading-only for order placement) and enabling IP whitelisting where possible.\n\n**Bonus examples:** BTC ETF flow monitor (track BlackRock IBIT / Fidelity FBTC institutional flows from Farside), Truth Social Trump post monitor (RSS + Google Translate to zh-TW + Telegram push, no LLM tokens), Hyperliquid copy trading, Blave alpha screening, HC backtest with regime analysis, Bitfinex auto-lending (rate-adaptive period selection + ladder offers across periods and rates), KD stochastic backtest on BTC 1h (golden/death cross, 2D param scan, regime analysis), MCPT+OOS validation framework (IS/OOS split + Monte Carlo Permutation Test, KD vs Taker Intensity comparison).\n\n**Safety Mode (enforced):** Every order, cancel, transfer, or funding action requires the user to reply exactly `CONFIRM` in the current conversation before execution. One CONFIRM authorizes one action; no auto-trading loop can bypass this. READ operations (quotes, balances, positions, alpha data) do not require CONFIRM. This rule applies to every exchange in this skill and cannot be disabled by the agent.\n\n**Broker attribution:** All BitMart requests include `X-BM-BROKER-ID: BlaveData666666`; all Bybit requests include `referer: Ue001036`; all BingX requests include `X-SOURCE-KEY: BX-AI-SKILL`; all Bitfinex orders include `aff_code: ZZDLtrXMF` in the meta field; all Binance orders include a `newClientOrderId` prefixed with `x-GBN6HWR2` (spot) or `x-52DDFAFN` (USDS-M futures). These identify Blave as the referring partner. They do not affect your trading fees as a user.",
  "version": "1.6.10",
  "requires": {
    "env": [
      "blave_api_key",
      "blave_secret_key"
    ]
  },
  "optional": {
    "env": [
      "BITMART_API_KEY",
      "BITMART_API_SECRET",
      "BITMART_API_MEMO",
      "OKX_API_KEY",
      "OKX_SECRET_KEY",
      "OKX_PASSPHRASE",
      "BYBIT_API_KEY",
      "BYBIT_API_SECRET",
      "BINGX_API_KEY",
      "BINGX_SECRET_KEY",
      "BITGET_API_KEY",
      "BITGET_SECRET_KEY",
      "BITGET_PASSPHRASE",
      "BINANCE_API_KEY",
      "BINANCE_SECRET_KEY",
      "BITFINEX_API_KEY",
      "BITFINEX_API_SECRET"
    ]
  },
  "category": "data",
  "tags": ["crypto", "trading", "bitmart", "okx", "bybit", "bingx", "bitget", "binance", "bitfinex", "blave", "futures", "spot", "alpha", "market-data", "funding", "lending", "truth-social", "trump", "etf", "blackrock"],
  "license": "MIT",
  "pricing": "free",
  "support_url": "https://github.com/Blave-TW/blave-quant-skill/issues",
  "homepage": "https://blave.org"
}


path: examples/backtest-holder-concentration.md
content: # Example: Backtest — Holder Concentration Long Strategy

## Strategy Logic

Go long when smart money (institutions / large players) is concentrating into a coin. Exit when they start distributing.

- **Entry:** HC alpha > `1.0` → open long
- **Exit:** HC alpha < `-0.5` → close long
- **Hold:** between thresholds, maintain current position ("strict entry, loose exit")
- **Vol-targeting:** size each position so the strategy targets 30% annualized volatility regardless of the coin's own volatility
- **Long only** — no short positions

---

## Data Required

```
GET /kline?symbol=<SYMBOL>&period=1h&start_date=<YYYY-MM-DD>&end_date=<YYYY-MM-DD>
GET /holder_concentration/get_alpha?symbol=<SYMBOL>&period=1h&start_date=<YYYY-MM-DD>&end_date=<YYYY-MM-DD>
```

Both return time series. Align them on timestamp before backtesting.

For history beyond 1 year, send one request per year and concatenate.

---

## Full Backtest Code

```python
import numpy as np
import pandas as pd
import requests
import matplotlib.pyplot as plt
import numba
from dotenv import dotenv_values

_env = dotenv_values()  # reads .env from current directory

# ── Config ──────────────────────────────────────────────────────────────────
SYMBOL         = "BTCUSDT"
START_DATE     = "2023-01-01"
END_DATE       = "2024-12-31"
PERIOD         = "1h"
ENTRY_TH       = 1.0            # HC alpha > 1.0 → open long
EXIT_TH        = -0.5           # HC alpha < -0.5 → close long
TARGET_VOL     = 0.30           # 30% annualized target volatility
MAX_LEV        = 2.0            # max position size (leverage cap)
VOL_WINDOW     = 720            # 30 days × 24h
HOURS_PER_YEAR = 8760
FEE            = 0.0005         # 0.05% per side (taker fee)
SHARPE_MODE    = "mean"         # "mean" = r.mean()/r.std()  |  "slope" = OLS slope / residual std

API_BASE   = "https://api.blave.org"
API_KEY    = _env["blave_api_key"]
API_SECRET = _env["blave_secret_key"]
HEADERS    = {"api-key": API_KEY, "secret-key": API_SECRET}


# ── Fetch helpers ────────────────────────────────────────────────────────────
def fetch_range(endpoint, params):
    """Fetch one year at a time and concatenate if needed."""
    from datetime import datetime, timedelta

    start = datetime.strptime(params["start_date"], "%Y-%m-%d")
    end   = datetime.strptime(params["end_date"],   "%Y-%m-%d")
    all_data = []

    cursor = start
    while cursor < end:
        chunk_end = min(cursor + timedelta(days=365), end)
        p = {**params, "start_date": cursor.strftime("%Y-%m-%d"),
                        "end_date":   chunk_end.strftime("%Y-%m-%d")}
        r = requests.get(f"{API_BASE}/{endpoint}", headers=HEADERS, params=p)
        r.raise_for_status()
        all_data.append(r.json())
        cursor = chunk_end

    return all_data


def load_kline(symbol, start, end, period):
    chunks = fetch_range("kline", {"symbol": symbol, "period": period,
                                   "start_date": start, "end_date": end})
    rows = [row for chunk in chunks for row in chunk]
    df = pd.DataFrame(rows, columns=["time", "open", "high", "low", "close"])
    df["time"] = pd.to_datetime(df["time"], unit="s", utc=True)
    df = df.set_index("time").sort_index().drop_duplicates()
    df["close"] = df["close"].astype(float)
    return df


def load_hc(symbol, start, end, period):
    chunks = fetch_range("holder_concentration/get_alpha",
                         {"symbol": symbol, "period": period,
                          "start_date": start, "end_date": end})
    timestamps, alphas = [], []
    for chunk in chunks:
        data = chunk.get("data", {})
        timestamps.extend(data.get("timestamp", []))
        alphas.extend(data.get("alpha", []))

    df = pd.DataFrame({"time": pd.to_datetime(timestamps, unit="s", utc=True),
                       "hc": alphas})
    df = df.set_index("time").sort_index().drop_duplicates()
    df["hc"] = pd.to_numeric(df["hc"], errors="coerce")
    return df


# ── Helpers ──────────────────────────────────────────────────────────────────
def _sharpe(r):
    """
    mean mode : (mean / std) * sqrt(N)           — industry standard
    slope mode: OLS slope of cumulative PnL / diff(residual).std() * sqrt(N)
                ≈ mean mode for stationary returns, but penalises front-loaded
                or decaying strategies and rewards improving ones.
    """
    s = r.std()
    if s == 0: return np.nan
    if SHARPE_MODE == "slope":
        cum = np.cumsum(r)
        n   = len(r)
        x   = np.arange(n, dtype=np.float64); x -= x.mean()
        slope = x.dot(cum) / x.dot(x)               # return per bar
        resid = cum - (cum.mean() + slope * x)
        rvol  = np.diff(resid).std()                 # per-bar residual vol
        return (slope / rvol) * np.sqrt(HOURS_PER_YEAR) if rvol > 0 else np.nan
    return (r.mean() / s) * np.sqrt(HOURS_PER_YEAR)


# ── Numba: only the stateful signal loop ─────────────────────────────────────
# Rolling vol  → Pandas rolling (Cython/C, fastest for window stats)
# Signal loop  → Numba njit    (stateful: each bar depends on previous state)
# Everything else → NumPy      (vectorized, no overhead)
# First call triggers JIT compilation (~2–5 s, once per session).

@numba.njit(cache=True)
def _signal_loop(signal, entry_th, exit_th):
    n = len(signal)
    position = np.zeros(n)
    in_pos = False
    for i in range(n):
        if np.isnan(signal[i]):
            position[i] = 1.0 if in_pos else 0.0
            continue
        if not in_pos and signal[i] > entry_th:
            in_pos = True
        elif in_pos and signal[i] < exit_th:
            in_pos = False
        position[i] = 1.0 if in_pos else 0.0
    return position


# ── Backtest ──────────────────────────────────────────────────────────────────
def run_backtest(df):
    close = df["close"].values.astype(np.float64)
    hc    = df["hc"].values.astype(np.float64)
    n     = len(df)

    # NumPy — vectorized
    log_ret = np.concatenate([[0.0], np.log(close[1:] / close[:-1])])
    fwd_ret = np.empty(n)
    fwd_ret[:-1] = np.diff(close) / close[:-1]
    fwd_ret[-1]  = 0.0

    # Pandas rolling — C-implemented window stats, fastest for this operation
    realized_vol = pd.Series(log_ret).rolling(VOL_WINDOW).std().values * np.sqrt(HOURS_PER_YEAR)

    # Numba njit — only the stateful entry/exit loop benefits from JIT
    position = _signal_loop(hc, ENTRY_TH, EXIT_TH)

    # NumPy — vectorized vol-targeting and fee calculation
    vol_scalar = np.where(
        (realized_vol > 0) & ~np.isnan(realized_vol),
        np.clip(TARGET_VOL / realized_vol, 0, MAX_LEV),
        1.0
    )
    sized     = position * vol_scalar
    fee_cost  = np.abs(np.diff(sized, prepend=0)) * FEE
    strat_ret = sized * fwd_ret - fee_cost

    # ── Bar-level metrics ────────────────────────────────────────────────────
    r = strat_ret[~np.isnan(strat_ret)]
    cum      = np.cumprod(1 + r)
    peak     = np.maximum.accumulate(cum)
    total_years = len(r) / HOURS_PER_YEAR

    total_return  = cum[-1] - 1
    ann_ret       = (1 + total_return) ** (1 / total_years) - 1
    ann_vol       = r.std() * np.sqrt(HOURS_PER_YEAR)
    sharpe        = _sharpe(r)
    max_dd        = ((cum - peak) / peak).min()

    # ── Trade-level metrics ──────────────────────────────────────────────────
    # Each trade = one entry→exit cycle; compute per-trade net return
    entries = np.where(np.diff(position.astype(int)) == 1)[0] + 1
    exits   = np.where(np.diff(position.astype(int)) == -1)[0] + 1
    # Do NOT force-close the last open position — leave it open if still in trade at end of data

    trade_returns = []
    for entry_i, exit_i in zip(entries, exits):
        trade_r = strat_ret[entry_i:exit_i]
        trade_r = trade_r[~np.isnan(trade_r)]
        if len(trade_r) > 0:
            trade_returns.append(np.prod(1 + trade_r) - 1)

    trade_returns = np.array(trade_returns)
    n_trades      = len(trade_returns)
    win_rate      = (trade_returns > 0).mean() if n_trades > 0 else np.nan
    avg_win       = trade_returns[trade_returns > 0].mean() if (trade_returns > 0).any() else np.nan
    avg_loss      = trade_returns[trade_returns <= 0].mean() if (trade_returns <= 0).any() else np.nan
    avg_trades_yr = n_trades / total_years

    return {
        "total_return":  total_return,
        "ann_ret":       ann_ret,
        "ann_vol":       ann_vol,
        "sharpe":        sharpe,
        "max_dd":        max_dd,
        "win_rate":      win_rate,
        "avg_win":       avg_win,
        "avg_loss":      avg_loss,
        "n_trades":      n_trades,
        "avg_trades_yr": avg_trades_yr,
        "position":      position,
        "sized":         sized,
        "strat_ret":     strat_ret,
        "realized_vol":  realized_vol,
        "cum":           cum,
    }


# ── Regime Analysis ──────────────────────────────────────────────────────────
def regime_analysis(df, result):
    """Break down performance by: calendar year, Bull/Bear, High/Low volatility."""
    strat_ret    = result["strat_ret"]
    realized_vol = result["realized_vol"]
    close        = df["close"].values
    dates        = df.index

    # Bull/Bear: price vs 200-period rolling MA (window scales with VOL_WINDOW)
    ma_window = VOL_WINDOW * 200 // 30          # 200 days expressed in bars
    ma200     = pd.Series(close).rolling(ma_window).mean().values
    valid_ma  = ~np.isnan(ma200)

    # High/Low vol: above vs below median realized vol
    vol_median  = np.nanmedian(realized_vol)
    valid_vol   = ~np.isnan(realized_vol)

    def _stats(mask):
        r = strat_ret[mask]
        r = r[~np.isnan(r)]
        if len(r) < 2:
            return None
        total_years = len(r) / HOURS_PER_YEAR
        cum_r   = np.prod(1 + r) - 1
        ann_r   = (1 + cum_r) ** (1 / total_years) - 1 if total_years > 0 else np.nan
        ann_vol = r.std() * np.sqrt(HOURS_PER_YEAR)
        sharpe  = _sharpe(r)
        cum_curve = np.cumprod(1 + r)
        peak    = np.maximum.accumulate(cum_curve)
        mdd     = ((cum_curve - peak) / peak).min()
        n_total = len(strat_ret[~np.isnan(strat_ret)])
        return dict(ann_ret=ann_r, ann_vol=ann_vol, sharpe=sharpe,
                    max_dd=mdd, pct_time=len(r) / n_total)

    rows = []

    # ── By calendar year ────────────────────────────────────────────────────
    for yr in sorted(dates.year.unique()):
        mask = (dates.year == yr)
        s = _stats(mask)
        if s:
            rows.append({"label": str(yr), **s})

    rows.append({"label": "─" * 20})   # separator

    # ── Bull vs Bear ─────────────────────────────────────────────────────────
    bull = close > ma200
    for label, mask in [("Bull (price > MA200)", bull & valid_ma),
                         ("Bear (price < MA200)", ~bull & valid_ma)]:
        s = _stats(mask)
        if s:
            rows.append({"label": label, **s})

    rows.append({"label": "─" * 20})

    # ── High vol vs Low vol ───────────────────────────────────────────────────
    highvol = realized_vol > vol_median
    for label, mask in [("High Vol (>median)",  highvol & valid_vol),
                         ("Low  Vol (≤median)",  ~highvol & valid_vol)]:
        s = _stats(mask)
        if s:
            rows.append({"label": label, **s})

    # ── Print table ───────────────────────────────────────────────────────────
    hdr = f"  {'Regime':<22} {'Ann Ret':>9} {'Ann Vol':>9} {'Sharpe':>8} {'MDD':>8} {'Time%':>7}"
    print(f"\n{'─' * len(hdr)}")
    print("  Regime Analysis")
    print('─' * len(hdr))
    print(hdr)
    print('─' * len(hdr))
    for row in rows:
        if "ann_ret" not in row:          # separator row
            print(f"  {row['label']}")
            continue
        print(f"  {row['label']:<22} {row['ann_ret']*100:>8.1f}% {row['ann_vol']*100:>8.1f}%"
              f" {row['sharpe']:>8.2f} {row['max_dd']*100:>7.1f}% {row['pct_time']*100:>6.1f}%")
    print('─' * len(hdr))


# ── Regime Chart ──────────────────────────────────────────────────────────────
def plot_regime(df, result, symbol):
    strat_ret    = result["strat_ret"]
    realized_vol = result["realized_vol"]
    close        = df["close"].values
    dates        = df.index

    ma_window  = VOL_WINDOW * 200 // 30
    ma200      = pd.Series(close).rolling(ma_window).mean().values
    valid_ma   = ~np.isnan(ma200)
    vol_median = np.nanmedian(realized_vol)
    valid_vol  = ~np.isnan(realized_vol)
    bull       = close > ma200
    highvol    = realized_vol > vol_median

    def _stats(mask):
        r = strat_ret[mask]; r = r[~np.isnan(r)]
        if len(r) < 2: return None
        total_years = len(r) / HOURS_PER_YEAR; cum_r = np.prod(1 + r) - 1
        ann_r   = (1 + cum_r) ** (1 / total_years) - 1 if total_years > 0 else np.nan
        ann_vol = r.std() * np.sqrt(HOURS_PER_YEAR)
        sharpe  = _sharpe(r)
        cc = np.cumprod(1 + r); pk = np.maximum.accumulate(cc)
        return dict(ann_ret=ann_r, sharpe=sharpe, max_dd=((cc - pk) / pk).min())

    groups = {
        "By Year":           [(str(yr), _stats(dates.year == yr))
                               for yr in sorted(dates.year.unique())],
        "Trend Regime":      [("Bull\n(>MA200)", _stats(bull & valid_ma)),
                               ("Bear\n(<MA200)", _stats(~bull & valid_ma))],
        "Volatility Regime": [("High Vol\n(>median)", _stats(highvol & valid_vol)),
                               ("Low Vol\n(≤median)",  _stats(~highvol & valid_vol))],
    }
    groups = {k: [(lbl, s) for lbl, s in v if s is not None] for k, v in groups.items()}

    fig, axes = plt.subplots(1, 3, figsize=(16, 6))
    for ax, (group_name, items) in zip(axes, groups.items()):
        labels  = [lbl for lbl, _ in items]
        ann_ret = [s["ann_ret"] * 100 for _, s in items]
        sharpe  = [s["sharpe"]        for _, s in items]
        mdd     = [s["max_dd"] * 100  for _, s in items]
        x = np.arange(len(labels)); w = 0.25
        b1 = ax.bar(x - w, ann_ret, w, label="Ann Ret (%)", color="#3498db", alpha=0.85)
        b2 = ax.bar(x,     sharpe,  w, label="Sharpe",      color="#2ecc71", alpha=0.85)
        b3 = ax.bar(x + w, mdd,     w, label="MDD (%)",     color="#e74c3c", alpha=0.85)
        for bars in [b1, b2, b3]:
            for bar in bars:
                h = bar.get_height()
                ax.text(bar.get_x() + bar.get_width() / 2,
                        h + (0.3 if h >= 0 else -1.5),
                        f"{h:.1f}", ha="center",
                        va="bottom" if h >= 0 else "top", fontsize=8)
        ax.axhline(0, color="#555", lw=0.8)
        ax.set_title(group_name, fontsize=13, fontweight="bold")
        ax.set_xticks(x); ax.set_xticklabels(labels, fontsize=10)
        ax.set_ylabel("Value", fontsize=10); ax.legend(fontsize=9)
        all_vals = ann_ret + sharpe + mdd
        ax.set_ylim(min(all_vals) - 5, max(all_vals) + 8)

    fig.suptitle(f"{symbol} — Regime Analysis", fontsize=13, fontweight="bold", y=1.01)
    plt.tight_layout()
    fname = f"{symbol}_hc_regime.png"
    plt.savefig(fname, dpi=150, bbox_inches="tight")
    plt.show()
    print(f"Saved: {fname}")


# ── PnL Chart ─────────────────────────────────────────────────────────────────
def plot_pnl(df, result, symbol):
    close  = df["close"].values
    hc     = df["hc"].values
    dates  = df.index
    cum    = result["cum"]
    pos    = result["position"]

    peak = np.maximum.accumulate(cum)
    dd   = (cum - peak) / peak

    fig, axes = plt.subplots(3, 1, figsize=(14, 10), sharex=True,
                              gridspec_kw={'height_ratios': [3, 1, 1]})

    # Panel 1: Price (left y) + Strategy PnL (right y)
    ax1 = axes[0]
    ax2 = ax1.twinx()

    ax1.plot(dates, close, color="#3498db", lw=1, alpha=0.7, label="Price")
    ax1.set_ylabel("Price (USD)", fontsize=11, color="#3498db")
    ax1.tick_params(axis='y', labelcolor="#3498db")
    ax1.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"${x:,.0f}"))

    ax2.plot(dates, (cum - 1) * 100, color="#2ecc71", lw=1.5, label="HC Strategy (+fees)")
    ax2.axhline(0, color="#888", lw=0.5, ls="--")
    ax2.set_ylabel("Strategy Return (%)", fontsize=11, color="#2ecc71")
    ax2.tick_params(axis='y', labelcolor="#2ecc71")
    ax2.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x:.0f}%"))

    # Shade in-position periods
    prev = False
    for i, (date, inp) in enumerate(zip(dates, pos > 0)):
        if inp and not prev:
            start = date
        if not inp and prev:
            ax1.axvspan(start, date, alpha=0.08, color="#2ecc71")
        prev = inp
    if prev:
        ax1.axvspan(start, dates[-1], alpha=0.08, color="#2ecc71")

    lines1, labels1 = ax1.get_legend_handles_labels()
    lines2, labels2 = ax2.get_legend_handles_labels()
    ax1.legend(lines1 + lines2, labels1 + labels2, fontsize=10, loc="upper left")
    ax1.set_title(
        f"{symbol} — HC Strategy (entry>{ENTRY_TH}, exit<{EXIT_TH})\n"
        f"Vol-Target {TARGET_VOL*100:.0f}% | Max Lev {MAX_LEV}x | Fee {FEE*100:.2f}%/side",
        fontsize=13
    )

    # Panel 2: Drawdown
    axes[1].fill_between(dates, dd * 100, 0, color="#e74c3c", alpha=0.6)
    axes[1].set_ylabel("Drawdown (%)", fontsize=11)
    axes[1].yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x:.0f}%"))
    axes[1].axhline(0, color="#888", lw=0.5)

    # Panel 3: HC signal
    axes[2].plot(dates, hc, color="#9b59b6", lw=0.8, alpha=0.8)
    axes[2].axhline(ENTRY_TH, color="#2ecc71", lw=1, ls="--", label=f"Entry={ENTRY_TH}")
    axes[2].axhline(EXIT_TH,  color="#e74c3c", lw=1, ls="--", label=f"Exit={EXIT_TH}")
    axes[2].axhline(0, color="#888", lw=0.5)
    axes[2].set_ylabel("HC Alpha", fontsize=11)
    axes[2].legend(fontsize=9, loc="upper right")

    plt.tight_layout()
    fname = f"{symbol}_hc_pnl.png"
    plt.savefig(fname, dpi=150)
    plt.show()
    print(f"Saved: {fname}")


# ── 2D Parameter Scan ────────────────────────────────────────────────────────
THRESHOLDS = [-2, -1.5, -1, -0.5, 0, 0.5, 1, 1.5, 2]

def param_scan(df):
    sharpe_grid = np.full((len(THRESHOLDS), len(THRESHOLDS)), np.nan)
    for i, entry in enumerate(THRESHOLDS):
        for j, exit_ in enumerate(THRESHOLDS):
            if exit_ > entry:
                continue
            r = run_backtest_params(df, entry, exit_)
            if r is not None:
                sharpe_grid[i, j] = r["sharpe"]
    return sharpe_grid


def run_backtest_params(df, entry_th, exit_th):
    """Lightweight param-scan version: same stack as run_backtest, returns Sharpe only."""
    close = df["close"].values.astype(np.float64)
    hc    = df["hc"].values.astype(np.float64)
    n     = len(df)

    log_ret = np.concatenate([[0.0], np.log(close[1:] / close[:-1])])
    fwd_ret = np.empty(n)
    fwd_ret[:-1] = np.diff(close) / close[:-1]
    fwd_ret[-1]  = 0.0

    realized_vol = pd.Series(log_ret).rolling(VOL_WINDOW).std().values * np.sqrt(HOURS_PER_YEAR)
    position     = _signal_loop(hc, entry_th, exit_th)
    vol_scalar   = np.where(
        (realized_vol > 0) & ~np.isnan(realized_vol),
        np.clip(TARGET_VOL / realized_vol, 0, MAX_LEV),
        1.0
    )
    sized     = position * vol_scalar
    fee_cost  = np.abs(np.diff(sized, prepend=0)) * FEE
    strat_ret = sized * fwd_ret - fee_cost

    r = strat_ret[~np.isnan(strat_ret)]
    if len(r) == 0 or r.std() == 0:
        return None
    return {"sharpe": _sharpe(r)}


def find_plateau(sharpe_grid, window=1):
    """
    Find the parameter plateau: the cell whose neighborhood has the
    highest mean Sharpe. Uses a (2*window+1) × (2*window+1) window.
    This is more robust than picking the single best cell, as it
    avoids isolated peaks that may be overfitted.
    """
    rows, cols = sharpe_grid.shape
    neighborhood_mean = np.full((rows, cols), np.nan)

    for i in range(rows):
        for j in range(cols):
            if np.isnan(sharpe_grid[i, j]):
                continue
            neighbors = []
            for di in range(-window, window + 1):
                for dj in range(-window, window + 1):
                    ni, nj = i + di, j + dj
                    if 0 <= ni < rows and 0 <= nj < cols:
                        v = sharpe_grid[ni, nj]
                        if not np.isnan(v):
                            neighbors.append(v)
            if neighbors:
                neighborhood_mean[i, j] = np.mean(neighbors)

    best_idx = np.unravel_index(np.nanargmax(neighborhood_mean), neighborhood_mean.shape)
    return best_idx, neighborhood_mean


def plot_heatmap(sharpe_grid, neighborhood_mean, plateau_idx, symbol):
    peak_idx = np.unravel_index(np.nanargmax(sharpe_grid), sharpe_grid.shape)

    fig, axes = plt.subplots(1, 2, figsize=(18, 7))

    for ax, mark_idx, title, note in [
        (axes[0], peak_idx,    "Peak — highest single Sharpe",
         f"Selected: entry={THRESHOLDS[peak_idx[0]]}, exit={THRESHOLDS[peak_idx[1]]}\n"
         f"Sharpe={sharpe_grid[peak_idx]:.2f} — may be overfitted if isolated"),
        (axes[1], plateau_idx, "Plateau — most stable region",
         f"Selected: entry={THRESHOLDS[plateau_idx[0]]}, exit={THRESHOLDS[plateau_idx[1]]}\n"
         f"Sharpe={sharpe_grid[plateau_idx]:.2f} — neighbors also perform well → more robust"),
    ]:
        masked = np.ma.masked_invalid(sharpe_grid)
        cmap = plt.cm.RdYlGn.copy()
        cmap.set_bad(color="#cccccc")
        vals = sharpe_grid[~np.isnan(sharpe_grid)]
        im = ax.imshow(masked, cmap=cmap, vmin=min(0, vals.min()),
                       vmax=np.percentile(vals, 95), aspect="auto")

        ax.add_patch(plt.Rectangle(
            (mark_idx[1] - 0.5, mark_idx[0] - 0.5), 1, 1,
            fill=False, edgecolor="gold", linewidth=3
        ))
        for i in range(len(THRESHOLDS)):
            for j in range(len(THRESHOLDS)):
                v = sharpe_grid[i, j]
                ax.text(j, i, f"{v:.2f}" if not np.isnan(v) else "N/A",
                        ha="center", va="center", fontsize=8,
                        color="#888" if np.isnan(v) else "black")

        ax.set_xticks(range(len(THRESHOLDS))); ax.set_xticklabels(THRESHOLDS)
        ax.set_yticks(range(len(THRESHOLDS))); ax.set_yticklabels(THRESHOLDS)
        ax.set_xlabel("Exit Threshold")
        ax.set_ylabel("Entry Threshold")
        ax.set_title(f"{symbol} — {title}\n{note}", fontsize=10)
        plt.colorbar(im, ax=ax, label="Sharpe Ratio")

    plt.suptitle("Same Sharpe grid, different selection method — prefer Plateau", fontsize=12)
    plt.tight_layout()
    fname = f"{symbol}_hc_heatmap.png"
    plt.savefig(fname, dpi=150)
    plt.show()
    print(f"Saved: {fname}")


# ── Main ──────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
    print(f"Loading data for {SYMBOL} ({START_DATE} → {END_DATE}, {PERIOD})...")
    kline = load_kline(SYMBOL, START_DATE, END_DATE, PERIOD)
    hc    = load_hc(SYMBOL, START_DATE, END_DATE, PERIOD)

    df = kline[["close"]].join(hc[["hc"]], how="inner").dropna(subset=["close"])
    print(f"Aligned rows: {len(df)}")

    # Step 1: 2D scan → find plateau
    print("Running 2D parameter scan...")
    sharpe_grid = param_scan(df)
    plateau_idx, neighborhood_mean = find_plateau(sharpe_grid, window=1)

    best_entry = THRESHOLDS[plateau_idx[0]]
    best_exit  = THRESHOLDS[plateau_idx[1]]
    print(f"Plateau center: entry={best_entry}, exit={best_exit} "
          f"(neighborhood mean Sharpe={neighborhood_mean[plateau_idx]:.2f})")

    plot_heatmap(sharpe_grid, neighborhood_mean, plateau_idx, SYMBOL)

    # Step 2: full backtest with plateau parameters
    global ENTRY_TH, EXIT_TH
    ENTRY_TH, EXIT_TH = best_entry, best_exit

    result = run_backtest(df)
    print(f"\nResults (entry>{ENTRY_TH}, exit<{EXIT_TH}):")
    print(f"  總報酬率            : {result['total_return']*100:.1f}%")
    print(f"  年化報酬率           : {result['ann_ret']*100:.1f}%")
    print(f"  年化波動度           : {result['ann_vol']*100:.1f}%")
    print(f"  Sharpe Ratio      : {result['sharpe']:.2f}")
    print(f"  最大回撤 (MDD)      : {result['max_dd']*100:.1f}%")
    print(f"  勝率                : {result['win_rate']*100:.1f}%")
    print(f"  平均獲利 (per trade): {result['avg_win']*100:.2f}%")
    print(f"  平均虧損 (per trade): {result['avg_loss']*100:.2f}%")
    print(f"  總交易次數           : {result['n_trades']}")
    print(f"  平均年交易次數        : {result['avg_trades_yr']:.1f}")

    regime_analysis(df, result)
    plot_regime(df, result, SYMBOL)
    plot_pnl(df, result, SYMBOL)
```

---

## Parameters

| Parameter | Value | Notes |
|---|---|---|
| `ENTRY_TH` | `1.0` | HC alpha must exceed this to open long |
| `EXIT_TH` | `-0.5` | HC alpha must fall below this to close |
| `PERIOD` | `1h` | HC signal + kline timeframe |
| `VOL_WINDOW` | `720` | 30 days × 24h rolling vol |
| `TARGET_VOL` | `0.30` | 30% annualized target |
| `MAX_LEV` | `2.0` | Position size cap |
| `FEE` | `0.0005` | 0.05% per side (taker fee, e.g. Hyperliquid) |

---

## Alpha Scale Reference

| HC Alpha | Signal |
|---|---|
| > 3 | Over Concentrated (long) |
| 2 – 3 | Highly Concentrated (long) |
| **> 1** | **→ Entry threshold** |
| 0.5 – 1 | Concentrated (long) |
| -0.5 – 0.5 | Neutral |
| **< -0.5** | **→ Exit threshold** |
| < -2 | Concentrated (short) |

---

## Notes

- **Long only** — no short positions taken
- Entry threshold is stricter than exit — gives the position room to breathe through short-term noise
- Vol-targeting scales down automatically during high-volatility periods; a coin with 3× BTC vol receives ~1/3 the position size for the same signal
- Signals update every 5 minutes; on `1h` period each bar reflects the last finalized hourly HC value
- **Performance stack:** Rolling vol uses `pd.Series.rolling().std()` (Pandas Cython/C — fastest for window statistics). The entry/exit signal loop uses `@numba.njit` — this is the only loop that benefits from JIT because each bar depends on the previous bar's state (cannot be vectorized). Everything else (fwd_ret, vol-targeting, fees, returns) uses NumPy vectorized ops. First call triggers JIT compilation (~2–5 s, once per session). If numba is not installed: `pip install numba`

### Live Trading Execution Timing

The backtest computes `fwd_ret[i] = (close[i+1] - close[i]) / close[i]`, which means it assumes the order is **executed at bar i's close price** — the same bar where the signal fires.

In live trading, the correct sequence is:

```
bar i closes
  → fetch the latest signal
  → signal changed → place market order immediately
  → fill ≈ bar i+1 open (for liquid pairs like BTC, this is effectively bar i close)
```

**Do NOT wait for bar i+1 to close before placing the order.** Waiting an extra bar means your execution price is one full bar later than what the backtest assumes, causing live performance to diverge from backtest results.

---

### Parameter Selection: Plateau vs Peak

**Do not simply pick the highest Sharpe cell.** A single peak is often an overfitted outlier — it performs well in-sample but is fragile out-of-sample.

Instead, use `find_plateau()` to compute the **neighborhood mean Sharpe** for each cell (average of the cell and its surrounding valid cells within a 3×3 window). The cell with the highest neighborhood mean sits at the center of a stable region — a **parameter plateau** — where nearby combinations also perform well. This indicates robustness: small changes to the thresholds do not collapse performance.

The heatmap shows the same Sharpe grid twice. Left panel marks the **peak** (highest single cell); right panel marks the **plateau center** (cell whose neighborhood has the highest mean Sharpe). Both show the true Sharpe value in each cell — the difference is only which cell is highlighted. Prefer the plateau selection even if its raw Sharpe is slightly lower than the peak.


path: examples/backtest-kd-btc-1h.md
content: # Example: Backtest — KD Stochastic Golden/Death Cross (BTC 1h)

## Strategy Logic

Trade BTC on the 1-hour chart using the KD Stochastic Oscillator.

- **Entry:** %K crosses above %D (Golden Cross) → open long
- **Exit:** %K crosses below %D (Death Cross) → close long
- **Long only** — no short positions
- **Vol-targeting:** size each position to target 30% annualized volatility

KD formula:
1. Raw %K = (Close − LowestLow_N) / (HighestHigh_N − LowestLow_N) × 100
2. Slow %K = SMA(Raw %K, K_SMOOTH)
3. %D     = SMA(Slow %K, D_SMOOTH)

---

## Data Required

```
GET /kline?symbol=BTCUSDT&period=1h&start_date=<YYYY-MM-DD>&end_date=<YYYY-MM-DD>
```

Returns `[time, open, high, low, close]`. KD is computed locally from OHLCV — no additional endpoint needed.

For history beyond 1 year, send one request per year and concatenate.

---

## Full Backtest Code

```python
import numpy as np
import pandas as pd
import requests
import matplotlib.pyplot as plt
import numba
from dotenv import dotenv_values

_env = dotenv_values()

# ── Config ────────────────────────────────────────────────────────────────────
from datetime import datetime, timedelta, timezone

SYMBOL         = "BTCUSDT"
END_DATE       = datetime.now(timezone.utc).strftime("%Y-%m-%d")
START_DATE     = (datetime.now(timezone.utc) - timedelta(days=365)).strftime("%Y-%m-%d")
PERIOD         = "1h"
K_PERIOD       = 9          # stochastic lookback window (raw %K)
K_SMOOTH       = 3          # %K smoothing period (SMA → slow %K)
D_SMOOTH       = 3          # %D smoothing period (SMA of slow %K)
TARGET_VOL     = 0.30       # 30% annualized target volatility
MAX_LEV        = 2.0        # position size cap
VOL_WINDOW     = 720        # rolling vol window: 30 days × 24h
HOURS_PER_YEAR = 8760
FEE            = 0.0005     # 0.05% per side (taker fee)

API_BASE   = "https://api.blave.org"
API_KEY    = _env["blave_api_key"]
API_SECRET = _env["blave_secret_key"]
HEADERS    = {"api-key": API_KEY, "secret-key": API_SECRET}


# ── Fetch ─────────────────────────────────────────────────────────────────────
def load_kline(symbol, start, end, period):
    s = datetime.strptime(start, "%Y-%m-%d")
    e = datetime.strptime(end,   "%Y-%m-%d")
    rows = []
    cursor = s
    while cursor < e:
        chunk_end = min(cursor + timedelta(days=365), e)
        r = requests.get(f"{API_BASE}/kline", headers=HEADERS, params={
            "symbol": symbol, "period": period,
            "start_date": cursor.strftime("%Y-%m-%d"),
            "end_date":   chunk_end.strftime("%Y-%m-%d"),
        }, timeout=20)
        r.raise_for_status()
        rows.extend(r.json())
        cursor = chunk_end

    df = pd.DataFrame(rows, columns=["time", "open", "high", "low", "close"])
    df["time"] = pd.to_datetime(df["time"], unit="s", utc=True)
    df = df.set_index("time").sort_index().drop_duplicates()
    for col in ["open", "high", "low", "close"]:
        df[col] = df[col].astype(float)
    return df


# ── KD Computation ────────────────────────────────────────────────────────────
def compute_kd(df, k_period, k_smooth, d_smooth):
    low_min  = df["low"].rolling(k_period).min()
    high_max = df["high"].rolling(k_period).max()
    denom    = high_max - low_min
    raw_k    = pd.Series(
        np.where(denom > 0, (df["close"] - low_min) / denom * 100, 50.0),
        index=df.index,
    )
    slow_k = raw_k.rolling(k_smooth).mean()
    d      = slow_k.rolling(d_smooth).mean()
    return slow_k, d


# ── Helpers ───────────────────────────────────────────────────────────────────
def _sharpe(r):
    s = r.std()
    if s == 0:
        return np.nan
    return (r.mean() / s) * np.sqrt(HOURS_PER_YEAR)


# ── Numba: crossover signal loop ──────────────────────────────────────────────
# Each bar depends on the previous bar's K/D values and position state →
# must be sequential; Numba njit gives ~20× speedup over pure Python.

@numba.njit(cache=True)
def _kd_signal_loop(k, d):
    n = len(k)
    position = np.zeros(n)
    in_pos = False
    for i in range(1, n):
        ki,  di  = k[i],   d[i]
        kp,  dp  = k[i-1], d[i-1]
        if np.isnan(ki) or np.isnan(di) or np.isnan(kp) or np.isnan(dp):
            position[i] = 1.0 if in_pos else 0.0
            continue
        if not in_pos and kp <= dp and ki > di:   # golden cross → enter
            in_pos = True
        elif in_pos and kp >= dp and ki < di:     # death cross → exit
            in_pos = False
        position[i] = 1.0 if in_pos else 0.0
    return position


# ── Backtest ──────────────────────────────────────────────────────────────────
def run_backtest(df, k_period=None, k_smooth=None, d_smooth=None):
    k_period = k_period or K_PERIOD
    k_smooth = k_smooth or K_SMOOTH
    d_smooth = d_smooth or D_SMOOTH

    slow_k, d = compute_kd(df, k_period, k_smooth, d_smooth)
    close = df["close"].values.astype(np.float64)
    k_arr = slow_k.values.astype(np.float64)
    d_arr = d.values.astype(np.float64)
    n     = len(df)

    log_ret = np.concatenate([[0.0], np.log(close[1:] / close[:-1])])
    fwd_ret = np.empty(n)
    fwd_ret[:-1] = np.diff(close) / close[:-1]
    fwd_ret[-1]  = 0.0

    realized_vol = pd.Series(log_ret).rolling(VOL_WINDOW).std().values * np.sqrt(HOURS_PER_YEAR)
    position     = _kd_signal_loop(k_arr, d_arr)

    vol_scalar = np.where(
        (realized_vol > 0) & ~np.isnan(realized_vol),
        np.clip(TARGET_VOL / realized_vol, 0, MAX_LEV),
        1.0,
    )
    sized     = position * vol_scalar
    fee_cost  = np.abs(np.diff(sized, prepend=0)) * FEE
    strat_ret = sized * fwd_ret - fee_cost

    r   = strat_ret[~np.isnan(strat_ret)]
    cum = np.cumprod(1 + r)
    pk  = np.maximum.accumulate(cum)
    total_years = len(r) / HOURS_PER_YEAR

    total_return = cum[-1] - 1
    ann_ret      = (1 + total_return) ** (1 / total_years) - 1
    ann_vol      = r.std() * np.sqrt(HOURS_PER_YEAR)
    sharpe       = _sharpe(r)
    max_dd       = ((cum - pk) / pk).min()

    entries = np.where(np.diff(position.astype(int)) == 1)[0] + 1
    exits   = np.where(np.diff(position.astype(int)) == -1)[0] + 1
    # Do NOT force-close the last open position — leave it open if still in trade at end of data

    trade_returns = []
    for e_i, x_i in zip(entries, exits):
        tr = strat_ret[e_i:x_i]
        tr = tr[~np.isnan(tr)]
        if len(tr) > 0:
            trade_returns.append(np.prod(1 + tr) - 1)

    trade_returns  = np.array(trade_returns)
    n_trades       = len(trade_returns)
    win_rate       = (trade_returns > 0).mean() if n_trades > 0 else np.nan
    avg_win        = trade_returns[trade_returns > 0].mean() if (trade_returns > 0).any() else np.nan
    avg_loss       = trade_returns[trade_returns <= 0].mean() if (trade_returns <= 0).any() else np.nan
    avg_trades_yr  = n_trades / total_years

    return {
        "total_return":  total_return,
        "ann_ret":       ann_ret,
        "ann_vol":       ann_vol,
        "sharpe":        sharpe,
        "max_dd":        max_dd,
        "win_rate":      win_rate,
        "avg_win":       avg_win,
        "avg_loss":      avg_loss,
        "n_trades":      n_trades,
        "avg_trades_yr": avg_trades_yr,
        "position":      position,
        "sized":         sized,
        "strat_ret":     strat_ret,
        "realized_vol":  realized_vol,
        "cum":           cum,
        "slow_k":        slow_k.values,
        "d":             d.values,
    }


# ── Regime Analysis ───────────────────────────────────────────────────────────
def regime_analysis(df, result):
    strat_ret    = result["strat_ret"]
    realized_vol = result["realized_vol"]
    close        = df["close"].values
    dates        = df.index

    ma_window = VOL_WINDOW * 200 // 30
    ma200     = pd.Series(close).rolling(ma_window).mean().values
    valid_ma  = ~np.isnan(ma200)
    vol_med   = np.nanmedian(realized_vol)
    valid_vol = ~np.isnan(realized_vol)

    def _stats(mask):
        r = strat_ret[mask]; r = r[~np.isnan(r)]
        if len(r) < 2: return None
        years   = len(r) / HOURS_PER_YEAR
        cum_r   = np.prod(1 + r) - 1
        ann_r   = (1 + cum_r) ** (1 / years) - 1 if years > 0 else np.nan
        ann_v   = r.std() * np.sqrt(HOURS_PER_YEAR)
        sh      = _sharpe(r)
        cc      = np.cumprod(1 + r); pk = np.maximum.accumulate(cc)
        mdd     = ((cc - pk) / pk).min()
        n_tot   = len(strat_ret[~np.isnan(strat_ret)])
        return dict(ann_ret=ann_r, ann_vol=ann_v, sharpe=sh,
                    max_dd=mdd, pct_time=len(r) / n_tot)

    rows = []
    for yr in sorted(dates.year.unique()):
        s = _stats(dates.year == yr)
        if s: rows.append({"label": str(yr), **s})

    rows.append({"label": "─" * 20})
    bull = close > ma200
    for label, mask in [("Bull (price > MA200)", bull & valid_ma),
                         ("Bear (price < MA200)", ~bull & valid_ma)]:
        s = _stats(mask)
        if s: rows.append({"label": label, **s})

    rows.append({"label": "─" * 20})
    hv = realized_vol > vol_med
    for label, mask in [("High Vol (>median)",  hv & valid_vol),
                         ("Low  Vol (≤median)", ~hv & valid_vol)]:
        s = _stats(mask)
        if s: rows.append({"label": label, **s})

    hdr = f"  {'Regime':<22} {'Ann Ret':>9} {'Ann Vol':>9} {'Sharpe':>8} {'MDD':>8} {'Time%':>7}"
    print(f"\n{'─' * len(hdr)}\n  Regime Analysis\n{'─' * len(hdr)}")
    print(hdr); print('─' * len(hdr))
    for row in rows:
        if "ann_ret" not in row:
            print(f"  {row['label']}"); continue
        print(f"  {row['label']:<22} {row['ann_ret']*100:>8.1f}% {row['ann_vol']*100:>8.1f}%"
              f" {row['sharpe']:>8.2f} {row['max_dd']*100:>7.1f}% {row['pct_time']*100:>6.1f}%")
    print('─' * len(hdr))


# ── PnL Chart ─────────────────────────────────────────────────────────────────
def plot_pnl(df, result, symbol):
    close  = df["close"].values
    dates  = df.index
    cum    = result["cum"]
    pos    = result["position"]
    slow_k = result["slow_k"]
    d_line = result["d"]
    peak   = np.maximum.accumulate(cum)
    dd     = (cum - peak) / peak

    fig, axes = plt.subplots(3, 1, figsize=(14, 10), sharex=True,
                             gridspec_kw={"height_ratios": [3, 1, 1.2]})

    # Panel 1: Price + cumulative PnL
    ax1 = axes[0]; ax2 = ax1.twinx()
    ax1.plot(dates, close, color="#3498db", lw=1, alpha=0.7, label="Price")
    ax1.set_ylabel("Price (USD)", fontsize=11, color="#3498db")
    ax1.tick_params(axis="y", labelcolor="#3498db")
    ax1.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"${x:,.0f}"))

    ax2.plot(dates, (cum - 1) * 100, color="#2ecc71", lw=1.5, label="KD Strategy (+fees)")
    ax2.axhline(0, color="#888", lw=0.5, ls="--")
    ax2.set_ylabel("Strategy Return (%)", fontsize=11, color="#2ecc71")
    ax2.tick_params(axis="y", labelcolor="#2ecc71")
    ax2.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x:.0f}%"))

    prev = False
    for i, (date, inp) in enumerate(zip(dates, pos > 0)):
        if inp and not prev: start = date
        if not inp and prev: ax1.axvspan(start, date, alpha=0.08, color="#2ecc71")
        prev = inp
    if prev: ax1.axvspan(start, dates[-1], alpha=0.08, color="#2ecc71")

    l1, lb1 = ax1.get_legend_handles_labels()
    l2, lb2 = ax2.get_legend_handles_labels()
    ax1.legend(l1 + l2, lb1 + lb2, fontsize=10, loc="upper left")
    ax1.set_title(
        f"{symbol} — KD Strategy  K({K_PERIOD},{K_SMOOTH})  D({D_SMOOTH})\n"
        f"Vol-Target {TARGET_VOL*100:.0f}%  |  Max Lev {MAX_LEV}x  |  Fee {FEE*100:.2f}%/side",
        fontsize=13,
    )

    # Panel 2: Drawdown
    axes[1].fill_between(dates, dd * 100, 0, color="#e74c3c", alpha=0.6)
    axes[1].set_ylabel("Drawdown (%)", fontsize=11)
    axes[1].yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x:.0f}%"))
    axes[1].axhline(0, color="#888", lw=0.5)

    # Panel 3: KD lines + entry/exit markers
    valid = ~np.isnan(slow_k) & ~np.isnan(d_line)
    axes[2].plot(dates[valid], slow_k[valid], color="#f39c12", lw=0.9, label="%K")
    axes[2].plot(dates[valid], d_line[valid], color="#9b59b6", lw=0.9, label="%D")
    axes[2].axhline(80, color="#e74c3c", lw=0.7, ls="--", alpha=0.6, label="OB 80")
    axes[2].axhline(20, color="#2ecc71", lw=0.7, ls="--", alpha=0.6, label="OS 20")
    axes[2].set_ylabel("KD", fontsize=11)
    axes[2].set_ylim(-5, 105)
    axes[2].legend(fontsize=9, loc="upper right", ncol=2)

    plt.tight_layout()
    fname = f"{symbol}_kd_pnl.png"
    plt.savefig(fname, dpi=150)
    plt.show()
    print(f"Saved: {fname}")


# ── Regime Chart ──────────────────────────────────────────────────────────────
def plot_regime(df, result, symbol):
    strat_ret    = result["strat_ret"]
    realized_vol = result["realized_vol"]
    close        = df["close"].values
    dates        = df.index
    ma_window    = VOL_WINDOW * 200 // 30
    ma200        = pd.Series(close).rolling(ma_window).mean().values
    valid_ma     = ~np.isnan(ma200)
    vol_med      = np.nanmedian(realized_vol)
    valid_vol    = ~np.isnan(realized_vol)
    bull         = close > ma200
    hv           = realized_vol > vol_med

    def _stats(mask):
        r = strat_ret[mask]; r = r[~np.isnan(r)]
        if len(r) < 2: return None
        years = len(r) / HOURS_PER_YEAR; cum_r = np.prod(1 + r) - 1
        ann_r = (1 + cum_r) ** (1 / years) - 1 if years > 0 else np.nan
        cc = np.cumprod(1 + r); pk = np.maximum.accumulate(cc)
        return dict(ann_ret=ann_r, sharpe=_sharpe(r), max_dd=((cc - pk) / pk).min())

    groups = {
        "By Year":           [(str(yr), _stats(dates.year == yr))
                               for yr in sorted(dates.year.unique())],
        "Trend Regime":      [("Bull\n(>MA200)", _stats(bull & valid_ma)),
                               ("Bear\n(<MA200)", _stats(~bull & valid_ma))],
        "Volatility Regime": [("High Vol\n(>median)", _stats(hv & valid_vol)),
                               ("Low  Vol\n(≤median)", _stats(~hv & valid_vol))],
    }
    groups = {k: [(lb, s) for lb, s in v if s is not None] for k, v in groups.items()}

    fig, axes = plt.subplots(1, 3, figsize=(16, 6))
    for ax, (gname, items) in zip(axes, groups.items()):
        labels  = [lb for lb, _ in items]
        ann_ret = [s["ann_ret"] * 100 for _, s in items]
        sharpe  = [s["sharpe"]        for _, s in items]
        mdd     = [s["max_dd"] * 100  for _, s in items]
        x = np.arange(len(labels)); w = 0.25
        b1 = ax.bar(x - w, ann_ret, w, label="Ann Ret (%)", color="#3498db", alpha=0.85)
        b2 = ax.bar(x,     sharpe,  w, label="Sharpe",      color="#2ecc71", alpha=0.85)
        b3 = ax.bar(x + w, mdd,     w, label="MDD (%)",     color="#e74c3c", alpha=0.85)
        for bars in [b1, b2, b3]:
            for bar in bars:
                h = bar.get_height()
                ax.text(bar.get_x() + bar.get_width() / 2,
                        h + (0.3 if h >= 0 else -1.5),
                        f"{h:.1f}", ha="center",
                        va="bottom" if h >= 0 else "top", fontsize=8)
        ax.axhline(0, color="#555", lw=0.8)
        ax.set_title(gname, fontsize=13, fontweight="bold")
        ax.set_xticks(x); ax.set_xticklabels(labels, fontsize=10)
        ax.set_ylabel("Value", fontsize=10); ax.legend(fontsize=9)
        all_vals = ann_ret + sharpe + mdd
        ax.set_ylim(min(all_vals) - 5, max(all_vals) + 8)

    fig.suptitle(f"{symbol} — KD Regime Analysis", fontsize=13, fontweight="bold", y=1.01)
    plt.tight_layout()
    fname = f"{symbol}_kd_regime.png"
    plt.savefig(fname, dpi=150, bbox_inches="tight")
    plt.show()
    print(f"Saved: {fname}")


# ── 2D Parameter Scan (K_PERIOD × K_SMOOTH) ──────────────────────────────────
K_PERIOD_SCAN = [5, 9, 14, 21, 34, 55]
K_SMOOTH_SCAN = [2, 3, 5, 8, 13]

def param_scan(df):
    grid = np.full((len(K_PERIOD_SCAN), len(K_SMOOTH_SCAN)), np.nan)
    for i, kp in enumerate(K_PERIOD_SCAN):
        for j, ks in enumerate(K_SMOOTH_SCAN):
            res = run_backtest(df, k_period=kp, k_smooth=ks, d_smooth=D_SMOOTH)
            if res is not None and not np.isnan(res["sharpe"]):
                grid[i, j] = res["sharpe"]
    return grid


def find_plateau(grid, window=1):
    rows, cols = grid.shape
    nbr_mean = np.full((rows, cols), np.nan)
    for i in range(rows):
        for j in range(cols):
            if np.isnan(grid[i, j]): continue
            neighbors = [grid[i+di, j+dj]
                         for di in range(-window, window+1)
                         for dj in range(-window, window+1)
                         if 0 <= i+di < rows and 0 <= j+dj < cols
                         and not np.isnan(grid[i+di, j+dj])]
            if neighbors: nbr_mean[i, j] = np.mean(neighbors)
    best = np.unravel_index(np.nanargmax(nbr_mean), nbr_mean.shape)
    return best, nbr_mean


def plot_heatmap(grid, nbr_mean, plateau_idx, symbol):
    peak_idx = np.unravel_index(np.nanargmax(grid), grid.shape)
    fig, axes = plt.subplots(1, 2, figsize=(16, 6))

    for ax, mark_idx, title, note in [
        (axes[0], peak_idx,    "Peak — highest single Sharpe",
         f"K_PERIOD={K_PERIOD_SCAN[mark_idx[0]]}, K_SMOOTH={K_SMOOTH_SCAN[mark_idx[1]]}\n"
         f"Sharpe={grid[mark_idx]:.2f} — may be overfitted if isolated"),
        (axes[1], plateau_idx, "Plateau — most stable region",
         f"K_PERIOD={K_PERIOD_SCAN[plateau_idx[0]]}, K_SMOOTH={K_SMOOTH_SCAN[plateau_idx[1]]}\n"
         f"Sharpe={grid[plateau_idx]:.2f} — neighbors also perform well → more robust"),
    ]:
        masked = np.ma.masked_invalid(grid)
        cmap = plt.cm.RdYlGn.copy(); cmap.set_bad(color="#cccccc")
        vals = grid[~np.isnan(grid)]
        im = ax.imshow(masked, cmap=cmap, vmin=min(0, vals.min()),
                       vmax=np.percentile(vals, 95), aspect="auto")
        ax.add_patch(plt.Rectangle(
            (mark_idx[1] - 0.5, mark_idx[0] - 0.5), 1, 1,
            fill=False, edgecolor="gold", linewidth=3,
        ))
        for i in range(len(K_PERIOD_SCAN)):
            for j in range(len(K_SMOOTH_SCAN)):
                v = grid[i, j]
                ax.text(j, i, f"{v:.2f}" if not np.isnan(v) else "N/A",
                        ha="center", va="center", fontsize=9,
                        color="#888" if np.isnan(v) else "black")
        ax.set_xticks(range(len(K_SMOOTH_SCAN)));  ax.set_xticklabels(K_SMOOTH_SCAN)
        ax.set_yticks(range(len(K_PERIOD_SCAN)));  ax.set_yticklabels(K_PERIOD_SCAN)
        ax.set_xlabel("K_SMOOTH (slow %K period)")
        ax.set_ylabel("K_PERIOD (stochastic lookback)")
        ax.set_title(f"{symbol} — {title}\n{note}", fontsize=10)
        plt.colorbar(im, ax=ax, label="Sharpe Ratio")

    plt.suptitle("Same Sharpe grid, different selection method — prefer Plateau", fontsize=12)
    plt.tight_layout()
    fname = f"{symbol}_kd_heatmap.png"
    plt.savefig(fname, dpi=150)
    plt.show()
    print(f"Saved: {fname}")


# ── Main ──────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
    print(f"Loading kline for {SYMBOL} ({START_DATE} → {END_DATE}, {PERIOD})...")
    df = load_kline(SYMBOL, START_DATE, END_DATE, PERIOD)
    print(f"Bars loaded: {len(df)}")

    # Step 1: 2D scan → find plateau
    print("Running 2D parameter scan (K_PERIOD × K_SMOOTH)...")
    grid = param_scan(df)
    plateau_idx, nbr_mean = find_plateau(grid, window=1)

    best_kp = K_PERIOD_SCAN[plateau_idx[0]]
    best_ks = K_SMOOTH_SCAN[plateau_idx[1]]
    print(f"Plateau: K_PERIOD={best_kp}, K_SMOOTH={best_ks}  "
          f"(neighborhood mean Sharpe={nbr_mean[plateau_idx]:.2f})")

    plot_heatmap(grid, nbr_mean, plateau_idx, SYMBOL)

    # Step 2: full backtest with plateau parameters
    K_PERIOD, K_SMOOTH = best_kp, best_ks
    result = run_backtest(df, k_period=K_PERIOD, k_smooth=K_SMOOTH, d_smooth=D_SMOOTH)

    print(f"\nResults — KD({K_PERIOD},{K_SMOOTH},{D_SMOOTH}):")
    print(f"  總報酬率            : {result['total_return']*100:.1f}%")
    print(f"  年化報酬率           : {result['ann_ret']*100:.1f}%")
    print(f"  年化波動度           : {result['ann_vol']*100:.1f}%")
    print(f"  Sharpe Ratio      : {result['sharpe']:.2f}")
    print(f"  最大回撤 (MDD)      : {result['max_dd']*100:.1f}%")
    print(f"  勝率                : {result['win_rate']*100:.1f}%")
    print(f"  平均獲利 (per trade): {result['avg_win']*100:.2f}%")
    print(f"  平均虧損 (per trade): {result['avg_loss']*100:.2f}%")
    print(f"  總交易次數           : {result['n_trades']}")
    print(f"  平均年交易次數        : {result['avg_trades_yr']:.1f}")

    regime_analysis(df, result)
    plot_regime(df, result, SYMBOL)
    plot_pnl(df, result, SYMBOL)
```

---

## Parameters

| Parameter | Default | Notes |
|---|---|---|
| `K_PERIOD` | `9` | Stochastic lookback — lower = faster, more signals |
| `K_SMOOTH` | `3` | Slow %K smoothing (SMA) — higher = smoother |
| `D_SMOOTH` | `3` | %D smoothing (SMA of slow %K) — fixed at 3 in scan |
| `PERIOD` | `1h` | Kline timeframe |
| `VOL_WINDOW` | `720` | 30 days × 24h rolling vol |
| `TARGET_VOL` | `0.30` | 30% annualized target |
| `MAX_LEV` | `2.0` | Position size cap |
| `FEE` | `0.0005` | 0.05% per side (taker fee) |

---

## Notes

- **Long only** — no short positions
- **Parameter scan** sweeps K_PERIOD (5/9/14/21/34/55) × K_SMOOTH (2/3/5/8/13); D_SMOOTH is fixed at 3. The plateau selection (neighborhood mean Sharpe) is preferred over the single best cell — see HC backtest example for explanation
- **OB/OS filter (optional):** only enter golden cross when %K < 20 (oversold zone) for a stricter variant. Not implemented by default — add `and ki < 20` to the golden cross condition in `_kd_signal_loop`
- **Smoothing method:** this example uses SMA for both %K and %D, matching the classic formula. EWM (`pd.Series.ewm(span=k_smooth)`) gives more weight to recent bars and is common in real-time systems; swap in if preferred
- **Performance stack:** same as HC backtest — rolling vol via Pandas rolling (C/Cython), crossover loop via `@numba.njit` (stateful, can't vectorize), everything else NumPy vectorized

### Live Execution Timing

Backtest uses `fwd_ret[i] = (close[i+1] - close[i]) / close[i]`, meaning the order is filled at bar i's close price — the same bar where the crossover fires.

In live trading:
```
bar i closes
  → KD recomputes with new close
  → crossover detected → place market order immediately
  → fill ≈ bar i+1 open (for BTC 1h this is effectively bar i close)
```

Do NOT wait for bar i+1 to close. Waiting an extra bar introduces one-bar slippage that compounds over hundreds of trades and causes live PnL to diverge from backtest.

### KD vs RSI

KD (Stochastic) measures where the close sits within the recent high-low range; RSI measures the speed of price change. KD tends to generate more signals and responds faster to reversals, making it better suited for range-bound conditions. In strong trending markets, frequent death-cross exits can cut profitable trades short — consider adding a trend filter (e.g. price > MA200 to only trade bull regime signals) if regime analysis shows poor bear/high-vol performance.


path: examples/backtest-validation-mcpt-oos.md
content: # Example: Strategy Validation — MCPT + Out-of-Sample (KD vs Taker Intensity)

## What This Does

Validates two DOGE 1h long-only strategies side by side using:

1. **IS (In-Sample) Parameter Optimization** — 2D param scan on the first 2 years to find the most robust (plateau) parameters for each strategy
2. **OOS (Out-of-Sample) Validation** — run the IS-selected params on the held-out final year; a strategy with real edge should degrade gracefully, not collapse
3. **MCPT (Monte Carlo Permutation Test)** — shuffle the position array 1000 times, recompute Sharpe on each shuffle; p-value = fraction of shuffled Sharpes ≥ actual OOS Sharpe; p < 0.05 means the timing adds statistically significant value

**Strategies compared:**

| | KD Stochastic | Taker Intensity (多空力道) |
|---|---|---|
| Signal source | Price-derived (OHLCV) | Market microstructure (Blave alpha) |
| Entry | K crosses above D | alpha > ENTRY_TH |
| Exit | K crosses below D | alpha < EXIT_TH |
| Params scanned | K_PERIOD × K_SMOOTH | ENTRY_TH × EXIT_TH |

Same DOGE 1h data, same vol-targeting, same fees — apples-to-apples.

---

## Data Required

```
GET /kline?symbol=DOGEUSDT&period=1h                          (for KD + fwd_ret)
GET /taker_intensity/get_alpha?symbol=DOGEUSDT&period=1h      (for TI signal)
```

IS: trailing 3 → 1 year ago | OOS: trailing 1 year → today

---

## Full Code

```python
import numpy as np
import pandas as pd
import requests
import matplotlib.pyplot as plt
import numba
from datetime import datetime, timedelta, timezone
from dotenv import dotenv_values

_env = dotenv_values()

# ── Date Ranges ───────────────────────────────────────────────────────────────
_now      = datetime.now(timezone.utc)
END_DATE  = _now.strftime("%Y-%m-%d")
OOS_START = (_now - timedelta(days=365)).strftime("%Y-%m-%d")
IS_START  = (_now - timedelta(days=3 * 365)).strftime("%Y-%m-%d")
# IS : [IS_START,  OOS_START)  — 2 years
# OOS: [OOS_START, END_DATE)   — 1 year

# ── Strategy Params ───────────────────────────────────────────────────────────
# KD
KD_PERIOD_SCAN = [5, 9, 14, 21, 34]
KD_SMOOTH_SCAN = [2, 3, 5, 8]
D_SMOOTH       = 3

# TI (same threshold grid as HC example)
TI_THRESHOLDS  = [-2, -1.5, -1, -0.5, 0, 0.5, 1, 1.5, 2]

# Shared backtest settings
TARGET_VOL     = 0.30
MAX_LEV        = 2.0
VOL_WINDOW     = 720        # 30 days × 24h
HOURS_PER_YEAR = 8760
FEE            = 0.0005
N_PERMUTATIONS = 2000

API_BASE   = "https://api.blave.org"
API_KEY    = _env["blave_api_key"]
API_SECRET = _env["blave_secret_key"]
HEADERS    = {"api-key": API_KEY, "secret-key": API_SECRET}


# ── Fetch ─────────────────────────────────────────────────────────────────────
def _fetch_year_chunks(endpoint, params):
    s = datetime.strptime(params["start_date"], "%Y-%m-%d")
    e = datetime.strptime(params["end_date"],   "%Y-%m-%d")
    out = []
    while s < e:
        chunk_end = min(s + timedelta(days=365), e)
        r = requests.get(f"{API_BASE}/{endpoint}", headers=HEADERS, params={
            **params,
            "start_date": s.strftime("%Y-%m-%d"),
            "end_date":   chunk_end.strftime("%Y-%m-%d"),
        }, timeout=20)
        r.raise_for_status()
        out.append(r.json())
        s = chunk_end
    return out


def load_kline(start, end):
    chunks = _fetch_year_chunks("kline", {"symbol": "DOGEUSDT", "period": "1h",
                                           "start_date": start, "end_date": end})
    rows = [row for chunk in chunks for row in chunk]
    df = pd.DataFrame(rows, columns=["time", "open", "high", "low", "close"])
    df["time"] = pd.to_datetime(df["time"], unit="s", utc=True)
    df = df.set_index("time").sort_index().drop_duplicates()
    for col in ["open", "high", "low", "close"]:
        df[col] = df[col].astype(float)
    return df


def load_ti(start, end):
    chunks = _fetch_year_chunks("taker_intensity/get_alpha",
                                {"symbol": "DOGEUSDT", "period": "1h",
                                 "start_date": start, "end_date": end})
    ts, alphas = [], []
    for chunk in chunks:
        data = chunk.get("data", {})
        ts.extend(data.get("timestamp", []))
        alphas.extend(data.get("alpha", []))
    df = pd.DataFrame({"time": pd.to_datetime(ts, unit="s", utc=True), "ti": alphas})
    df = df.set_index("time").sort_index().drop_duplicates()
    df["ti"] = pd.to_numeric(df["ti"], errors="coerce")
    return df


# ── Signal Computation ────────────────────────────────────────────────────────
def compute_kd(df, k_period, k_smooth, d_smooth=D_SMOOTH):
    low_min  = df["low"].rolling(k_period).min()
    high_max = df["high"].rolling(k_period).max()
    denom    = high_max - low_min
    raw_k    = pd.Series(
        np.where(denom > 0, (df["close"] - low_min) / denom * 100, 50.0),
        index=df.index,
    )
    slow_k = raw_k.rolling(k_smooth).mean()
    d      = slow_k.rolling(d_smooth).mean()
    return slow_k.values, d.values


@numba.njit(cache=True)
def _kd_signal_loop(k, d):
    n = len(k); position = np.zeros(n); in_pos = False
    for i in range(1, n):
        ki, di, kp, dp = k[i], d[i], k[i-1], d[i-1]
        if np.isnan(ki) or np.isnan(di) or np.isnan(kp) or np.isnan(dp):
            position[i] = 1.0 if in_pos else 0.0; continue
        if not in_pos and kp <= dp and ki > di:  in_pos = True
        elif in_pos  and kp >= dp and ki < di:   in_pos = False
        position[i] = 1.0 if in_pos else 0.0
    return position


@numba.njit(cache=True)
def _ti_signal_loop(signal, entry_th, exit_th):
    n = len(signal); position = np.zeros(n); in_pos = False
    for i in range(n):
        if np.isnan(signal[i]):
            position[i] = 1.0 if in_pos else 0.0; continue
        if not in_pos and signal[i] > entry_th:  in_pos = True
        elif in_pos  and signal[i] < exit_th:    in_pos = False
        position[i] = 1.0 if in_pos else 0.0
    return position


# ── Core Backtest Engine (shared) ─────────────────────────────────────────────
def _run(close, position):
    """Given close prices and a position array, return strategy metrics."""
    n       = len(close)
    log_ret = np.concatenate([[0.0], np.log(close[1:] / close[:-1])])
    fwd_ret = np.empty(n)
    fwd_ret[:-1] = np.diff(close) / close[:-1]
    fwd_ret[-1]  = 0.0

    realized_vol = pd.Series(log_ret).rolling(VOL_WINDOW).std().values * np.sqrt(HOURS_PER_YEAR)
    vol_scalar   = np.where(
        (realized_vol > 0) & ~np.isnan(realized_vol),
        np.clip(TARGET_VOL / realized_vol, 0, MAX_LEV),
        1.0,
    )
    sized     = position * vol_scalar
    fee_cost  = np.abs(np.diff(sized, prepend=0)) * FEE
    strat_ret = sized * fwd_ret - fee_cost

    r   = strat_ret[~np.isnan(strat_ret)]
    cum = np.cumprod(1 + r)
    pk  = np.maximum.accumulate(cum)
    total_years  = len(r) / HOURS_PER_YEAR
    total_return = cum[-1] - 1
    ann_ret      = (1 + total_return) ** (1 / total_years) - 1
    sharpe       = (r.mean() / r.std()) * np.sqrt(HOURS_PER_YEAR) if r.std() > 0 else np.nan
    max_dd       = ((cum - pk) / pk).min()
    return dict(sharpe=sharpe, ann_ret=ann_ret, max_dd=max_dd,
                cum=cum, strat_ret=strat_ret, position=position)


# ── MCPT ──────────────────────────────────────────────────────────────────────
def mcpt(close, position, n=N_PERMUTATIONS):
    """
    Permute the forward return series (not positions).
    Position is held fixed, so fees and vol-scaling are identical for all permutations.
    Null: the return periods selected by this strategy are no better than random.
    p-value: fraction of permuted Sharpes >= actual OOS Sharpe.

    Why not permute position?
    A random shuffle of a binary 0/1 array produces ~N*p*(1-p) transitions vs the
    strategy's much smaller number. With FEE=0.0005 that creates 30-40x more fee drag
    on every permutation, forcing all permuted Sharpes deeply negative and producing
    a biased p-value regardless of whether the strategy has real edge.
    """
    log_ret = np.concatenate([[0.0], np.log(close[1:] / close[:-1])])
    fwd_ret = np.concatenate([np.diff(close) / close[:-1], [0.0]])

    realized_vol = pd.Series(log_ret).rolling(VOL_WINDOW).std().values * np.sqrt(HOURS_PER_YEAR)
    vol_scalar   = np.where(
        (realized_vol > 0) & ~np.isnan(realized_vol),
        np.clip(TARGET_VOL / realized_vol, 0, MAX_LEV),
        1.0,
    )
    sized    = position * vol_scalar
    fee_cost = np.abs(np.diff(sized, prepend=0)) * FEE  # identical for all permutations

    def _sharpe_from_ret(ret):
        sr = sized * ret - fee_cost
        r  = sr[~np.isnan(sr)]
        return (r.mean() / r.std()) * np.sqrt(HOURS_PER_YEAR) if r.std() > 0 else np.nan

    actual  = _sharpe_from_ret(fwd_ret)
    dist    = np.array([_sharpe_from_ret(np.random.permutation(fwd_ret)) for _ in range(n)])
    p_value = float((dist >= actual).mean())
    return actual, p_value, dist


# ── IS Param Scans ────────────────────────────────────────────────────────────
def find_plateau(grid, row_vals, col_vals, window=1):
    rows, cols  = grid.shape
    nbr_mean    = np.full((rows, cols), np.nan)
    for i in range(rows):
        for j in range(cols):
            if np.isnan(grid[i, j]): continue
            nb = [grid[i+di, j+dj]
                  for di in range(-window, window+1)
                  for dj in range(-window, window+1)
                  if 0 <= i+di < rows and 0 <= j+dj < cols
                  and not np.isnan(grid[i+di, j+dj])]
            if nb: nbr_mean[i, j] = np.mean(nb)
    best = np.unravel_index(np.nanargmax(nbr_mean), nbr_mean.shape)
    return best, nbr_mean, row_vals[best[0]], col_vals[best[1]]


def kd_param_scan(df_kline):
    grid = np.full((len(KD_PERIOD_SCAN), len(KD_SMOOTH_SCAN)), np.nan)
    close = df_kline["close"].values.astype(np.float64)
    for i, kp in enumerate(KD_PERIOD_SCAN):
        for j, ks in enumerate(KD_SMOOTH_SCAN):
            k_arr, d_arr = compute_kd(df_kline, kp, ks)
            pos = _kd_signal_loop(k_arr, d_arr)
            res = _run(close, pos)
            if not np.isnan(res["sharpe"]): grid[i, j] = res["sharpe"]
    return grid


def ti_param_scan(df_kline, df_ti):
    df = df_kline[["close"]].join(df_ti[["ti"]], how="inner").dropna(subset=["close"])
    close = df["close"].values.astype(np.float64)
    ti    = df["ti"].values.astype(np.float64)
    n     = len(TI_THRESHOLDS)
    grid  = np.full((n, n), np.nan)
    for i, entry in enumerate(TI_THRESHOLDS):
        for j, exit_ in enumerate(TI_THRESHOLDS):
            if exit_ > entry: continue
            pos = _ti_signal_loop(ti, entry, exit_)
            res = _run(close, pos)
            if not np.isnan(res["sharpe"]): grid[i, j] = res["sharpe"]
    return grid, df


# ── Chart ─────────────────────────────────────────────────────────────────────
def plot_results(symbol, results):
    """
    results: dict with keys "kd" and "ti", each containing:
      is_res, oos_res, mcpt_actual, mcpt_pvalue, mcpt_dist,
      is_dates, oos_dates, is_label, oos_label
    """
    fig = plt.figure(figsize=(16, 13))
    fig.suptitle(f"{symbol} — Strategy Validation: KD vs Taker Intensity",
                 fontsize=14, fontweight="bold", y=0.98)

    gs = fig.add_gridspec(3, 2, hspace=0.45, wspace=0.3)

    for col, (key, label, color) in enumerate([
        ("kd", "KD Stochastic",     "#3498db"),
        ("ti", "Taker Intensity",   "#e67e22"),
    ]):
        r = results[key]

        # Row 0: IS PnL
        ax_is = fig.add_subplot(gs[0, col])
        ax_is.plot(r["is_dates"], (r["is_res"]["cum"] - 1) * 100, color=color, lw=1.5)
        ax_is.axhline(0, color="#888", lw=0.6, ls="--")
        ax_is.set_title(
            f"{label} — IS (2y)\n"
            f"Sharpe={r['is_res']['sharpe']:.2f}  "
            f"Ann={r['is_res']['ann_ret']*100:.1f}%  "
            f"MDD={r['is_res']['max_dd']*100:.1f}%",
            fontsize=10,
        )
        ax_is.set_ylabel("Return (%)")
        ax_is.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x:.0f}%"))

        # Row 1: OOS PnL
        ax_oos = fig.add_subplot(gs[1, col])
        oos_color = "#2ecc71" if r["oos_res"]["sharpe"] > 0 else "#e74c3c"
        ax_oos.plot(r["oos_dates"], (r["oos_res"]["cum"] - 1) * 100, color=oos_color, lw=1.5)
        ax_oos.axhline(0, color="#888", lw=0.6, ls="--")
        sig_str = f"p={r['mcpt_pvalue']:.3f} {'✅' if r['mcpt_pvalue'] < 0.05 else '❌'}"
        ax_oos.set_title(
            f"{label} — OOS (1y)\n"
            f"Sharpe={r['oos_res']['sharpe']:.2f}  "
            f"Ann={r['oos_res']['ann_ret']*100:.1f}%  "
            f"MDD={r['oos_res']['max_dd']*100:.1f}%  {sig_str}",
            fontsize=10,
        )
        ax_oos.set_ylabel("Return (%)")
        ax_oos.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x:.0f}%"))

        # Row 2: MCPT distribution
        ax_mc = fig.add_subplot(gs[2, col])
        ax_mc.hist(r["mcpt_dist"], bins=40, color="#95a5a6", alpha=0.7, edgecolor="white")
        ax_mc.axvline(r["mcpt_actual"], color="#e74c3c", lw=2,
                      label=f"Actual={r['mcpt_actual']:.2f}")
        pct = np.percentile(r["mcpt_dist"], 95)
        ax_mc.axvline(pct, color="#f39c12", lw=1.5, ls="--",
                      label=f"95th pct={pct:.2f}")
        ax_mc.set_title(
            f"MCPT — OOS ({N_PERMUTATIONS} permutations)\np={r['mcpt_pvalue']:.3f}  "
            f"{'Significant (p<0.05)' if r['mcpt_pvalue'] < 0.05 else 'Not significant'}",
            fontsize=10,
        )
        ax_mc.set_xlabel("Permuted Sharpe"); ax_mc.set_ylabel("Count")
        ax_mc.legend(fontsize=9)

    fname = f"{symbol}_mcpt_oos.png"
    plt.savefig(fname, dpi=150, bbox_inches="tight")
    plt.show()
    print(f"Saved: {fname}")


# ── Main ──────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
    print(f"IS : {IS_START} → {OOS_START}")
    print(f"OOS: {OOS_START} → {END_DATE}")

    # ── Load data ─────────────────────────────────────────────────────────────
    print("\nLoading IS data...")
    kline_is = load_kline(IS_START, OOS_START)
    ti_is    = load_ti(IS_START, OOS_START)
    print(f"  kline IS : {len(kline_is)} bars")

    print("Loading OOS data...")
    kline_oos = load_kline(OOS_START, END_DATE)
    ti_oos    = load_ti(OOS_START, END_DATE)
    print(f"  kline OOS: {len(kline_oos)} bars")

    # ── KD: IS param scan ────────────────────────────────────────────────────
    print("\n── KD: IS param scan ──")
    kd_grid = kd_param_scan(kline_is)
    kd_idx, _, best_kp, best_ks = find_plateau(
        kd_grid, KD_PERIOD_SCAN, KD_SMOOTH_SCAN)
    print(f"  Plateau: K_PERIOD={best_kp}, K_SMOOTH={best_ks}  "
          f"IS Sharpe={kd_grid[kd_idx]:.2f}")

    # ── KD: IS full backtest ──────────────────────────────────────────────────
    kd_k_is, kd_d_is = compute_kd(kline_is, best_kp, best_ks)
    kd_pos_is        = _kd_signal_loop(kd_k_is, kd_d_is)
    kd_is_res        = _run(kline_is["close"].values.astype(np.float64), kd_pos_is)

    # ── KD: OOS validation ────────────────────────────────────────────────────
    kd_k_oos, kd_d_oos = compute_kd(kline_oos, best_kp, best_ks)
    kd_pos_oos         = _kd_signal_loop(kd_k_oos, kd_d_oos)
    kd_oos_res         = _run(kline_oos["close"].values.astype(np.float64), kd_pos_oos)

    # ── KD: MCPT on OOS ──────────────────────────────────────────────────────
    print("  Running MCPT (KD OOS)...")
    kd_actual, kd_pvalue, kd_dist = mcpt(
        kline_oos["close"].values.astype(np.float64), kd_pos_oos)

    # ── TI: IS param scan ────────────────────────────────────────────────────
    print("\n── TI: IS param scan ──")
    ti_grid, df_ti_is = ti_param_scan(kline_is, ti_is)
    ti_idx, _, best_entry, best_exit = find_plateau(
        ti_grid, TI_THRESHOLDS, TI_THRESHOLDS)
    print(f"  Plateau: entry={best_entry}, exit={best_exit}  "
          f"IS Sharpe={ti_grid[ti_idx]:.2f}")

    # ── TI: IS full backtest ──────────────────────────────────────────────────
    ti_close_is  = df_ti_is["close"].values.astype(np.float64)
    ti_signal_is = df_ti_is["ti"].values.astype(np.float64)
    ti_pos_is    = _ti_signal_loop(ti_signal_is, best_entry, best_exit)
    ti_is_res    = _run(ti_close_is, ti_pos_is)

    # ── TI: OOS validation ────────────────────────────────────────────────────
    df_ti_oos   = kline_oos[["close"]].join(ti_oos[["ti"]], how="inner").dropna(subset=["close"])
    ti_close_oos  = df_ti_oos["close"].values.astype(np.float64)
    ti_signal_oos = df_ti_oos["ti"].values.astype(np.float64)
    ti_pos_oos    = _ti_signal_loop(ti_signal_oos, best_entry, best_exit)
    ti_oos_res    = _run(ti_close_oos, ti_pos_oos)

    # ── TI: MCPT on OOS ──────────────────────────────────────────────────────
    print("  Running MCPT (TI OOS)...")
    ti_actual, ti_pvalue, ti_dist = mcpt(ti_close_oos, ti_pos_oos)

    # ── Summary table ─────────────────────────────────────────────────────────
    kd_deg = kd_oos_res["sharpe"] / kd_is_res["sharpe"] if kd_is_res["sharpe"] != 0 else np.nan
    ti_deg = ti_oos_res["sharpe"] / ti_is_res["sharpe"] if ti_is_res["sharpe"] != 0 else np.nan

    hdr = f"  {'Metric':<28} {'KD':>12} {'TI':>12}"
    sep = "─" * len(hdr)
    print(f"\n{sep}\n  Validation Summary\n{sep}")
    print(hdr); print(sep)
    rows = [
        ("IS Sharpe",             f"{kd_is_res['sharpe']:.2f}",   f"{ti_is_res['sharpe']:.2f}"),
        ("OOS Sharpe",            f"{kd_oos_res['sharpe']:.2f}",  f"{ti_oos_res['sharpe']:.2f}"),
        ("Degradation (OOS/IS)",  f"{kd_deg:.2f}",               f"{ti_deg:.2f}"),
        ("IS Ann Return",         f"{kd_is_res['ann_ret']*100:.1f}%", f"{ti_is_res['ann_ret']*100:.1f}%"),
        ("OOS Ann Return",        f"{kd_oos_res['ann_ret']*100:.1f}%", f"{ti_oos_res['ann_ret']*100:.1f}%"),
        ("IS MDD",                f"{kd_is_res['max_dd']*100:.1f}%", f"{ti_is_res['max_dd']*100:.1f}%"),
        ("OOS MDD",               f"{kd_oos_res['max_dd']*100:.1f}%", f"{ti_oos_res['max_dd']*100:.1f}%"),
        ("MCPT p-value (OOS)",    f"{kd_pvalue:.3f}",             f"{ti_pvalue:.3f}"),
        ("MCPT significant?",     "✅" if kd_pvalue < 0.05 else "❌", "✅" if ti_pvalue < 0.05 else "❌"),
    ]
    for label, kd_val, ti_val in rows:
        print(f"  {label:<28} {kd_val:>12} {ti_val:>12}")
    print(sep)
    print("  Degradation > 0.5 is acceptable; < 0 means strategy broke down in OOS.")
    print("  MCPT p < 0.05: entry/exit timing is statistically significant.")

    # ── Chart ─────────────────────────────────────────────────────────────────
    plot_results("DOGEUSDT", {
        "kd": dict(
            is_res=kd_is_res, oos_res=kd_oos_res,
            mcpt_actual=kd_actual, mcpt_pvalue=kd_pvalue, mcpt_dist=kd_dist,
            is_dates=kline_is.index[~np.isnan(kd_is_res["strat_ret"])],
            oos_dates=kline_oos.index[~np.isnan(kd_oos_res["strat_ret"])],
        ),
        "ti": dict(
            is_res=ti_is_res, oos_res=ti_oos_res,
            mcpt_actual=ti_actual, mcpt_pvalue=ti_pvalue, mcpt_dist=ti_dist,
            is_dates=df_ti_is.index[~np.isnan(ti_is_res["strat_ret"])],
            oos_dates=df_ti_oos.index[~np.isnan(ti_oos_res["strat_ret"])],
        ),
    })
```

---

## Output Interpretation

### Degradation Ratio (OOS Sharpe / IS Sharpe)

| Ratio | Meaning |
|---|---|
| > 0.8 | Excellent — almost no degradation |
| 0.5 – 0.8 | Acceptable — typical for real strategies |
| 0.2 – 0.5 | Weak — likely overfitted to IS period |
| < 0 | Failed — strategy broke down in OOS |

### MCPT p-value

| p-value | Meaning |
|---|---|
| < 0.01 | Highly significant — timing very unlikely to be random |
| 0.01 – 0.05 | Significant — timing adds measurable value |
| 0.05 – 0.10 | Borderline — marginal evidence |
| > 0.10 | Not significant — strategy may be luck |

### What to look for

- A strategy that passes both tests (degradation > 0.5 AND p < 0.05) has genuine, robust edge.
- High IS Sharpe + poor OOS = overfitted. Common with technical indicators on short IS windows.
- Low IS Sharpe + maintains in OOS = underfit but potentially real. Widen the param search.
- MCPT passing but high OOS degradation = edge exists but parameter selection was too specific to IS data. Use the plateau (not peak) cell, or widen the plateau window.

---

## Notes

- **MCPT method — position permutation:** shuffles the entry/exit timing (position array) while keeping the return series fixed. Tests: "given BTC's actual return distribution, does the timing of this strategy's trades matter?" This is comparable across both strategies because neither strategy has its signal permuted — only when it acts.
- **Why not permute returns?** For KD, the signal is derived from price; permuting returns changes the price path and therefore the KD signal itself, making it circular. Position permutation is clean for both strategies.
- **IS/OOS split is time-based, not random.** Shuffling time order for a financial backtest introduces look-ahead bias. The IS period always precedes OOS.
- **Walk-forward extension:** for more rigorous validation, replace the single IS/OOS split with multiple expanding windows (e.g., 12-month IS → 3-month OOS, rolling forward). Not implemented here to keep the example readable.
- **Both strategies use the same backtest engine** (`_run`), same vol-targeting, same fee model — the only difference is how `position` is computed.


path: examples/bitfinex-auto-lending.md
content: # Example: Bitfinex Auto-Lending

Automatically lend idle funds on Bitfinex. Adapts to market conditions:
- **Period:** FRR high → allocate more to long periods (lock in). FRR low → stay short (flexibility)
- **Rate:** median to P75 ladder per period, floored by order book best bid
- **Large amounts:** split across multiple periods and rate levels for better fill

---

## Dependencies

```bash
pip install requests
```

---

## Code

```python
#!/usr/bin/env python3
"""
Bitfinex 自動放貸：
  - FRR 決定天期分配（高利率→長期為主，低利率→短期為主）
  - 大單拆成梯形掛單：多天期 × 多利率（median → P75）
  - 不低於 order book best bid

cron 每 15 分鐘執行一次
"""

import hmac, hashlib, json, time
from datetime import datetime, timezone
import requests

# ── Config ────────────────────────────────────────────────────────────────
BITFINEX_API_KEY    = "YOUR_API_KEY"
BITFINEX_API_SECRET = "YOUR_API_SECRET"
CURRENCY            = "UST"              # UST = USDT on Bitfinex
MIN_OFFER           = 150.0              # Bitfinex minimum: $150 equivalent
DEMAND_THRESHOLD    = 50_000             # book: require 50K+ cumulative bid depth
TRANCHES_PER_PERIOD = 3                  # rate levels per period (median → P75)

BASE_URL = "https://api.bitfinex.com"
PUB_URL  = "https://api-pub.bitfinex.com"

# FRR → period allocation weights
# High FRR: weight long (lock in). Low FRR: weight short (stay flexible).
PERIOD_WEIGHTS = {
    10.0: {2: 0.1, 30: 0.2, 120: 0.7},           # FRR ≥ 10%
    6.0:  {2: 0.2, 7: 0.2, 30: 0.3, 120: 0.3},   # FRR ≥ 6%
    3.0:  {2: 0.4, 7: 0.3, 30: 0.2, 120: 0.1},   # FRR ≥ 3%
    0.0:  {2: 0.7, 7: 0.2, 30: 0.1},              # FRR < 3%
}


# ── Auth ──────────────────────────────────────────────────────────────────
def bfx_request(path, body=None):
    body = body or {}
    nonce = str(int(time.time() * 1_000_000))
    body_json = json.dumps(body)
    sig = hmac.new(
        BITFINEX_API_SECRET.encode(),
        f"/api/{path}{nonce}{body_json}".encode(),
        hashlib.sha384,
    ).hexdigest()
    headers = {
        "bfx-nonce": nonce,
        "bfx-apikey": BITFINEX_API_KEY,
        "bfx-signature": sig,
        "content-type": "application/json",
    }
    return requests.post(f"{BASE_URL}/{path}", headers=headers,
                         data=body_json, timeout=15)


# ── Market Analysis ───────────────────────────────────────────────────────
def fetch_trades_3d(symbol="fUST"):
    """Fetch 3 days of funding trades with pagination."""
    end = int(time.time() * 1000)
    start = end - 3 * 24 * 60 * 60 * 1000
    all_trades = []
    cursor = end

    while cursor > start:
        r = requests.get(
            f"{PUB_URL}/v2/trades/{symbol}/hist",
            params={"limit": 10000, "start": start, "end": cursor, "sort": -1},
            timeout=15,
        )
        r.raise_for_status()
        batch = r.json()
        if not batch:
            break
        all_trades.extend(batch)
        cursor = batch[-1][1] - 1
        time.sleep(0.5)

    seen = set()
    return [t for t in all_trades if t[0] not in seen and not seen.add(t[0])]


def analyze_rates(trades, period):
    """Compute rate statistics for a specific period."""
    rates = sorted([t[3] for t in trades if t[4] == period])
    if not rates:
        return None
    n = len(rates)
    return {
        "count":  n,
        "min":    rates[0],
        "max":    rates[-1],
        "median": rates[n // 2],
        "p75":    rates[3 * n // 4],
    }


def get_frr(symbol="fUST"):
    r = requests.get(f"{PUB_URL}/v2/ticker/{symbol}", timeout=10)
    r.raise_for_status()
    return r.json()[0]


def get_book_best_bid(symbol="fUST", demand_threshold=50_000):
    """Rate where cumulative borrower demand ≥ threshold."""
    r = requests.get(f"{PUB_URL}/v2/book/{symbol}/P0",
                     params={"len": 250}, timeout=10)
    r.raise_for_status()
    bids = sorted([e for e in r.json() if e[3] > 0], key=lambda x: -x[0])
    if not bids:
        return None, 0
    cumulative = 0
    for b in bids:
        cumulative += b[3]
        if cumulative >= demand_threshold:
            return b[0], cumulative
    return bids[-1][0], cumulative


def choose_weights(frr):
    """Pick period allocation weights based on FRR."""
    frr_ann = frr * 365 * 100
    for threshold in sorted(PERIOD_WEIGHTS.keys(), reverse=True):
        if frr_ann >= threshold:
            return PERIOD_WEIGHTS[threshold], threshold
    return PERIOD_WEIGHTS[0.0], 0.0


# ── Account ───────────────────────────────────────────────────────────────
def get_available_balance(currency="UST"):
    r = bfx_request("v2/auth/r/wallets")
    r.raise_for_status()
    for w in r.json():
        if w[0] == "funding" and w[1] == currency:
            return float(w[4] or w[2] or 0)
    return 0.0


def get_active_offers(symbol=None):
    path = f"v2/auth/r/funding/offers/{symbol}" if symbol else "v2/auth/r/funding/offers"
    r = bfx_request(path)
    r.raise_for_status()
    return r.json()


def get_active_credits(symbol=None):
    path = f"v2/auth/r/funding/credits/{symbol}" if symbol else "v2/auth/r/funding/credits"
    r = bfx_request(path)
    r.raise_for_status()
    return r.json()


# ── Execute ───────────────────────────────────────────────────────────────
def submit_offer(currency, amount, rate, period):
    body = {
        "type": "LIMIT",
        "symbol": f"f{currency}",
        "amount": str(round(amount, 8)),
        "rate": str(rate),
        "period": period,
    }
    r = bfx_request("v2/auth/w/funding/offer/submit", body)
    r.raise_for_status()
    data = r.json()
    if isinstance(data, list) and data[0] == "error":
        return False, data[2]
    status = data[6] if len(data) > 6 else ""
    return status == "SUCCESS", data


def cancel_offer(offer_id):
    r = bfx_request("v2/auth/w/funding/offer/cancel", {"id": offer_id})
    r.raise_for_status()
    data = r.json()
    if isinstance(data, list) and data[0] == "error":
        return False, data[2]
    return True, data


# ── Build Ladder ──────────────────────────────────────────────────────────
def build_ladder(total_amount, weights, all_trades, book_rate, frr):
    """
    Build a list of (period, rate, amount) offers.
    Splits across periods by weight, then within each period
    spreads rates from median to P75 in TRANCHES_PER_PERIOD steps.
    All rates floored by book_rate.
    """
    ann = lambda r: f"{r * 365 * 100:.2f}%"
    offers = []

    for period, weight in weights.items():
        period_amount = total_amount * weight
        if period_amount < MIN_OFFER:
            continue

        stats = analyze_rates(all_trades, period)

        if not stats or stats["count"] < 5:
            # Thin market — single offer at max(FRR, book)
            rate = max(frr, book_rate) if book_rate else frr
            offers.append((period, rate, period_amount))
            continue

        median = stats["median"]
        p75 = stats["p75"]

        # How many tranches can we fit? (each ≥ MIN_OFFER)
        n = min(TRANCHES_PER_PERIOD, int(period_amount // MIN_OFFER))
        n = max(n, 1)
        tranche_amount = period_amount / n

        for i in range(n):
            # Linear interpolation: median → P75
            frac = i / (n - 1) if n > 1 else 0.5
            rate = median + frac * (p75 - median)
            # Floor: don't go below book best bid
            if book_rate:
                rate = max(rate, book_rate)
            offers.append((period, rate, tranche_amount))

    return offers


# ── Main ──────────────────────────────────────────────────────────────────
def run():
    symbol = f"f{CURRENCY}"
    ann = lambda r: f"{r * 365 * 100:.2f}%"

    # 1. FRR → allocation weights
    frr = get_frr(symbol)
    frr_ann = frr * 365 * 100
    weights, threshold = choose_weights(frr)
    print(f"FRR: {ann(frr)} (≥ {threshold}%)")
    print(f"Allocation: {', '.join(f'{p}d={w:.0%}' for p, w in weights.items())}")

    # 2. Fetch market data
    print(f"Fetching 3d trades...")
    trades = fetch_trades_3d(symbol)
    print(f"  {len(trades)} trades")

    book_rate, book_depth = get_book_best_bid(symbol, DEMAND_THRESHOLD)
    if book_rate:
        print(f"  Book bid (≥{DEMAND_THRESHOLD/1000:.0f}K): {ann(book_rate)} ({book_depth:,.0f} UST)")

    # 3. Check balance
    available = get_available_balance(CURRENCY)
    print(f"\nAvailable {CURRENCY}: {available:.2f}")

    if available < MIN_OFFER:
        print(f"Below minimum ({MIN_OFFER})")
        return

    # 4. Cancel all existing offers (rebuild ladder fresh each run)
    offers = get_active_offers(symbol)
    if offers:
        print(f"Cancelling {len(offers)} existing offers...")
        for o in offers:
            cancel_offer(o[0])
            available += abs(o[4])
            time.sleep(0.3)

    # 5. Build ladder
    ladder = build_ladder(available, weights, trades, book_rate, frr)

    if not ladder:
        print("No valid offers to submit")
        return

    # 6. Submit
    print(f"\n{'Period':>6} {'Rate':>10} {'Amount':>12}")
    print("-" * 32)
    submitted = 0
    for period, rate, amount in ladder:
        if amount < MIN_OFFER:
            continue
        print(f"{period:>4}d {ann(rate):>10} {amount:>11,.0f}", end="")
        ok, result = submit_offer(CURRENCY, amount, rate, period)
        if ok:
            offer = result[4][0] if isinstance(result[4][0], list) else result[4]
            print(f"  ✓ {offer[0]}")
            submitted += 1
        else:
            print(f"  ✗ {result}")
        time.sleep(0.3)

    print(f"\nSubmitted {submitted} offers")

    # 7. Summary
    credits = get_active_credits(symbol)
    if credits:
        total_lent = sum(abs(c[5]) for c in credits)
        rates = [c[11] for c in credits]
        periods = [c[12] for c in credits]
        print(f"Lent out: {total_lent:,.0f} {CURRENCY} in {len(credits)} credits")
        print(f"  Rates: {ann(min(rates))} — {ann(max(rates))}")
        print(f"  Periods: {min(periods)}d — {max(periods)}d")


if __name__ == "__main__":
    run()
```

---

## How the Ladder Works

### Period allocation (FRR decides)

| FRR | 2d | 7d | 30d | 120d |
|---|---|---|---|---|
| ≥ 10% | 10% | — | 20% | **70%** |
| 6–10% | 20% | 20% | 30% | 30% |
| 3–6% | **40%** | 30% | 20% | 10% |
| < 3% | **70%** | 20% | 10% | — |

### Rate ladder (per period)

Within each period, the amount is split into `TRANCHES_PER_PERIOD` (default 3) offers with rates linearly spaced from **median** to **P75** of 3-day historical trades:

```
Tranche 1: median         (most competitive, fills first)
Tranche 2: (median+P75)/2 (mid)
Tranche 3: P75            (highest rate, fills last)
```

**All rates floored by order book best bid** — never offer below what borrowers are already willing to pay.

### Example output (10,000 UST at FRR 8.87%)

```
FRR: 8.87% (≥ 6%)
Allocation: 2d=20%, 7d=20%, 30d=30%, 120d=30%

Period       Rate       Amount
--------------------------------
   2d      5.20%       667  ✓  (book floor)
   2d      5.20%       667  ✓  (book floor)
   2d      5.20%       667  ✓  (book floor)
   7d      5.20%       667  ✓  (book floor)
   7d      5.20%       667  ✓  (book floor)
   7d      5.20%       667  ✓  (book floor)
  30d      5.20%     1,000  ✓  (median, floored by book)
  30d      7.06%     1,000  ✓  (mid)
  30d      9.44%     1,000  ✓  (P75)
 120d      9.69%     1,000  ✓  (median)
 120d      9.84%     1,000  ✓  (mid)
 120d     10.00%     1,000  ✓  (P75)

Submitted 12 offers
```

Short periods (2d, 7d) hit the book floor so all tranches are at the same rate. Long periods (30d, 120d) have natural spread between median and P75.

---

## Small amounts (< $1,000)

If total amount is small, fewer tranches fit (each must be ≥ $150). The script automatically reduces tranches:
- $150–$449: 1 offer at best single period
- $450–$899: up to 3 offers
- $900+: full ladder

---

## Deployment

```bash
*/15 * * * * cd /path/to/project && python3 bitfinex_auto_lend.py >> /var/log/bfx_lend.log 2>&1
```

Each run:
1. FRR → pick period weights
2. Fetch 3d trades → rate stats per period
3. Fetch order book → floor rate
4. Cancel all existing offers (rebuild fresh)
5. Build ladder → submit offers
6. Report lent-out credits

---

## Customising

### More aggressive (lock long earlier)

```python
PERIOD_WEIGHTS = {
    8.0:  {2: 0.1, 30: 0.2, 120: 0.7},
    5.0:  {2: 0.2, 7: 0.2, 30: 0.3, 120: 0.3},
    2.0:  {2: 0.4, 7: 0.3, 30: 0.2, 120: 0.1},
    0.0:  {2: 0.7, 7: 0.2, 30: 0.1},
}
```

### More tranches (finer rate spread)

```python
TRANCHES_PER_PERIOD = 5   # 5 levels from median to P75
```

---

## Notes

- **Minimum offer:** $150 USD equivalent per tranche
- **Rebuild vs update:** each run cancels all offers and rebuilds. Simple and idempotent — no stale offer tracking needed
- **Book floor:** prevents offering below current borrower demand. Short periods often have their median below the book bid, so they get raised
- **Thin periods:** if a period has <5 trades in 3 days, a single offer at max(FRR, book) is used instead of a ladder
- **Amount sign:** positive = lend, negative = borrow


path: examples/blave-alpha-screening.md
content: # Example: Blave Alpha Screening — Find High-Conviction Small-Cap Tokens

## Strategy

Screen for small-cap tokens where smart money is quietly accumulating:
- **Small cap** — market cap percentile ≤ 50 (bottom half of all listed coins)
- **High Holder Concentration** (籌碼集中) — alpha > 0.5, long side concentrated
- **High Whale Activity** (巨鯨警報) — 24h OI score > 0.5, large players moving in
- **Output** — top 10 ranked by combined signal strength

When both signals are high on a small-cap coin, it often means accumulation is happening before a sharp move. The coin may already be moving (+24h%) or still coiling — both are worth watching.

---

## Step 1: Pull Alpha Table

```
GET /alpha_table
```

One request covers all symbols. Each symbol contains all indicator values plus `statistics`.

---

## Step 2: Screen

```python
results = []

for symbol, d in alpha_table['data'].items():
    try:
        mc_pct  = float(d.get('market_cap_percentile', {}).get('-', ''))
        hc      = float(d.get('holder_concentration', {}).get('-', ''))
        wh_24h  = float(d.get('whale_hunter', {}).get('24h-score_oi', ''))
        stats   = d.get('statistics', {})
        mc_usd  = float(d.get('market_cap', {}).get('-', 0) or 0)
        up_prob = float(stats.get('up_prob', 0) or 0)
        exp_val = float(stats.get('exp_value', 0) or 0)

        if not stats.get('is_data_sufficient', False):
            continue

        # Filter
        if mc_pct <= 50 and hc > 0.5 and wh_24h > 0.5:
            results.append({
                'symbol':   symbol,
                'hc':       hc,
                'whale':    wh_24h,
                'mc_pct':   mc_pct,
                'mc_usd':   mc_usd,
                'up_prob':  up_prob,
                'exp_value': exp_val,
                'price_chg_24h': d.get('price_change', {}).get('24h', None),
            })
    except (ValueError, TypeError):
        continue

# Rank by combined signal strength
results.sort(key=lambda x: x['hc'] + x['whale'], reverse=True)
top10 = results[:10]
```

---

## Step 3: Output

Present as a ranked table:

| Rank | Symbol | HC | Whale | MC% | Market Cap | Up Prob | Exp Value | 24h Change |
|---|---|---|---|---|---|---|---|---|
| 1 | TRU | 8.16 | 14.66 | 7.4% | $8M | 2.2% | -3.78% | +72.0% |
| 2 | KOMA | 4.91 | 5.58 | 1.7% | $4M | 37.7% | -0.79% | +56.3% |
| 3 | HIPPO | 3.09 | 7.46 | 3.2% | $6M | 42.1% | -0.20% | -4.0% |

**Reading the results:**
- **Already moving** (large 24h change + strong signals) — accumulation phase may be ending, entering now is chasing
- **Not yet moved** (flat 24h + strong signals) — potential setup still coiling, higher risk/reward
- `up_prob` and `exp_value` from `statistics` give a historical base rate — use as context, not as a trigger

---

## Alpha Scale Reference

| Alpha Value | Holder Concentration | Whale Hunter |
|---|---|---|
| > 3 | Over Concentrated (long) | Overly Bullish |
| 2 – 3 | Highly Concentrated (long) | Highly Bullish |
| 0.5 – 2 | Concentrated (long) | Bullish |
| -0.5 – 0.5 | Neutral | Neutral |
| < -0.5 | Concentrated (short) | Bearish |

---

## Optional: Add Taker Intensity for Confirmation

If you want to confirm buying pressure is actually present (not just silent accumulation):

```python
wh_vol = float(d.get('whale_hunter', {}).get('24h-score_volume', ''))
taker  = float(d.get('taker_intensity', {}).get('24h', ''))  # field name may vary

# Add to filter:
if taker > 0:  # net buying pressure
    ...
```

---

## Risk Notes

- Small-cap coins are illiquid — large slippage on entry/exit
- Strong signals on coins that already pumped 50%+ today = chasing; look at coins with signals but flat price instead
- Always check funding rate (`funding_rate` field in alpha_table) — high positive funding on a small cap = crowded long, squeeze risk
- Use position sizing accordingly: higher signal strength does not mean lower risk


path: examples/btc-etf-flow-monitor.md
content: # Example: Bitcoin ETF Flow Monitor — Track Institutional Accumulation

Track daily Bitcoin spot ETF fund flows (IBIT, FBTC, GBTC, etc.) from Farside Investors. Detect when BlackRock and other institutions are accumulating or distributing.

---

## Dependencies

```bash
pip install curl-cffi beautifulsoup4
```

---

## Code

```python
import re
from curl_cffi import requests as cf_requests
from bs4 import BeautifulSoup

IMPERSONATE = "chrome110"
URL = "https://farside.co.uk/bitcoin-etf-flow-all-data/"  # full history since Jan 2024

FUNDS = ["IBIT", "FBTC", "BITB", "ARKB", "BTCO", "EZBC",
         "BRRR", "HODL", "BTCW", "MSBT", "GBTC", "BTC"]


def parse_value(s: str) -> float | None:
    """Parse Farside format: '269.3' or '(86.5)' (negative) or '-' (no data)."""
    s = s.strip()
    if not s or s == "-":
        return None
    neg = "(" in s
    s = s.replace("(", "").replace(")", "").replace(",", "")
    try:
        v = float(s)
        return -v if neg else v
    except ValueError:
        return None


def fetch_flows() -> list[dict]:
    """Fetch all daily ETF flow rows from Farside. Returns list of dicts."""
    s = cf_requests.Session(impersonate=IMPERSONATE)
    r = s.get(URL, headers={"Accept": "text/html"})
    r.raise_for_status()

    soup = BeautifulSoup(r.text, "html.parser")
    table = soup.find_all("table")[1]  # second table = flow data
    rows = table.find_all("tr")

    data = []
    for row in rows:
        cells = [td.text.strip() for td in row.find_all("td")]
        if not cells or len(cells) < 14:
            continue
        date_str = cells[0]
        if any(k in date_str for k in ["Total", "Average", "Maximum", "Minimum"]):
            continue

        entry = {"date": date_str}
        for i, fund in enumerate(FUNDS):
            entry[fund] = parse_value(cells[i + 1])
        entry["Total"] = parse_value(cells[13])
        data.append(entry)

    # Deduplicate by date (Farside sometimes has dupes)
    seen = set()
    deduped = []
    for row in data:
        if row["date"] not in seen:
            seen.add(row["date"])
            deduped.append(row)
    return deduped


def print_recent(data: list[dict], days: int = 20):
    """Print the most recent N trading days."""
    recent = data[-days:]

    header = f"{'Date':<16}"
    for fund in ["IBIT", "FBTC", "ARKB", "GBTC", "Total"]:
        header += f"{fund:>9}"
    print(header)
    print("-" * len(header))

    for row in recent:
        line = f"{row['date']:<16}"
        for fund in ["IBIT", "FBTC", "ARKB", "GBTC", "Total"]:
            v = row.get(fund)
            if v is None:
                line += f"{'—':>9}"
            elif v < 0:
                line += f"{'(' + f'{abs(v):.1f}' + ')':>9}"
            else:
                line += f"{v:>9.1f}"
        print(line)


def summarise(data: list[dict], month: str = None):
    """Summarise flows for a given month (e.g. 'Apr 2026') or all data."""
    filtered = data
    if month:
        filtered = [r for r in data if month in r["date"]]

    if not filtered:
        print(f"No data for {month}")
        return

    # Per-fund totals
    totals = {}
    for fund in FUNDS + ["Total"]:
        vals = [r[fund] for r in filtered if r.get(fund) is not None]
        totals[fund] = sum(vals)

    inflow_days = sum(1 for r in filtered if (r.get("IBIT") or 0) > 0)
    outflow_days = sum(1 for r in filtered if (r.get("IBIT") or 0) < 0)
    trading_days = sum(1 for r in filtered if r.get("IBIT") is not None)

    print(f"\n{'Fund':<8} {'Net Flow ($M)':>14}")
    print("-" * 24)
    for fund in ["IBIT", "FBTC", "BITB", "ARKB", "GBTC", "Total"]:
        v = totals.get(fund, 0)
        print(f"{fund:<8} {v:>13.1f}")

    print(f"\nIBIT: {inflow_days} inflow days / {outflow_days} outflow days / {trading_days} total")


if __name__ == "__main__":
    data = fetch_flows()
    print(f"Fetched {len(data)} trading days\n")
    print_recent(data, days=15)
    summarise(data, month="Apr 2026")
```

---

## Example Output

```
Date                 IBIT     FBTC     ARKB     GBTC    Total
--------------------------------------------------------------
01 Apr 2026        (86.5)   (78.6)      0.0   (13.3)  (173.7)
02 Apr 2026         (3.0)      7.3      0.0      0.0      9.0
06 Apr 2026        181.9    147.3    118.8      0.0    471.4
09 Apr 2026        269.3     53.3      4.8      0.0    358.1
10 Apr 2026        137.6     78.0      3.6      0.0    256.7
14 Apr 2026        213.8     45.3    113.1      0.0    411.4
15 Apr 2026        291.9    (47.3)   (42.2)   (23.4)   186.1

Fund       Net Flow ($M)
------------------------
IBIT           1,063.0
FBTC             178.4
ARKB              98.3
GBTC             (87.0)
Total          1,376.1

IBIT: 7 inflow days / 3 outflow days / 10 total
```

---

## How to Read

- **Unit:** millions USD. `(86.5)` = net outflow of $86.5M
- **IBIT** = BlackRock iShares Bitcoin Trust (largest BTC ETF, ~45% market share)
- **FBTC** = Fidelity, **ARKB** = Ark/21Shares, **GBTC** = Grayscale (legacy, high fee)
- **Divergence signal:** when IBIT is positive but Total is negative → BlackRock is buying while others are selling. This often precedes recoveries
- **GBTC consistently negative** = continued outflow from Grayscale's 1.5% fee product into cheaper alternatives

---

## Combining with Blave Alpha

Use ETF flow direction as a macro filter for Blave signals:

```python
# If IBIT has been net positive for 3+ consecutive days,
# bias toward long signals from Blave alpha_table
recent_ibit = [r["IBIT"] for r in data[-5:] if r.get("IBIT") is not None]
consecutive_inflow = all(v > 0 for v in recent_ibit[-3:])

if consecutive_inflow:
    # Institutional tailwind — Blave long signals have higher conviction
    ...
```

---

## Notes

- Farside publishes data after US market close (typically available by ~21:00 UTC)
- Data covers all US-listed spot Bitcoin ETFs since launch (Jan 2024)
- Weekend/holiday dates are skipped (no trading)
- `curl_cffi` with `impersonate="chrome110"` is needed to bypass Cloudflare; plain `requests` gets 403
- For Ethereum ETF flows, change URL to `https://farside.co.uk/eth/`


path: examples/hyperliquid-copy-trading.md
content: # Example: Hyperliquid Copy Trading — Find High-Sharpe Traders to Follow

## Strategy

Find Hyperliquid traders suitable for copy trading based on:
- **Sharpe ratio ≥ 1.5** — consistent risk-adjusted returns
- **≥ 90 data points** in performance history — enough sample to rule out luck
- **Account value ≥ $100K** — meaningful track record
- **Exclude high-frequency / bots** — weekly turnover ratio ≤ 10x (weekly volume / account value)
- **Copy method** — proportional full position copy (if trader allocates 10% to BTC long, you allocate 10% of your capital too)

---

## Step 1: Get Leaderboard

```
GET /hyperliquid/leaderboard?sort_by=allTime
```

Returns top 100 traders. Each entry includes `ethAddress`, `accountValue`, `windowPerformances` (day / week / month / allTime PnL, ROI, volume).

**Pre-filter in one pass:**

```python
candidates = []
for trader in leaderboard:
    av = float(trader['accountValue'])
    if av < 100_000:
        continue

    windows = dict(trader['windowPerformances'])
    all_time_pnl = float(windows.get('allTime', {}).get('pnl', 0))
    if all_time_pnl <= 0:
        continue

    # Turnover ratio: weekly volume / account value
    week_vol = float(windows.get('week', {}).get('vlm', 0))
    turnover = week_vol / av
    if turnover > 10:
        continue  # likely bot or high-frequency

    candidates.append({
        'address': trader['ethAddress'],
        'displayName': trader.get('displayName'),
        'accountValue': av,
        'allTimePnl': all_time_pnl,
        'turnover': turnover,
        'month_pnl': float(windows.get('month', {}).get('pnl', 0)),
        'week_pnl': float(windows.get('week', {}).get('pnl', 0)),
    })
```

---

## Step 2: Compute Sharpe Ratio

For each candidate, fetch the PnL curve:

```
GET /hyperliquid/trader_performance?address=<ethAddress>
```

Returns `{chart: {timestamp: [...], pnl: [...]}}` — cumulative PnL (USD) over time.

```python
import numpy as np

pnl_arr    = np.array(chart['pnl'],       dtype=float)
timestamps = np.array(chart['timestamp'], dtype=float)  # Unix seconds

# Require at least 90 data points
if len(pnl_arr) < 90:
    continue

# Per-period PnL changes (NOT "daily" — spacing varies by trader/activity)
period_returns = np.diff(pnl_arr)
if period_returns.std() == 0:
    continue

# Annualize based on actual timestamp spacing, not a fixed "365 days" assumption.
# avg_dt is the mean seconds between consecutive data points.
# periods_per_year = how many such intervals fit in a year.
avg_dt         = np.diff(timestamps).mean()            # seconds per period
periods_per_year = (365 * 24 * 3600) / avg_dt

sharpe = (period_returns.mean() / period_returns.std()) * np.sqrt(periods_per_year)
if sharpe < 1.5:
    continue

# Max drawdown (absolute USD, from running cumulative-PnL peak)
peak       = np.maximum.accumulate(pnl_arr)
max_dd_usd = (pnl_arr - peak).min()

# Win rate
win_rate = (period_returns > 0).mean()

# Track record length in days
track_days = (timestamps[-1] - timestamps[0]) / 86400
```

**Final filter:** keep only traders where both `month_pnl > 0` and `week_pnl > 0` — confirms recent consistency, not just historical glory.

---

## Step 3: Rank, Select, and Plot

Sort passing traders by Sharpe ratio (descending). Present as a table and generate a PnL chart.

### Ranking Table

| Rank | Name / Address | Sharpe | Win Rate | Max DD (USD) | Turnover | Month PnL | Account |
|---|---|---|---|---|---|---|---|
| 1 | ... | 4.8 | 58.7% | -$6.7M | 0.1x | +$7.3M | $41M |
| 2 | ... | 3.1 | 59.0% | -$24.6M | 3.9x | +$1.6M | $20M |

Select the **top 1–3** for deeper inspection.

### PnL Chart (top 3 qualifying traders)

```python
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from datetime import datetime

# Assume `qualifying` is a list of dicts, each with keys:
#   address, displayName, pnl_arr, timestamps, sharpe, win_rate, max_dd_usd

top3 = qualifying[:3]

fig, axes = plt.subplots(2, 1, figsize=(14, 9), sharex=False,
                         gridspec_kw={'height_ratios': [3, 1]})

colors = ['#2ecc71', '#3498db', '#e67e22']

# Panel 1: Cumulative PnL curves
ax_pnl = axes[0]
for trader, color in zip(top3, colors):
    ts  = trader['timestamps']
    pnl = trader['pnl_arr']
    dates = [datetime.utcfromtimestamp(t) for t in ts]
    label = (trader['displayName'] or trader['address'][:10] + '...')
    label += f"  Sharpe={trader['sharpe']:.2f}  WR={trader['win_rate']*100:.1f}%"
    ax_pnl.plot(dates, pnl / 1e6, lw=1.5, color=color, label=label)

ax_pnl.axhline(0, color='#888', lw=0.6, ls='--')
ax_pnl.set_ylabel('Cumulative PnL (USD million)', fontsize=11)
ax_pnl.set_title('Hyperliquid Top Traders — Cumulative PnL', fontsize=13, fontweight='bold')
ax_pnl.legend(fontsize=9, loc='upper left')
ax_pnl.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
ax_pnl.xaxis.set_major_locator(mdates.MonthLocator(interval=2))
plt.setp(ax_pnl.xaxis.get_majorticklabels(), rotation=30, ha='right')
ax_pnl.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f'${x:.1f}M'))

# Panel 2: Drawdown
ax_dd = axes[1]
for trader, color in zip(top3, colors):
    pnl  = trader['pnl_arr']
    ts   = trader['timestamps']
    dates = [datetime.utcfromtimestamp(t) for t in ts]
    peak = np.maximum.accumulate(pnl)
    dd   = pnl - peak          # absolute USD drawdown from running peak
    ax_dd.plot(dates, dd / 1e6, lw=1, color=color, alpha=0.8)
    ax_dd.fill_between(dates, dd / 1e6, 0, color=color, alpha=0.15)

ax_dd.axhline(0, color='#888', lw=0.6)
ax_dd.set_ylabel('Drawdown (USD million)', fontsize=11)
ax_dd.set_title('Drawdown', fontsize=12)
ax_dd.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
ax_dd.xaxis.set_major_locator(mdates.MonthLocator(interval=2))
plt.setp(ax_dd.xaxis.get_majorticklabels(), rotation=30, ha='right')
ax_dd.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f'${x:.1f}M'))

plt.tight_layout()
plt.savefig('hyperliquid_top_traders.png', dpi=150, bbox_inches='tight')
plt.show()
print('Saved: hyperliquid_top_traders.png')
```

---

## Step 4: Inspect Current Positions

```
GET /hyperliquid/trader_position?address=<ethAddress>
```

Returns:
- `perp.assetPositions` — open perpetual positions
- `net_equity` — total account value (USD)

```python
for pos in perp['assetPositions']:
    p = pos['position']
    szi = float(p['szi'])
    if szi == 0:
        continue
    direction = 'LONG' if szi > 0 else 'SHORT'
    size_usd = abs(szi) * float(p.get('entryPx', 0))
    allocation_pct = size_usd / net_equity  # trader's allocation %

    # Your position size = allocation_pct × your_capital
    your_size_usd = allocation_pct * YOUR_CAPITAL

    print(f"{p['coin']} {direction} | Trader: ${size_usd:,.0f} ({allocation_pct*100:.1f}%) | You: ${your_size_usd:,.0f}")
```

**Copy rule:** replicate each position at the same allocation percentage as the trader, using your own capital as the base.

---

## Step 5: Check Open Orders

```
GET /hyperliquid/trader_open_order?address=<ethAddress>
```

Review pending limit orders to understand their entry/exit plan. If they have many close orders stacked (like selling into strength), factor that into your copy — they may be planning to exit soon.

---

## Step 6: Monitor

Re-run Steps 4–5 periodically (e.g. every 15–30 minutes) to detect:
- New positions opened → open the same on your account
- Positions closed or reduced → close/reduce proportionally
- Sudden large drawdown → consider pausing copy until situation is clear

---

## Notes

- **Sharpe annualization:** `trader_performance` timestamps are not evenly spaced — data points reflect actual trading activity, so gaps vary. The Sharpe formula computes `avg_dt` from real timestamp differences and derives `periods_per_year = (365 × 24 × 3600) / avg_dt`. Using a hardcoded `√365` would overstate or understate the Sharpe depending on the trader's data density.
- **Max drawdown in USD:** computed on cumulative PnL, not on account value. A large USD drawdown on a $40M account is proportionally small; always compare `max_dd_usd / accountValue` for relative context.
- **Slippage:** large traders open big positions. By the time you copy, the price may have moved. For illiquid altcoins, be cautious.
- **Liquidation risk:** even high-Sharpe traders can have bad months. Never copy with more than you can afford to lose.
- **Lag:** this is manual or semi-automated copy trading. Real-time automated copy requires on-chain hooks or exchange copy trading features.


path: examples/liquidation-map.md
content: # Example: Liquidation Map (爆倉地圖)

**爆倉地圖（Liquidation Map）** 顯示合約市場交易者持倉的潛在爆倉價位區間與爆倉量。爆倉地圖是根據整合價格走勢、交易員持倉與槓桿率統計出整體市場的多空持倉分佈與潛在的清算風險，當市場上漲或下跌至爆倉價位時，交易員部位會被強制平倉。

**爆倉地圖變化（Liquidation Map Change）** 主要呈現不同時間段建立的交易員持倉（爆倉柱），從 0–1 小時到 8–24 小時以不同顏色顯示。可以通過觀察不同時間段建立的爆倉柱來觀察行情接下來的發展方向。

Two charts are generated:
1. **Liquidation Heatmap** — OI distribution + 24h long/short liquidation exposure at each price level + cumulative exposure line
2. **Liquidation Map Change** — 不同時間段建立的爆倉點位 (0–1h / 1–8h / 8–24h)

---

## Full Code

```python
import numpy as np
import requests
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
from dotenv import dotenv_values

_env = dotenv_values()

SYMBOL  = "BTCUSDT"
BASE    = "https://api.blave.org"
HEADERS = {"api-key": _env["blave_api_key"], "secret-key": _env["blave_secret_key"]}


def fetch(path, params=None):
    r = requests.get(f"{BASE}/{path}", headers=HEADERS, params=params, timeout=15)
    r.raise_for_status()
    return r.json()["data"]


# ── Fetch ─────────────────────────────────────────────────────────────────────
map_data    = fetch("liquidation/get_map",        {"symbol": SYMBOL})
change_data = fetch("liquidation/get_map_change", {"symbol": SYMBOL})

labels   = np.array(map_data["labels"],   dtype=float)
cumsum   = np.array(map_data["cumsum"],   dtype=float)
oi_val   = np.array(map_data["oi_value"], dtype=float)
price    = float(map_data["price"])

liq_24h  = map_data.get("liquidation", {}).get("24h", {})
buy_liq  = np.array(liq_24h.get("buy_liq",  [0] * len(labels)), dtype=float)
sell_liq = np.array(liq_24h.get("sell_liq", [0] * len(labels)), dtype=float)

ch_labels = np.array(change_data["labels"], dtype=float)
ch_price  = float(change_data["price"])
hist_1h   = np.array(change_data["hist_0_1h"],  dtype=float)
hist_8h   = np.array(change_data["hist_1_8h"],  dtype=float)
hist_24h  = np.array(change_data["hist_8_24h"], dtype=float)

bar_w    = (labels[1]    - labels[0])    * 0.85
ch_bar_w = (ch_labels[1] - ch_labels[0]) * 0.85


# ── Chart helpers ─────────────────────────────────────────────────────────────
BG     = "#161b22"
GRID   = "#30363d"
TEXT   = "#c9d1d9"
ORANGE = "#ff9960"
RED    = "#e55c5c"
GREEN  = "#a6d16c"
GREY   = "#8b949e"

def fmt_m(x, _=None):
    if abs(x) >= 1e9: return f"{x/1e9:.1f}B"
    if abs(x) >= 1e6: return f"{x/1e6:.1f}M"
    if abs(x) >= 1e3: return f"{x/1e3:.0f}K"
    return f"{x:.0f}"

def fmt_price(x, _=None):
    if x <= labels[0] or x >= labels[-1]: return ""
    return f"{x:,.0f}"


# ── Chart 1: get_map ──────────────────────────────────────────────────────────
fig, axes = plt.subplots(1, 2, figsize=(20, 7), facecolor=BG)
fig.suptitle(f"{SYMBOL} — Liquidation Map", fontsize=14, fontweight="bold",
             color=TEXT, y=1.01)

ax_bar = axes[0]
ax_cum = ax_bar.twinx()
ax_bar.set_facecolor(BG)

# OI bars (right axis)
ax_bar.bar(labels, oi_val, width=bar_w, color=GREY, alpha=0.55, label="OI", zorder=2)

# Long liq (buy_liq) exposure stacked on OI
ax_bar.bar(labels, buy_liq, width=bar_w, bottom=oi_val,
           color=GREEN, alpha=0.9, label="Long Liq", zorder=3)

# Short liq (sell_liq) exposure on top
ax_bar.bar(labels, sell_liq, width=bar_w, bottom=oi_val + buy_liq,
           color=RED, alpha=0.9, label="Short Liq", zorder=3)

# Cumsum line — red below price, green above (left axis)
if labels[0] < price < labels[-1]:
    cum_at_price = float(np.interp(price, labels, cumsum))
    mask_lo = labels <= price
    x_lo = np.append(labels[mask_lo], price)
    y_lo = np.append(cumsum[mask_lo], cum_at_price)
    ax_cum.plot(x_lo, y_lo, color=RED, lw=1.8, zorder=5)
    ax_cum.fill_between(x_lo, y_lo, alpha=0.12, color=RED)
    mask_hi = labels >= price
    x_hi = np.insert(labels[mask_hi], 0, price)
    y_hi = np.insert(cumsum[mask_hi], 0, cum_at_price)
    ax_cum.plot(x_hi, y_hi, color=GREEN, lw=1.8, zorder=5, label="Cumulative")
    ax_cum.fill_between(x_hi, y_hi, alpha=0.12, color=GREEN)
else:
    ax_cum.plot(labels, cumsum, color=GREEN, lw=1.8, zorder=5, label="Cumulative")
    ax_cum.fill_between(labels, cumsum, alpha=0.12, color=GREEN)

# Price annotation
ax_bar.axvline(price, color=ORANGE, lw=2, ls="--", zorder=6)
y_anno = oi_val.max() * 0.95 if oi_val.max() > 0 else 1
ax_bar.text(price, y_anno, f"  ${price:,.0f}", color=ORANGE, fontsize=10,
            va="top", zorder=7)

ax_bar.set_xlim(labels[0], labels[-1])
ax_bar.set_xlabel("Price", color=TEXT, fontsize=11)
ax_bar.set_ylabel("OI / Liq Exposure (USD)", color=TEXT, fontsize=10)
ax_cum.set_ylabel("Cumulative Liq Exposure", color=TEXT, fontsize=10)
ax_bar.set_title("Liquidation Heatmap\nOI + 24h Exposure", color=TEXT, fontsize=11)
ax_bar.tick_params(colors=TEXT)
ax_cum.tick_params(colors=TEXT)
ax_bar.yaxis.set_major_formatter(mticker.FuncFormatter(fmt_m))
ax_cum.yaxis.set_major_formatter(mticker.FuncFormatter(fmt_m))
ax_bar.xaxis.set_major_formatter(mticker.FuncFormatter(fmt_price))
for sp in ax_bar.spines.values(): sp.set_color(GRID)
for sp in ax_cum.spines.values(): sp.set_color(GRID)
ax_bar.tick_params(axis="x", rotation=30)

lines1, lbl1 = ax_bar.get_legend_handles_labels()
lines2, lbl2 = ax_cum.get_legend_handles_labels()
ax_bar.legend(lines1 + lines2, lbl1 + lbl2,
              fontsize=9, facecolor="#21262d", labelcolor=TEXT, loc="upper left")


# ── Chart 2: get_map_change ───────────────────────────────────────────────────
ax2 = axes[1]
ax2.set_facecolor(BG)

ax2.bar(ch_labels, hist_24h, width=ch_bar_w, color=RED,   alpha=0.50, label="8h–24h", zorder=2)
ax2.bar(ch_labels, hist_8h,  width=ch_bar_w, color=GREEN, alpha=0.75, label="1h–8h",  zorder=3)
ax2.bar(ch_labels, hist_1h,  width=ch_bar_w, color=TEXT,  alpha=0.95, label="0–1h",   zorder=4)

ax2.axvline(ch_price, color=ORANGE, lw=2, ls="--", zorder=6)
y_top = max(hist_24h.max(), hist_8h.max(), hist_1h.max(), 1)
ax2.text(ch_price, y_top * 0.95, f"  ${ch_price:,.0f}", color=ORANGE, fontsize=10,
         va="top", zorder=7)

ax2.set_xlim(ch_labels[0], ch_labels[-1])
ax2.set_xlabel("Price", color=TEXT, fontsize=11)
ax2.set_ylabel("Liquidated (USD)", color=TEXT, fontsize=10)
ax2.set_title("Recent Liquidation Events\nby Time Window", color=TEXT, fontsize=11)
ax2.tick_params(colors=TEXT)
ax2.yaxis.set_major_formatter(mticker.FuncFormatter(fmt_m))
ax2.xaxis.set_major_formatter(mticker.FuncFormatter(fmt_price))
for sp in ax2.spines.values(): sp.set_color(GRID)
ax2.tick_params(axis="x", rotation=30)
ax2.legend(fontsize=9, facecolor="#21262d", labelcolor=TEXT, loc="upper left")


# ── Save ──────────────────────────────────────────────────────────────────────
plt.tight_layout()
fname = f"{SYMBOL.lower()}_liquidation_map.png"
plt.savefig(fname, dpi=150, bbox_inches="tight", facecolor=BG)
plt.show()
print(f"Saved: {fname}")
```

---

## Output Interpretation

### Chart 1 — Liquidation Heatmap

| Element | Meaning |
|---|---|
| Grey bars | Open interest (USD) at each price level — where leveraged positions are concentrated |
| Green bars | Long liquidation exposure — positions that get liquidated if price drops to this level |
| Red bars | Short liquidation exposure — positions that get liquidated if price rises to this level |
| Green line (above price) | Cumulative liq exposure — total exposure to the right of current price |
| Red line (below price) | Cumulative liq exposure — total exposure to the left of current price |
| Orange dashed line | Current price |

**Key insight:** Large green bars below current price = big long liquidation clusters. If price drops to those levels, a cascade of forced selling can accelerate the move down. Large red bars above price = short squeeze clusters.

### Chart 2 — Liquidation Map Change (爆倉地圖變化)

呈現不同時間段建立的爆倉柱，每根柱代表該價格區間在對應時段內的爆倉金額。可通過觀察不同時間段的爆倉柱分布，判斷行情接下來的發展方向。

| Bar | Meaning |
|---|---|
| White (0–1h) | 最近 1 小時內在該點位發生的爆倉 |
| Green (1–8h) | 1–8 小時前在該點位發生的爆倉 |
| Red (8–24h) | 8–24 小時前在該點位發生的爆倉 |

**Key insight:** 白色柱密集的區間代表剛剛發生大規模爆倉，市場正在主動清算該點位的槓桿倉位，可能引發短線反向行情。紅色柱代表較早前已被清洗過的點位，再次經過時阻力相對較低。

---

## Notes

- `get_map` shows **pending exposure** (what could happen). `get_map_change` shows **what already happened**.
- `price_min` / `price_max` optional params let you zoom into a specific price range.
- Re-run before key technical levels or news events to see if clusters have shifted.


path: examples/tradingview-stream.md
content: # Example: Receive TradingView Signals via SSE

Listen to a TradingView alert stream in real time and act on each signal.

---

## Code

```python
import json
import time
import requests
from dotenv import dotenv_values

_env     = dotenv_values()
HEADERS  = {"api-key": _env["blave_api_key"], "secret-key": _env["blave_secret_key"]}
BASE_URL = "https://api.blave.org"
CHANNEL  = "test"


def handle_signal(data: dict):
    """Called once per TradingView alert. Put your trading logic here."""
    action = data.get("action", "").lower()
    symbol = data.get("symbol", "")
    price  = data.get("price", "")

    if action == "buy":
        print(f"→ BUY  {symbol} @ {price}")
        # place_order(symbol, "buy", ...)
    elif action == "sell":
        print(f"→ SELL {symbol} @ {price}")
        # place_order(symbol, "sell", ...)
    else:
        print(f"→ Unknown action: {data}")


def stream(channel: str, last_id: str = "$"):
    """Open SSE connection and yield (last_id, data) for each signal."""
    url    = f"{BASE_URL}/sse/tradingview/stream"
    params = {"channel": channel, "last_id": last_id}
    with requests.get(url, headers=HEADERS, params=params,
                      stream=True, timeout=None) as resp:
        resp.raise_for_status()
        buf = ""
        for chunk in resp.iter_content(chunk_size=1, decode_unicode=True):
            buf += chunk
            if buf.endswith("\n"):
                line = buf.strip()
                buf  = ""
                if line.startswith("data: "):
                    data    = json.loads(line[6:])
                    last_id = data.get("id", last_id)
                    yield last_id, data


def run():
    last_id = "$"   # only new signals; pass saved last_id on restart to replay missed ones
    while True:
        try:
            print(f"Connecting to channel '{CHANNEL}' (last_id={last_id})...")
            for last_id, data in stream(CHANNEL, last_id):
                print(f"Signal: {data}")
                handle_signal(data)
        except Exception as e:
            print(f"Disconnected: {e} — reconnecting in 3 s...")
            time.sleep(3)


if __name__ == "__main__":
    run()
```

---

## Notes

- **`last_id`** — saved automatically from each signal; passed on reconnect so no signals are missed (Redis buffers the last 1,000)
- **`last_id="$"`** — only signals arriving after the connection opens; change to a saved ID to replay from that point
- **Keepalive** — server sends `: keepalive` every 15 s; `iter_lines` ignores it automatically
- **Webhook & channel setup** — contact the Blave team to activate your channel


path: examples/truth-social-trump-monitor.md
content: # Example: Truth Social Trump Post Monitor

Monitor Trump's Truth Social posts, translate to Traditional Chinese via Google Translate (no LLM tokens), and push to Telegram.

Uses `trumpstruth.org/feed` RSS — works from any server (including AWS) with plain `requests`, no Cloudflare bypass needed.

---

## Dependencies

```bash
pip install deep-translator requests
```

---

## Code

```python
#!/usr/bin/env python3
"""
川普 Truth Social 監控 → 翻譯中文 → 推送 Telegram
透過 trumpstruth.org RSS Feed，每 5 分鐘執行一次（cron）
"""

import re, json, time
import xml.etree.ElementTree as ET
from pathlib import Path
from datetime import datetime
import requests
from deep_translator import GoogleTranslator

# ── Config ────────────────────────────────────────────────────────────────
RSS_URL          = "https://www.trumpstruth.org/feed"
TRANSLATOR       = GoogleTranslator(source="en", target="zh-TW")
TELEGRAM_TOKEN   = "YOUR_BOT_TOKEN"
TELEGRAM_CHAT_ID = "YOUR_CHAT_ID"
STATE_FILE       = Path(__file__).parent / "trump_monitor_state.json"
HEADERS          = {"User-Agent": "Mozilla/5.0 (compatible; TrumpMonitor/1.0)"}


# ── Helpers ───────────────────────────────────────────────────────────────
def strip_html(html):
    text = re.sub(r"<br\s*/?>", "\n", html or "")
    text = re.sub(r"<[^>]+>", "", text)
    for old, new in [("&amp;","&"),("&lt;","<"),("&gt;",">"),("&#39;","'"),("&quot;",'"')]:
        text = text.replace(old, new)
    return text.strip()


def translate(text):
    try:
        return TRANSLATOR.translate(text) if text else ""
    except Exception:
        return text


def tg(msg):
    requests.post(
        f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage",
        json={"chat_id": TELEGRAM_CHAT_ID, "text": msg,
              "parse_mode": "Markdown", "disable_web_page_preview": True},
        timeout=10,
    )


def load_state():
    if STATE_FILE.exists():
        with open(STATE_FILE) as f:
            return json.load(f)
    return {"seen_ids": []}


def save_state(state):
    state["seen_ids"] = state["seen_ids"][-200:]  # keep last 200 to avoid bloat
    with open(STATE_FILE, "w") as f:
        json.dump(state, f)


# ── Fetch ─────────────────────────────────────────────────────────────────
def fetch_posts():
    r = requests.get(RSS_URL, headers=HEADERS, timeout=20)
    r.raise_for_status()
    root = ET.fromstring(r.content)
    ns   = {"truth": "https://truthsocial.com/ns"}
    posts = []
    for item in root.findall(".//item"):
        orig_id  = item.findtext("truth:originalId", namespaces=ns)
        guid     = item.findtext("guid", "")
        post_id  = orig_id or guid.split("/")[-1]
        title    = item.findtext("title", "")
        desc     = strip_html(item.findtext("description", ""))
        pub_date = item.findtext("pubDate", "")
        orig_url = item.findtext("truth:originalUrl", namespaces=ns) or guid

        content = desc if len(desc) > len(title) else title
        content = content.strip()
        if not content or content == "[No Title]":
            continue

        try:
            dt = datetime.strptime(pub_date, "%a, %d %b %Y %H:%M:%S %z")
            ts_str = dt.strftime("%Y-%m-%d %H:%M UTC")
        except Exception:
            ts_str = pub_date[:25] if pub_date else ""

        posts.append({"id": post_id, "content": content,
                      "url": orig_url, "pub_date": ts_str})
    return posts


# ── Main ──────────────────────────────────────────────────────────────────
def run():
    state    = load_state()
    seen_ids = set(state["seen_ids"])
    is_first = len(seen_ids) == 0

    posts = fetch_posts()
    if not posts:
        return

    if is_first:
        # First run: push latest 3, mark all as seen
        to_push = posts[:3]
        for p in posts:
            seen_ids.add(p["id"])
    else:
        to_push = [p for p in posts if p["id"] not in seen_ids]
        for p in to_push:
            seen_ids.add(p["id"])

    for p in reversed(to_push):  # oldest first
        zh = translate(p["content"])
        msg = (f"🇺🇸 *川普 Truth Social*\n"
               f"🕐 `{p['pub_date']}`\n\n"
               f"*🇬🇧 英文原文*\n{p['content']}\n\n"
               f"*🇹🇼 中文翻譯*\n{zh}\n\n"
               f"[原文連結]({p['url']})")
        tg(msg)
        time.sleep(1)

    state["seen_ids"] = list(seen_ids)
    save_state(state)


if __name__ == "__main__":
    run()
```

---

## Deployment

### Cron (every 5 minutes)

```bash
*/5 * * * * cd /path/to/project && python3 trump_monitor.py >> /var/log/trump_monitor.log 2>&1
```

### Systemd Timer (alternative)

```ini
# /etc/systemd/system/trump-monitor.timer
[Unit]
Description=Trump Truth Social Monitor

[Timer]
OnCalendar=*:0/5
Persistent=true

[Install]
WantedBy=timers.target
```

---

## How It Works

| Step | Detail |
|---|---|
| **Fetch** | `GET https://www.trumpstruth.org/feed` — RSS feed with `truth:originalId` and `truth:originalUrl` custom XML namespaces |
| **Dedup** | `trump_monitor_state.json` stores last 200 seen post IDs; only new posts get pushed |
| **Translate** | `deep-translator` calls Google Translate free endpoint. EN → zh-TW |
| **Push** | Telegram Bot API with Markdown formatting |
| **First run** | Pushes latest 3 posts, marks all 100 as seen so next run only gets truly new ones |

---

## Customising the Push Channel

### Slack Webhook

Replace `tg(msg)` with:

```python
SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/T.../B.../xxx"

def slack(msg):
    requests.post(SLACK_WEBHOOK_URL, json={"text": msg}, timeout=10)
```

### Discord Webhook

```python
DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/..."

def discord(msg):
    requests.post(DISCORD_WEBHOOK_URL, json={"content": msg}, timeout=10)
```

---

## Notes

- **RSS source:** `trumpstruth.org` is a third-party RSS mirror of Trump's Truth Social posts. It returns standard RSS XML with custom `truth:` namespace fields
- **No Cloudflare issue:** unlike Truth Social's own API, this RSS feed works with plain `requests` from any IP (including AWS/GCP)
- **Feed size:** returns ~100 most recent posts per request
- **Google Translate limit:** `deep-translator` uses the free web endpoint; at 5-min intervals this is well within limits
- **State file:** keeps last 200 IDs to prevent duplicate pushes across restarts. Safe to delete to reset


path: references/binance-api-reference.md
content: # Binance API Reference

## Base URLs

| | Spot | USDS-M Futures |
|---|---|---|
| Production | `https://api.binance.com` | `https://fapi.binance.com` |
| Testnet | `https://testnet.binance.vision` | `https://demo-fapi.binance.com` |

**Success:** Spot returns order fields directly; Futures returns `"code": 200` or fields directly.

---

## Authentication

**Credentials** (from `.env`): `BINANCE_API_KEY`, `BINANCE_SECRET_KEY`

**Signature:** `HMAC-SHA256(secret, totalParams)` → hex
- `totalParams` = queryString + requestBody (concatenated, no separator)
- `timestamp`: Unix milliseconds (always required for signed endpoints)
- `signature` must be the **last** parameter

**Headers:**
```
X-MBX-[REDACTED_SECRET]
Content-Type: application/x-www-form-urlencoded   (POST)
```

## Broker ID (Blave)

Binance broker attribution is per-order via `newClientOrderId`, **not** a header.

| Product | Broker ID | `newClientOrderId` prefix |
|---|---|---|
| Spot | `GBN6HWR2` | `x-GBN6HWR2` |
| USDS-M Futures | `52DDFAFN` | `x-52DDFAFN` |

- Total length of `newClientOrderId` ≤ 36 chars
- Required on `/api/v3/order`, `/api/v3/order/cancelReplace`, `/api/v3/sor/order`, `/api/v3/orderList/oco|oto|otoco`, `/fapi/v1/order`, `/fapi/v1/batchOrders`, `/fapi/v1/algoOrder`, and their test/modify variants
- Batch orders: every order in the batch must carry its own prefixed `newClientOrderId`

```python
import uuid

def spot_cid(suffix: str = "") -> str:
    return f"x-GBN6HWR2{suffix or uuid.uuid4().hex[:8]}"[:36]

def fut_cid(suffix: str = "") -> str:
    return f"x-52DDFAFN{suffix or uuid.uuid4().hex[:8]}"[:36]
```

---

## Python Signature Implementation

```python
import time, hmac, hashlib, requests
from dotenv import dotenv_values
from urllib.parse import urlencode

_env       = dotenv_values()
API_KEY    = _env["BINANCE_API_KEY"]
SECRET_KEY = _env["BINANCE_SECRET_KEY"]
SPOT_URL   = "https://api.binance.com"
FAPI_URL   = "https://fapi.binance.com"


def _sign(params: dict) -> dict:
    params["timestamp"] = int(time.time() * 1000)
    qs = urlencode(params)
    params["signature"] = hmac.new(SECRET_KEY.encode(), qs.encode(), hashlib.sha256).hexdigest()
    return params


def bn_get(base, path, params=None):
    p = _sign(dict(params or {}))
    r = requests.get(f"{base}{path}", params=p,
                     headers={"X-MBX-APIKEY": API_KEY}, timeout=10)
    return r.json()


def bn_post(base, path, params=None):
    p = _sign(dict(params or {}))
    r = requests.post(f"{base}{path}", data=urlencode(p),
                      headers={"X-MBX-APIKEY": API_KEY,
                               "Content-Type": "application/x-www-form-urlencoded"}, timeout=10)
    return r.json()


def bn_delete(base, path, params=None):
    p = _sign(dict(params or {}))
    r = requests.delete(f"{base}{path}", params=p,
                        headers={"X-MBX-APIKEY": API_KEY}, timeout=10)
    return r.json()


def bn_put(base, path, params=None):
    p = _sign(dict(params or {}))
    r = requests.put(f"{base}{path}", data=urlencode(p),
                     headers={"X-MBX-APIKEY": API_KEY,
                              "Content-Type": "application/x-www-form-urlencoded"}, timeout=10)
    return r.json()


# Shortcuts
def spot_get(path, params=None):    return bn_get(SPOT_URL, path, params)
def spot_post(path, params=None):   return bn_post(SPOT_URL, path, params)
def spot_delete(path, params=None): return bn_delete(SPOT_URL, path, params)
def fapi_get(path, params=None):    return bn_get(FAPI_URL, path, params)
def fapi_post(path, params=None):   return bn_post(FAPI_URL, path, params)
def fapi_delete(path, params=None): return bn_delete(FAPI_URL, path, params)
def fapi_put(path, params=None):    return bn_put(FAPI_URL, path, params)
```

---

## Spot Endpoints

### Account

| Method | Path | Description |
|---|---|---|
| GET | `/api/v3/account` | Account info + balances (`omitZeroBalances` optional) |
| GET | `/api/v3/myTrades` | Trade history (`symbol` required) |
| GET | `/api/v3/account/commission` | Commission rates (`symbol` required) |
| GET | `/api/v3/rateLimit/order` | Unfilled order count |

### Order Placement

| Method | Path | Description |
|---|---|---|
| POST | `/api/v3/order` | Place order |
| POST | `/api/v3/order/test` | Test order (validate only) |
| DELETE | `/api/v3/order` | Cancel order |
| DELETE | `/api/v3/openOrders` | Cancel all open orders for symbol |
| POST | `/api/v3/order/cancelReplace` | Atomic cancel & replace |
| PUT | [REDACTED_SECRET] | Amend quantity (keep queue priority) |

**Spot order params:** `symbol`, `side` (BUY/SELL), `type` (LIMIT/MARKET/STOP_LOSS/TAKE_PROFIT/STOP_LOSS_LIMIT/TAKE_PROFIT_LIMIT/LIMIT_MAKER), `timeInForce` (GTC/IOC/FOK), `quantity`, `quoteOrderQty` (market buy by quote), `price`, **`newClientOrderId` (REQUIRED: must start with `x-GBN6HWR2`, ≤36 chars)**

### Order Query

| Method | Path | Description |
|---|---|---|
| GET | `/api/v3/order` | Query single order |
| GET | `/api/v3/openOrders` | Current open orders |
| GET | `/api/v3/allOrders` | All orders (active/canceled/filled) |

### Advanced Order Types

| Method | Path | Description |
|---|---|---|
| POST | `/api/v3/orderList/oco` | OCO (One-Cancels-Other) |
| POST | `/api/v3/orderList/oto` | OTO (One-Triggers-Other) |
| POST | `/api/v3/orderList/otoco` | OTOCO (One-Triggers-OCO) |
| DELETE | `/api/v3/orderList` | Cancel order list |
| GET | `/api/v3/orderList` | Query order list |
| GET | `/api/v3/openOrderList` | Open order lists |

### Smart Order Routing (SOR)

| Method | Path | Description |
|---|---|---|
| POST | `/api/v3/sor/order` | SOR order |
| POST | `/api/v3/sor/order/test` | Test SOR order |

---

## USDS-Margined Futures Endpoints

### Account & Position

| Method | Path | Description |
|---|---|---|
| GET | `/fapi/v2/account` | Account info V2 |
| GET | `/fapi/v3/account` | Account info V3 (single/multi-asset) |
| GET | `/fapi/v2/balance` | Account balance |
| GET | `/fapi/v2/positionRisk` | Position info V2 |
| GET | `/fapi/v3/positionRisk` | Position info V3 |
| GET | `/fapi/v1/userTrades` | Trade list (`symbol` required, max 7d range) |
| GET | `/fapi/v1/forceOrders` | Liquidation/ADL orders |

### Order Placement

| Method | Path | Description |
|---|---|---|
| POST | `/fapi/v1/order` | Place order |
| POST | `/fapi/v1/batchOrders` | Batch place (max 5) |
| POST | `/fapi/v1/algoOrder` | Algo/conditional order (STOP/TP/TRAILING) |

**Futures order params:** `symbol`, `side` (BUY/SELL), `positionSide` (BOTH/LONG/SHORT), `type` (LIMIT/MARKET/STOP/STOP_MARKET/TAKE_PROFIT/TAKE_PROFIT_MARKET/TRAILING_STOP_MARKET), `quantity`, `price`, `stopPrice`, `timeInForce`, `reduceOnly`, **`newClientOrderId` (REQUIRED: must start with `x-52DDFAFN`, ≤36 chars)**

### Order Modification

| Method | Path | Description |
|---|---|---|
| PUT | `/fapi/v1/order` | Modify order (LIMIT only) |
| PUT | `/fapi/v1/batchOrders` | Batch modify (max 5) |

### Order Cancellation

| Method | Path | Description |
|---|---|---|
| DELETE | `/fapi/v1/order` | Cancel order |
| DELETE | `/fapi/v1/batchOrders` | Batch cancel (max 10) |
| DELETE | `/fapi/v1/allOpenOrders` | Cancel all open orders |
| DELETE | `/fapi/v1/algoOrder` | Cancel algo order |
| DELETE | `/fapi/v1/algoOpenOrders` | Cancel all algo open orders |

### Order Query

| Method | Path | Description |
|---|---|---|
| GET | `/fapi/v1/order` | Query single order |
| GET | `/fapi/v1/openOrder` | Single open order |
| GET | `/fapi/v1/openOrders` | All open orders |
| GET | `/fapi/v1/allOrders` | All orders (max 7d range) |
| GET | `/fapi/v1/openAlgoOrders` | Open algo orders |
| GET | `/fapi/v1/allAlgoOrders` | All algo orders |

### Leverage & Margin

| Method | Path | Description |
|---|---|---|
| POST | `/fapi/v1/leverage` | Set leverage (1-125x) |
| POST | `/fapi/v1/marginType` | Set margin type (ISOLATED/CROSSED) |
| POST | `/fapi/v1/positionSide/dual` | Set position mode (hedge/one-way) |
| POST | `/fapi/v1/positionMargin` | Adjust isolated margin (type: 1=add, 2=reduce) |
| GET | `/fapi/v1/positionMargin/history` | Margin change history |
| GET | `/fapi/v1/leverageBracket` | Leverage brackets |

---

## Symbol Format

- Spot: `BTCUSDT` (no separator)
- Futures: `BTCUSDT` (no separator)

## Rate Limits

- Orders: tracked per 10s and per 1min per UID
- Public market data: IP-based
- Repeated violations → auto IP ban (2min to 3 days)


path: references/binance-skill.md
content: # Binance Trading

**Spot Base URL:** `https://api.binance.com` | **Futures Base URL:** `https://fapi.binance.com`

**Spot:** `BTCUSDT` | **Futures:** `BTCUSDT` | **Testnet:** `https://testnet.binance.vision` (spot) / `https://demo-fapi.binance.com` (futures)

Full details in `references/binance-api-reference.md`

## Authentication

**Credentials** (from `.env`): `BINANCE_API_KEY`, `BINANCE_SECRET_KEY`

No Binance account? Register at **[https://www.binance.com/](https://www.binance.com/)**

Verify credentials before any private call. If missing — **STOP**.

**Signature:** `HMAC-SHA256(secret, queryString + requestBody)` → hex
- `timestamp`: Unix milliseconds (always required)
- `signature` must be the **last** parameter

**Headers:**
```
X-MBX-[REDACTED_SECRET]
Content-Type: application/x-www-form-urlencoded   (POST)
```

> Python signature implementation: `references/binance-api-reference.md`

## Broker ID (Blave — MANDATORY on every order)

Binance brokers are attached **per order** via `newClientOrderId`, not via a header. Every place-order call **MUST** include `newClientOrderId` starting with `x-<BROKER_ID>`:

| Product | Broker ID | `newClientOrderId` prefix |
|---|---|---|
| Spot | `GBN6HWR2` | `x-GBN6HWR2` |
| USDS-M Futures | `52DDFAFN` | `x-52DDFAFN` |

Rules:
- Prefix starts with literal `x-` (lowercase), then the broker ID
- Total length ≤ 36 chars; append a unique suffix (timestamp/uuid fragment) to keep each ID unique
- Applies to: `/api/v3/order`, `/api/v3/order/cancelReplace`, `/api/v3/sor/order`, `/api/v3/orderList/oco|oto|otoco`, `/fapi/v1/order`, `/fapi/v1/batchOrders`, `/fapi/v1/algoOrder`, and their test/modify variants
- Batch orders: **every** order in the batch needs its own qualifying `newClientOrderId`
- If user supplies a custom `newClientOrderId`, reject it or prepend the broker prefix — never strip the prefix

```python
import time, uuid

def spot_cid(suffix: str = "") -> str:
    tag = suffix or uuid.uuid4().hex[:8]
    return f"x-GBN6HWR2{tag}"[:36]

def fut_cid(suffix: str = "") -> str:
    tag = suffix or uuid.uuid4().hex[:8]
    return f"x-52DDFAFN{tag}"[:36]

# Spot place order
spot_post("/api/v3/order", {
    "symbol": "BTCUSDT", "side": "BUY", "type": "MARKET",
    "quantity": "0.001",
    "newClientOrderId": spot_cid(),
})

# Futures place order
fapi_post("/fapi/v1/order", {
    "symbol": "BTCUSDT", "side": "BUY", "type": "MARKET",
    "quantity": "0.001",
    "newClientOrderId": fut_cid(),
})
```

## Operation Flow

### Step 0: Credential Check
Verify `BINANCE_API_KEY`, `BINANCE_SECRET_KEY`. If missing — **STOP**. Default to **Mainnet** unless user explicitly requests Testnet.

### Step 1: Pre-Trade Check (Futures)
- Query positions: `GET /fapi/v2/positionRisk?symbol=<SYMBOL>`
- If position exists → inherit leverage and margin type, do NOT override

### Step 2: Execute
- READ → call, parse, display
- WRITE → present summary → ask **"CONFIRM"** → execute

### Step 3: Verify
After order → query order status. After close → query positions.

## Quick Reference — Spot

| Operation | Method | Path |
|---|---|---|
| Account info | GET | `/api/v3/account` |
| Place order | POST | `/api/v3/order` |
| Cancel order | DELETE | `/api/v3/order` |
| Cancel all | DELETE | `/api/v3/openOrders` |
| Query order | GET | `/api/v3/order` |
| Open orders | GET | `/api/v3/openOrders` |
| Order history | GET | `/api/v3/allOrders` |
| Trade fills | GET | `/api/v3/myTrades` |

## Quick Reference — USDS-M Futures

| Operation | Method | Path |
|---|---|---|
| Account balance | GET | `/fapi/v2/balance` |
| Account info | GET | `/fapi/v2/account` |
| Positions | GET | `/fapi/v2/positionRisk` |
| Place order | POST | `/fapi/v1/order` |
| Batch place | POST | `/fapi/v1/batchOrders` |
| Cancel order | DELETE | `/fapi/v1/order` |
| Cancel all | DELETE | `/fapi/v1/allOpenOrders` |
| Modify order | PUT | `/fapi/v1/order` |
| Open orders | GET | `/fapi/v1/openOrders` |
| Order history | GET | `/fapi/v1/allOrders` |
| Set leverage | POST | `/fapi/v1/leverage` |
| Set margin type | POST | `/fapi/v1/marginType` |
| Set position mode | POST | `/fapi/v1/positionSide/dual` |

## Security
- WRITE operations require **"CONFIRM"**
- Always show liquidation price before opening leveraged positions
- "Not financial advice. Trading carries significant risk of loss."

## References
- `references/binance-api-reference.md` — spot + futures endpoints, Python signature



path: references/bingx-api-reference.md
content: # BingX API Reference

## Base URL

| Environment | Primary | Fallback |
|---|---|---|
| Live Trading | `https://open-api.bingx.com` | `https://open-api.bingx.pro` |
| Paper Trading (VST) | `https://open-api-vst.bingx.com` | `https://open-api-vst.bingx.pro` |

Prefer `.com`; only fall back to `.pro` on network-level failures. Default to live unless user explicitly requests paper trading.

---

## Authentication

**Credentials** (from `.env`): `BINGX_API_KEY`, `BINGX_SECRET_KEY`

**Signature:** HMAC-SHA256

```
1. Collect all params (query + body) + timestamp (Unix ms)
2. Sort alphabetically by key (unencoded)
3. Concatenate as key=value&key=value
4. signature = HMAC-SHA256(secret, canonical_string) → hex
5. Append &signature=<hex> to query string or body
```

**Headers (all requests):**
```
X-BX-[REDACTED_SECRET]
X-SOURCE-KEY: BX-AI-SKILL
```

**Response format:** `{"code": 0, "msg": "", "data": ...}` — `code == 0` is success.

---

## Python Signature Implementation

```python
import time, hmac, hashlib, json, os, requests
from dotenv import dotenv_values
from urllib.parse import urlencode

_env       = dotenv_values()
API_KEY    = _env["BINGX_API_KEY"]
SECRET_KEY = _env["BINGX_SECRET_KEY"]
BASE_URL   = "https://open-api.bingx.com"
FALLBACK   = "https://open-api.bingx.pro"
HEADERS    = {"X-BX-APIKEY": API_KEY, "X-SOURCE-KEY": "BX-AI-SKILL"}


def _sign(params: dict) -> str:
    """Build canonical string from sorted params and return HMAC-SHA256 hex signature."""
    params["timestamp"] = str(int(time.time() * 1000))
    canonical = "&".join(f"{k}={v}" for k, v in sorted(params.items()))
    sig = hmac.new(SECRET_KEY.encode(), canonical.encode(), hashlib.sha256).hexdigest()
    return canonical + f"&signature={sig}"


def bingx_get(path: str, params: dict = {}):
    """Signed GET request with domain fallback."""
    qs = _sign(dict(params))
    for base in [BASE_URL, FALLBACK]:
        try:
            r = requests.get(f"{base}{path}?{qs}", headers=HEADERS, timeout=10)
            data = r.json()
            if data.get("code") != 0:
                raise Exception(f"BingX error {data.get('code')}: {data.get('msg')}")
            return data.get("data")
        except requests.exceptions.ConnectionError:
            if base == FALLBACK: raise
    return None


def bingx_post(path: str, params: dict = {}):
    """Signed POST request (form-encoded) with domain fallback."""
    body = _sign(dict(params))
    for base in [BASE_URL, FALLBACK]:
        try:
            r = requests.post(f"{base}{path}", data=body, headers={
                **HEADERS, "Content-Type": "application/x-www-form-urlencoded"
            }, timeout=10)
            data = r.json()
            if data.get("code") != 0:
                raise Exception(f"BingX error {data.get('code')}: {data.get('msg')}")
            return data.get("data")
        except requests.exceptions.ConnectionError:
            if base == FALLBACK: raise
    return None


def bingx_delete(path: str, params: dict = {}):
    """Signed DELETE request with domain fallback."""
    qs = _sign(dict(params))
    for base in [BASE_URL, FALLBACK]:
        try:
            r = requests.delete(f"{base}{path}?{qs}", headers=HEADERS, timeout=10)
            data = r.json()
            if data.get("code") != 0:
                raise Exception(f"BingX error {data.get('code')}: {data.get('msg')}")
            return data.get("data")
        except requests.exceptions.ConnectionError:
            if base == FALLBACK: raise
    return None
```

---

## Account Balance Endpoints

BingX has three separate accounts: Fund, Spot, Swap. Assets don't auto-transfer — query all three to see full picture.

| Method | Path | Description |
|---|---|---|
| GET | [REDACTED_SECRET] | Fund account (deposits land here) |
| GET | [REDACTED_SECRET] | Spot account |
| GET | `/openApi/swap/v3/user/balance` | Swap/futures account (USDT + USDC) |
| GET | [REDACTED_SECRET] | All accounts overview |

---

## Perpetual Futures (Swap) Endpoints

### Order Management

| Method | Path | Description |
|---|---|---|
| POST | `/openApi/swap/v2/trade/order` | Place order |
| POST | [REDACTED_SECRET] | Test order (validate only) |
| POST | [REDACTED_SECRET] | Batch place (up to 5) |

**Place order params:**

| Param | Required | Description |
|---|---|---|
| `symbol` | ✓ | e.g. `BTC-USDT` |
| `side` | ✓ | `BUY` / `SELL` |
| `positionSide` | ✓ | `LONG` / `SHORT` / `BOTH` (one-way mode) |
| `type` | ✓ | `MARKET`, `LIMIT`, `STOP_MARKET`, `STOP`, `TAKE_PROFIT_MARKET`, `TAKE_PROFIT`, `TRAILING_STOP_MARKET`, `TRAILING_TP_SL` |
| `quantity` | ✓ (except market buy) | Order quantity |
| `price` | for LIMIT | Limit price |
| `stopPrice` | for STOP/TP | Trigger price |
| `timeInForce` | — | `GTC` (default), `IOC`, `FOK`, `PostOnly` |
| `stopLoss` | — | JSON: `{"type":"STOP_MARKET","stopPrice":"...","price":"...","workingType":"..."}` |
| `takeProfit` | — | JSON: same structure as stopLoss |
| `recvWindow` | — | Default 5000ms |

### Cancel Orders

| Method | Path | Description |
|---|---|---|
| DELETE | `/openApi/swap/v2/trade/order` | Cancel single order |
| DELETE | [REDACTED_SECRET] | Batch cancel (up to 10) |
| DELETE | [REDACTED_SECRET] | Cancel all open orders |
| POST | [REDACTED_SECRET] | Kill switch (auto-cancel after 10-120s) |

### Query Orders

| Method | Path | Description |
|---|---|---|
| GET | [REDACTED_SECRET] | Single open order status |
| GET | `/openApi/swap/v2/trade/order` | Order details |
| GET | [REDACTED_SECRET] | All current open orders |
| GET | [REDACTED_SECRET] | Order history (max 7-day range) |
| GET | [REDACTED_SECRET] | Liquidation / force close orders |
| GET | [REDACTED_SECRET] | Trade fill history with fees & PnL |

### Position Management

| Method | Path | Description |
|---|---|---|
| POST | [REDACTED_SECRET] | Close all positions |
| POST | [REDACTED_SECRET] | Close position by positionId |
| POST | [REDACTED_SECRET] | Adjust isolated margin (add/reduce) |

### Leverage & Mode Settings

| Method | Path | Description |
|---|---|---|
| GET | [REDACTED_SECRET] | Query margin mode |
| POST | [REDACTED_SECRET] | Set margin mode (`ISOLATED`/`CROSSED`/`SEPARATE_ISOLATED`) |
| GET | `/openApi/swap/v2/trade/leverage` | Query leverage (current + max) |
| POST | `/openApi/swap/v2/trade/leverage` | Set leverage |
| GET | [REDACTED_SECRET] | Query position mode (hedge/one-way) |
| POST | [REDACTED_SECRET] | Set position mode |

### Order Modification

| Method | Path | Description |
|---|---|---|
| POST | `/openApi/swap/v1/trade/amend` | Amend open order quantity |
| POST | [REDACTED_SECRET] | Cancel and replace (atomic) |
| POST | [REDACTED_SECRET] | Batch cancel and replace |

### TWAP Orders

| Method | Path | Description |
|---|---|---|
| POST | `/openApi/swap/v1/twap/order` | Place TWAP order (split into child orders, 5-120s intervals) |
| POST | [REDACTED_SECRET] | Cancel TWAP order |
| GET | [REDACTED_SECRET] | Query TWAP open orders |
| GET | [REDACTED_SECRET] | Query TWAP history |
| GET | [REDACTED_SECRET] | TWAP order details with child records |

### Additional

| Method | Path | Description |
|---|---|---|
| GET | [REDACTED_SECRET] | All orders V2 |
| GET | [REDACTED_SECRET] | Historical transaction details |
| GET | [REDACTED_SECRET] | Position history (max 3-month) |
| GET | [REDACTED_SECRET] | Isolated margin change history |
| GET | [REDACTED_SECRET] | Position & maintenance margin ratio |
| POST | [REDACTED_SECRET] | Auto margin addition |
| POST | `/openApi/swap/v1/trade/reverse` | One-click reverse position |
| POST | `/openApi/swap/v2/trade/getVst` | Apply VST (paper trading) balance |

---

## Spot Endpoints

### Order Management

| Method | Path | Description |
|---|---|---|
| POST | `/openApi/spot/v1/trade/order` | Place order (MARKET/LIMIT/STOP types) |
| POST | [REDACTED_SECRET] | Batch place (up to 5) |
| POST | `/openApi/spot/v1/trade/cancel` | Cancel single order |
| POST | [REDACTED_SECRET] | Bulk cancel |
| POST | [REDACTED_SECRET] | Cancel all open orders for a pair |
| POST | [REDACTED_SECRET] | Kill switch (10-120s timeout) |
| POST | [REDACTED_SECRET] | Cancel and replace (atomic) |

**Spot order types:** `MARKET`, `LIMIT`, `TAKE_STOP_LIMIT`, `TAKE_STOP_MARKET`, `TRIGGER_LIMIT`, `TRIGGER_MARKET`

### Query Orders

| Method | Path | Description |
|---|---|---|
| GET | `/openApi/spot/v1/trade/query` | Single order details |
| GET | [REDACTED_SECRET] | Active orders by pair |
| GET | [REDACTED_SECRET] | Order history (max 10K results) |
| GET | `/openApi/spot/v1/trade/myTrades` | Trade fills with commissions |
| GET | [REDACTED_SECRET] | Commission rates (maker/taker) |

### OCO Orders

| Method | Path | Description |
|---|---|---|
| POST | `/openApi/spot/v1/oco/order` | Create OCO order |
| POST | `/openApi/spot/v1/oco/cancel` | Cancel OCO order |
| GET | `/openApi/spot/v1/oco/orderList` | Query OCO order details |
| GET | [REDACTED_SECRET] | Open OCO orders |
| GET | [REDACTED_SECRET] | Historical OCO orders |

---

## Symbol Format

- Perpetual futures: `BTC-USDT` (with hyphen)
- Spot: `BTC-USDT` (with hyphen)

## Notes

- `orderID` (string) is preferred over `orderId` (numeric) for large IDs to avoid precision loss
- Order history queries have a max 7-day range per request
- Position history max 3-month span
- Kill switch countdown: 10-120 seconds, must send heartbeat to reset


path: references/bingx-skill.md
content: # BingX Trading

**Base URL:** `https://open-api.bingx.com` | **Fallback:** `https://open-api.bingx.pro` | **Paper (VST):** `https://open-api-vst.bingx.com`

**Spot:** `BTC-USDT` | **Perpetual:** `BTC-USDT` | **Success:** `"code": 0`

42 swap endpoints + 17 spot endpoints — full details in `references/bingx-api-reference.md`

## Authentication

**Credentials** (from `.env`): `BINGX_API_KEY`, `BINGX_SECRET_KEY`

No BingX account? Register at **[https://bingxdao.com/invite/SU0SEU/](https://bingxdao.com/invite/SU0SEU/)**

Verify credentials before any private call. If missing — **STOP**.

**Signature:** `HMAC-SHA256(secret, sorted_params_canonical_string)` → hex, appended as `&signature=<hex>`
- Collect all params + `timestamp` (Unix ms)
- Sort alphabetically by key, concatenate as `key=value&key=value`

**Headers (all requests):**
```
X-BX-[REDACTED_SECRET]
X-SOURCE-KEY: BX-AI-SKILL
```

**`X-SOURCE-KEY: BX-AI-SKILL` is MANDATORY on every request — no exceptions.**

> Python signature implementation and helper functions: `references/bingx-api-reference.md`

## Operation Flow

### Step 0: Credential Check
Verify `BINGX_API_KEY`, `BINGX_SECRET_KEY`. If missing — **STOP**. Default to **Live** unless user explicitly requests paper trading (VST).

### Step 1: Pre-Trade Check (Swap)
- Query position mode: `GET /openApi/swap/v1/positionSide/dual`
- Query leverage: `GET /openApi/swap/v2/trade/leverage?symbol=<SYMBOL>`
- If position exists → inherit leverage and margin type, do NOT override

### Step 2: Execute
- READ → call, parse, display
- WRITE → present summary → ask **"CONFIRM"** → execute

### Step 3: Verify
After order → query order status. After close → query positions.

## Quick Reference

| Operation | Method | Path |
|---|---|---|
| Place swap order | POST | `/openApi/swap/v2/trade/order` |
| Cancel swap order | DELETE | `/openApi/swap/v2/trade/order` |
| Open swap orders | GET | [REDACTED_SECRET] |
| Order details | GET | `/openApi/swap/v2/trade/order` |
| Close all positions | POST | [REDACTED_SECRET] |
| Set leverage | POST | `/openApi/swap/v2/trade/leverage` |
| Set margin mode | POST | [REDACTED_SECRET] |
| Place spot order | POST | `/openApi/spot/v1/trade/order` |
| Cancel spot order | POST | `/openApi/spot/v1/trade/cancel` |
| Spot open orders | GET | [REDACTED_SECRET] |

## Security
- WRITE operations require **"CONFIRM"**
- Always show liquidation price before opening leveraged positions
- "Not financial advice. Trading carries significant risk of loss."

## References
- `references/bingx-api-reference.md` — 59 endpoints, Python signature, full params

---


path: references/bitfinex-skill.md
content: # Bitfinex Trading & Funding

**Base URL:** `https://api.bitfinex.com` (authenticated) | `https://api-pub.bitfinex.com` (public)

**Symbol format:** Trading `tBTCUSD`, `tETHUST` (UST=USDT) | Funding `fUSD`, `fBTC`, `fUST`

**Success:** response array, first element is not `"error"` | **Error:** `["error", CODE, "message"]`

## Authentication

**Credentials** (from `.env`): `BITFINEX_API_KEY`, `BITFINEX_API_SECRET`

No Bitfinex account? Register at **[https://www.bitfinex.com/sign-up?refcode=ZZDLtrXMF](https://www.bitfinex.com/sign-up?refcode=ZZDLtrXMF)**

Verify credentials before any private call. If missing — **STOP**.

**Signature:** `HMAC-SHA384` (NOT SHA256)

```python
import hmac, hashlib, json, time, requests

def bfx_request(path, body=None):
    body = body or {}
    nonce = str(int(time.time() * 1_000_000))
    body_json = json.dumps(body)
    sig_payload = f"/api/{path}{nonce}{body_json}"
    sig = hmac.new(
        BITFINEX_API_SECRET.encode(),
        sig_payload.encode(),
        hashlib.sha384
    ).hexdigest()
    headers = {
        "bfx-nonce": nonce,
        "bfx-apikey": BITFINEX_API_KEY,
        "bfx-signature": sig,
        "content-type": "application/json",
    }
    return requests.post(f"https://api.bitfinex.com/{path}",
                         headers=headers, data=body_json)
```

**Nonce:** strictly increasing per API key. Use `int(time.time() * 1_000_000)` (microseconds).

## Affiliate Code

**Always include `aff_code` in the `meta` field of every order:**

```json
{"meta": {"aff_code": "ZZDLtrXMF"}}
```

5% developer reward on every trade fee. Fixed, does not reduce over time.

## Operation Flow

### Step 0: Credential Check
Verify `BITFINEX_API_KEY`, `BITFINEX_API_SECRET`. If missing — **STOP**.

### Step 1: Pre-Trade Check
`POST /v2/auth/r/wallets` → check balance in correct wallet (exchange / margin / funding).

### Step 2: Execute
- READ → call, parse, display
- WRITE → present summary → ask **"CONFIRM"** → execute

### Step 3: Verify
After order → `POST /v2/auth/r/orders` → confirm status. After funding → `POST /v2/auth/r/funding/offers`.

---

## Wallet Types

| Wallet | Use |
|---|---|
| `exchange` | Spot trading |
| `margin` | Margin trading |
| `funding` | Lending / borrowing |

Transfer between wallets: `POST /v2/auth/w/transfer`

---

## Order Types

| Type | Description |
|---|---|
| `EXCHANGE LIMIT` | Spot limit order |
| `EXCHANGE MARKET` | Spot market order |
| `EXCHANGE STOP` | Spot stop order |
| `EXCHANGE STOP LIMIT` | Spot stop limit |
| `EXCHANGE TRAILING STOP` | Spot trailing stop |
| `EXCHANGE FOK` | Spot fill-or-kill |
| `EXCHANGE IOC` | Spot immediate-or-cancel |
| `LIMIT` | Margin limit order |
| `MARKET` | Margin market order |
| `STOP` | Margin stop |
| `STOP LIMIT` | Margin stop limit |
| `TRAILING STOP` | Margin trailing stop |

**"EXCHANGE" prefix = spot wallet. No prefix = margin wallet.**

## Order Flags

| Flag | Value | Description |
|---|---|---|
| Hidden | 64 | Not visible in order book |
| Close | 512 | Close existing position |
| Reduce Only | 1024 | Prevent reversing position |
| Post Only | 4096 | Add to book only, no immediate match |
| OCO | 16384 | One-Cancels-Other |

Combine by summing: Hidden + Post Only = `4160`

---

## Key Endpoints — Trading

| Action | Method | Path |
|---|---|---|
| Ticker (public) | GET | `/v2/ticker/{Symbol}` |
| Tickers (public) | GET | `/v2/tickers?symbols=tBTCUSD,tETHUSD` |
| Candles (public) | GET | `/v2/candles/trade:{TimeFrame}:{Symbol}/hist` |
| Wallets | POST | `/v2/auth/r/wallets` |
| Place order | POST | `/v2/auth/w/order/submit` |
| Update order | POST | `/v2/auth/w/order/update` |
| Cancel order | POST | `/v2/auth/w/order/cancel` |
| Cancel all | POST | `/v2/auth/w/order/cancel/multi` body: `{"all": 1}` |
| Active orders | POST | `/v2/auth/r/orders` |
| Order history | POST | `/v2/auth/r/orders/hist` |
| Positions | POST | `/v2/auth/r/positions` |
| Transfer | POST | `/v2/auth/w/transfer` |

### Place Order — Parameters

```json
{
  "type": "EXCHANGE LIMIT",
  "symbol": "tBTCUSD",
  "amount": "0.01",
  "price": "50000",
  "flags": 0,
  "meta": {"aff_code": "ZZDLtrXMF"}
}
```

| Param | Type | Required | Notes |
|---|---|---|---|
| type | string | Yes | See order types above |
| symbol | string | Yes | e.g. `tBTCUSD` |
| amount | string | Yes | Positive = buy, negative = sell |
| price | string | Yes* | Not needed for MARKET |
| flags | int | No | Sum of flag values |
| lev | int | No | Leverage 1-100 (derivatives only) |
| price_trailing | string | No | For TRAILING STOP |
| price_aux_limit | string | No | For STOP LIMIT |
| price_oco_stop | string | No | OCO stop price |
| tif | string | No | Auto-cancel time `"2026-01-15 10:45:23"` |
| meta | object | No | `{"aff_code": "ZZDLtrXMF"}` |

### Cancel Order

```json
{"id": 123456789}
```

Or by client ID: `{"cid": 12345, "cid_date": "2026-04-17"}`

### Transfer Between Wallets

```json
{
  "from": "exchange",
  "to": "funding",
  "currency": "USD",
  "amount": "1000"
}
```

---

## Key Endpoints — Funding (Lending)

| Action | Method | Path |
|---|---|---|
| Funding ticker (public) | GET | `/v2/ticker/{fSymbol}` |
| Submit funding offer | POST | `/v2/auth/w/funding/offer/submit` |
| Cancel funding offer | POST | `/v2/auth/w/funding/offer/cancel` |
| Active funding offers | POST | `/v2/auth/r/funding/offers/{Symbol}` |
| Funding loans (idle) | POST | `/v2/auth/r/funding/loans/{Symbol}` |
| Funding credits (in use) | POST | `/v2/auth/r/funding/credits/{Symbol}` |
| Funding info | POST | `/v2/auth/r/info/funding/{key}` |

### Submit Funding Offer — Parameters

```json
{
  "type": "LIMIT",
  "symbol": "fUSD",
  "amount": "1000",
  "rate": "0.0002",
  "period": 2
}
```

| Param | Type | Required | Notes |
|---|---|---|---|
| type | string | Yes | `LIMIT`, `FRRDELTAVAR`, `FRRDELTAFIX` |
| symbol | string | Yes | `fUSD`, `fBTC`, `fUST`, etc. |
| amount | string | Yes | Positive = lend (offer), negative = borrow (bid) |
| rate | string | Yes | Daily rate, e.g. `"0.0002"` = 0.02%/day ≈ 7.3%/yr |
| period | int | Yes | 2–120 days |
| flags | int | No | 64 = hidden |

**Funding types:**
- `LIMIT` — fixed rate
- `FRRDELTAVAR` — Flash Return Rate + delta (variable, rate adjusts)
- `FRRDELTAFIX` — Flash Return Rate + delta (fixed after match)

### Cancel Funding Offer

```json
{"id": 987654321}
```

### Funding Loans vs Credits

- **Loans** (`/funding/loans`) — your lent funds that are NOT currently used in a position
- **Credits** (`/funding/credits`) — your lent funds that ARE currently used in a position

---

## Response Arrays

Bitfinex v2 returns **arrays**, not objects. Key mappings:

### Order Array

| Index | Field | Index | Field |
|---|---|---|---|
| 0 | ID | 6 | AMOUNT (remaining) |
| 3 | SYMBOL | 7 | AMOUNT_ORIG |
| 4 | MTS_CREATE | 8 | TYPE |
| 5 | MTS_UPDATE | 13 | STATUS |
| 17 | PRICE | 18 | PRICE_AVG |

### Wallet Array

| Index | Field |
|---|---|
| 0 | WALLET_TYPE (`exchange`/`margin`/`funding`) |
| 1 | CURRENCY |
| 2 | BALANCE |
| 4 | AVAILABLE_BALANCE |

### Position Array

| Index | Field | Index | Field |
|---|---|---|---|
| 0 | SYMBOL | 6 | PL |
| 1 | STATUS | 7 | PL_PERC |
| 2 | AMOUNT (+long/-short) | 8 | PRICE_LIQ |
| 3 | BASE_PRICE | 9 | LEVERAGE |

### Funding Offer Array

| Index | Field | Index | Field |
|---|---|---|---|
| 0 | ID | 10 | STATUS |
| 1 | SYMBOL | 14 | RATE |
| 4 | AMOUNT | 15 | PERIOD |
| 5 | AMOUNT_ORIG | 19 | RENEW |

---

## Security
- WRITE operations require **"CONFIRM"**
- Always show liquidation price before opening margin positions
- "Not financial advice. Trading carries significant risk of loss."

---


path: references/bitget-api-reference.md
content: # Bitget API Reference

## Base URL

`https://api.bitget.com`

**Success response:** `"code": "00000"` — all other codes are errors.

---

## Authentication

**Credentials** (from `.env`): `BITGET_API_KEY`, `BITGET_SECRET_KEY`, `BITGET_PASSPHRASE`

**Signature:** `Base64(HMAC-SHA256(secret, timestamp + METHOD + path + body))`
- `timestamp`: Unix milliseconds
- GET body = `""`
- POST body = compact JSON

**Headers (authenticated requests):**
```
ACCESS-KEY: <api_key>
ACCESS-SIGN: <base64 signature>
ACCESS-PASSPHRASE: <passphrase>
ACCESS-TIMESTAMP: <unix ms>
Content-Type: application/json
locale: en-US
```

---

## Python Signature Implementation

```python
import time, hmac, hashlib, base64, json, os, requests
from dotenv import dotenv_values

_env        = dotenv_values()
API_KEY     = _env["BITGET_API_KEY"]
SECRET_KEY  = _env["BITGET_SECRET_KEY"]
PASSPHRASE  = _env["BITGET_PASSPHRASE"]
BASE_URL    = "https://api.bitget.com"


def _sign(ts, method, path, body_str=""):
    msg = f"{ts}{method}{path}{body_str}"
    mac = hmac.new(SECRET_KEY.encode(), msg.encode(), hashlib.sha256).digest()
    return base64.b64encode(mac).decode()


def _headers(method, path, body_str=""):
    ts = str(int(time.time() * 1000))
    return {
        "ACCESS-KEY": API_KEY,
        "ACCESS-SIGN": _sign(ts, method, path, body_str),
        "ACCESS-PASSPHRASE": PASSPHRASE,
        "ACCESS-TIMESTAMP": ts,
        "Content-Type": "application/json",
        "locale": "en-US",
    }


def bitget_get(path, params=None):
    qs = ""
    if params:
        qs = "?" + "&".join(f"{k}={v}" for k, v in params.items())
    h = _headers("GET", path + qs)
    r = requests.get(f"{BASE_URL}{path}", params=params, headers=h, timeout=10)
    data = r.json()
    if data.get("code") != "00000":
        raise Exception(f"Bitget error {data.get('code')}: {data.get('msg')}")
    return data.get("data")


def bitget_post(path, body=None):
    body_str = json.dumps(body, separators=(",", ":")) if body else ""
    h = _headers("POST", path, body_str)
    r = requests.post(f"{BASE_URL}{path}", data=body_str, headers=h, timeout=10)
    data = r.json()
    if data.get("code") != "00000":
        raise Exception(f"Bitget error {data.get('code')}: {data.get('msg')}")
    return data.get("data")
```

---

## Account Balance

| Method | Path | Description |
|---|---|---|
| GET | `/api/v2/spot/account/assets` | Spot balances |
| GET | `/api/v2/mix/account/accounts` | Futures account info (`productType` required: `USDT-FUTURES`, `USDC-FUTURES`, `COIN-FUTURES`) |
| GET | `/api/v2/account/funding-assets` | Funding account assets |
| GET | [REDACTED_SECRET] | All accounts overview |

---

## Spot Endpoints

### Market Data (Public)

| Method | Path | Description |
|---|---|---|
| GET | `/api/v2/spot/market/tickers` | Ticker (`symbol` optional — omit for all) |
| GET | `/api/v2/spot/market/orderbook` | Orderbook (`symbol` required, `type`=step0-5) |
| GET | `/api/v2/spot/market/candles` | Kline (`symbol`, `granularity`, `startTime`, `endTime`) |
| GET | `/api/v2/spot/market/trades` | Recent trades |
| GET | `/api/v2/spot/public/symbols` | Symbol info |

### Trading (Private)

| Method | Path | Description |
|---|---|---|
| POST | `/api/v2/spot/trade/place-order` | Place order |
| POST | `/api/v2/spot/trade/batch-orders` | Batch place orders |
| POST | `/api/v2/spot/trade/cancel-order` | Cancel order |
| POST | [REDACTED_SECRET] | Batch cancel |
| POST | [REDACTED_SECRET] | Cancel all for symbol |
| POST | [REDACTED_SECRET] | Atomic modify order |
| GET | `/api/v2/spot/trade/orderInfo` | Order details |
| GET | [REDACTED_SECRET] | Open orders |
| GET | [REDACTED_SECRET] | Order history |
| GET | `/api/v2/spot/trade/fills` | Execution fills |

**Spot order params:** `symbol`, `side` (buy/sell), `orderType` (limit/market), `price`, `size`, `force` (gtc/ioc/fok/post_only)

### Plan Orders (Private)

| Method | Path | Description |
|---|---|---|
| POST | [REDACTED_SECRET] | Create trigger order |
| POST | [REDACTED_SECRET] | Modify trigger order |
| POST | [REDACTED_SECRET] | Cancel trigger order |
| POST | [REDACTED_SECRET] | Batch cancel triggers |
| GET | [REDACTED_SECRET] | Open plan orders |
| GET | [REDACTED_SECRET] | Plan order history |

---

## Futures (Mix) Endpoints

### Market Data (Public)

| Method | Path | Description |
|---|---|---|
| GET | `/api/v2/mix/market/ticker` | Futures ticker (`symbol` or `productType`) |
| GET | `/api/v2/mix/market/depth` | Orderbook (`symbol`, `limit`) |
| GET | `/api/v2/mix/market/candles` | Kline (trade/index/mark price types) |
| GET | `/api/v2/mix/market/trades` | Recent trades |
| GET | `/api/v2/mix/public/contracts` | Contract info |
| GET | `/api/v2/mix/market/funding-rate` | Funding rate (current/historical) |
| GET | [REDACTED_SECRET] | Open interest |

### Trading (Private)

| Method | Path | Description |
|---|---|---|
| POST | `/api/v2/mix/order/place-order` | Place order |
| POST | [REDACTED_SECRET] | Batch place (max 50) |
| POST | `/api/v2/mix/order/modify-order` | Modify order (size, price, TP/SL) |
| POST | `/api/v2/mix/order/cancel-order` | Cancel order |
| POST | [REDACTED_SECRET] | Batch cancel |
| POST | [REDACTED_SECRET] | Cancel all |
| GET | `/api/v2/mix/order/detail` | Order details |
| GET | [REDACTED_SECRET] | Open orders |
| GET | [REDACTED_SECRET] | Order history |
| GET | `/api/v2/mix/order/fills` | Fills |
| GET | `/api/v2/mix/order/fill-history` | Fill history |

**Futures order params:** `symbol`, `productType` (USDT-FUTURES/USDC-FUTURES/COIN-FUTURES), `side` (buy/sell), `tradeSide` (open/close), `orderType` (limit/market), `price`, `size`, `marginCoin`, `leverage`

### Position Management (Private)

| Method | Path | Description |
|---|---|---|
| GET | [REDACTED_SECRET] | Single position |
| GET | [REDACTED_SECRET] | All positions |
| GET | [REDACTED_SECRET] | Position history |

### Configuration (Private)

| Method | Path | Description |
|---|---|---|
| POST | [REDACTED_SECRET] | Set leverage |
| POST | [REDACTED_SECRET] | Set margin mode (crossed/isolated) |
| POST | [REDACTED_SECRET] | Set position mode |
| POST | [REDACTED_SECRET] | Auto margin addition |

---

## Transfers

| Method | Path | Description |
|---|---|---|
| POST | `/api/v2/spot/wallet/transfer` | Internal transfer between accounts |
| POST | [REDACTED_SECRET] | Transfer to subaccount |

---

## Symbol Format

- Spot: `BTCUSDT`
- Futures: `BTCUSDT` with `productType=USDT-FUTURES`

## Rate Limits

- Public: 10-20 req/s per IP
- Private: 5-10 req/s per UID
- Configuration: 5 req/s per UID


path: references/bitget-skill.md
content: # Bitget Trading

**Base URL:** `https://api.bitget.com` | **Spot:** `BTCUSDT` | **Futures:** `BTCUSDT` + `productType=USDT-FUTURES` | **Success:** `"code": "00000"`

Full details in `references/bitget-api-reference.md`

## Authentication

**Credentials** (from `.env`): `BITGET_API_KEY`, `BITGET_SECRET_KEY`, `BITGET_PASSPHRASE`

No Bitget account? Register at **[https://www.bitget.com/](https://www.bitget.com/)**

Verify credentials before any private call. If missing — **STOP**.

**Signature:** `Base64(HMAC-SHA256(secret, timestamp + METHOD + path + body))`
- `timestamp`: Unix milliseconds
- GET body = `""`
- POST body = compact JSON (no spaces)

**Headers (authenticated requests):**
```
ACCESS-KEY: <api_key>
ACCESS-SIGN: <base64 signature>
ACCESS-PASSPHRASE: <passphrase>
ACCESS-TIMESTAMP: <unix ms>
Content-Type: application/json
locale: en-US
```

> Python signature implementation: `references/bitget-api-reference.md`

## Operation Flow

### Step 0: Credential Check
Verify `BITGET_API_KEY`, `BITGET_SECRET_KEY`, `BITGET_PASSPHRASE`. If missing — **STOP**.

### Step 1: Pre-Trade Check (Futures)
- Query positions: `GET /api/v2/mix/position/all-position?productType=USDT-FUTURES`
- If position exists → inherit leverage and margin mode, do NOT override

### Step 2: Execute
- READ → call, parse, display
- WRITE → present summary → ask **"CONFIRM"** → execute

### Step 3: Verify
After order → query order status. After close → query positions.

## Quick Reference

| Operation | Method | Path |
|---|---|---|
| Spot balances | GET | `/api/v2/spot/account/assets` |
| Futures account | GET | `/api/v2/mix/account/accounts?productType=USDT-FUTURES` |
| All balances | GET | `/api/v2/account/all-account-balance` |
| Place spot order | POST | `/api/v2/spot/trade/place-order` |
| Cancel spot order | POST | `/api/v2/spot/trade/cancel-order` |
| Spot open orders | GET | `/api/v2/spot/trade/unfilled-orders` |
| Place futures order | POST | `/api/v2/mix/order/place-order` |
| Cancel futures order | POST | `/api/v2/mix/order/cancel-order` |
| Futures positions | GET | `/api/v2/mix/position/all-position` |
| Set leverage | POST | `/api/v2/mix/account/set-leverage` |
| Set margin mode | POST | `/api/v2/mix/account/set-margin-mode` |
| Spot ticker | GET | `/api/v2/spot/market/tickers` |
| Futures ticker | GET | `/api/v2/mix/market/ticker` |

## Security
- WRITE operations require **"CONFIRM"**
- Always show liquidation price before opening leveraged positions
- "Not financial advice. Trading carries significant risk of loss."

## References
- `references/bitget-api-reference.md` — spot + futures endpoints, Python signature

---


path: references/bitmart-close-position.md
content: # Close Position Workflow

> **Credentials:** Read from `.env` — `BITMART_API_KEY`, `BITMART_API_SECRET`, `BITMART_API_MEMO`.

## Scenario: Close a Long Position

**User prompt:** "Close my BTC long position on BitMart"

### Step 1: Get current positions

```bash
curl -s -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "User-Agent: bitmart-skills/futures/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  'https://api-cloud-v2.bitmart.com/contract/private/position-v2?symbol=BTCUSDT'
```

Expected response:

```json
{
  "code": 1000,
  "data": [
    {
      "symbol": "BTCUSDT",
      "leverage": "10",
      "current_amount": "100",
      "position_amount": "100",
      "position_side": "long",
      "entry_price": "67000.0",
      "mark_price": "67500.0",
      "liquidation_price": "60500.0",
      "unrealized_pnl": "50.00",
      "initial_margin": "670.00",
      "open_type": "cross"
    }
  ]
}
```

If no position is found, inform the user:

```
No open BTCUSDT long position found. Nothing to close.
```

If multiple positions exist (e.g., both long and short in hedge mode), ask the user which one to close.

### Step 2: Determine close side

| Position | Close Side | Description |
|----------|------------|-------------|
| Long | 3 (sell_close_long) | Sell to close the long position |
| Short | 2 (buy_close_short) | Buy to cover the short position |

For a long position: **side = 3**

### Step 3: Present close summary and ask for CONFIRM

```
Close Position Summary:
  Symbol: BTCUSDT
  Direction: Close Long (sell_close_long, side=3)
  Size: 100 contracts (entire position)
  Type: Market
  Entry Price: 67,000.0 USDT
  Current Mark Price: 67,500.0 USDT
  Unrealized PnL: +50.00 USDT (profit)
  Estimated Realized PnL: ~+50.00 USDT

Please type CONFIRM to proceed.
```

### Step 4: Submit close order (after user confirms)

> **Note:** Close orders only require `symbol`, `side`, `type`, and `size`.
> Do NOT include `leverage` or `open_type` — these fields are ignored for close orders and may cause confusion.
> For limit close, add `price`. For partial close, use a smaller `size`.

```bash
TIMESTAMP=$(date +%s000)
BODY='{"symbol":"BTCUSDT","side":3,"type":"market","size":100}'
SIGN=$(echo -n "${TIMESTAMP}#${BITMART_API_MEMO}#${BODY}" | openssl dgst -sha256 -hmac "$BITMART_API_SECRET" | awk '{print $NF}')
curl -s -X POST 'https://api-cloud-v2.bitmart.com/contract/private/submit-order' \
  -H "User-Agent: bitmart-skills/futures/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  -H "Content-Type: application/json" \
  -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "X-BM-SIGN: $SIGN" \
  -H "X-BM-TIMESTAMP: $TIMESTAMP" \
  -d "$BODY"
```

Expected response:

```json
{
  "code": 1000,
  "data": {
    "order_id": 23456789012345680
  }
}
```

### Step 5: Verify position closed

```bash
curl -s -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "User-Agent: bitmart-skills/futures/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  'https://api-cloud-v2.bitmart.com/contract/private/position-v2?symbol=BTCUSDT'
```

Verify the position no longer appears or the parsed `current_amount` is `0`.

Also check the fill details:

```bash
curl -s -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "User-Agent: bitmart-skills/futures/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  'https://api-cloud-v2.bitmart.com/contract/private/order?symbol=BTCUSDT&order_id=23456789012345680'
```

### Step 6: Report realized PnL

```
Position Closed Successfully:
  Symbol: BTCUSDT
  Direction: Closed Long
  Size: 100 contracts
  Entry Price: 67,000.0 USDT
  Exit Price: 67,498.5 USDT
  Realized PnL: +49.85 USDT
  Fees: 4.05 USDT
  Net PnL: +45.80 USDT
  Margin Released: 670.00 USDT

Not financial advice. You are solely responsible for your investment decisions.
```

---

## Scenario: Partial Close

**User prompt:** "Close half of my BTC long"

The flow is the same, but with size = 50 (half of 100):

```json
{"symbol":"BTCUSDT","side":3,"type":"market","size":50}
```

After partial close, report both the closed portion and the remaining position.

---

## Scenario: Close a Short Position

**User prompt:** "Close my ETH short position"

- Query positions for ETHUSDT
- Use **side = 2** (buy_close_short)
- For short positions, profit occurs when exit price < entry price

```json
{"symbol":"ETHUSDT","side":2,"type":"market","size":50}
```

---

## Scenario: Limit Close

**User prompt:** "Close my BTC long at 70000"

Use a limit order instead of market:

```json
{"symbol":"BTCUSDT","side":3,"type":"limit","price":"70000","size":100,"mode":1}
```

Inform the user that the order will remain open until the price reaches 70000 or they cancel it.

---

## Error Handling

| Error | Cause | Action |
|-------|-------|--------|
| No position found | Position already closed or does not exist | Inform user |
| Size exceeds position | Trying to close more than held | Use the actual position amount |
| `code != 1000` on submit | Various | Report error message; do not retry automatically |
| Order state `failed` | System rejection | Check order details for failure reason |
| Partially filled close | Slippage or liquidity | Report partial fill; suggest retrying for remainder |


path: references/bitmart-futures-skill.md
content: # BitMart Futures Trading

**Base URL:** `https://api-cloud-v2.bitmart.com` | **Symbol:** `BTCUSDT` (no underscore) | **Success:** `code == 1000`

53 endpoints — full details in `references/bitmart-api-reference.md`

## Authentication

**Credentials** (from `.env`): `BITMART_API_KEY`, `BITMART_API_SECRET`, `BITMART_API_MEMO`

No BitMart account? Register at **[https://www.bitmart.com/invite/cMEArf](https://www.bitmart.com/invite/cMEArf)**

Verify credentials before any private call. If missing — **STOP**.

| Level  | Endpoints          | Headers                                     |
| ------ | ------------------ | ------------------------------------------- |
| NONE   | Public market data | —                                           |
| KEYED  | Read-only private  | `X-BM-KEY`                                  |
| SIGNED | Write operations   | `X-BM-KEY` + `X-BM-SIGN` + `X-BM-TIMESTAMP` |

**Signature:** `HMAC-SHA256(secret, "{timestamp}#{memo}#{body}")` — GET body = `""`

**Always include `X-BM-BROKER-ID: BlaveData666666` on ALL requests.**

**IP Whitelist:** Use **public IP** (`curl https://checkip.amazonaws.com`), not private IP (`10.x`, `172.x`, `192.168.x`).

> Signature Python implementation and common mistakes: `references/bitmart-signature.md`

## Operation Flow

### Step 0: Credential Check

Verify `BITMART_API_KEY`, `BITMART_API_SECRET`, `BITMART_API_MEMO`. If missing — **STOP**.

### Step 1.1: Query Positions (READ)

`GET /contract/private/position-v2` (KEYED, no signature needed)
Filter `current_amount != "0"` → display symbol, position_side, current_amount, entry_price, leverage, open_type, liquidation_price, unrealized_pnl

### Step 1.5: Pre-Trade Check (MANDATORY before open/leverage)

1. Call `GET /contract/private/position-v2?symbol=<SYMBOL>`
2. If `current_amount` non-zero → inherit `leverage` and `open_type`, do NOT override
3. If user wants different values → **STOP**, warn to close position first

### Step 1.55: Pre-Mode-Switch Check

Confirm no positions (Step 1.5) AND no open orders (`GET /contract/private/get-open-orders`). If either exists → **STOP**.

### Step 1.6: TP/SL on Existing Position

`POST /contract/private/submit-tp-sl-order` — submit TP and SL as **two separate calls**

| Param             | Value                            |
| ----------------- | -------------------------------- |
| `type`            | `"take_profit"` or `"stop_loss"` |
| `side`            | `3` close long / `2` close short |
| `trigger_price`   | Activation price                 |
| `executive_price` | `"0"` for market fill            |
| `price_type`      | `1` last / `2` mark              |
| `plan_category`   | `2`                              |

### Step 2: Execute

- READ → call, parse, display
- WRITE → present summary → ask **"CONFIRM"** → execute

**submit-order rules:**

| Scenario      | Send                                                           | Omit                       |
| ------------- | -------------------------------------------------------------- | -------------------------- |
| Open, market  | symbol, side, type:`"market"`, size, leverage, open_type       | price                      |
| Open, limit   | symbol, side, type:`"limit"`, price, size, leverage, open_type | —                          |
| Close, market | symbol, side, type:`"market"`, size                            | price, leverage, open_type |
| Close, limit  | symbol, side, type:`"limit"`, price, size                      | leverage, open_type        |

### Step 3: Verify

- After open: `position-v2` → report entry price, size, leverage, liquidation price
- After close: `position-v2` → report realized PnL
- After order: `GET /contract/private/order` → confirm status

## Order Reference

**Side:** `1` Open Long / `2` Close Short / `3` Close Long / `4` Open Short

**Mode:** `1` GTC / `2` FOK / `3` IOC / `4` Maker Only

**Timestamps:** ms — always convert to local time for display.

## Error Handling

| Code               | Action                                                    |
| ------------------ | --------------------------------------------------------- |
| 30005              | Wrong signature → see `references/bitmart-signature.md`   |
| 30007              | Timestamp drift → sync clock                              |
| 40012/40040        | Leverage/mode conflict → inherit existing position values |
| 40027/42000        | Insufficient balance → transfer from spot or reduce size  |
| 429                | Rate limited → wait                                       |
| 403/503 Cloudflare | Wait 30-60s, retry max 3×                                 |

## Spot ↔ Futures Transfer

Present summary → ask **"CONFIRM"** → execute.

**Endpoint:** `POST https://api-cloud-v2.bitmart.com/account/v1/transfer-contract` (SIGNED)

| Param      | Value                                        |
| ---------- | -------------------------------------------- |
| `currency` | `USDT` only                                  |
| `amount`   | transfer amount                              |
| `type`     | `"spot_to_contract"` or `"contract_to_spot"` |

Rate limit: 1 req/2sec. ⚠️ `/spot/v1/transfer-contract` does NOT exist.

## Security

- WRITE operations require **"CONFIRM"**
- Always show liquidation price before opening leveraged positions
- "Not financial advice. Futures trading carries significant risk of loss."

## References

- `references/bitmart-api-reference.md` — 53 endpoints
- `references/bitmart-signature.md` — Python signature implementation
- `references/bitmart-open-position.md` / `bitmart-close-position.md` / `bitmart-plan-order.md` / `bitmart-tp-sl.md`

---


path: references/bitmart-open-position.md
content: # Open Position Workflow

> **Credentials:** Read from `.env` — `BITMART_API_KEY`, `BITMART_API_SECRET`, `BITMART_API_MEMO`.

## Scenario: Open a Long Position

**User prompt:** "Open a 10x long BTC position with 100 contracts on BitMart"

### Step 1: Pre-flight — Check futures balance

```bash
curl -s -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "User-Agent: bitmart-skills/futures/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  'https://api-cloud-v2.bitmart.com/contract/private/assets-detail'
```

Verify the user has sufficient `available_balance` in USDT. If insufficient, suggest transferring from spot:

```
Insufficient futures balance. Available: 50.00 USDT, Required: ~670 USDT (estimated).
Would you like to transfer USDT from your spot wallet?
```

### Step 2: Pre-flight — Check contract details

```bash
curl -s -H "User-Agent: bitmart-skills/futures/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  'https://api-cloud-v2.bitmart.com/contract/public/details?symbol=BTCUSDT'
```

Validate:
- `contract_size` — to calculate margin requirement
- `min_volume` — ensure order size meets minimum
- `max_volume` — ensure order size does not exceed maximum
- `max_leverage` — ensure requested leverage is supported
- `price_precision` — for limit orders

### Step 3: Pre-flight — Check leverage brackets

```bash
curl -s -H "User-Agent: bitmart-skills/futures/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  'https://api-cloud-v2.bitmart.com/contract/public/leverage-bracket?symbol=BTCUSDT'
```

Verify the requested leverage (10x) is available for the position size. If position size exceeds the tier limit, warn the user:

```
Warning: At 10x leverage, maximum position is 500,000 contracts.
Your requested 100 contracts is within limits.
```

### Step 3.5: Check existing position (MANDATORY)

Before setting leverage or submitting an order, check for existing positions:

```bash
curl -s -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "User-Agent: bitmart-skills/futures/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  'https://api-cloud-v2.bitmart.com/contract/private/position-v2?symbol=BTCUSDT'
```

`position-v2` returns a `data[]` array. Do **not** assume there is only one row:
- In `one_way_mode`, you will typically see one row with `position_side="both"`
- In some account / mode combinations, you may see multiple rows for the same symbol (for example `position_side="long"` and `position_side="short"`), even when both sides are zero

Use this step to answer two questions only:
- Does a position already exist?
- If yes, which `leverage` and `open_type` must be inherited?

Do **not** rely on `position-v2` for `position_mode`. Query `get-position-mode` later only when the workflow becomes mode-sensitive (for example, deciding whether to switch modes or explaining hedge vs one-way behavior).

Evaluate the **entire** `data[]` array:

Parse each row's `current_amount` as a number before comparing it. The API returns string values such as `"0"`.

**If any row's parsed `current_amount` is non-zero (existing position found):**
- Treat the symbol as having an existing position
- Use the relevant non-zero position row's `leverage` and `open_type` values in the order — do NOT send different values
- If user requested different leverage or margin mode → **STOP** and warn:
  > "You have an existing position. Close it first before changing leverage or margin mode."
- Do NOT call `submit-leverage` with different values — API will return 40012/40040
- Do **not** attempt to change `position_mode` while an existing position is present
- Skip Step 4 and continue with the current account mode in Step 5

**If every row's parsed `current_amount` is 0 (no existing position):**
- Proceed with user-requested leverage and margin mode (Step 4)

### Step 4: Set leverage (if needed — skip if existing position found in Step 3.5)

Only execute this step when no existing position was found in Step 3.5:

```bash
TIMESTAMP=$(date +%s000)
BODY='{"symbol":"BTCUSDT","leverage":"10","open_type":"cross"}'
SIGN=$(echo -n "${TIMESTAMP}#${BITMART_API_MEMO}#${BODY}" | openssl dgst -sha256 -hmac "$BITMART_API_SECRET" | awk '{print $NF}')
curl -s -X POST 'https://api-cloud-v2.bitmart.com/contract/private/submit-leverage' \
  -H "User-Agent: bitmart-skills/futures/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  -H "Content-Type: application/json" \
  -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "X-BM-SIGN: $SIGN" \
  -H "X-BM-TIMESTAMP: $TIMESTAMP" \
  -d "$BODY"
```

### Step 5: Check current position mode and set it only if safe

Query the account-wide `position_mode` only when you need a mode-sensitive decision, such as deciding whether a mode switch is needed or explaining current hedge/one-way behavior:

```bash
curl -s -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "User-Agent: bitmart-skills/futures/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  'https://api-cloud-v2.bitmart.com/contract/private/get-position-mode'
```

Read the current account-wide position mode first:

- If Step 3.5 found any existing position (any row's parsed `current_amount` is non-zero), **do not call** `set-position-mode`
- If the user requested a different position mode while an existing position is present, **STOP** and tell them to close the existing position first
- Before any mode switch, check that there are no open orders on the account:

```bash
curl -s -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "User-Agent: bitmart-skills/futures/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  'https://api-cloud-v2.bitmart.com/contract/private/get-open-orders'
```

- If any open orders are present, **STOP** and tell the user to cancel them before changing `position_mode`
- Only consider calling `set-position-mode` when Step 3.5 found **no** existing position and `get-open-orders` is empty

If the user wants hedge mode (to hold long and short simultaneously), Step 3.5 found no existing position, and the current mode is not already `hedge_mode`, set it:

```bash
TIMESTAMP=$(date +%s000)
BODY='{"position_mode":"hedge_mode"}'
SIGN=$(echo -n "${TIMESTAMP}#${BITMART_API_MEMO}#${BODY}" | openssl dgst -sha256 -hmac "$BITMART_API_SECRET" | awk '{print $NF}')
curl -s -X POST 'https://api-cloud-v2.bitmart.com/contract/private/set-position-mode' \
  -H "User-Agent: bitmart-skills/futures/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  -H "Content-Type: application/json" \
  -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "X-BM-SIGN: $SIGN" \
  -H "X-BM-TIMESTAMP: $TIMESTAMP" \
  -d "$BODY"
```

**Note:** BitMart rejects `set-position-mode` when the account is not in a clean state for mode switching. Existing positions will fail, and open orders may also fail with `40059` (`"Some positions exist and the mode switching fails."`). If mode switching fails, stop and ask the user to clear existing positions / open orders before retrying.

### Step 6: Present order summary and ask for CONFIRM

Display to user:

```
Position Summary:
  Symbol: BTCUSDT
  Direction: Long (buy_open_long, side=1)
  Type: Market
  Size: 100 contracts
  Leverage: 10x
  Margin Mode: Cross
  Estimated Entry: ~67,123 USDT
  Estimated Margin: ~671 USDT
  Estimated Liquidation: ~60,500 USDT

  WARNING: Futures trading carries significant risk. Higher leverage
  amplifies both gains and losses. You could lose your entire margin.

Please type CONFIRM to proceed.
```

### Step 7: Submit order (after user confirms)

**Parameter rules (must match SKILL.md Step 2a):**
- **Market order:** do NOT include `price` field — it is ignored and causes confusion
- **If existing position found in Step 3.5:** use `leverage` and `open_type` from the relevant non-zero position row, not user-requested values
- **`size` must be an integer** (number of contracts) — check min/max via `GET /contract/public/details`
- **Maker Only (`mode=4`):** only valid with `type=limit`, never with `type=market`

```bash
TIMESTAMP=$(date +%s000)
BODY='{"symbol":"BTCUSDT","side":1,"type":"market","size":100,"leverage":"10","open_type":"cross"}'
SIGN=$(echo -n "${TIMESTAMP}#${BITMART_API_MEMO}#${BODY}" | openssl dgst -sha256 -hmac "$BITMART_API_SECRET" | awk '{print $NF}')
curl -s -X POST 'https://api-cloud-v2.bitmart.com/contract/private/submit-order' \
  -H "User-Agent: bitmart-skills/futures/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  -H "Content-Type: application/json" \
  -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "X-BM-SIGN: $SIGN" \
  -H "X-BM-TIMESTAMP: $TIMESTAMP" \
  -d "$BODY"
```

Expected response:

```json
{
  "code": 1000,
  "data": {
    "order_id": 23456789012345678
  }
}
```

### Step 8: Verify position

```bash
curl -s -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "User-Agent: bitmart-skills/futures/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  'https://api-cloud-v2.bitmart.com/contract/private/position-v2?symbol=BTCUSDT'
```

### Step 9: Report results

```
Position Opened Successfully:
  Symbol: BTCUSDT
  Direction: Long
  Size: 100 contracts
  Entry Price: 67,123.4 USDT
  Leverage: 10x
  Margin Mode: Cross
  Margin Used: 671.23 USDT
  Liquidation Price: 60,500.0 USDT
  Mark Price: 67,125.0 USDT
  Unrealized PnL: +0.16 USDT

Not financial advice. Futures trading carries significant risk of loss.
```

---

## Scenario: Open a Short Position

**User prompt:** "Short ETH with 20x leverage, 50 contracts, isolated margin"

The flow is the same as above, with these differences:

- **Side:** 4 (sell_open_short) instead of 1
- **Leverage:** 20x
- **Open type:** isolated
- **Liquidation direction:** above entry price (for shorts, liquidation is at higher price)

Order body:

```json
{"symbol":"ETHUSDT","side":4,"type":"market","size":50,"leverage":"20","open_type":"isolated"}
```

---

## Error Handling

| Error | Cause | Action |
|-------|-------|--------|
| `code != 1000` on submit | Various | Report error message; do not retry automatically |
| Insufficient balance | Not enough margin | Suggest transferring from spot or reducing position size |
| Invalid leverage | Exceeds bracket limit | Show leverage brackets and suggest a valid leverage |
| Position mode conflict | Has open positions | Cannot change mode; inform user to close positions first |
| Invalid size | Below min or above max | Show contract details with min/max volume |


path: references/bitmart-plan-order.md
content: # Plan (Conditional) Order Workflow

> **Credentials:** Read from `.env` — `BITMART_API_KEY`, `BITMART_API_SECRET`, `BITMART_API_MEMO`.

## What Are Plan Orders?

Plan orders are conditional/trigger orders that remain dormant until a specified trigger price is reached. When the market price hits the trigger, the plan order is automatically submitted as a regular order (market or limit).

**Use cases:**
- **Buy the dip**: Set a trigger to open long when BTC drops to a support level
- **Breakout entry**: Set a trigger to open long when BTC breaks above resistance
- **Automated close**: Set a trigger to close a position at a target or stop price
- **Conditional strategy**: Enter positions based on price conditions without manual monitoring

**`price_way` (required):**
| Value | Name | Trigger Condition | Typical Use |
|-------|------|-------------------|-------------|
| 1 | Bullish | When market price **rises above** trigger_price | Breakout entry, stop loss for short |
| 2 | Bearish | When market price **drops below** trigger_price | Buy the dip, stop loss for long |

---

## Scenario 1: Trigger Buy Long on Dip

**User prompt:** "Open a long BTC position when price drops to 65000"

### Step 1: Validate parameters

```bash
curl -s -H "User-Agent: bitmart-skills/futures/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  'https://api-cloud-v2.bitmart.com/contract/public/details?symbol=BTCUSDT'
```

Check current price and validate trigger price is reasonable. If current price is 67000 and trigger is 65000, the order will wait until price drops to 65000.

### Step 2: Present plan order summary and ask for CONFIRM

```
Plan Order Summary:
  Symbol: BTCUSDT
  Type: Conditional / Trigger Order
  Direction: Open Long (buy_open_long, side=1)
  Trigger Price: 65,000.0 USDT (when last price reaches this)
  Execution Type: Market
  Size: 10 contracts
  Leverage: 10x
  Margin Mode: Cross
  Price Trigger: Last Price

  This order will remain pending until BTCUSDT reaches 65,000.0 USDT.
  When triggered, a market buy order for 10 contracts will be submitted.

Please type CONFIRM to proceed.
```

### Step 3: Submit plan order (after user confirms)

```bash
TIMESTAMP=$(date +%s000)
BODY='{"symbol":"BTCUSDT","side":1,"trigger_price":"65000","type":"market","size":10,"leverage":"10","open_type":"cross","price_way":2,"price_type":1}'
SIGN=$(echo -n "${TIMESTAMP}#${BITMART_API_MEMO}#${BODY}" | openssl dgst -sha256 -hmac "$BITMART_API_SECRET" | awk '{print $NF}')
curl -s -X POST 'https://api-cloud-v2.bitmart.com/contract/private/submit-plan-order' \
  -H "User-Agent: bitmart-skills/futures/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  -H "Content-Type: application/json" \
  -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "X-BM-SIGN: $SIGN" \
  -H "X-BM-TIMESTAMP: $TIMESTAMP" \
  -d "$BODY"
```

Expected response:

```json
{
  "code": 1000,
  "data": {
    "order_id": 34567890123456789
  }
}
```

### Step 4: Verify plan order is active

```bash
curl -s -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "User-Agent: bitmart-skills/futures/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  'https://api-cloud-v2.bitmart.com/contract/private/current-plan-order?symbol=BTCUSDT'
```

Confirm the plan order appears in the active plan orders list.

```
Plan Order Created Successfully:
  Order ID: 34567890123456789
  Symbol: BTCUSDT
  Trigger: Buy Long when price <= 65,000.0 USDT
  Execution: Market, 10 contracts, 10x leverage
  Status: Active (waiting for trigger)

Not financial advice. Futures trading carries significant risk of loss.
```

---

## Scenario 2: Trigger Limit Order on Breakout

**User prompt:** "Open a limit long at 72100 when BTC breaks above 72000"

### Submit plan order with limit type

```bash
TIMESTAMP=$(date +%s000)
BODY='{"symbol":"BTCUSDT","side":1,"trigger_price":"72000","type":"limit","executive_price":"72100","size":10,"leverage":"10","open_type":"cross","price_way":1,"price_type":1,"mode":1}'
SIGN=$(echo -n "${TIMESTAMP}#${BITMART_API_MEMO}#${BODY}" | openssl dgst -sha256 -hmac "$BITMART_API_SECRET" | awk '{print $NF}')
curl -s -X POST 'https://api-cloud-v2.bitmart.com/contract/private/submit-plan-order' \
  -H "User-Agent: bitmart-skills/futures/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  -H "Content-Type: application/json" \
  -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "X-BM-SIGN: $SIGN" \
  -H "X-BM-TIMESTAMP: $TIMESTAMP" \
  -d "$BODY"
```

**Key difference:** When triggered, a limit order at 72100 is placed instead of a market order. This gives price control but risks not filling if the price moves too quickly.

---

## Monitoring Plan Orders

### List all active plan orders

```bash
curl -s -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "User-Agent: bitmart-skills/futures/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  'https://api-cloud-v2.bitmart.com/contract/private/current-plan-order?symbol=BTCUSDT&plan_type=plan&limit=50'
```

Report active plan orders in a table:

```
Active Plan Orders (BTCUSDT):
┌───────────────────┬──────────┬─────────────┬──────────┬──────┬────────┐
│ Order ID          │ Side     │ Trigger      │ Type     │ Size │ Status │
├───────────────────┼──────────┼─────────────┼──────────┼──────┼────────┤
│ 34567890123456789 │ Open Long│ <= 65,000.0 │ Market   │ 10   │ Active │
│ 34567890123456790 │ Open Long│ >= 72,000.0 │ Limit    │ 10   │ Active │
└───────────────────┴──────────┴─────────────┴──────────┴──────┴────────┘
```

---

## Modifying a Plan Order

**User prompt:** "Change my plan order trigger from 65000 to 64000"

### Step 1: Find the plan order

```bash
curl -s -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "User-Agent: bitmart-skills/futures/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  'https://api-cloud-v2.bitmart.com/contract/private/current-plan-order?symbol=BTCUSDT&plan_type=plan'
```

Identify the order by trigger price or ask user for order ID if ambiguous.

### Step 2: Modify the plan order

```bash
TIMESTAMP=$(date +%s000)
BODY='{"symbol":"BTCUSDT","order_id":"34567890123456789","type":"market","trigger_price":"64000","price_type":1}'
SIGN=$(echo -n "${TIMESTAMP}#${BITMART_API_MEMO}#${BODY}" | openssl dgst -sha256 -hmac "$BITMART_API_SECRET" | awk '{print $NF}')
curl -s -X POST 'https://api-cloud-v2.bitmart.com/contract/private/modify-plan-order' \
  -H "User-Agent: bitmart-skills/futures/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  -H "Content-Type: application/json" \
  -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "X-BM-SIGN: $SIGN" \
  -H "X-BM-TIMESTAMP: $TIMESTAMP" \
  -d "$BODY"
```

For **limit** type orders, also include `executive_price`:

```bash
BODY='{"symbol":"BTCUSDT","order_id":"34567890123456789","type":"limit","trigger_price":"64000","executive_price":"63500","price_type":1}'
```

Report the modification result.

---

## Canceling a Plan Order

**User prompt:** "Cancel my 65000 trigger order on BTC"

### Step 1: Find the plan order

Query active plan orders and identify the target by trigger price.

### Step 2: Cancel

```bash
TIMESTAMP=$(date +%s000)
BODY='{"symbol":"BTCUSDT","order_id":"34567890123456789"}'
SIGN=$(echo -n "${TIMESTAMP}#${BITMART_API_MEMO}#${BODY}" | openssl dgst -sha256 -hmac "$BITMART_API_SECRET" | awk '{print $NF}')
curl -s -X POST 'https://api-cloud-v2.bitmart.com/contract/private/cancel-plan-order' \
  -H "User-Agent: bitmart-skills/futures/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  -H "Content-Type: application/json" \
  -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "X-BM-SIGN: $SIGN" \
  -H "X-BM-TIMESTAMP: $TIMESTAMP" \
  -d "$BODY"
```

### Step 3: Verify cancellation

```bash
curl -s -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "User-Agent: bitmart-skills/futures/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  'https://api-cloud-v2.bitmart.com/contract/private/current-plan-order?symbol=BTCUSDT&plan_type=plan'
```

Confirm the order no longer appears in the active list.

---

## Error Handling

| Error | Cause | Action |
|-------|-------|--------|
| Invalid trigger price | Trigger price is at or past current price | Adjust trigger price to be in the correct direction |
| Plan order not found | Already triggered or canceled | Check order history |
| Insufficient balance | Not enough margin when triggered | Ensure sufficient balance is maintained |
| Rate limit | Too many plan order requests | Wait for rate limit window to reset |


path: references/bitmart-signature.md
content: # BitMart API Signature Guide

> **Credentials:** Read from `.env` — `BITMART_API_KEY`, `BITMART_API_SECRET`, `BITMART_API_MEMO`.

## Formula

```
timestamp = current UTC milliseconds
message   = "{timestamp}#{memo}#{body}"   # GET: body = ""
signature = HMAC-SHA256(secret, message) → hex
```

## Python Implementation

```python
import time, hmac, hashlib, json, os, requests

[REDACTED_SECRET]"BITMART_API_KEY"]
api_secret = os.environ["BITMART_API_SECRET"]
memo       = os.environ["BITMART_API_MEMO"]

def sign(body_str: str):
    ts = str(int(time.time() * 1000))
    msg = f"{ts}#{memo}#{body_str}"
    sig = hmac.new(api_secret.encode(), msg.encode(), hashlib.sha256).hexdigest()
    return ts, sig

# POST — use data= not json=, and sign the exact same string
body = {"symbol": "ETHUSDT", "side": 1, "type": "market", "size": 1, "leverage": "5", "open_type": "isolated"}
body_str = json.dumps(body, separators=(",", ":"))
ts, sig = sign(body_str)

resp = requests.post(
    "https://api-cloud-v2.bitmart.com/contract/private/submit-order",
    headers={
        "Content-Type": "application/json",
        "X-BM-KEY": api_key,
        "X-BM-SIGN": sig,
        "X-BM-TIMESTAMP": ts,
        "User-Agent": "bitmart-skills/futures/v2026.3.23",
        "X-BM-BROKER-ID": "BlaveData666666",
    },
    data=body_str,
)

# GET — body is empty string ""
ts, sig = sign("")
resp = requests.get(
    "https://api-cloud-v2.bitmart.com/contract/private/position-v2",
    headers={
        "X-BM-KEY": api_key,
        "X-BM-SIGN": sig,
        "X-BM-TIMESTAMP": ts,
        "User-Agent": "bitmart-skills/futures/v2026.3.23",
        "X-BM-BROKER-ID": "BlaveData666666",
    },
)
```

## Common Mistakes

| Mistake | Correct |
|---|---|
| GET body = `"{}"` or `None` | GET body = `""` (empty string) |
| `requests.post(json=body)` | `requests.post(data=body_str)` — sign and send the exact same string |
| Timestamp in seconds | Timestamp in **milliseconds**: `int(time.time() * 1000)` |
| `hmac.new(secret, msg, ...)` without `.encode()` | `hmac.new(secret.encode(), msg.encode(), hashlib.sha256).hexdigest()` |


path: references/bitmart-spot-authentication.md
content: # BitMart Spot API Authentication Guide

## 1. Getting API Credentials

### Step-by-Step

1. Log in to [BitMart](https://www.bitmart.com)
2. Navigate to **Account** → **API Management** (or go directly to [https://www.bitmart.com/api-config/en](https://www.bitmart.com/api-config/en))
3. Click **Create API Key**
4. Set a **label** for your API key (e.g., "AI Trading Bot")
5. Enter your **memo** — this is a passphrase you choose (required for signature generation)
6. Set **permissions**:
   - **Read-Only** — Required for balance and order queries
   - **Spot Trade** — Required for placing and canceling orders
   - Do NOT enable **Withdraw** permission unless absolutely necessary
7. (Recommended) Set **IP whitelist** to restrict API access to your server's IP
8. Complete 2FA verification
9. Save your credentials securely:
   - **API Key** (also called Access Key)
   - **Secret Key**
   - **Memo** (the passphrase you entered)

**Important:** The Secret Key is shown only once. Save it immediately. If lost, you must delete and recreate the API key.

---

## 2. Environment Variables Setup

Add the following to your `.env` file:

```
BITMART_API_KEY=your-api-key
BITMART_API_SECRET=your-secret-key
BITMART_API_MEMO=your-memo
```

**Security:** Never display the full secret key or memo.

---

## 3. Signature Generation

BitMart uses HMAC-SHA256 signatures for authenticated (SIGNED) API calls.

### Signature Formula

```
message   = "{timestamp}#{memo}#{body}"
signature = HMAC-SHA256(secret_key, message) → hex string
```

Where:
- `timestamp` — Current UTC time in milliseconds (e.g., `1709971200000`)
- `memo` — Your API memo string
- `body` — JSON body string for POST requests, or empty string `""` for GET requests
- The `#` character is a literal separator

### Example: POST Request Signature

For a limit buy order:

```bash
# Step 1: Set timestamp
TIMESTAMP=$(date +%s000)
# Example: 1709971200000

# Step 2: Define JSON body
BODY='{"symbol":"BTC_USDT","side":"buy","type":"limit","size":"0.001","price":"60000"}'

# Step 3: Build message string
# Format: {timestamp}#{memo}#{body}
MESSAGE="${TIMESTAMP}#${BITMART_API_MEMO}#${BODY}"
# Example: 1709971200000#my_memo#{"symbol":"BTC_USDT","side":"buy","type":"limit","size":"0.001","price":"60000"}

# Step 4: Generate HMAC-SHA256 signature
SIGN=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "$BITMART_API_SECRET" | awk '{print $NF}')
# Result: a hex string like "a1b2c3d4e5f6..."
```

### Example: GET Request Signature

For GET requests, the body portion is an empty string:

```bash
TIMESTAMP=$(date +%s000)

# For GET: body is empty, so message is "{timestamp}#{memo}#"
MESSAGE="${TIMESTAMP}#${BITMART_API_MEMO}#"

SIGN=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "$BITMART_API_SECRET" | awk '{print $NF}')
```

### Required Headers

| Header | Value | When |
|--------|-------|------|
| `X-BM-KEY` | Your API key | KEYED and SIGNED |
| `X-BM-SIGN` | Hex HMAC-SHA256 signature | SIGNED only |
| `X-BM-TIMESTAMP` | UTC milliseconds timestamp | SIGNED only |
| `Content-Type` | `application/json` | POST requests |

---

## 5. Verification

Test your credentials with a simple balance query.

### KEYED Verification (simplest)

```bash
curl -s -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "User-Agent: bitmart-skills/spot/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  'https://api-cloud.bitmart.com/account/v1/wallet' | python3 -m json.tool
```

Expected successful response:

```json
{
  "code": 1000,
  "message": "OK",
  "trace": "...",
  "data": {
    "wallet": [...]
  }
}
```

### SIGNED Verification (full auth test)

```bash
TIMESTAMP=$(date +%s000)
BODY='{}'
SIGN=$(echo -n "${TIMESTAMP}#${BITMART_API_MEMO}#${BODY}" | openssl dgst -sha256 -hmac "$BITMART_API_SECRET" | awk '{print $NF}')
curl -s -X POST 'https://api-cloud.bitmart.com/spot/v4/query/open-orders' \
  -H "User-Agent: bitmart-skills/spot/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  -H "Content-Type: application/json" \
  -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "X-BM-SIGN: $SIGN" \
  -H "X-BM-TIMESTAMP: $TIMESTAMP" \
  -d "$BODY" | python3 -m json.tool
```

If `code` is `1000`, your credentials are working correctly.

---

## 6. Troubleshooting

| Error Code | Message | Cause | Fix |
|------------|---------|-------|-----|
| 30002 | `X-BM-KEY not found` | API key header missing or empty | Verify `BITMART_API_KEY` env var is set. Check for typos in the header name `X-BM-KEY`. |
| 30005 | `X-BM-SIGN is wrong` | Signature mismatch | 1) Verify memo matches exactly what you set on BitMart. 2) Ensure timestamp is in milliseconds. 3) For POST: body JSON must match exactly (no extra spaces). 4) For GET: body must be empty string. |
| 30006 | `X-BM-TIMESTAMP is wrong` | `X-BM-TIMESTAMP` header missing or empty | Ensure `X-BM-TIMESTAMP` is present on signed requests and uses Unix milliseconds (for example `date +%s000`). |
| 30007 | `Timestamp/recvWindow validation failed` | Timestamp is outside the allowed `recvWindow`, or `recvWindow` is invalid | Sync your system clock, send `X-BM-TIMESTAMP` in Unix milliseconds, and for v4 endpoints ensure `recvWindow` is a Long in `(0,60000]` (default `5000`, max `60000`). Treat `recvWindow` as the effective validity window for current signed v4 requests. |
| 30010 | `IP forbidden` | Request IP not in whitelist | Add your current IP to the API key whitelist on BitMart, or remove IP restriction for testing. |
| 30011 | `No permission` | API key lacks required permission | Enable the required permission (Read-Only, Spot Trade) in API Management. |

### Common Pitfalls

1. **Memo mismatch**: The memo in your signature must match exactly what you entered when creating the API key. It is case-sensitive.

2. **Timestamp drift / recvWindow mismatch**: For signed v4 requests, BitMart validates `X-BM-TIMESTAMP` against `recvWindow` (default `5000ms`, maximum `60000ms`). In practice, use `recvWindow` as the request-validity source of truth. Keep your system clock synchronized, and increase `recvWindow` only when necessary:
   ```bash
   # macOS
   sudo sntp -sS time.apple.com

   # Linux
   sudo ntpdate pool.ntp.org
   ```

3. **Body formatting**: For POST requests, the JSON body used in the signature must be exactly the same string sent in the request. Do not add whitespace or reorder keys between signature generation and the actual request.

4. **Empty body for GET**: When signing GET requests, the body portion of the message is an empty string (not `"{}"`, not `null` — just `""`). The message format is: `{timestamp}#{memo}#`

5. **URL encoding**: Query parameters in GET requests are NOT included in the signature. Only the body is signed.


path: references/bitmart-spot-scenarios.md
content: # BitMart Spot Trading Scenarios

> **Credentials:** Read from `.env` — `BITMART_API_KEY`, `BITMART_API_SECRET`, `BITMART_API_MEMO`.

## Scenario 1: Market Buy

**User prompt:** "Buy 100 USDT worth of BTC on BitMart"

### Step 1: Get current ticker price

```bash
curl -s -H "User-Agent: bitmart-skills/spot/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  'https://api-cloud.bitmart.com/spot/quotation/v3/ticker?symbol=BTC_USDT'
```

Expected response:

```json
{
  "code": 1000,
  "data": {
    "symbol": "BTC_USDT",
    "last": "67123.45",
    "ask_px": "67125.00",
    "bid_px": "67120.00"
  }
}
```

### Step 2: Check available balance

```bash
curl -s -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "User-Agent: bitmart-skills/spot/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  'https://api-cloud.bitmart.com/spot/v1/wallet'
```

Verify the user has at least 100 USDT available. If insufficient, inform the user and STOP.

### Step 3: Present order summary and ask for confirmation

Display to user:
```
Order Summary:
  Action: Market Buy BTC_USDT
  Amount: 100 USDT (notional)
  Estimated Price: ~67,125.00
  Estimated BTC: ~0.00149

Please type CONFIRM to proceed.
```

### Step 4: Submit market buy order (after user confirms)

```bash
TIMESTAMP=$(date +%s000)
BODY='{"symbol":"BTC_USDT","side":"buy","type":"market","notional":"100"}'
SIGN=$(echo -n "${TIMESTAMP}#${BITMART_API_MEMO}#${BODY}" | openssl dgst -sha256 -hmac "$BITMART_API_SECRET" | awk '{print $NF}')
curl -s -X POST 'https://api-cloud.bitmart.com/spot/v2/submit_order' \
  -H "User-Agent: bitmart-skills/spot/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  -H "Content-Type: application/json" \
  -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "X-BM-SIGN: $SIGN" \
  -H "X-BM-TIMESTAMP: $TIMESTAMP" \
  -d "$BODY"
```

Expected response:

```json
{
  "code": 1000,
  "data": {
    "order_id": "12345678901234567"
  }
}
```

### Step 5: Verify fill

```bash
TIMESTAMP=$(date +%s000)
BODY='{"orderId":"12345678901234567"}'
SIGN=$(echo -n "${TIMESTAMP}#${BITMART_API_MEMO}#${BODY}" | openssl dgst -sha256 -hmac "$BITMART_API_SECRET" | awk '{print $NF}')
curl -s -X POST 'https://api-cloud.bitmart.com/spot/v4/query/order' \
  -H "User-Agent: bitmart-skills/spot/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  -H "Content-Type: application/json" \
  -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "X-BM-SIGN: $SIGN" \
  -H "X-BM-TIMESTAMP: $TIMESTAMP" \
  -d "$BODY"
```

Verify `state` is `"filled"`. Report to user: filled size, average price, and total cost.

### Error Handling

- If `code != 1000` on submit: report error message to user, do not retry automatically.
- If `state` is `"failed"`: report failure reason.
- If balance is insufficient: inform user before attempting order.

---

## Scenario 2: Limit Sell

**User prompt:** "Sell 0.5 ETH at 4000 USDT on BitMart"

### Step 1: Get current price to validate limit price

```bash
curl -s -H "User-Agent: bitmart-skills/spot/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  'https://api-cloud.bitmart.com/spot/quotation/v3/ticker?symbol=ETH_USDT'
```

Check that the limit price (4000) is reasonable relative to the current market price. Warn the user if it deviates significantly.

### Step 2: Check ETH balance

```bash
curl -s -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "User-Agent: bitmart-skills/spot/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  'https://api-cloud.bitmart.com/spot/v1/wallet'
```

Verify the user has at least 0.5 ETH available.

### Step 3: Present order summary and ask for confirmation

```
Order Summary:
  Action: Limit Sell ETH_USDT
  Size: 0.5 ETH
  Price: 4,000.00 USDT
  Total: ~2,000.00 USDT (if fully filled)

Please type CONFIRM to proceed.
```

### Step 4: Submit limit sell order

```bash
TIMESTAMP=$(date +%s000)
BODY='{"symbol":"ETH_USDT","side":"sell","type":"limit","size":"0.5","price":"4000"}'
SIGN=$(echo -n "${TIMESTAMP}#${BITMART_API_MEMO}#${BODY}" | openssl dgst -sha256 -hmac "$BITMART_API_SECRET" | awk '{print $NF}')
curl -s -X POST 'https://api-cloud.bitmart.com/spot/v2/submit_order' \
  -H "User-Agent: bitmart-skills/spot/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  -H "Content-Type: application/json" \
  -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "X-BM-SIGN: $SIGN" \
  -H "X-BM-TIMESTAMP: $TIMESTAMP" \
  -d "$BODY"
```

### Step 5: Monitor order status

```bash
TIMESTAMP=$(date +%s000)
BODY='{"orderId":"ORDER_ID"}'
SIGN=$(echo -n "${TIMESTAMP}#${BITMART_API_MEMO}#${BODY}" | openssl dgst -sha256 -hmac "$BITMART_API_SECRET" | awk '{print $NF}')
curl -s -X POST 'https://api-cloud.bitmart.com/spot/v4/query/order' \
  -H "User-Agent: bitmart-skills/spot/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  -H "Content-Type: application/json" \
  -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "X-BM-SIGN: $SIGN" \
  -H "X-BM-TIMESTAMP: $TIMESTAMP" \
  -d "$BODY"
```

Report order status: `new` (pending), `partially_filled` (partially matched), or `filled` (complete).

### Error Handling

- `40044`: Invalid order size — check `GET /spot/v1/symbols/details` for min size.
- Price too far from market: warn user the order may not fill.
- Insufficient balance: inform user before submitting.

---

## Scenario 3: Batch Orders

**User prompt:** "Place buy orders for BTC at 60000, 59000, and 58000, each 0.001 BTC"

### Step 1: Check balance

Verify the user has at least `0.001 * 60000 + 0.001 * 59000 + 0.001 * 58000 = 177 USDT` available.

### Step 2: Present order summary

```
Batch Order Summary:
  1. Limit Buy 0.001 BTC @ 60,000 USDT = 60.00 USDT
  2. Limit Buy 0.001 BTC @ 59,000 USDT = 59.00 USDT
  3. Limit Buy 0.001 BTC @ 58,000 USDT = 58.00 USDT
  Total: 177.00 USDT

Please type CONFIRM to proceed.
```

### Step 3: Submit batch orders

```bash
TIMESTAMP=$(date +%s000)
BODY='{"symbol":"BTC_USDT","orderParams":[{"side":"buy","type":"limit","size":"0.001","price":"60000"},{"side":"buy","type":"limit","size":"0.001","price":"59000"},{"side":"buy","type":"limit","size":"0.001","price":"58000"}]}'
SIGN=$(echo -n "${TIMESTAMP}#${BITMART_API_MEMO}#${BODY}" | openssl dgst -sha256 -hmac "$BITMART_API_SECRET" | awk '{print $NF}')
curl -s -X POST 'https://api-cloud.bitmart.com/spot/v4/batch_orders' \
  -H "User-Agent: bitmart-skills/spot/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  -H "Content-Type: application/json" \
  -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "X-BM-SIGN: $SIGN" \
  -H "X-BM-TIMESTAMP: $TIMESTAMP" \
  -d "$BODY"
```

### Step 4: Query each order

For each returned `order_id`, query its status:

```bash
TIMESTAMP=$(date +%s000)
BODY='{"orderId":"ORDER_ID"}'
SIGN=$(echo -n "${TIMESTAMP}#${BITMART_API_MEMO}#${BODY}" | openssl dgst -sha256 -hmac "$BITMART_API_SECRET" | awk '{print $NF}')
curl -s -X POST 'https://api-cloud.bitmart.com/spot/v4/query/order' \
  -H "User-Agent: bitmart-skills/spot/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  -H "Content-Type: application/json" \
  -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "X-BM-SIGN: $SIGN" \
  -H "X-BM-TIMESTAMP: $TIMESTAMP" \
  -d "$BODY"
```

Report the status of all three orders.

### Error Handling

- If some orders in the batch fail while others succeed, report each individually.
- Partial failures do not affect successful orders.

---

## Scenario 4: Cancel and Replace

**User prompt:** "Change my BTC limit buy from 60000 to 59500"

### Step 1: Query open orders to find the target order

```bash
TIMESTAMP=$(date +%s000)
BODY='{"symbol":"BTC_USDT"}'
SIGN=$(echo -n "${TIMESTAMP}#${BITMART_API_MEMO}#${BODY}" | openssl dgst -sha256 -hmac "$BITMART_API_SECRET" | awk '{print $NF}')
curl -s -X POST 'https://api-cloud.bitmart.com/spot/v4/query/open-orders' \
  -H "User-Agent: bitmart-skills/spot/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  -H "Content-Type: application/json" \
  -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "X-BM-SIGN: $SIGN" \
  -H "X-BM-TIMESTAMP: $TIMESTAMP" \
  -d "$BODY"
```

Find the order with `price: "60000"` and `side: "buy"`. If multiple matches, ask the user to specify.

### Step 2: Cancel the existing order

```bash
TIMESTAMP=$(date +%s000)
BODY='{"symbol":"BTC_USDT","order_id":"FOUND_ORDER_ID"}'
SIGN=$(echo -n "${TIMESTAMP}#${BITMART_API_MEMO}#${BODY}" | openssl dgst -sha256 -hmac "$BITMART_API_SECRET" | awk '{print $NF}')
curl -s -X POST 'https://api-cloud.bitmart.com/spot/v3/cancel_order' \
  -H "User-Agent: bitmart-skills/spot/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  -H "Content-Type: application/json" \
  -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "X-BM-SIGN: $SIGN" \
  -H "X-BM-TIMESTAMP: $TIMESTAMP" \
  -d "$BODY"
```

### Step 3: Submit new order at updated price

Present the replacement order for confirmation:

```
Replace Order:
  Cancel: Buy 0.001 BTC @ 60,000 (order: FOUND_ORDER_ID)
  New: Buy 0.001 BTC @ 59,500

Please type CONFIRM to proceed.
```

After confirmation:

```bash
TIMESTAMP=$(date +%s000)
BODY='{"symbol":"BTC_USDT","side":"buy","type":"limit","size":"0.001","price":"59500"}'
SIGN=$(echo -n "${TIMESTAMP}#${BITMART_API_MEMO}#${BODY}" | openssl dgst -sha256 -hmac "$BITMART_API_SECRET" | awk '{print $NF}')
curl -s -X POST 'https://api-cloud.bitmart.com/spot/v2/submit_order' \
  -H "User-Agent: bitmart-skills/spot/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  -H "Content-Type: application/json" \
  -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "X-BM-SIGN: $SIGN" \
  -H "X-BM-TIMESTAMP: $TIMESTAMP" \
  -d "$BODY"
```

### Step 4: Verify new order

Query the new order to confirm it was placed successfully.

### Error Handling

- If cancel fails (order already filled): inform user the order has been filled and cannot be modified.
- If the original order was partially filled: report the filled portion and place the new order for the remaining size.
- Always cancel before placing the new order to avoid double exposure.

---

## Scenario 5: Portfolio Check

**User prompt:** "Show me my BitMart spot balance"

### Step 1: Get all balances with USD valuation

```bash
curl -s -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "User-Agent: bitmart-skills/spot/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  'https://api-cloud.bitmart.com/account/v1/wallet?needUsdValuation=true'
```

### Step 2: Filter and format

From the response, filter out currencies with zero balances (`available == "0"` and `frozen == "0"`).

### Step 3: Present portfolio summary

```
BitMart Spot Portfolio:
┌──────────┬───────────────┬────────────┬────────────────┐
│ Currency │ Available     │ Frozen     │ USD Value      │
├──────────┼───────────────┼────────────┼────────────────┤
│ BTC      │ 1.23456789    │ 0.10000000 │ $82,934.56     │
│ ETH      │ 15.50000000   │ 0.00000000 │ $53,580.90     │
│ USDT     │ 50,000.00     │ 1,000.00   │ $50,000.00     │
└──────────┴───────────────┴────────────┴────────────────┘
Total Portfolio Value: ~$186,515.46

Note: Not financial advice. Values are approximate based on current market prices.
```

### Error Handling

- If `code == 30002`: API key not configured. Guide user to set up credentials.
- If response contains no wallets: inform user the account has no assets.

---

## Scenario 6: Margin Trade

**User prompt:** "Borrow 1000 USDT on margin and buy ETH on BitMart"

### Step 1: Check margin account

```bash
curl -s -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "User-Agent: bitmart-skills/spot/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  'https://api-cloud.bitmart.com/spot/v1/margin/isolated/account?symbol=ETH_USDT'
```

Verify the margin account is enabled and check available borrowing limit.

### Step 2: Borrow assets

Present borrow details for confirmation:

```
Margin Borrow:
  Pair: ETH_USDT (isolated)
  Borrow: 1,000 USDT

Please type CONFIRM to proceed.
```

After confirmation:

```bash
TIMESTAMP=$(date +%s000)
BODY='{"symbol":"ETH_USDT","currency":"USDT","amount":"1000"}'
SIGN=$(echo -n "${TIMESTAMP}#${BITMART_API_MEMO}#${BODY}" | openssl dgst -sha256 -hmac "$BITMART_API_SECRET" | awk '{print $NF}')
curl -s -X POST 'https://api-cloud.bitmart.com/spot/v1/margin/isolated/borrow' \
  -H "User-Agent: bitmart-skills/spot/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  -H "Content-Type: application/json" \
  -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "X-BM-SIGN: $SIGN" \
  -H "X-BM-TIMESTAMP: $TIMESTAMP" \
  -d "$BODY"
```

### Step 3: Place margin order

```bash
TIMESTAMP=$(date +%s000)
BODY='{"symbol":"ETH_USDT","side":"buy","type":"market","notional":"1000"}'
SIGN=$(echo -n "${TIMESTAMP}#${BITMART_API_MEMO}#${BODY}" | openssl dgst -sha256 -hmac "$BITMART_API_SECRET" | awk '{print $NF}')
curl -s -X POST 'https://api-cloud.bitmart.com/spot/v1/margin/submit_order' \
  -H "User-Agent: bitmart-skills/spot/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  -H "Content-Type: application/json" \
  -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "X-BM-SIGN: $SIGN" \
  -H "X-BM-TIMESTAMP: $TIMESTAMP" \
  -d "$BODY"
```

### Step 4: Monitor and repay

After the user is ready to close the margin position and repay:

```bash
TIMESTAMP=$(date +%s000)
BODY='{"symbol":"ETH_USDT","currency":"USDT","amount":"1000"}'
SIGN=$(echo -n "${TIMESTAMP}#${BITMART_API_MEMO}#${BODY}" | openssl dgst -sha256 -hmac "$BITMART_API_SECRET" | awk '{print $NF}')
curl -s -X POST 'https://api-cloud.bitmart.com/spot/v1/margin/isolated/repay' \
  -H "User-Agent: bitmart-skills/spot/v2026.3.23" \
  -H "X-BM-BROKER-ID: BlaveData666666" \
  -H "Content-Type: application/json" \
  -H "X-BM-KEY: $BITMART_API_KEY" \
  -H "X-BM-SIGN: $SIGN" \
  -H "X-BM-TIMESTAMP: $TIMESTAMP" \
  -d "$BODY"
```

### Error Handling

- If margin account not enabled: guide user to enable isolated margin for the pair on BitMart.
- If borrow limit exceeded: inform user of maximum borrowable amount.
- Always remind user about interest costs and liquidation risk for margin trading.
- Margin trading carries higher risk — always include appropriate warnings.


path: references/bitmart-spot-skill.md
content: # BitMart Spot Trading

**Base URL:** `https://api-cloud.bitmart.com` | **Symbol:** `BTC_USDT` (underscore) | **Success:** `code == 1000`

34 endpoints — full details in `references/bitmart-spot-api-reference.md`

## Authentication

Same signature method as Futures. Credentials from `.env`: `BITMART_API_KEY`, `BITMART_API_SECRET`, `BITMART_API_MEMO`

No BitMart account? Register at **[https://www.bitmart.com/invite/cMEArf](https://www.bitmart.com/invite/cMEArf)**

**Always include `X-BM-BROKER-ID: BlaveData666666` on ALL requests.**

**IP Whitelist:** Use **public IP** (`curl https://checkip.amazonaws.com`), not private IP.

> Signature Python implementation: `references/bitmart-signature.md`

## Operation Flow

### Step 0: Credential Check

Verify credentials. If missing — **STOP**.

### Step 1: Identify Intent

- **READ:** market data, balance, order history
- **WRITE:** submit/cancel orders, withdraw
- **TRANSFER:** spot ↔ futures → see Part 2 **Spot ↔ Futures Transfer**

### Step 2: Execute Orders

- READ → call, parse, display
- WRITE → present summary → ask **"CONFIRM"** → execute

**Endpoint:** `POST /spot/v2/submit_order`

| Scenario     | side   | type     | Key param                   |
| ------------ | ------ | -------- | --------------------------- |
| Buy, market  | `buy`  | `market` | `notional` (USDT to spend)  |
| Buy, limit   | `buy`  | `limit`  | `size` (base qty) + `price` |
| Sell, market | `sell` | `market` | `size` (base qty)           |
| Sell, limit  | `sell` | `limit`  | `size` + `price`            |

> Market buy uses `notional`, NOT `size`.

### Step 3: Verify

After order → query order detail. After cancel → check open orders.

## Order Reference

**Side:** `buy` / `sell` | **Type:** `limit` / `market` / `limit_maker` / `ioc`

**Status:** `new` / `partially_filled` / `filled` / `canceled` / `partially_canceled`

**Timestamps:** ms — always convert to local time.

## Error Handling

| Code               | Action                                                  |
| ------------------ | ------------------------------------------------------- |
| 30005              | Wrong signature → see `references/bitmart-signature.md` |
| 30007              | Timestamp drift → sync clock                            |
| 50000              | Insufficient balance                                    |
| 429                | Rate limited → wait                                     |
| 403/503 Cloudflare | Wait 30-60s, retry max 3×                               |

## Security

- WRITE operations require **"CONFIRM"**
- "Not financial advice. Spot trading carries risk of loss."

## References

- `references/bitmart-spot-api-reference.md` — 34 endpoints
- `references/bitmart-signature.md` — Python signature implementation
- `references/bitmart-spot-authentication.md` / `bitmart-spot-scenarios.md`

---


path: references/blave-api.md
content: # Blave API Examples

Full Python examples for all Blave API endpoints.

## Setup

```python
import requests, os
from dotenv import load_dotenv
load_dotenv()

headers = {
    "api-key": os.getenv("blave_api_key"),
    "secret-key": os.getenv("blave_secret_key"),
}
BASE_URL = "https://api.blave.org"
```

---

## Price

```python
params = {"symbol": "BTCUSDT"}
response = requests.get(f"{BASE_URL}/price", headers=headers, params=params, timeout=60)
print(response.json())
# {"symbol": "BTCUSDT", "price": 95000.0, "change_24h": 2.5}
```

---

## Alpha Table

```python
response = requests.get(f"{BASE_URL}/alpha_table", headers=headers, timeout=60)
print(response.json())
```

---

## Kline

```python
params = {"symbol": "BTCUSDT", "period": "1h", "start_date": "2025-01-01", "end_date": "2025-03-01"}
response = requests.get(f"{BASE_URL}/kline", headers=headers, params=params, timeout=60)
print(response.json())
```

---

## Market Direction

```python
params = {"period": "1h", "start_date": "2025-01-01", "end_date": "2025-03-01"}
response = requests.get(f"{BASE_URL}/market_direction/get_alpha", headers=headers, params=params, timeout=60)
print(response.json())
```

---

## Market Sentiment

```python
# Get symbols
response = requests.get(f"{BASE_URL}/market_sentiment/get_symbols", headers=headers, timeout=60)

# Get alpha
params = {"symbol": "BTCUSDT", "period": "1h", "start_date": "2025-01-01", "end_date": "2025-03-01"}
response = requests.get(f"{BASE_URL}/market_sentiment/get_alpha", headers=headers, params=params, timeout=60)
print(response.json())
```

---

## Capital Shortage

```python
params = {"period": "1h", "start_date": "2025-01-01", "end_date": "2025-03-01"}
response = requests.get(f"{BASE_URL}/capital_shortage/get_alpha", headers=headers, params=params, timeout=60)
print(response.json())
```

---

## Holder Concentration

```python
# Get symbols
response = requests.get(f"{BASE_URL}/holder_concentration/get_symbols", headers=headers, timeout=60)

# Get alpha
params = {"symbol": "BTCUSDT", "period": "1h", "start_date": "2025-01-01", "end_date": "2025-03-01"}
response = requests.get(f"{BASE_URL}/holder_concentration/get_alpha", headers=headers, params=params, timeout=60)
print(response.json())
```

---

## Taker Intensity

```python
# Get symbols
response = requests.get(f"{BASE_URL}/taker_intensity/get_symbols", headers=headers, timeout=60)

# Get alpha
params = {"symbol": "BTCUSDT", "period": "1h", "timeframe": "24h", "start_date": "2025-01-01", "end_date": "2025-03-01"}
response = requests.get(f"{BASE_URL}/taker_intensity/get_alpha", headers=headers, params=params, timeout=60)
print(response.json())
```

---

## Whale Hunter

```python
# Get symbols
response = requests.get(f"{BASE_URL}/whale_hunter/get_symbols", headers=headers, timeout=60)

# Get alpha
params = {"symbol": "BTCUSDT", "period": "1h", "timeframe": "24h", "score_type": "score_oi"}
response = requests.get(f"{BASE_URL}/whale_hunter/get_alpha", headers=headers, params=params, timeout=60)
print(response.json())
```

---

## Squeeze Momentum

```python
# Get symbols
response = requests.get(f"{BASE_URL}/squeeze_momentum/get_symbols", headers=headers, timeout=60)

# Get alpha (period fixed to 1d)
params = {"symbol": "BTCUSDT", "start_date": "2025-01-01", "end_date": "2025-03-01"}
response = requests.get(f"{BASE_URL}/squeeze_momentum/get_alpha", headers=headers, params=params, timeout=60)
print(response.json())
```

---

## Sector Rotation

```python
response = requests.get(f"{BASE_URL}/sector_rotation/get_history_data", headers=headers, timeout=60)
print(response.json())
```

---

## Blave Top Trader Exposure

```python
params = {"period": "1h", "start_date": "2025-01-01", "end_date": "2025-03-01"}
response = requests.get(f"{BASE_URL}/blave_top_trader/get_exposure", headers=headers, params=params, timeout=60)
print(response.json())
```

---

## alpha_table Field Reference

Each symbol in `/alpha_table` contains:

| Field | Description |
|---|---|
| `statistics` | `up_prob` (prob of 24h upward move), `exp_value` (expected return), `avg_up_return`, `avg_down_return`, `return_ratio`, `is_data_sufficient` |
| `price` | `{"-": 70000}` — current price |
| `price_change` | `{"15min": ..., "1h": ..., "24h": ...}` — % change |
| `market_cap` | `{"-": 1234567890}` — USD market cap |
| `market_cap_percentile` | `{"-": 85.3}` — percentile among all listed coins |
| `funding_rate` | `{"binance": -0.01, ...}` — per exchange |
| `oi_imbalance` | `{"-": 0.12}` — OI imbalance |

`fields` = indicator metadata. `note` = color ranges. `""` = insufficient data.

Use `statistics.up_prob` and `statistics.exp_value` for screening. Always check `is_data_sufficient` before using `statistics`.


path: references/bybit-skill.md
content: # Bybit Trading

**Base URL (Mainnet):** `https://api.bybit.com` | **Backup:** `https://api.bytick.com` | **Testnet:** `https://api-testnet.bybit.com`

**Spot:** `BTCUSDT` | **Perpetual:** `BTCUSDT` (Linear) | **Success:** `"retCode": 0`

## Authentication

**Credentials** (from `.env`): `BYBIT_API_KEY`, `BYBIT_API_SECRET`

No Bybit account? Register at **[https://partner.bybit.com/b/BLAVE](https://partner.bybit.com/b/BLAVE)**

Verify credentials before any private call. If missing — **STOP**.

**Signature:** `HMAC-SHA256(secret, {timestamp}{apiKey}{recvWindow}{queryString|jsonBody})`
- GET: sign `{timestamp}{apiKey}{recvWindow}{queryString}`
- POST: sign `{timestamp}{apiKey}{recvWindow}{jsonBody}` — use **compact JSON** (no spaces, no newlines)

**Headers (all authenticated requests):**
```
X-BAPI-[REDACTED_SECRET]
X-BAPI-TIMESTAMP: <unix ms>
X-BAPI-SIGN: <hmac signature>
X-BAPI-RECV-WINDOW: 5000
referer: Ue001036
Content-Type: application/json   (POST only)
```

**`referer: Ue001036` is MANDATORY on every request — no exceptions.**

## Operation Flow

### Step 0: Credential Check
Verify `BYBIT_API_KEY`, `BYBIT_API_SECRET`. If missing — **STOP**. Default to **Mainnet** unless user explicitly requests Testnet.

### Step 1: Pre-Trade Check
`GET /v5/position/list?category=linear&symbol=<SYMBOL>` → if position exists, inherit side and leverage.

### Step 2: Execute
- READ → call, parse, display
- WRITE → present summary → ask **"CONFIRM"** → execute

### Step 3: Verify
After order → `GET /v5/order/realtime` → confirm status. After close → `GET /v5/position/list`.

## Key Endpoints

| Action | Method | Path |
|---|---|---|
| Market info | GET | `/v5/market/instruments-info` |
| Ticker | GET | `/v5/market/tickers` |
| Wallet balance | GET | `/v5/account/wallet-balance` |
| Place order | POST | `/v5/order/create` |
| Cancel order | POST | `/v5/order/cancel` |
| Open orders | GET | `/v5/order/realtime` |
| Positions | GET | `/v5/position/list` |
| Set leverage | POST | `/v5/position/set-leverage` |
| Set TP/SL | POST | `/v5/position/set-tpsl` |
| Order history | GET | `/v5/order/history` |

## Security
- WRITE operations require **"CONFIRM"**
- Always show liquidation price before opening leveraged positions
- "Not financial advice. Trading carries significant risk of loss."

---


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)