
Aicoin Trading
- 463 installs
- 51 repo stars
- Updated June 9, 2026
- aicoincom/coinos-skills
aicoin-trading is a Claude Code skill that runs disciplined crypto trading workflows—signals, position sizing, execution checks, and risk guardrails—for developers operating live or paper strategies with agents.
About
aicoin-trading is an agent-oriented skill for crypto trading workflows on the CoinOS skills stack. It structures how coding agents handle signal generation, position sizing, pre-trade execution checks, and risk guardrails for both live and paper strategies. Developers reach for aicoin-trading when automating or supervising algorithmic crypto trades and need repeatable guardrails instead of ad hoc prompts. The skill emphasizes disciplined execution patterns suitable for agent-operated trading loops rather than one-off market guesses.
- signal-to-execution workflow
- position sizing guardrails
- risk and slippage checks
- paper vs live trading modes
- exchange API execution patterns
Aicoin Trading by the numbers
- 463 all-time installs (skills.sh)
- Ranked #221 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/aicoincom/coinos-skills --skill aicoin-tradingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 463 |
|---|---|
| repo stars | ★ 51 |
| Last updated | June 9, 2026 |
| Repository | aicoincom/coinos-skills ↗ |
How do agents run disciplined crypto trading workflows?
Run disciplined crypto trading workflows—signals, sizing, execution checks, and risk guardrails—for live or paper strategies operated by agents.
Who is it for?
Developers building or operating agent-supervised crypto strategies who need structured signals, sizing, and risk checks on live or paper trading.
Skip if: Developers seeking general portfolio advice, non-crypto markets, or discretionary trading without automated guardrails.
When should I use this skill?
A developer asks to automate crypto signals, size positions, validate executions, or enforce trading risk limits with an agent.
What you get
Validated trade signals, sized orders, execution check results, and enforced risk guardrail decisions.
- Trade signal evaluation
- Sized order plan
- Risk guardrail report
Files
运行脚本: 从 SKILL.md 所在目录运行node scripts/exchange.mjs <action>. 三引擎(OpenClaw / Hermes / Claude Code)容器自动加载 skill, 直接cd到 skill 目录即可.
AiCoin Trading — 下单专用
⛔ 铁律(违反任何一条都是严重错误)
1. 禁止写代码下单。 不准写 import ccxt、new ccxt.okx()、fetch("https://...") 或任何自定义代码来下单。所有订单只能通过 node scripts/exchange.mjs create_order 执行。 2. 禁止自动确认。 create_order / close_position 第一次调用返回预览(含风险提示),你必须把预览完整展示给用户,等用户回复"确认"或"yes"后,才能第二次调用加 "confirmed":"true" 执行。 3. 禁止修改用户参数。 余额不够就告诉用户,不准自动调整数量或杠杆。 4. 禁止主动平仓。 除非用户明确要求。 5. 平仓必须用 `close_position`。 禁止用 create_order 构建平仓单(容易开反向单)。 6. 杠杆 / 保证金模式改动必须先确认。 set_trading_params 和 set_leverage 不是只读操作 — 它们改交易所账户的合约配置,直接影响后续所有订单的保证金占用、爆仓价、强平距离。100x 杠杆和 5x 杠杆的爆仓距离差 20 倍,用户没明确说改之前不准 silent set。调用前必须:用自然语言告诉用户你准备把哪个交易所、哪个交易对的杠杆 / margin_mode 从什么改成什么、影响是什么,等用户回复"确认"或"yes"才能执行。
反例 ❌:用户说"开 100x 多 BTC",你不反问杠杆是不是写错了直接 set_trading_params leverage=100 然后下单 — 用户可能是口误想说 10x,100x 直接 silent 设了风险极高。正确 ✅:先回"100x 杠杆爆仓距离只有约 0.95%(不算手续费),BTC 一根 5 分钟 K 线就能扫掉。确认是 100x 还是想说 10x?",等用户明确回答再 set。
下单流程(两步,不可跳过)
步骤1: node scripts/exchange.mjs create_order '{"exchange":"okx","symbol":"BTC/USDT:USDT","type":"market","side":"buy","amount":1,"market_type":"swap"}'
→ 返回预览(交易对、方向、数量、价格、杠杆、保证金、风险提示)
→ 你必须把所有字段展示给用户
步骤2: 用户确认后
node scripts/exchange.mjs create_order '{"exchange":"okx","symbol":"BTC/USDT:USDT","type":"market","side":"buy","amount":1,"market_type":"swap","confirmed":"true"}'
→ 实际下单平仓流程(两步,不可跳过)
平仓必须用 `close_position`,禁止用 `create_order` 手动构建平仓单(容易开反向单)。
步骤1: node scripts/exchange.mjs close_position '{"exchange":"okx","market_type":"swap"}'
→ 返回所有持仓预览(交易对、方向、张数、盈亏)
→ 展示给用户
步骤2: 用户确认后
node scripts/exchange.mjs close_position '{"exchange":"okx","market_type":"swap","confirmed":"true"}'
→ 市价平掉所有持仓(自动 reduceOnly)
步骤3: 执行后必须验证 + 总结(不可省略)
node scripts/exchange.mjs positions '{"exchange":"okx","market_type":"swap"}'
→ 确认仓位已清空,然后用一句话告诉用户结果(平了什么、盈亏多少)指定交易对只平部分:加 "symbol":"BTC/USDT:USDT"
为什么有步骤3: close_position 的返回有时被 streaming 截断,用户看不到结果。多查一次 positions 既能确认平仓成功,又能把结论写进最终消息让用户看到。
止盈止损 / 条件单流程(两步,不可跳过)
给已有仓位挂止损 / 止盈,必须用 `set_stop`,禁止手搓 `create_order` 的 `STOP_MARKET`+原始 params。set_stop 会从交易所真实持仓自动推导方向、reduceOnly / posSide / positionSide(币安双向持仓、OKX、Bybit 各自适配),并校验触发价在现价正确一侧——手搓极易开成反向单或方向搞反。
步骤1: node scripts/exchange.mjs set_stop '{"exchange":"binance","symbol":"HYPE/USDT:USDT","market_type":"swap","stop_loss":63.5,"take_profit":66.5}'
→ 返回预览(持仓方向、触发价、触发后动作、平仓模式、方向校验结果)
→ 你必须把预览展示给用户
步骤2: 用户确认后
node scripts/exchange.mjs set_stop '{"exchange":"binance","symbol":"HYPE/USDT:USDT","market_type":"swap","stop_loss":63.5,"take_profit":66.5,"confirmed":"true"}'
→ 实际挂条件单
步骤3: 执行后复核(不可省略)
node scripts/exchange.mjs stop_orders '{"exchange":"binance","symbol":"HYPE/USDT:USDT","market_type":"swap"}'
→ 确认条件单已挂上,再把结论告诉用户- 参数:
stop_loss(止损触发价)/take_profit(止盈触发价)至少给一个,可同时给;只给单一trigger_price会按方向自动归类为止损或止盈。amount可选,默认全仓,超过持仓自动 clamp。side(long/short):当同一交易对同时持有多空两个仓(双向持仓)时必须指定,否则set_stop会报错让你选边,绝不替你猜。 - 多单:止损 < 现价、止盈 > 现价;空单反之。设反会被
set_stop拦下报错(防瞬间触发)。拿不到当前价时默认中止(无法校验方向),确需跳过校验可传"force":true自负风险。 - 双向持仓(hedge mode)全自动适配:开仓自动补方向参数(币安
positionSide、OKXposSide、BybitpositionIdx),止损/止盈/平仓从真实持仓推导方向 —— 币安/OKX/Bybit/Bitget/HTX 双向账户都无需手动指定。其中 Bitget/HTX 平仓由 ccxt 翻成tradeSide:Close/offset:close(纯reduceOnly在它们的双向模式会被当反向开仓,已专门处理);Bitget 持仓模式万一识别不出会中止并报错而非冒险反向开仓。set_stop挂的止损/止盈是交易所条件/算法单(stop_orders可查),平仓后用cancel_order可一并清掉残留条件单。 - 确认执行前
set_stop会再读一次持仓把数量校准到当前仓位;若确认期间仓位已被平掉/反手,会直接报告“持仓已不存在”而不挂废单。 - 为什么有步骤3:条件单在部分交易所(OKX 等)属算法/委托单,不出现在普通 `open_orders` 列表,只能用
stop_orders或交易所 APP 的「条件委托」栏查到。别用open_orders误判“没挂上”。 - 兜底:ccxt 跨所条件单细节有差异(币安触发用标记价/最新价、OKX algo 单等),若
set_stop某条返回失败,如实告诉用户失败原因,并建议去交易所 APP 手动挂——不要谎报已挂上。 - 条件单支持矩阵(ccxt 4.5.47 实测):止损/止盈/触发单 在 Binance / OKX / Bybit / Bitget / Gate / HTX 6 家 CEX 全部支持(各映射到该所 native 条件单)。下单前有安全网:若触发价被某所静默丢弃(会变成立即成交的市价单),
placeOrder会直接拒绝而非误下单。每次升级 ccxt 后跑npm run verify-orders(请求体黄金矩阵)确认没漂移。Hyperliquid 走 trigger、属边角(USDC,多路由到 aicoin-onchain),以实盘为准。
下单前准备
| 步骤 | 命令 | 是否需要确认 |
|---|---|---|
| 设置杠杆+保证金模式 | node scripts/exchange.mjs set_trading_params '{"exchange":"okx","symbol":"BTC/USDT:USDT","leverage":10,"margin_mode":"isolated","market_type":"swap"}' | 需要(见铁律 #6) |
| 单独设杠杆 | node scripts/exchange.mjs set_leverage '{"exchange":"okx","symbol":"BTC/USDT:USDT","leverage":10,"market_type":"swap"}' | 需要(见铁律 #6) |
| 查合约信息 | node scripts/exchange.mjs markets '{"exchange":"okx","market_type":"swap","base":"BTC"}' | 不需要(只读) |
杠杆 / 保证金确认模板(直接照抄换数字):
"我准备把 OKX BTC/USDT 永续杠杆改为 {N}x,margin_mode = {isolated/cross}。这会影响后续这个交易对所有订单的保证金占用和爆仓距离({N}x 杠杆爆仓约 {1/N*100}% 不计手续费)。确认改吗?"
确认后再实际调 set_trading_params / set_leverage。如果用户说"算了"、"先别"、"我再想想",不要调脚本。
其他命令
| 操作 | 命令 |
|---|---|
| 平仓(全部或指定) | node scripts/exchange.mjs close_position '{"exchange":"okx","market_type":"swap"}' — 加 "symbol":"BTC/USDT:USDT" 只平单个 |
| 止盈止损(给已有仓位挂保护单) | node scripts/exchange.mjs set_stop '{"exchange":"binance","symbol":"HYPE/USDT:USDT","market_type":"swap","stop_loss":63.5,"take_profit":66.5}' — 两步确认,见上方「止盈止损流程」 |
| 查条件单/算法委托 | node scripts/exchange.mjs stop_orders '{"exchange":"binance","symbol":"HYPE/USDT:USDT","market_type":"swap"}' — 普通 open_orders 查不到的条件单用这个 |
| 取消订单 | node scripts/exchange.mjs cancel_order '{"exchange":"okx","symbol":"BTC/USDT","order_id":"xxx"}' — 也能取消条件单 |
| 存交易所 key(本地,用户在 chat 给了 key 时) | node scripts/exchange.mjs save_key '{"exchange":"binance","api_key":"...","api_secret":"..."}' — 写进 ~/.coinos/.env、chmod 600、不回显 secret(OKX/Bitget 还要 "password":"...")。容器内引导用户去 web UI EnvSection,别在 chat 收 key |
数量
合约自动换算: amount 一律是用户说的币数量(如 0.01 BTC、1000 DOGE),脚本统一按 amount / contractSize 自动换算成张数 —— 整数也按币数量算(旧版"整数=张数"的约定在 OKX/Gate 等 contractSize≠1 的所会把张数算错几个数量级,已废除)。确实要直接传张数时加 "amount_unit":"contracts"。 用 USDT 金额下单: 当用户说"用10U做多"或"花10 USDT开仓",传 cost=10(合约=USDT保证金金额,按价格+杠杆算张数;现货市价买入=花多少 USDT,按现价反算币数量),不要传 amount。 现货: amount = 币数量;或用 cost 按 USDT 金额买入。
格式: 现货 BTC/USDT,合约 BTC/USDT:USDT,Hyperliquid 用 USDC: BTC/USDC:USDC。
交易所: Binance, OKX, Bybit, Bitget, Gate.io, HTX, Pionex, Hyperliquid。
#!/usr/bin/env node
// Generic CLI dispatcher — parse `<action> [json-params]` from argv and run a handler.
export function cli(handlers) {
const [action, ...rest] = process.argv.slice(2);
if (!action || !handlers[action]) {
const available = Object.keys(handlers).join(', ');
console.log(JSON.stringify({
error: action ? `Unknown action "${action}"` : 'No action specified',
available_actions: available,
usage: 'node <script> <action> [json-params]',
}));
process.exit(1);
}
let params = {};
if (rest.length) {
const raw = rest.join(' ');
try {
params = JSON.parse(raw);
} catch {
console.log(JSON.stringify({
error: `Invalid JSON parameter: ${raw}`,
hint: 'Parameters must be a JSON object, e.g.: \'{"symbol":"BTC","interval":"1h"}\'',
example: `node <script> ${action} '{"key":"value"}'`,
}));
process.exit(1);
}
}
handlers[action](params).then(r => console.log(JSON.stringify(r, null, 2))).catch(e => {
// 运行时错误也输出结构化 JSON(与上面 dispatcher 的报错一致),agent 才能稳定解析、转述给用户;
// 早先只 console.error(纯文本) 会让 agent 把交易所/网络错误当成非预期输出。
console.log(JSON.stringify({ error: e?.message || String(e) }));
process.exit(1);
});
}
{
"comment": "Public free-tier AiCoin API key. IP rate-limited. Users can replace with their own key via env vars.",
"accessKeyId": "ronJ8uI0Yj2soAfGVs5H1YALUIINbE22",
"accessSecret": "CWHZcH2us1CLSE7grroR1TpS0Z1JxTwU"
}
// Shared .env auto-loader for coinos-skills.
// 各 skill 自包含 → 本文件在每个 skill 的 lib/ 下保留一份**字节相同**的副本,
// 由 scripts/validate-skills.mjs 的 drift guard 强制一致(改一处必须同步全部)。
//
// key 的规范存放位置(coinos 文件夹),不再靠"向上爬目录找 .env"的启发式:
// - macOS / Linux: ~/.coinos/.env
// - Windows: %USERPROFILE%\.coinos\.env
// - CoinClaw 容器: /workspace/.env (产品 web UI EnvSection → entrypoint 注入, 保留)
// 另外也读: 当前目录 .env(临时/项目本地)+ 旧引擎位置(~/.openclaw 等, 向后兼容, 最低优先级)。
//
// 规则: 候选按下面顺序, 同一个 key 先命中者生效; 已注入的 env(process.env)永远优先
// (if (!process.env[k]) 守卫)。所以把 key 放进 ~/.coinos/.env 后, 旧的 ~/.openclaw
// 免费 key 不会再抢 —— 它排在后面, 对应的 key 已经先被填上了。
import { readFileSync, existsSync } from 'node:fs';
import { resolve, join } from 'node:path';
const HOME = process.env.HOME || process.env.USERPROFILE || '';
// CoinClaw 容器 sentinel → 产品注入的 /workspace/.env。
function containerEnvFile() {
if (existsSync('/workspace/.hermes') || existsSync('/workspace/.claude')) return '/workspace/.env';
if (existsSync('/home/node/.openclaw')) return '/home/node/.openclaw/workspace/.env';
return null;
}
// coinos 规范配置文件 —— 跨平台 ~/.coinos/.env(Windows: %USERPROFILE%\.coinos\.env)。
export function coinosEnvFile() {
return HOME ? join(HOME, '.coinos', '.env') : null;
}
// 候选 .env 路径(有序;同一个 key 先命中者生效,且注入 env 永远优先)。
export function envCandidates() {
const list = [];
const container = containerEnvFile();
if (container) list.push(container); // 1. 容器: 产品注入位置
const coinos = coinosEnvFile();
if (coinos) list.push(coinos); // 2. ~/.coinos/.env —— 规范位置
list.push(resolve(process.cwd(), '.env')); // 3. 当前目录(临时/项目本地)
if (HOME) { // 4. 旧引擎位置, 向后兼容(最低优先级)
list.push(resolve(HOME, '.openclaw', 'workspace', '.env'));
list.push(resolve(HOME, '.openclaw', '.env'));
list.push(resolve(HOME, '.hermes', '.env'));
}
return [...new Set(list)];
}
// 把候选 .env 载入 process.env,不覆盖已注入的变量。
export function loadEnv() {
for (const envFile of envCandidates()) {
try {
for (const line of readFileSync(envFile, 'utf-8').split('\n')) {
const t = line.trim();
if (!t || t.startsWith('#')) continue;
const eq = t.indexOf('=');
if (eq < 1) continue;
const k = t.slice(0, eq).trim();
let v = t.slice(eq + 1).trim();
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
if (!process.env[k]) process.env[k] = v;
}
} catch { /* 文件不存在或不可读,跳过 */ }
}
}
// saveKey 应写入的 .env 路径 —— 规范位置 ~/.coinos/.env(容器内写 /workspace/.env)。
// 调用方写入前需 mkdir -p 父目录(~/.coinos 可能还不存在)。
export function writeEnvPath() {
const container = containerEnvFile();
if (container) return container;
const coinos = coinosEnvFile();
if (coinos) return coinos;
return resolve(process.cwd(), '.env');
}
{
"name": "aicoin-trading",
"version": "3.7.3",
"private": true,
"type": "module",
"optionalDependencies": {
"ccxt": "^4.4.0"
}
}
#!/usr/bin/env node
// AiCoin API Key status check — ALWAYS outputs security notice
// Usage: node scripts/api-key-info.mjs [check]
// When user asks about configuring/checking AiCoin API key, run this script.
import { loadEnv, writeEnvPath, envCandidates } from '../lib/env-loader.mjs';
import { readFileSync } from 'node:fs';
// .env auto-load (宿主可能不向子进程注入 env)。共享 loader,见 lib/env-loader.mjs。
loadEnv();
function findKey() {
const val = process.env.AICOIN_ACCESS_KEY_ID?.trim();
if (!val) return { found: false };
// 找出这把 key 真正来自哪个候选文件(扫描顺序与 loadEnv 一致,首个命中即来源);
// 都没命中 = 来自注入的环境变量。汇报真实位置,而不是"建议写入位置"。
let source = null;
for (const f of envCandidates()) {
try {
if (readFileSync(f, 'utf-8').split('\n').some((l) => l.trim().startsWith('AICOIN_ACCESS_KEY_ID='))) { source = f; break; }
} catch { /* 文件不存在或不可读,跳过 */ }
}
return { found: true, key_id: val.slice(0, 8) + '...', source: source || '环境变量(已注入,非文件)' };
}
const status = findKey();
const envPath = writeEnvPath();
const result = {
aicoin_key_status: status.found
? { configured: true, key_preview: status.key_id, env_file: status.source }
: {
configured: false,
setup_steps: [
'访问 https://www.aicoin.com/opendata 注册并创建 API Key',
'在 .env 文件中添加:AICOIN_ACCESS_KEY_ID=your-key-id',
'在 .env 文件中添加:AICOIN_ACCESS_SECRET=your-secret',
`.env 文件位置:${envPath}(CoinClaw 容器内请在 web UI EnvSection 配置)`,
],
tier_options: [
{ tier: '免费版', price: '$0', highlights: '价格、K线、热门币' },
{ tier: '基础版', price: '$29/mo', highlights: '+ 资金费率、多空比、新闻' },
{ tier: '标准版', price: '$79/mo', highlights: '+ 鲸鱼单、信号、灰度' },
{ tier: '高级版', price: '$299/mo', highlights: '+ 爆仓热力图、指标K线' },
{ tier: '专业版', price: '$699/mo', highlights: '全部接口:AI分析、OI、美股' },
],
},
security_notice: {
message: 'AiCoin API Key 与交易所 API Key 是完全独立的两套密钥',
details: [
'AiCoin API Key 仅用于获取市场数据(行情、K线、资金费率等),无法进行任何交易操作,也无法读取你在交易所的任何信息',
'如需在交易所下单交易,需要单独到各交易所后台申请交易 API Key',
'所有密钥(AiCoin key 和交易所 key)仅保存在你的本地设备 .env 文件中,不会上传到任何服务器',
],
},
};
console.log(JSON.stringify(result, null, 2));
#!/usr/bin/env node
// Automated Trading — config management + trade execution helper
// Strategy decisions are made by the AI agent, not this script.
import { cli } from '../lib/cli.mjs';
import { execFileSync } from 'node:child_process';
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
const __dir = dirname(fileURLToPath(import.meta.url));
const WORKSPACE = resolve(process.env.HOME || '', '.openclaw', 'workspace');
const CONFIG_PATH = resolve(WORKSPACE, 'aicoin-trade-config.json');
const DEFAULT_CONFIG = {
exchange: 'okx',
symbol: 'BTC/USDT:USDT',
market_type: 'swap',
capital_pct: 0.5,
leverage: 20,
stop_loss_pct: 0.025,
take_profit_pct: 0.05,
};
function loadConfig() {
if (existsSync(CONFIG_PATH)) {
try { return { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')) }; } catch {}
}
return { ...DEFAULT_CONFIG };
}
function saveConfig(cfg) {
writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
}
function ex(action, params) {
const args = [resolve(__dir, 'exchange.mjs'), action, JSON.stringify(params)];
try {
return JSON.parse(execFileSync(process.execPath, args, { encoding: 'utf-8', cwd: resolve(__dir, '..'), timeout: 30000, env: { ...process.env, AICOIN_INTERNAL_CALL: '1' } }));
} catch (e) {
return { error: `exchange.mjs ${action} failed: ${e.message}` };
}
}
cli({
// Save trading config
setup: async (params) => {
const cfg = { ...loadConfig(), ...params };
saveConfig(cfg);
return { saved: CONFIG_PATH, config: cfg };
},
// Show config + balance + positions
status: async (params) => {
const cfg = { ...loadConfig(), ...params };
let balance, positions, openOrders;
try { balance = ex('balance', { exchange: cfg.exchange, market_type: cfg.market_type }); } catch (e) { balance = { error: e.message }; }
try { positions = ex('positions', { exchange: cfg.exchange, market_type: cfg.market_type }); } catch (e) { positions = { error: e.message }; }
try { openOrders = ex('open_orders', { exchange: cfg.exchange, symbol: cfg.symbol, market_type: cfg.market_type }); } catch (e) { openOrders = { error: e.message }; }
return { config: cfg, balance, positions, open_orders: openOrders };
},
// Execute a trade with risk management (agent decides direction)
open: async (params) => {
const cfg = { ...loadConfig(), ...params };
const { direction } = params; // 'long' or 'short' — decided by agent
if (!direction || !['long', 'short'].includes(direction)) {
throw new Error('Missing "direction": must be "long" or "short"');
}
// 1. Check balance (derive quote currency from symbol)
const bal = ex('balance', { exchange: cfg.exchange, market_type: cfg.market_type });
if (bal && bal.error) throw new Error(bal.error);
const quote = cfg.symbol.split('/')[1]?.split(':')[0] || 'USDT';
const available = Number(bal[quote]?.free || 0);
if (available < 1) throw new Error(`Insufficient ${quote} balance: ${available}`);
// 2. Get current price
const ticker = ex('ticker', { exchange: cfg.exchange, symbol: cfg.symbol, market_type: cfg.market_type });
if (ticker && ticker.error) throw new Error(ticker.error);
const price = ticker.last || ticker.close;
if (!Number.isFinite(price) || price <= 0) throw new Error('invalid price from ticker');
// 3. Check market minimums & get contract size
const base = cfg.symbol.split('/')[0];
const mkts = ex('markets', { exchange: cfg.exchange, market_type: cfg.market_type, base });
if (!Array.isArray(mkts)) throw new Error(mkts && mkts.error || 'markets fetch failed');
const mkt = mkts.find(m => m.symbol === cfg.symbol);
const contractSize = mkt?.contractSize || 1; // e.g. OKX BTC = 0.01 BTC/contract
const amountStep = mkt?.precision?.amount || 0.01; // exchange precision step
const amountMin = mkt?.limits?.amount?.min || amountStep;
// 4. Calculate position size (convert base amount to contracts for futures)
const capital = available * cfg.capital_pct;
const positionValue = capital * cfg.leverage;
const amountInBase = positionValue / price;
// For futures/swap, CCXT amount is in contracts; convert using contractSize
const rawAmount = cfg.market_type !== 'spot' && contractSize
? amountInBase / contractSize
: amountInBase;
// Round down to exchange precision step & enforce minimum
const amount = Math.max(Math.floor(rawAmount / amountStep) * amountStep, amountMin);
if (!Number.isFinite(amount) || amount <= 0) throw new Error('computed amount invalid');
if (amount * (contractSize || 1) * price < 1) throw new Error(`Position too small: ${amount} contracts ≈ ${(amount * contractSize).toFixed(6)} ${base}`);
// 5. Set leverage
try { ex('set_leverage', { exchange: cfg.exchange, symbol: cfg.symbol, leverage: cfg.leverage, market_type: cfg.market_type }); } catch {}
// 6. Place market order
const side = direction === 'long' ? 'buy' : 'sell';
const order = ex('create_order', {
exchange: cfg.exchange, symbol: cfg.symbol, type: 'market', side,
amount, market_type: cfg.market_type, confirmed: 'true',
});
// 7. Place stop-loss & take-profit (conditional orders with reduceOnly)
const slPrice = direction === 'long' ? price * (1 - cfg.stop_loss_pct) : price * (1 + cfg.stop_loss_pct);
const tpPrice = direction === 'long' ? price * (1 + cfg.take_profit_pct) : price * (1 - cfg.take_profit_pct);
const closeSide = direction === 'long' ? 'sell' : 'buy';
let sl, tp;
try { sl = ex('create_order', { exchange: cfg.exchange, symbol: cfg.symbol, type: 'market', side: closeSide, amount, market_type: cfg.market_type, confirmed: 'true', params: { stopLossPrice: Number(slPrice.toPrecision(6)), reduceOnly: true } }); } catch (e) { sl = { error: e.message }; }
try { tp = ex('create_order', { exchange: cfg.exchange, symbol: cfg.symbol, type: 'market', side: closeSide, amount, market_type: cfg.market_type, confirmed: 'true', params: { takeProfitPrice: Number(tpPrice.toPrecision(6)), reduceOnly: true } }); } catch (e) { tp = { error: e.message }; }
return {
direction, amount,
amount_base: `${Number((amount * contractSize).toPrecision(4))} ${base}`,
contract_size: contractSize !== 1 ? `1 contract = ${contractSize} ${base}` : null,
entry_price: price, stop_loss: Number(slPrice.toPrecision(6)), take_profit: Number(tpPrice.toPrecision(6)),
order_id: order.id, sl_order: sl?.id || sl?.error, tp_order: tp?.id || tp?.error,
capital_used: capital.toFixed(2), position_value: positionValue.toFixed(2),
};
},
// Close current position
close: async (params) => {
const cfg = { ...loadConfig(), ...params };
// Cancel open orders first
try { ex('cancel_order', { exchange: cfg.exchange, symbol: cfg.symbol, market_type: cfg.market_type }); } catch {}
// Get position
const positions = ex('positions', { exchange: cfg.exchange, market_type: cfg.market_type });
if (!Array.isArray(positions)) throw new Error(positions && positions.error || 'positions fetch failed');
const pos = positions.find(p => p.symbol === cfg.symbol && Math.abs(Number(p.contracts || 0)) > 0);
if (!pos) return { closed: false, reason: 'No open position' };
const amount = Math.abs(Number(pos.contracts));
const posDir = pos.side || (Number(pos.contracts) > 0 ? 'long' : 'short');
const side = posDir === 'long' ? 'sell' : 'buy';
const order = ex('create_order', {
exchange: cfg.exchange, symbol: cfg.symbol, type: 'market', side, amount, market_type: cfg.market_type, confirmed: 'true',
params: { reduceOnly: true },
});
return { closed: true, side, amount, order_id: order.id };
},
});
#!/usr/bin/env node
// CCXT Exchange Trading CLI
// Requires: npm install ccxt
import { cli } from '../lib/cli.mjs';
import { loadEnv, writeEnvPath } from '../lib/env-loader.mjs';
import { execSync } from 'node:child_process';
import { readFileSync, writeFileSync, unlinkSync, mkdirSync, chmodSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
const __dir = dirname(fileURLToPath(import.meta.url));
// .env auto-load (宿主可能不向子进程注入 env)。共享 loader,见 lib/env-loader.mjs。
loadEnv();
// pionex 在当前 ccxt 版本无实现 → 不列入可交易所(REFERRALS 仍保留作注册引流)。getExchange 另有兜底。
const SUPPORTED = ['binance','okx','bybit','bitget','gate','htx','hyperliquid'];
// AiCoin referral links — shown in exchanges list and missing-key errors
const REFERRALS = {
okx: { name: 'OKX', code: 'aicoin20', benefit: '永久返20%手续费', link: 'https://jump.do/zh-Hans/xlink-proxy?id=2' },
binance: { name: 'Binance', code: 'aicoin668', benefit: '返10% + $500', link: 'https://jump.do/zh-Hans/xlink-proxy?id=3' },
bitget: { name: 'Bitget', code: 'hktb3191', benefit: '返10%手续费', link: 'https://jump.do/zh-Hans/xlink-proxy?id=6' },
htx: { name: 'HTX', code: 'j2us6223', benefit: '', link: 'https://jump.do/zh-Hans/xlink-proxy?id=4' },
gate: { name: 'Gate.io', code: 'AICOINGO', benefit: '', link: 'https://jump.do/zh-Hans/xlink-proxy?id=5' },
bybit: { name: 'Bybit', code: '34429', benefit: '', link: 'https://jump.do/zh-Hans/xlink-proxy?id=15' },
pionex: { name: 'Pionex', code: '4vgi0zUF', benefit: '', link: 'https://www.pionex.com/zh-CN/signUp?r=4vgi0zUF' },
hyperliquid: { name: 'Hyperliquid', code: 'AICOIN88', benefit: '返4%手续费', link: 'https://app.hyperliquid.xyz/join/AICOIN88' },
};
const SECURITY_NOTICE = '⚠️ AiCoin API Key 与交易所 API Key 是完全独立的两套密钥:(1) AiCoin API Key 仅用于获取市场数据(行情、K线、资金费率等),无法进行任何交易操作,也无法读取你在交易所的任何信息。(2) 交易所 API Key 需要单独到各交易所后台申请和授权。(3) 所有密钥仅保存在本地设备 .env 文件中,不会上传到任何服务器。';
// AiCoin broker tags — ensures orders are attributed to AiCoin, not CCXT default
const BROKER_CONFIG = {
binance: {
options: { broker: { spot: 'x-MGFCMH4U', margin: 'x-MGFCMH4U', future: 'x-FaeSBrMa', swap: 'x-FaeSBrMa', delivery: 'x-FaeSBrMa' } },
},
okx: {
options: { brokerId: 'c6851dd5f01e4aBC' },
},
bybit: {
options: { brokerId: 'AiCoin' },
},
bitget: {
options: { broker: 'tpequ' },
},
gate: {
headers: { 'X-Gate-Channel-Id': 'AiCoin1' },
},
htx: {
options: { broker: { id: 'AAf0e4f2ef' } },
},
};
async function getExchange(id, marketType, skipAuth = false) {
let ccxt;
try {
ccxt = await import('ccxt');
} catch {
// Auto-install ccxt if missing
try {
execSync('npm install --omit=dev', { cwd: resolve(__dir, '..'), stdio: 'pipe', timeout: 60000 });
ccxt = await import('ccxt');
} catch {
throw new Error('ccxt not installed. Run: cd <skill-dir>/aicoin && npm install');
}
}
const opts = {};
if (!skipAuth) {
if (id === 'hyperliquid') {
// HL 用钱包签名,不是 api_key/secret: walletAddress=主钱包(查余额/持仓), privateKey=API钱包(agent)私钥(签单)。
opts.walletAddress = process.env.HYPERLIQUID_WALLET_ADDRESS || process.env.HYPERLIQUID_MAIN_WALLET || '';
opts.privateKey = process.env.HYPERLIQUID_PRIVATE_KEY || '';
if (!opts.walletAddress || !opts.privateKey) {
const ref = REFERRALS.hyperliquid || {};
throw new Error(
`未配置 Hyperliquid 钱包凭证。HL 用钱包签名(非 api key):需要主钱包地址 + API 钱包私钥。` +
(ref.link ? `\n注册 HL(AiCoin 返佣):${ref.link} 邀请码 ${ref.code}` : '') +
`\n配置:让 AI 用 save_key({"exchange":"hyperliquid","wallet_address":"0x主钱包","private_key":"0xAPI钱包私钥"}),写进 ~/.coinos/.env、chmod 600、不回显。` +
`\n⚠️ 务必用 HL「API 钱包/agent wallet」私钥(可在 HL 后台单独授权与撤销),绝不要用主钱包私钥。\n${SECURITY_NOTICE}`
);
}
} else {
const pre = id.toUpperCase();
opts.apiKey = process.env[`${pre}_API_KEY`];
opts.secret = process.env[`${pre}_API_SECRET`] || process.env[`${pre}_SECRET`];
if (process.env[`${pre}_PASSWORD`] || process.env[`${pre}_PASSPHRASE`]) {
opts.password = process.env[`${pre}_PASSWORD`] || process.env[`${pre}_PASSPHRASE`];
}
if (!opts.apiKey) {
const ref = REFERRALS[id] || {};
throw new Error(
`未配置 ${ref.name || id} 交易所 API Key。` +
(ref.link ? `\n注册${ref.name}(AiCoin专属优惠):${ref.link}\n邀请码:${ref.code}${ref.benefit ? ',' + ref.benefit : ''}` : '') +
`\n配置方法:把 key 放进 ~/.coinos/.env(${pre}_API_KEY=xxx / ${pre}_API_SECRET=xxx),或让 AI 用 save_key 动作代写(自动 chmod 600、不回显)。` +
`\n${SECURITY_NOTICE}`
);
}
}
}
// Proxy support: PROXY_URL (MCP-compatible) or HTTPS_PROXY/HTTP_PROXY
const proxyUrl = process.env.PROXY_URL
|| process.env.HTTPS_PROXY || process.env.https_proxy
|| process.env.HTTP_PROXY || process.env.http_proxy
|| process.env.ALL_PROXY || process.env.all_proxy;
if (proxyUrl) {
if (proxyUrl.startsWith('socks')) {
let socksUrl = proxyUrl;
if (socksUrl.startsWith('socks5://')) socksUrl = socksUrl.replace('socks5://', 'socks5h://');
else if (socksUrl.startsWith('socks4://')) socksUrl = socksUrl.replace('socks4://', 'socks4a://');
opts.socksProxy = socksUrl;
} else if (proxyUrl.startsWith('https://')) {
opts.httpsProxy = proxyUrl;
} else {
opts.httpProxy = proxyUrl;
}
}
// Set market type
if (marketType && marketType !== 'spot') {
opts.options = { ...(opts.options || {}), defaultType: marketType };
}
// Apply AiCoin broker tags (overrides CCXT defaults)
const brokerCfg = BROKER_CONFIG[id];
if (brokerCfg) {
if (brokerCfg.options) {
opts.options = { ...(opts.options || {}), ...brokerCfg.options };
}
if (brokerCfg.headers) {
opts.headers = { ...(opts.headers || {}), ...brokerCfg.headers };
}
}
const Ex = ccxt.default?.[id] || ccxt[id];
if (typeof Ex !== 'function') {
// 防 `Ex is not a constructor` 裸崩 —— 交易所在当前 ccxt 版本无实现(如旧版没有的所、或拼写错)。
const avail = (ccxt.exchanges || []).filter((e) => SUPPORTED.includes(e)).join(', ');
throw new Error(`交易所 "${id}" 在当前 ccxt 版本中不可用。当前支持: ${avail || SUPPORTED.join(', ')}。`);
}
return new Ex(opts);
}
// createOrder 兜底各所"账户配置相关"的方向/参数差异(ccxt 只翻译不替你判断该传什么)。
// 处理: OKX 单向 posSide、币安/Bybit 双向(hedge)缺方向参数、Hyperliquid 市价需参考价。
async function placeOrder(ex, symbol, type, side, amount, price, params, exchange, marketType) {
const p = { ...(params || {}) };
const isSwap = marketType && marketType !== 'spot';
const isOkxSwap = exchange === 'okx' && isSwap;
const isBinanceSwap = exchange === 'binance' && isSwap;
const isBybitSwap = exchange === 'bybit' && isSwap;
// Hyperliquid 市价单需要参考价算滑点 —— ccxt 对 market + price 缺失直接 ArgumentsRequired,
// 导致 HL 上 create_order/close_position/set_stop 全失败。用现价喂给 ccxt(它按默认滑点转 IOC 限价)。
if (exchange === 'hyperliquid' && type === 'market' && (price == null || price === '')) {
try { const t = await ex.fetchTicker(symbol); price = t.last ?? t.close ?? t.mark; } catch {}
if (price == null) throw new Error(`Hyperliquid 市价单需要参考价(算滑点上限),但取不到 ${symbol} 现价,请稍后重试。`);
}
// 仅给"开/加仓"单(非 reduceOnly)自动补 posSide。OKX 双向平仓由 closeParamsFor 显式给 posSide;
// OKX 单向(net)不该带 posSide(早先无条件猜会让 net 平仓单先必失败再 retry,无幂等键有重复挂单风险)。
if (isOkxSwap && !p.posSide && !p.reduceOnly) {
p.posSide = side === 'buy' ? 'long' : 'short';
}
// ── 条件单安全网(fail-safe)──
// 调用方传了触发价意图(止盈止损/触发单/追踪),但该所 ccxt 没把它映射成真条件单时,拒绝下单。
// 实测 Gate / HTX 的 stopLossPrice/takeProfitPrice 经 ccxt 映射成**空**(触发价被静默丢弃)→ 会被
// 当成"立即市价单"下出去(止损瞬间变成立即成交)。这比"不支持"更危险,必须在下单前拦下。
const TRIGGER_INTENT = ['stopLossPrice', 'takeProfitPrice', 'triggerPrice', 'stopPrice', 'trailingPercent', 'callbackRate', 'trailingTriggerPrice'];
if (TRIGGER_INTENT.some((k) => p[k] != null) && typeof ex.createOrderRequest === 'function') {
let req = null;
try { req = ex.createOrderRequest(symbol, type, side, amount, price, p); } catch { /* 构建报错则放行,真调用会抛同样的错 */ }
if (req) {
const s = JSON.stringify(req).toLowerCase();
// 各所 native 触发信号(由 7 家 createOrderRequest 实测归纳): binance type=STOP*/TAKE_PROFIT*/TRAILING*、
// okx ordType=conditional/trigger/move_order_stop+slTriggerPx/tpTriggerPx、bybit triggerPrice、
// bitget planType、htx trigger_type/formula_price、通用 stopPrice/callbackRate/trailing。
const SIGNALS = ['trigger', 'stopprice', 'sltriggerpx', 'tptriggerpx', 'plantype', 'algotype', 'callback', 'trailing', 'conditional', 'move_order_stop', 'formula_price', 'stop_market', 'take_profit', 'activationprice'];
if (!SIGNALS.some((sig) => s.includes(sig))) {
throw new Error(`${exchange} 无法把这种条件单(止盈止损/触发/追踪)通过统一参数下出 —— 触发价会被静默丢弃、变成立即成交的市价单。已拒绝下单以防误成交。请改用支持的交易所(Binance/OKX/Bybit/Bitget),或到交易所端手动挂条件单。`);
}
}
}
try {
return await ex.createOrder(symbol, type, side, amount, price, p);
} catch (e) {
const errMsg = String(e);
// OKX net mode 不接受 posSide → 删掉重试(仅 OKX swap 且确是 posSide 报错;不在 spot retry)。
if (isOkxSwap && p.posSide && errMsg.includes('posSide')) {
delete p.posSide;
return await ex.createOrder(symbol, type, side, amount, price, p);
}
// 币安双向(hedge): 缺 positionSide 报 -4061(下单前硬拒绝、订单未入场、retry 无重复成交风险)。
// 开/加仓: buy→LONG / sell→SHORT。
// reduceOnly 平仓单(如 auto-trade 的裸 SL/TP): hedge 不接受 reduceOnly,删掉它并按"平的哪侧仓"补
// positionSide —— 平多(sell)挂 LONG、平空(buy)挂 SHORT(与开仓相反)。
if (isBinanceSwap && !p.positionSide
&& (errMsg.includes('-4061') || errMsg.includes('position side does not match'))) {
if (p.reduceOnly) { delete p.reduceOnly; p.positionSide = side === 'buy' ? 'SHORT' : 'LONG'; }
else p.positionSide = side === 'buy' ? 'LONG' : 'SHORT';
return await ex.createOrder(symbol, type, side, amount, price, p);
}
// Bybit 双向(hedge): 缺 positionIdx 报 10001 "position idx not match position mode"。
// 开/加仓: buy→1(多)/ sell→2(空)。reduceOnly 平仓单按平的哪侧: 平多(sell)→1、平空(buy)→2。
// Bybit reduceOnly 可与 positionIdx 共存,保留 reduceOnly。同属下单前硬拒绝,retry 安全。
if (isBybitSwap && p.positionIdx == null
&& (errMsg.includes('10001') || /position idx/i.test(errMsg) || /position mode/i.test(errMsg))) {
p.positionIdx = p.reduceOnly ? (side === 'buy' ? 2 : 1) : (side === 'buy' ? 1 : 2);
return await ex.createOrder(symbol, type, side, amount, price, p);
}
throw e;
}
}
// 根据"真实持仓 + 交易所"推导平仓/减仓单该带的方向参数 —— 这是平仓/止损类操作防"反向单"和
// "hedge 模式 reduceOnly 被拒"的安全底线。方向只取自交易所返回的真实持仓 (pos.info),绝不靠
// agent 传的 side 猜。各所规则不同:
// - 币安双向 (hedge): 必须带 positionSide=LONG/SHORT,且**不能**带 reduceOnly
// (币安在 hedge 模式收到 reduceOnly 会直接拒单 → 这正是 close_position 早期"返回异常"的根因)。
// - 币安单向 (positionSide=BOTH 或缺省) / 多数交易所: reduceOnly:true。
// - OKX 双向: posSide=long/short + reduceOnly;单向 (net): 只给 reduceOnly,
// posSide 交给 placeOrder 的 OKX 兜底逻辑 + 51000 重试处理。
// - Bybit 双向: positionIdx 1=多/2=空 + reduceOnly;单向 (0): reduceOnly。
function closeParamsFor(exchange, marketType, pos) {
const out = {};
if (!marketType || marketType === 'spot') return out;
const info = pos?.info || {};
if (exchange === 'binance') {
const ps = String(info.positionSide || '').toUpperCase();
if (ps === 'LONG' || ps === 'SHORT') out.positionSide = ps; // hedge: 带 positionSide,不带 reduceOnly
else out.reduceOnly = true; // one-way
return out;
}
if (exchange === 'okx') {
out.reduceOnly = true;
const ps = String(info.posSide || '').toLowerCase();
if (ps === 'long' || ps === 'short') out.posSide = ps;
return out;
}
if (exchange === 'bybit') {
out.reduceOnly = true;
const idx = info.positionIdx != null ? Number(info.positionIdx) : null;
if (idx === 1 || idx === 2) out.positionIdx = idx;
return out;
}
// Bitget 双向: reduceOnly 在 hedge 被忽略,裸 side 单=反向开仓(ccxt #17817 实锤)。必须 hedged:true,
// ccxt 才翻 side + tradeSide:Close(实证: {reduceOnly:true,hedged:true}+sell → {side:buy,tradeSide:Close}=平多;
// SL → holdSide:long 护多)。检测靠 pos.hedged(ccxt 据 posMode/holdMode 可靠填);模式完全测不出时**拒绝**,
// 绝不退回裸 reduceOnly(那会让隐藏的 hedge 账户反向开仓)。
if (exchange === 'bitget') {
const pm = String(info.posMode || info.holdMode || '').toLowerCase();
const isHedge = pos?.hedged === true || pm === 'hedge_mode';
const isOneWay = pos?.hedged === false || pm === 'one_way_mode';
if (isHedge) { out.hedged = true; out.reduceOnly = true; }
else if (isOneWay) { out.reduceOnly = true; }
else throw new Error('无法确定 Bitget 持仓模式(单向/双向)。为防双向账户被误当反向开仓,已中止 —— 请重试或在交易所核对持仓模式后再平。');
return out;
}
// HTX 双向(dual_side): 平仓靠 offset 不靠 reduce_only;ccxt 仅在 hedged:true 时写 offset,offset 由
// reduceOnly 决定(实证: {reduceOnly:true,hedged:true}+sell → offset:close=平多;漏 reduceOnly → offset:open=反向)。
// pos.hedged 在 htx 永远 undefined,只能读 info.position_mode。测不出退 reduceOnly(若实为双向会缺 offset 被拒=fail-safe)。
if (exchange === 'htx' || exchange === 'huobi' || exchange === 'huobipro') {
out.reduceOnly = true;
if (String(info.position_mode || '').toLowerCase() === 'dual_side') out.hedged = true;
return out;
}
out.reduceOnly = true;
return out;
}
// 查"条件/算法单"(止盈止损/触发/追踪),合并各 ordType 再去重 —— OKX 的止盈止损是 conditional 类,
// 与 trigger 类**分开存**,必须各类都查再合并,不能"第一次返回空就 return"(否则 OKX 的 set_stop
// 单会被漏掉:实测 stop_orders 旧逻辑先查 trigger 返回空就 return,读不到 conditional 类的 SL/TP)。
async function fetchConditionalOrders(ex, exchange, symbol) {
const variants = [{ trigger: true }, { stop: true }];
if (exchange === 'okx') variants.push({ ordType: 'conditional' }, { ordType: 'oco' }, { ordType: 'trigger' }, { ordType: 'move_order_stop' });
const seen = new Map();
let any = false, lastErr = null;
for (const extra of variants) {
try {
const os = await ex.fetchOpenOrders(symbol, undefined, undefined, extra);
any = true;
for (const o of (os || [])) if (o && o.id != null) seen.set(o.id, o);
} catch (e) { lastErr = e; }
}
if (!any) throw lastErr || new Error('无法查询条件单');
return [...seen.values()];
}
cli({
exchanges: async () => ({
supported: SUPPORTED.map(id => {
const ref = REFERRALS[id] || {};
return { exchange: id, name: ref.name || id, register_link: ref.link || '', invite_code: ref.code || '', benefit: ref.benefit || '' };
}),
security_notice: SECURITY_NOTICE,
}),
register: async ({ exchange: exName }) => {
if (!exName) return { exchanges: Object.keys(REFERRALS), usage: 'node exchange.mjs register \'{"exchange":"okx"}\'' };
const key = exName.toLowerCase().replace(/[.\s]/g, '');
const ALIASES = { 币安: 'binance', 火币: 'htx', 派网: 'pionex', hl: 'hyperliquid', gateio: 'gate' };
const id = ALIASES[key] || key;
const ref = REFERRALS[id];
if (!ref) return { error: `不支持 ${exName}`, supported: Object.keys(REFERRALS) };
return {
exchange: ref.name, invite_code: ref.code, benefit: ref.benefit || '无额外优惠', register_link: ref.link,
steps: ['打开注册链接', '选择手机或邮箱注册', '填入验证码、设置密码', '完成身份验证(KYC)', '如需API交易,到API管理创建key,配置到.env'],
security_notice: SECURITY_NOTICE,
};
},
// 本地 host 模式: 用户在 chat 里给了交易所 key 时, 把它写进规范位置 ~/.coinos/.env
// (chmod 600), 绝不把 secret 回显。容器内有 web UI EnvSection, 不该走这个动作。
save_key: async ({ exchange, api_key, api_secret, secret, password, passphrase, wallet_address, private_key }) => {
if (!exchange) throw new Error('需要 exchange,例: {"exchange":"binance","api_key":"...","api_secret":"..."}');
const id = exchange.toLowerCase().replace(/[.\s]/g, '');
const pre = id.toUpperCase();
const target = writeEnvPath();
let lines = [];
try { lines = readFileSync(target, 'utf-8').split('\n'); } catch { /* 文件还不存在 */ }
const set = (key, val) => {
const i = lines.findIndex(l => l.trim().startsWith(key + '='));
if (i >= 0) lines[i] = `${key}=${val}`; else lines.push(`${key}=${val}`);
};
const flush = () => {
try { mkdirSync(dirname(target), { recursive: true }); } catch {}
writeFileSync(target, lines.join('\n').replace(/\n*$/, '\n'));
try { chmodSync(target, 0o600); } catch {}
};
// Hyperliquid: 钱包签名模式(非 api key)。wallet_address=主钱包, private_key=API钱包(agent)私钥。
if (id === 'hyperliquid') {
const w = wallet_address || api_key; // 容错: 误用 api_key 传地址
const pk = private_key || api_secret || secret; // 容错: 误用 api_secret 传私钥
if (!w || !pk) throw new Error('Hyperliquid 需要 wallet_address(主钱包地址) 和 private_key(API钱包/agent 私钥)');
set('HYPERLIQUID_WALLET_ADDRESS', w);
set('HYPERLIQUID_PRIVATE_KEY', pk);
flush();
return {
saved: true, exchange: id, env_file: target, keys_written: ['HYPERLIQUID_WALLET_ADDRESS', 'HYPERLIQUID_PRIVATE_KEY'],
_security: '私钥已写入并 chmod 600,未回显。⚠️ HL 私钥能签所有交易 —— 务必用 HL「API 钱包/agent wallet」私钥(可在 HL 后台单独授权与随时撤销),绝不要用主钱包私钥;明文已留在本次对话记录里,在意就去 HL 重新授权一个 agent。',
next: `已就绪,可直接查: node scripts/exchange.mjs balance '{"exchange":"hyperliquid"}'`,
};
}
const k = api_key;
const s = api_secret || secret;
const p = password || passphrase;
if (!k || !s) throw new Error('需要 api_key 和 api_secret(OKX/Bitget 还需 password/passphrase;Hyperliquid 用 wallet_address + private_key)');
set(`${pre}_API_KEY`, k);
set(`${pre}_API_SECRET`, s);
if (p) set(`${pre}_PASSWORD`, p);
flush();
const written = [`${pre}_API_KEY`, `${pre}_API_SECRET`].concat(p ? [`${pre}_PASSWORD`] : []);
return {
saved: true, exchange: id, env_file: target, keys_written: written,
_security: 'secret 已写入并 chmod 600,未回显。注意:你刚在 chat 里发的明文 key 会留在对话记录里,在意可去交易所后台重新生成。强烈建议只勾「读取」权限、绑定 IP 白名单、不要开提现。',
next: `已就绪,可直接查: node scripts/exchange.mjs balance '{"exchange":"${id}"}'`,
};
},
markets: async ({ exchange, market_type, base, quote, limit = 100 }) => {
const ex = await getExchange(exchange, market_type, true);
await ex.loadMarkets();
let m = Object.values(ex.markets).map(x => ({
symbol: x.symbol, base: x.base, quote: x.quote, type: x.type, active: x.active,
contractSize: x.contractSize || null,
limits: x.limits || null,
precision: x.precision || null,
}));
if (market_type) m = m.filter(x => x.type === market_type);
if (base) m = m.filter(x => x.base === base.toUpperCase());
if (quote) m = m.filter(x => x.quote === quote.toUpperCase());
return m.slice(0, limit);
},
ticker: async ({ exchange, symbol, symbols, market_type }) => {
const ex = await getExchange(exchange, market_type, true);
if (symbol) return ex.fetchTicker(symbol);
return ex.fetchTickers(symbols);
},
orderbook: async ({ exchange, symbol, market_type, limit }) => {
const ex = await getExchange(exchange, market_type, true);
return ex.fetchOrderBook(symbol, limit);
},
trades: async ({ exchange, symbol, market_type, limit }) => {
const ex = await getExchange(exchange, market_type, true);
return ex.fetchTrades(symbol, undefined, limit);
},
ohlcv: async ({ exchange, symbol, market_type, timeframe = '1h', limit }) => {
const ex = await getExchange(exchange, market_type, true);
return ex.fetchOHLCV(symbol, timeframe, undefined, limit);
},
balance: async ({ exchange, market_type, show_dust }) => {
const ex = await getExchange(exchange, market_type);
const bal = await ex.fetchBalance();
// Return only non-zero balances for cleaner output
const summary = {};
for (const [ccy, amt] of Object.entries(bal.total || {})) {
const total = Number(amt);
if (total <= 0) continue;
// Filter dust tokens (< $0.01 equivalent) unless show_dust is set
// Stablecoins check: if < 0.01, it's dust
const isStable = ['USDT','USDC','BUSD','DAI','TUSD','FDUSD'].includes(ccy);
if (!show_dust && isStable && total < 0.01) continue;
if (!show_dust && !isStable && total < 1e-7) continue;
summary[ccy] = { free: bal.free[ccy], used: bal.used[ccy], total: bal.total[ccy] };
}
// OKX unified account note
if (exchange === 'okx') {
summary._note = 'OKX统一账户:现货和合约共用同一余额,无需划转。';
}
return summary;
},
positions: async ({ exchange, symbols, market_type }) => {
const ex = await getExchange(exchange, market_type);
const all = await ex.fetchPositions(symbols);
// Filter out zero-size positions (Binance returns 100+ empty entries)
return all.filter(p => Math.abs(Number(p.contracts || 0)) > 0);
},
open_orders: async ({ exchange, symbol, market_type }) => {
const ex = await getExchange(exchange, market_type);
if (symbol) return ex.fetchOpenOrders(symbol);
try {
return await ex.fetchOpenOrders();
} catch (err) {
if (err.message?.includes('symbol') || err.message?.includes('argument')) {
throw new Error(`${exchange} 查询未成交订单需要指定交易对,例如: {"symbol":"BTC/USDT"}`);
}
throw err;
}
},
closed_orders: async ({ exchange, symbol, market_type, since, limit = 50 }) => {
const ex = await getExchange(exchange, market_type);
const sinceTs = since ? new Date(since).getTime() : undefined;
return ex.fetchClosedOrders(symbol, sinceTs, Number(limit));
},
my_trades: async ({ exchange, symbol, market_type, since, limit = 50 }) => {
const ex = await getExchange(exchange, market_type);
const sinceTs = since ? new Date(since).getTime() : undefined;
return ex.fetchMyTrades(symbol, sinceTs, Number(limit));
},
fetch_order: async ({ exchange, symbol, order_id, market_type }) => {
const ex = await getExchange(exchange, market_type);
return ex.fetchOrder(order_id, symbol);
},
create_order: async ({ exchange, symbol, type, side, amount, amount_unit, cost, leverage, price, market_type, params, confirmed }) => {
const pendingFile = resolve(__dir, '..', '.pending-order.json');
// Internal calls (from auto-trade.mjs) bypass file-based confirmation
const isInternal = process.env.AICOIN_INTERNAL_CALL === '1';
// Step 2: Confirmation — only works if a pending order file exists from Step 1
if (confirmed === 'true' || confirmed === true) {
if (isInternal) {
// Internal call: execute directly with provided params
const ex = await getExchange(exchange, market_type);
const order = await placeOrder(ex, symbol, type, side, amount, price, params, exchange, market_type);
if (market_type && market_type !== 'spot') {
try {
await ex.loadMarkets();
const mkt = ex.markets[symbol];
if (mkt?.contractSize) {
order._contractSize = mkt.contractSize;
order._amountInBase = amount * mkt.contractSize;
order._unit = `${amount} contracts × ${mkt.contractSize} ${mkt.base}/contract = ${amount * mkt.contractSize} ${mkt.base}`;
}
} catch {}
}
return order;
}
let pending;
try { pending = JSON.parse(readFileSync(pendingFile, 'utf8')); }
catch { throw new Error('没有待确认的订单。请先不带 confirmed 参数调用 create_order 来预览订单,等用户确认后再重新调用并带上 confirmed=true。'); }
// Expire after 5 minutes
if (Date.now() - pending.timestamp > 5 * 60 * 1000) {
try { unlinkSync(pendingFile); } catch {}
throw new Error('订单预览已过期(超过5分钟),请重新创建订单预览。');
}
// Execute with stored params (prevents model from tampering between preview and confirm)
const ex = await getExchange(pending.exchange, pending.market_type);
const order = await placeOrder(ex, pending.symbol, pending.type, pending.side, pending.amount, pending.price, pending.params, pending.exchange, pending.market_type);
try { unlinkSync(pendingFile); } catch {}
if (pending.market_type && pending.market_type !== 'spot') {
try {
await ex.loadMarkets();
const mkt = ex.markets[pending.symbol];
if (mkt?.contractSize) {
order._contractSize = mkt.contractSize;
order._amountInBase = pending.amount * mkt.contractSize;
order._unit = `${pending.amount} contracts × ${mkt.contractSize} ${mkt.base}/contract = ${pending.amount * mkt.contractSize} ${mkt.base}`;
}
} catch {}
}
return order;
}
// Step 1: Preview — save pending order to file, return preview
const ex = await getExchange(exchange, market_type);
await ex.loadMarkets();
const mkt = ex.markets[symbol];
// Round contract amount to market precision/min (e.g. OKX BTC swap = 0.01 contract step)
// Avoids the old `Math.max(1, Math.round(x))` floor that broke sub-1-contract orders.
const roundContracts = (raw) => {
const minStep = mkt.precision?.amount || mkt.limits?.amount?.min || 1;
const minAmt = mkt.limits?.amount?.min || minStep;
let v = Number(raw);
if (!isFinite(v) || v <= 0) return minAmt;
// Clamp to min first so amountToPrecision doesn't throw on values < precision step
if (v < minAmt) v = minAmt;
try { v = Number(ex.amountToPrecision(symbol, v)); } catch { v = Math.round(v / minStep) * minStep; }
if (v < minAmt) v = minAmt;
return v;
};
// cost param: user says "用XU做多" → calculate amount from USDT margin budget
if (cost && mkt?.contractSize && market_type && market_type !== 'spot') {
const tick = await ex.fetchTicker(symbol);
const curP = tick.last;
// 杠杆来源:入参 > 已有仓位 > 交易所账户杠杆配置(fetchLeverage)。
// 都拿不到时**不要静默假设 1x** —— 按 1x 算出的张数会与实际杠杆差数量级(AGENTS.md 铁则二)。
let lev = leverage ? Number(leverage) : null;
if (!lev) {
try {
const positions = await ex.fetchPositions([symbol]);
const pos = positions.find(p => p.symbol === symbol);
if (pos?.leverage) lev = Number(pos.leverage);
} catch {}
}
if (!lev && ex.has?.fetchLeverage) {
try {
const lv = await ex.fetchLeverage(symbol);
const n = Number(lv?.longLeverage ?? lv?.leverage ?? lv?.info?.lever ?? lv?.shortLeverage);
if (n > 0) lev = n;
} catch {}
}
if (!(lev > 0)) {
throw new Error(`无法确定 ${symbol} 的杠杆倍数(当前无持仓、交易所也未返回杠杆配置)。请显式传 leverage,例如 {"cost":${cost},"leverage":10,...},避免按 1x 误算张数。`);
}
amount = roundContracts(Number(cost) * lev / (mkt.contractSize * curP));
}
// 现货按金额买入(cost):反算 base 数量 amount = cost / 价格(通用,不依赖各所 cost 通道)。
// 否则 cost 在现货被静默丢弃 → amount=undefined → 预览 NaN、下出无数量的废单。
const isSpot = !market_type || market_type === 'spot';
if (cost && isSpot && !mkt?.contractSize) {
if (side !== 'buy') throw new Error('现货 cost(按金额下单)目前只支持买入;卖出请用 amount 指定币数量。');
let px = Number(price);
if (!(px > 0)) { try { px = (await ex.fetchTicker(symbol)).last; } catch {} }
if (!(px > 0)) throw new Error(`无法获取 ${symbol} 价格以按金额反算数量,请改用 amount(币数量)。`);
try { amount = Number(ex.amountToPrecision(symbol, Number(cost) / px)); } catch { amount = Number(cost) / px; }
}
// 非 cost 路径必须有有效 amount —— 这个校验要放在换算**之前**:roundContracts 会把 NaN/缺失
// 兜成 minAmt,若放换算后再校验就被掩盖,变成静默下出一个最小单(废单)。
if (!cost && (amount == null || !(Number(amount) > 0))) {
throw new Error('数量无效: 请提供 amount(币数量;合约要传张数时加 amount_unit:"contracts"),或现货市价买入用 cost(USDT 金额)。');
}
// 合约: amount 默认按"币数量"理解,统一 /contractSize 换算成张数。整数也换 —— 旧版用 Number.isInteger
// 猜单位(整数=张/小数=币),在 contractSize≠1 的所(OKX DOGE cs=1000、Gate BTC cs=0.0001 等)会把
// 张数算错几个数量级。显式 amount_unit:"contracts" 才跳过换算(给已按张数传入的调用方)。
if (!cost && mkt?.contractSize && market_type && market_type !== 'spot' && amount_unit !== 'contracts') {
amount = roundContracts(Number(amount) / mkt.contractSize);
}
// 最终兜底: cost 路径若算出 0/NaN(金额过小、价格异常等)也拒绝,绝不落盘空 amount。
if (amount == null || !(Number(amount) > 0)) {
throw new Error('数量无效(经金额/张数换算后仍无效)。请检查 cost/amount 与当前价格、最小下单额。');
}
const pendingOrder = { exchange, symbol, type, side, amount, price, market_type, params, timestamp: Date.now() };
writeFileSync(pendingFile, JSON.stringify(pendingOrder));
// Build order details
const sideLabel = side === 'buy' ? '买入/做多' : '卖出/做空';
// 识别条件单(params 带触发价),避免对无 price 的 STOP_MARKET 显示"限价 undefined"。
// 注:挂"保护已有仓位"的止盈止损请用 set_stop(自动算方向/reduceOnly),别在这里手搓。
const trigPx = params && (params.stopLossPrice ?? params.takeProfitPrice ?? params.triggerPrice ?? params.stopPrice);
const trailRate = params && (params.trailingPercent ?? params.callbackRate);
const typeLabel = trailRate != null ? `追踪止损(回调 ${trailRate}%${params.activationPrice ? `,激活 ${params.activationPrice}` : ''})`
: trigPx != null ? `条件单(触发价 ${trigPx})`
: type === 'market' ? '市价' : `限价 ${price}`;
const mktType = market_type || 'spot';
const orderInfo = { 交易所: exchange, 交易对: symbol, 方向: sideLabel, 类型: typeLabel };
// Fetch current price
let curPrice = null;
if (type === 'market' || !price) {
try {
const tick = await ex.fetchTicker(symbol);
curPrice = tick.last;
orderInfo['当前价格'] = `$${curPrice.toLocaleString()}`;
} catch {}
}
// Contract details
if (mkt?.contractSize) {
orderInfo['合约数量'] = `${amount} 张`;
orderInfo['换算'] = `${amount} × ${mkt.contractSize} ${mkt.base}/张 = ${amount * mkt.contractSize} ${mkt.base}`;
if (curPrice) orderInfo['预估价值'] = `${(amount * mkt.contractSize * curPrice).toFixed(2)} USDT`;
} else {
orderInfo['数量'] = `${amount}`;
if (curPrice) orderInfo['预估价值'] = `${(amount * curPrice).toFixed(2)} USDT`;
}
// Leverage & margin info for futures
if (mktType !== 'spot') {
let lev = leverage ? Number(leverage) : null;
let mgnMode = null;
try {
const positions = await ex.fetchPositions([symbol]);
const pos = positions.find(p => p.symbol === symbol);
if (pos) {
if (!lev && pos.leverage) lev = Number(pos.leverage);
mgnMode = pos.marginMode || pos.marginType;
}
} catch {}
if (lev) {
orderInfo['杠杆'] = `${lev}x`;
if (curPrice) {
const notional = mkt?.contractSize ? amount * mkt.contractSize * curPrice : amount * curPrice;
orderInfo['预估保证金'] = `${(notional / lev).toFixed(2)} USDT`;
}
}
if (mgnMode) orderInfo['保证金模式'] = mgnMode;
}
return {
_preview: true,
status: '⚠️ 订单未下达',
风险提示: '⚠️ 交易风险声明:加密货币交易具有高风险,可能导致本金全部损失。合约使用杠杆会放大收益和亏损。本工具仅提供交易执行功能,不构成投资建议。继续下单即表示你已知悉并接受以上风险。',
用户须知: '下单前请确认:(1) 你已了解该交易的风险 (2) 投入的资金在可承受范围内 (3) 你已设置合适的止损',
订单详情: orderInfo,
操作指引: '请确认以上订单信息无误。回复「确认」或「yes」执行下单,回复「取消」放弃。',
};
},
close_position: async ({ exchange, symbol, market_type, confirmed }) => {
const mt = market_type || 'swap';
const ex = await getExchange(exchange, mt);
const positions = await ex.fetchPositions(symbol ? [symbol] : undefined);
const open = positions.filter(p => Math.abs(Number(p.contracts || 0)) > 0);
if (!open.length) return { message: '当前没有持仓需要平仓。', positions: [] };
// Preview
if (confirmed !== 'true' && confirmed !== true) {
return {
_preview: true,
status: '⚠️ 平仓预览 — 订单未下达',
待平仓位: open.map(p => ({
交易对: p.symbol, 方向: p.side === 'long' ? '多' : '空',
张数: Math.abs(Number(p.contracts)), 开仓价: p.entryPrice,
未实现盈亏: p.unrealizedPnl, 杠杆: p.leverage,
})),
操作指引: '请确认平掉以上仓位。回复「确认」执行,回复「取消」放弃。',
};
}
// Execute
const results = [];
for (const pos of open) {
// 方向必须明确取自交易所;拿不到就跳过,绝不默认 buy(否则对多仓会变成加仓而非平仓)。
if (pos.side !== 'long' && pos.side !== 'short') {
results.push({ symbol: pos.symbol, status: '跳过', error: '交易所未返回持仓方向,拒绝盲目平仓' });
continue;
}
const closeSide = pos.side === 'long' ? 'sell' : 'buy';
const amount = Math.abs(Number(pos.contracts));
try {
// 方向参数从真实持仓推导(hedge/one-way 自适应)。放进 try: Bitget 模式测不出时 closeParamsFor
// 会 throw(防反向开仓),应作为该仓的失败结果,不连累其它仓。
const cp = closeParamsFor(exchange, mt, pos);
const order = await placeOrder(ex, pos.symbol, 'market', closeSide, amount, undefined, cp, exchange, mt);
results.push({ symbol: pos.symbol, side: pos.side, amount, status: '已平仓', orderId: order.id });
} catch (e) {
results.push({ symbol: pos.symbol, side: pos.side, amount, status: '失败', error: e.message });
}
}
return { 平仓结果: results };
},
// 止盈止损 / 条件单 —— 给"已有仓位"挂服务器端保护单。两步确认。
// 方向、reduceOnly/posSide 全部从真实持仓推导(防反向单、防 hedge 模式 reduceOnly 被拒),
// 触发价做"在现价正确一侧"校验(防搞反/瞬间触发),用 ccxt 统一参数 stopLossPrice/
// takeProfitPrice 下独立的 reduceOnly 条件单(跨 Binance/OKX/Bybit 等通用)。
set_stop: async ({ exchange, symbol, market_type, stop_loss, take_profit, trigger_price, amount, side, force, confirmed }) => {
const pendingFile = resolve(__dir, '..', '.pending-stop.json');
// Step 2: 确认执行(读 step1 落盘的已解析订单,防 model 在两步之间篡改方向/数量/触发价)
if (confirmed === 'true' || confirmed === true) {
let pending;
try { pending = JSON.parse(readFileSync(pendingFile, 'utf8')); }
catch { throw new Error('没有待确认的止盈止损单。请先不带 confirmed 调用 set_stop 预览,等用户确认后再带 confirmed=true。'); }
if (Date.now() - pending.timestamp > 5 * 60 * 1000) {
try { unlinkSync(pendingFile); } catch {}
throw new Error('止盈止损预览已过期(超过5分钟),请重新预览。');
}
const mt = pending.market_type || 'swap';
const ex = await getExchange(pending.exchange, mt);
// 防"两步窗口内持仓缩小/反手":再读一次盘,把数量 clamp 到当前同向持仓;持仓没了就不挂废单。
// 这对币安双向(走 positionSide、不带 reduceOnly)尤其重要 —— 它没有 reduceOnly 兜底。best-effort:
// 读盘失败则用原数量(OKX/Bybit/币安单向仍有 reduceOnly 兜住)。
let execAmount = pending.amount;
try {
const curPos = (await ex.fetchPositions([pending.symbol]))
.find(p => p.symbol === pending.symbol && p.side === pending.posSide && Math.abs(Number(p.contracts || 0)) > 0);
if (!curPos) {
try { unlinkSync(pendingFile); } catch {}
return { 止盈止损结果: [], _warning: `${pending.symbol} 的 ${pending.posSide} 持仓已不存在(可能在确认期间被平掉/反手),未挂任何止盈止损单。请重新查持仓后再决定。` };
}
const curSize = Math.abs(Number(curPos.contracts));
if (execAmount > curSize) execAmount = curSize;
} catch { /* 读盘失败,沿用原数量 */ }
const results = [];
for (const o of pending.orders) {
try {
const order = await placeOrder(ex, pending.symbol, 'market', pending.closeSide, execAmount, undefined, o.params, pending.exchange, mt);
results.push({ 类型: o.kind, 触发价: o.trigger, status: '已挂单', orderId: order.id });
} catch (e) {
results.push({ 类型: o.kind, 触发价: o.trigger, status: '失败', error: e.message });
}
}
try { unlinkSync(pendingFile); } catch {}
return {
止盈止损结果: results,
_note: '条件单在部分交易所(OKX 等)属算法/委托单,不出现在普通挂单列表 —— 用 stop_orders 动作或交易所 APP 的「条件委托」栏复核,别用 open_orders 误判没挂上。',
验证建议: `node scripts/exchange.mjs stop_orders '{"exchange":"${pending.exchange}","symbol":"${pending.symbol}","market_type":"${mt}"}'`,
};
}
// Step 1: 预览 —— 取真实持仓、推导方向、校验触发价、落盘待确认订单
if (!symbol) throw new Error('需要 symbol(止盈止损是给已有仓位挂保护单),例: {"exchange":"binance","symbol":"HYPE/USDT:USDT","market_type":"swap","stop_loss":63.5}');
const mt = market_type || 'swap';
if (mt === 'spot') throw new Error('set_stop 仅用于合约持仓的止盈止损。现货保护单请用 create_order 带 params。');
if (stop_loss == null && take_profit == null && trigger_price == null) {
throw new Error('至少给一个:stop_loss(止损触发价)/ take_profit(止盈触发价)/ trigger_price(单一触发价,按方向自动归类)。');
}
const ex = await getExchange(exchange, mt);
await ex.loadMarkets();
const mkt = ex.markets[symbol];
const positions = await ex.fetchPositions([symbol]);
const matching = positions.filter(p => p.symbol === symbol && Math.abs(Number(p.contracts || 0)) > 0);
if (!matching.length) throw new Error(`${exchange} 上没有 ${symbol} 的持仓。止盈止损是给已有仓位挂保护单;若要挂"条件入场单",用 create_order 带 params。`);
// 双向持仓(hedge)同一交易对可能同时有多/空两个仓 —— 选错边会把止损挂到反方向。必须指定 side。
const wantSide = side ? String(side).toLowerCase().replace('多', 'long').replace('空', 'short') : null;
let pos;
if (matching.length > 1) {
if (!wantSide) throw new Error(`${symbol} 同时持有多、空两个仓(双向持仓)。请指定 side("long"/"short")选择给哪个挂止盈止损。当前: ${matching.map(p => `${p.side} ${Math.abs(Number(p.contracts))}张`).join(' / ')}`);
pos = matching.find(p => p.side === wantSide);
if (!pos) throw new Error(`没找到 ${symbol} 的 ${wantSide} 持仓。当前持仓方向: ${matching.map(p => p.side).join(', ')}`);
} else {
pos = matching[0];
if (wantSide && pos.side !== wantSide) throw new Error(`你指定 side=${wantSide},但 ${symbol} 当前持仓是 ${pos.side},方向不符,已中止以防挂错边。`);
}
if (pos.side !== 'long' && pos.side !== 'short') throw new Error(`无法确定 ${symbol} 持仓方向(交易所未返回 side),已中止以防止盈止损挂错边。`);
const posSize = Math.abs(Number(pos.contracts));
const isLong = pos.side === 'long';
const closeSide = isLong ? 'sell' : 'buy';
const cp = closeParamsFor(exchange, mt, pos); // reduceOnly/posSide/positionSide 从真实持仓推导
// 数量:默认全仓;给了就取整到精度并 clamp 不超过持仓(reduceOnly 也会兜底,但提前 clamp 更直观)。
let amt = posSize;
let amtNote = `全仓 ${posSize}`;
if (amount != null) {
let v = Number(amount);
try { v = Number(ex.amountToPrecision(symbol, v)); } catch {}
if (!(v > 0)) throw new Error(`amount 非法: ${amount}`);
if (v > posSize) { v = posSize; amtNote = `给的数量超过持仓,已 clamp 到全仓 ${posSize}`; }
else amtNote = `部分 ${v} / 持仓 ${posSize}`;
amt = v;
}
// 当前价(用于触发价方向校验)
let cur = null;
try { cur = (await ex.fetchTicker(symbol)).last; } catch {}
// 拿不到现价就无法校验触发价方向(设反会瞬间触发或把止损当止盈)—— 默认中止,不靠 LLM 自觉。
// 确需无校验挂单,显式传 force:true 自负风险。
if (cur == null && !(force === true || force === 'true')) {
throw new Error(`拿不到 ${symbol} 当前价,无法校验止盈止损触发价方向(设反会瞬间触发或方向颠倒)。请稍后重试;确需跳过校验挂单,显式传 "force":true 自负风险。`);
}
// 触发价方向校验:多单止损<现价、止盈>现价;空单反之。设反会瞬间触发或把止损当止盈 → 直接报错。
const orders = [];
const checks = [];
const want = (px, kind) => {
const v = Number(px);
if (!isFinite(v) || v <= 0) throw new Error(`${kind}触发价非法: ${px}`);
if (cur != null) {
const okSide = kind === '止损' ? (isLong ? v < cur : v > cur) : (isLong ? v > cur : v < cur);
const rel = kind === '止损' ? (isLong ? '应 < 现价' : '应 > 现价') : (isLong ? '应 > 现价' : '应 < 现价');
if (!okSide) throw new Error(`${isLong ? '多' : '空'}单的${kind}触发价 ${v} 方向不对(${rel} ${cur})。设反了会瞬间触发或把止损当止盈,请核对。`);
checks.push(`${kind} ${v} ${rel} ${cur} ✓`);
}
return v;
};
if (stop_loss != null) { const v = want(stop_loss, '止损'); orders.push({ kind: '止损', trigger: v, params: { stopLossPrice: v, ...cp } }); }
if (take_profit != null) { const v = want(take_profit, '止盈'); orders.push({ kind: '止盈', trigger: v, params: { takeProfitPrice: v, ...cp } }); }
if (trigger_price != null) {
// 单一触发价:按它落在现价哪一侧 + 持仓方向,自动归类成止损或止盈。
const v = Number(trigger_price);
if (!isFinite(v) || v <= 0) throw new Error(`trigger_price 非法: ${trigger_price}`);
if (cur == null) throw new Error('拿不到当前价,无法自动归类 trigger_price。请改用 stop_loss 或 take_profit 明确指定。');
const kind = (isLong ? v < cur : v > cur) ? '止损' : '止盈';
const key = kind === '止损' ? 'stopLossPrice' : 'takeProfitPrice';
orders.push({ kind, trigger: v, params: { [key]: v, ...cp } });
checks.push(`trigger_price ${v} 按方向归类为「${kind}」(现价 ${cur})`);
}
const modeDesc = cp.positionSide ? `hedge / positionSide=${cp.positionSide}(币安双向,不带 reduceOnly)`
: cp.posSide ? `hedge / posSide=${cp.posSide} + reduceOnly`
: cp.positionIdx ? `hedge / positionIdx=${cp.positionIdx} + reduceOnly`
: cp.hedged ? `hedge / ${exchange}(ccxt 翻向 → tradeSide:Close / offset:close)`
: 'one-way / reduceOnly';
const pending = { exchange, symbol, market_type: mt, closeSide, posSide: pos.side, amount: amt, orders, timestamp: Date.now() };
writeFileSync(pendingFile, JSON.stringify(pending));
return {
_preview: true,
status: '⚠️ 止盈止损未挂单',
持仓: { 交易对: symbol, 方向: isLong ? '多' : '空', 张数: posSize, 开仓价: pos.entryPrice, 当前价: cur, 杠杆: pos.leverage, 未实现盈亏: pos.unrealizedPnl },
将挂条件单: orders.map(o => ({
类型: o.kind, 触发价: o.trigger,
触发后: `市价${closeSide === 'sell' ? '卖出平多' : '买入平空'}`,
数量: mkt?.contractSize ? `${amt} 张` : `${amt}`,
平仓模式: modeDesc,
})),
数量说明: amtNote,
方向校验: checks.length ? checks : '(拿不到当前价,跳过触发价方向校验 —— 请你自己确认方向)',
风险提示: '⚠️ 条件单是交易所服务器端触发的市价单,触发时按当时市价成交,极端行情可能滑点。ccxt 跨所行为有差异,挂单后务必用 stop_orders 或交易所 APP 复核确实挂上了。',
操作指引: '确认无误回复「确认」执行挂单,回复「取消」放弃。',
};
},
// 列出条件单/算法委托(止盈止损等)。OKX 等的算法单不在普通 open_orders 里,用这个查。
stop_orders: async ({ exchange, symbol, market_type }) => {
const ex = await getExchange(exchange, market_type || 'swap');
try { return await fetchConditionalOrders(ex, exchange, symbol); }
catch (e) { throw new Error(`查询条件单失败: ${e?.message || e}。部分交易所的算法/条件单需在交易所 APP 的「条件委托」栏查看。`); }
},
funding_rate: async ({ exchange, symbol, market_type }) => {
const ex = await getExchange(exchange, market_type || 'swap', true);
return ex.fetchFundingRate(symbol);
},
funding_rates: async ({ symbol, exchanges: exList, market_type }) => {
const list = exList ? exList.split(',').map(s => s.trim()) : SUPPORTED;
const sym = symbol || 'BTC/USDT:USDT';
const results = await Promise.allSettled(
list.map(async id => {
try {
const ex = await getExchange(id, market_type || 'swap', true);
const r = await ex.fetchFundingRate(sym);
return { exchange: id, symbol: sym, fundingRate: r.fundingRate, fundingDatetime: r.fundingDatetime, markPrice: r.markPrice };
} catch (e) {
return { exchange: id, symbol: sym, error: e.message };
}
})
);
const rates = results.map(r => r.status === 'fulfilled' ? r.value : { exchange: 'unknown', error: r.reason?.message });
const valid = rates.filter(r => !r.error && r.fundingRate != null);
if (valid.length >= 2) {
valid.sort((a, b) => a.fundingRate - b.fundingRate);
const spread = valid[valid.length - 1].fundingRate - valid[0].fundingRate;
return { rates, arbitrage: { lowestRate: valid[0], highestRate: valid[valid.length - 1], spread, spreadPct: (spread * 100).toFixed(6) + '%', annualized: (spread * 3 * 365 * 100).toFixed(2) + '%' } };
}
return { rates, arbitrage: null, _note: 'Need at least 2 successful rate queries to calculate arbitrage spread' };
},
cancel_order: async ({ exchange, symbol, order_id, market_type }) => {
const ex = await getExchange(exchange, market_type);
if (order_id) {
try { return await ex.cancelOrder(order_id, symbol); }
catch (e) {
// 普通订单端点找不到 → 多半是条件/算法单(币安 algoId、OKX/Bitget algo/plan 单走独立端点)。
// 各所 not-found 信号都回退到 {stop:true}(条件单端点)再撤一次,别只认币安的 -2011。
const m = String(e);
if (/-2011|51400|51401|51402|51603|40109|43001|Unknown order|does not exist|order ?not ?found|订单不存在/i.test(m)) {
return await ex.cancelOrder(order_id, symbol, { stop: true });
}
throw e;
}
}
// 无 id = 全撤: 普通单 + 条件/算法单都要清(set_stop 挂的止盈止损就是条件单)。
const out = {};
// 不支持一键全撤的所(OKX / Hyperliquid 的 cancelAllOrders has===false)别把 NotSupported 吞成假成功 —— 那会留下没撤掉的单。改 fetchOpenOrders 拉出来逐个撤。
const cancelByFetch = async (extra) => {
const orders = await ex.fetchOpenOrders(symbol, undefined, undefined, extra || {});
const res = [];
for (const o of orders) {
try { await ex.cancelOrder(o.id, symbol, extra || {}); res.push({ id: o.id, ok: true }); }
catch (e) { res.push({ id: o.id, ok: false, error: String(e).slice(0, 100) }); }
}
return { canceled: res.filter(r => r.ok).length, total: res.length, detail: res };
};
if (ex.has?.cancelAllOrders) {
try { out.regular = await ex.cancelAllOrders(symbol); } catch (e) { out.regular = { error: String(e).slice(0, 160) }; }
try { out.conditional = await ex.cancelAllOrders(symbol, { stop: true }); } catch (e) { out.conditional = { skipped: String(e).slice(0, 120) }; }
} else {
try { out.regular = await cancelByFetch(); } catch (e) { out.regular = { error: String(e).slice(0, 160) }; }
// 条件单: 用合并查询(覆盖 OKX conditional 类,不漏)再逐个撤
try {
const conds = await fetchConditionalOrders(ex, exchange, symbol);
const res = [];
for (const o of conds) {
try { await ex.cancelOrder(o.id, symbol, { stop: true }); res.push({ id: o.id, ok: true }); }
catch (e) { res.push({ id: o.id, ok: false, error: String(e).slice(0, 100) }); }
}
out.conditional = { canceled: res.filter((r) => r.ok).length, total: res.length, detail: res };
} catch (e) { out.conditional = { skipped: String(e).slice(0, 120) }; }
}
return out;
},
set_leverage: async ({ exchange, symbol, leverage, market_type }) => {
const ex = await getExchange(exchange, market_type);
return ex.setLeverage(leverage, symbol);
},
set_margin_mode: async ({ exchange, symbol, margin_mode, market_type, leverage }) => {
const ex = await getExchange(exchange, market_type);
try {
const modeParams = exchange === 'okx' && leverage ? { lever: String(leverage) } : {};
// OKX isolated mode: try hedge mode first (with posSide), fallback to one-way mode (without posSide)
if (exchange === 'okx' && margin_mode === 'isolated') {
// First try with posSide (hedge mode)
let hedgeModeSuccess = true;
const results = [];
for (const ps of ['long', 'short']) {
try {
results.push(await ex.setMarginMode(margin_mode, symbol, { ...modeParams, posSide: ps }));
} catch (e) {
const m = e.message || String(e);
if (m.includes('already') || m.includes('No need') || m.includes('margin mode is not modified')) {
results.push({ posSide: ps, unchanged: true });
} else if (m.includes('posSide') || m.includes('51000')) {
// posSide error = one-way position mode, try without posSide
hedgeModeSuccess = false;
break;
} else throw e;
}
}
if (hedgeModeSuccess) return { success: true, margin_mode, results };
// Fallback: one-way position mode (no posSide)
try {
const res = await ex.setMarginMode(margin_mode, symbol, modeParams);
return { success: true, margin_mode, response: res };
} catch (e2) {
const m2 = e2.message || String(e2);
if (m2.includes('already') || m2.includes('No need') || m2.includes('margin mode is not modified')) {
return { success: true, margin_mode, message: `已经是 ${margin_mode} 模式,无需切换。` };
}
throw e2;
}
}
const res = await ex.setMarginMode(margin_mode, symbol, modeParams);
if (res?.code === -4046 || res?.msg?.includes('No need to change') || res?.msg?.includes('margin mode is not modified')) {
return { success: true, margin_mode, message: `已经是 ${margin_mode} 模式,无需切换。` };
}
return { success: true, margin_mode, response: res };
} catch (err) {
const msg = err.message || String(err);
if (msg.includes('-4046') || msg.includes('No need to change') || msg.includes('already') || msg.includes('margin mode is not modified')) {
return { success: true, margin_mode, message: `已经是 ${margin_mode} 模式,无需切换。` };
}
throw err;
}
},
set_trading_params: async ({ exchange, symbol, leverage, margin_mode, market_type }) => {
if (!symbol) throw new Error('symbol is required, e.g. BTC/USDT:USDT');
if (!leverage && !margin_mode) throw new Error('At least one of leverage or margin_mode is required');
const ex = await getExchange(exchange, market_type || 'swap');
const results = { symbol, exchange };
// Step 1: Set margin mode FIRST (must be done before leverage on some exchanges)
if (margin_mode) {
const mode = margin_mode.toLowerCase();
if (!['cross', 'isolated'].includes(mode)) throw new Error('margin_mode must be "cross" or "isolated"');
try {
const modeParams = exchange === 'okx' && leverage ? { lever: String(leverage) } : {};
// OKX isolated: try hedge mode first, fallback to one-way mode
if (exchange === 'okx' && mode === 'isolated') {
let hedgeModeSuccess = true;
const modeResults = [];
for (const ps of ['long', 'short']) {
try {
modeResults.push(await ex.setMarginMode(mode, symbol, { ...modeParams, posSide: ps }));
} catch (e) {
const m = e.message || String(e);
if (m.includes('already') || m.includes('No need') || m.includes('margin mode is not modified')) {
modeResults.push({ posSide: ps, unchanged: true });
} else if (m.includes('posSide') || m.includes('51000')) {
hedgeModeSuccess = false;
break;
} else {
modeResults.push({ posSide: ps, error: m });
}
}
}
if (hedgeModeSuccess) {
results.margin_mode = { success: true, mode, details: modeResults };
} else {
// Fallback: one-way position mode
try {
const res = await ex.setMarginMode(mode, symbol, modeParams);
results.margin_mode = { success: true, mode, response: res };
} catch (e2) {
const m2 = e2.message || String(e2);
if (m2.includes('already') || m2.includes('No need') || m2.includes('margin mode is not modified')) {
results.margin_mode = { success: true, mode, message: `已经是 ${mode} 模式` };
} else {
results.margin_mode = { success: false, mode, error: m2 };
}
}
}
} else {
try {
const res = await ex.setMarginMode(mode, symbol, modeParams);
if (res?.code === -4046 || res?.msg?.includes('No need to change')) {
results.margin_mode = { success: true, mode, message: `已经是 ${mode} 模式` };
} else {
results.margin_mode = { success: true, mode, response: res };
}
} catch (e) {
const m = e.message || String(e);
if (m.includes('-4046') || m.includes('No need') || m.includes('already') || m.includes('margin mode is not modified')) {
results.margin_mode = { success: true, mode, message: `已经是 ${mode} 模式` };
} else {
results.margin_mode = { success: false, mode, error: m };
}
}
}
} catch (e) {
results.margin_mode = { success: false, error: e.message || String(e) };
}
}
// Step 2: Set leverage
if (leverage) {
try {
const res = await ex.setLeverage(Number(leverage), symbol);
results.leverage = { success: true, leverage: Number(leverage), response: res };
} catch (e) {
const m = e.message || String(e);
if (m.includes('already') || m.includes('No need') || m.includes('not modified')) {
results.leverage = { success: true, leverage: Number(leverage), message: `已经是 ${leverage}x 杠杆` };
} else {
results.leverage = { success: false, leverage: Number(leverage), error: m };
}
}
}
results.success = (!results.margin_mode || results.margin_mode.success) && (!results.leverage || results.leverage.success);
return results;
},
transfer: async ({ exchange, code, amount, from_account, to_account }) => {
// OKX unified account: no transfer needed
if (exchange === 'okx') {
return {
success: false,
reason: 'OKX_UNIFIED_ACCOUNT',
message: 'OKX 是统一账户,现货和合约共用同一个余额,不需要划转。直接下单即可。',
};
}
if (!from_account || !to_account || !code || amount == null) {
throw new Error('划转需要 code(币种)、amount(数量)、from_account、to_account。例: {"exchange":"binance","code":"USDT","amount":10,"from_account":"spot","to_account":"future"}');
}
const ex = await getExchange(exchange);
// Normalize account names to CCXT-recognized keys
// CCXT Binance only accepts: spot/main, future, delivery, margin/cross, linear, swap, inverse, funding, option
// AI agents may say "futures", "usdm", "coinm" etc. which CCXT misinterprets as isolated margin symbols
const ALIAS = { futures: 'future', usdm: 'future', coinm: 'delivery' };
const fromRaw = from_account.toLowerCase();
const toRaw = to_account.toLowerCase();
const from = ALIAS[fromRaw] || fromRaw;
const to = ALIAS[toRaw] || toRaw;
try {
return await ex.transfer(code, amount, from, to);
} catch (err) {
const msg = err.message || String(err);
// Binance: API key lacks Universal Transfer permission
if (exchange === 'binance' && (msg.includes('-1002') || msg.includes('not authorized'))) {
throw new Error(`Binance 划转失败: API Key 没有万向划转(Universal Transfer)权限。请在 Binance API 管理后台开启「Permits Universal Transfer / 允许万向划转」权限。原始错误: ${msg}`);
}
throw err;
}
},
});
#!/usr/bin/env node
// Exchange Registration — outputs AiCoin referral links
// Usage: node scripts/register.mjs <exchange>
// Example: node scripts/register.mjs okx
const REFERRALS = {
okx: { name: 'OKX', code: 'aicoin20', benefit: '永久返20%手续费', link: 'https://jump.do/zh-Hans/xlink-proxy?id=2' },
binance: { name: 'Binance', code: 'aicoin668', benefit: '返10% + $500', link: 'https://jump.do/zh-Hans/xlink-proxy?id=3' },
bitget: { name: 'Bitget', code: 'hktb3191', benefit: '返10%手续费', link: 'https://jump.do/zh-Hans/xlink-proxy?id=6' },
htx: { name: 'HTX', code: 'j2us6223', benefit: '', link: 'https://jump.do/zh-Hans/xlink-proxy?id=4' },
gate: { name: 'Gate.io', code: 'AICOINGO', benefit: '', link: 'https://jump.do/zh-Hans/xlink-proxy?id=5' },
bitmart: { name: 'Bitmart', code: 'cBMfHE', benefit: '', link: 'https://jump.do/zh-Hans/xlink-proxy?id=13' },
bybit: { name: 'Bybit', code: '34429', benefit: '', link: 'https://jump.do/zh-Hans/xlink-proxy?id=15' },
pionex: { name: 'Pionex', code: '4vgi0zUF', benefit: '', link: 'https://www.pionex.com/zh-CN/signUp?r=4vgi0zUF' },
hyperliquid: { name: 'Hyperliquid', code: 'AICOIN88', benefit: '返4%手续费', link: 'https://app.hyperliquid.xyz/join/AICOIN88' },
okx_dex: { name: 'OKX DEX', code: 'AICOIN88', benefit: '返20%手续费', link: 'https://web3.okx.com/ul/joindex?ref=AICOIN88' },
binance_dex: { name: 'Binance DEX', code: 'SEPRFR9Q', benefit: '返10%手续费', link: 'https://web3.binance.com/referral?ref=SEPRFR9Q' },
aster: { name: 'Aster', code: '9C50e2', benefit: '返5%手续费', link: 'https://www.asterdex.com/zh-CN/referral/9C50e2' },
};
// Normalize input: "OKX" -> "okx", "币安" -> "binance", "火币" -> "htx"
const ALIASES = {
'币安': 'binance', 'bian': 'binance', 'bn': 'binance',
'火币': 'htx', 'huobi': 'htx',
'派网': 'pionex',
'hl': 'hyperliquid',
'gateio': 'gate', 'gate.io': 'gate',
};
const raw = (process.argv[2] || '').trim().toLowerCase();
const key = ALIASES[raw] || raw;
if (!key || key === 'list') {
// List all exchanges
const result = {
message: '以下是所有支持的交易所及 AiCoin 专属注册链接:',
exchanges: Object.values(REFERRALS).map(r => ({
exchange: r.name,
invite_code: r.code,
benefit: r.benefit || '—',
register_link: r.link,
})),
note: '通过以上链接注册可享手续费返还优惠。用法:node scripts/register.mjs <exchange>',
};
console.log(JSON.stringify(result, null, 2));
} else if (REFERRALS[key]) {
const r = REFERRALS[key];
const result = {
exchange: r.name,
invite_code: r.code,
benefit: r.benefit || '—',
register_link: r.link,
steps: [
`打开注册链接:${r.link}`,
'选择手机号或邮箱注册,填入验证码、设置密码',
'进入「账户中心」→「身份验证」完成 KYC',
'如需 API 交易,到「API 管理」创建 API Key,写入 .env 文件',
],
security_note: 'AiCoin API Key 仅用于获取市场数据,无法交易。交易所 API Key 需单独到交易所申请。所有密钥仅保存在本地设备,不会上传。',
};
console.log(JSON.stringify(result, null, 2));
} else {
console.log(JSON.stringify({
error: `未知交易所: ${raw}`,
available: Object.keys(REFERRALS).join(', '),
hint: '用法:node scripts/register.mjs okx',
}));
process.exit(1);
}
#!/usr/bin/env node
// Alias: trade.mjs → exchange.mjs (models often guess "trade" instead of "exchange")
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
import { execFileSync } from 'node:child_process';
const __dir = dirname(fileURLToPath(import.meta.url));
try {
execFileSync(process.execPath, [resolve(__dir, 'exchange.mjs'), ...process.argv.slice(2)], { stdio: 'inherit' });
} catch (e) {
process.exit(e.status || 1);
}
#!/usr/bin/env node
// 订单形状黄金矩阵 —— 用 ccxt createOrderRequest(只建请求体、不下单、不需 API key)断言每家交易所
// 把 coinos 传的参数映射成"正确的 native 订单",ccxt 版本/交易所行为一漂移立刻变红。
//
// 为什么需要它: 7 家交易所的条件单/algo/止盈止损/方向参数是动态长尾,ccxt 是 leaky abstraction。
// 这个矩阵把"人工审查 + 实测一次"固化成可重复的门禁 —— 升级 ccxt 或周期性跑,FAIL 即说明某家行为变了。
//
// 用法: node scripts/verify-order-matrix.mjs (需要能连交易所; 走 HTTPS_PROXY/PROXY_URL 环境变量)
// 退出码: 0 = 全部符合黄金预期; 1 = 有漂移(请核对并更新 GOLDEN 或修 coinos)。
// 注意: 这验证的是"请求体形状"(下单前), 不验证服务器端真实成交 —— 后者靠每所小额实盘 smoke。
import ccxt from 'ccxt';
const PROXY = process.env.PROXY_URL || process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy;
// 触发信号(由 7 家 createOrderRequest 实测归纳;与 exchange.mjs placeOrder 安全网同源)。
// 出现任一 = ccxt 把它当成了真条件单; 全无 = 触发价被静默丢弃(会变立即单, 危险)。
const TRIGGER_SIGNALS = ['trigger', 'stopprice', 'sltriggerpx', 'tptriggerpx', 'plantype', 'algotype', 'callback', 'trailing', 'conditional', 'move_order_stop', 'formula_price', 'stop_market', 'take_profit', 'activationprice'];
const hasTrigger = (req) => { const s = JSON.stringify(req).toLowerCase(); return TRIGGER_SIGNALS.some((x) => s.includes(x)); };
// 黄金预期: 每家 × 条件单类型 → 是否应被 ccxt 当成真条件单(true)还是统一参数下不出(false=coinos 安全网会拒)。
// 实测自 2026-06-09 ccxt 4.5.47。改这张表前先确认是 ccxt 真变好/变坏, 不是手滑。
// 实测自 2026-06-09 ccxt 4.5.47: 7 家都把 stopLossPrice/takeProfitPrice/triggerPrice 映射成各自的 native
// 条件单(binance STOP_MARKET / okx conditional / bybit trigger / bitget plan / gate trigger 对象 /
// htx sl_trigger_price)。trailing 各所参数名不同(callbackRate / trailingPercent / trailingTriggerPrice)。
const GOLDEN = {
binance: { stopLoss: true, takeProfit: true, trigger: true, trailing: true },
okx: { stopLoss: true, takeProfit: true, trigger: true, trailing: true },
bybit: { stopLoss: true, takeProfit: true, trigger: true, trailing: true },
bitget: { stopLoss: true, takeProfit: true, trigger: true, trailing: true },
gate: { stopLoss: true, takeProfit: true, trigger: true, trailing: true },
htx: { stopLoss: true, takeProfit: true, trigger: true, trailing: true },
// Hyperliquid 不纳入严格矩阵: USDC 结算的边角所(多数流程路由到 aicoin-onchain),且其 ccxt
// createOrderRequest 对这些参数形状会内部报错(非可靠信号)。HL 的条件单支持以实盘 smoke 为准。
};
// 方向断言(防 hedge 平仓变反向开仓的回归): 对会翻向的所(bitget/htx), 平多单经 hedge 参数必须出 close 信号。
const DIR_GOLDEN = {
bitget: { params: { reduceOnly: true, hedged: true }, mustInclude: 'tradeside":"close', desc: 'hedge 平多 → tradeSide:Close(不是 Open)' },
htx: { params: { reduceOnly: true, hedged: true }, mustInclude: '"offset":"close', desc: 'hedge 平多 → offset:close' },
};
function pickSwapSymbol(ex) {
// HL 用 USDC 结算(BTC/USDC:USDC), 其它多为 USDT。
for (const s of ['DOGE/USDT:USDT', 'ETH/USDT:USDT', 'BTC/USDT:USDT', 'ETH/USDC:USDC', 'BTC/USDC:USDC', 'SOL/USDC:USDC']) if (ex.markets[s]) return s;
return Object.keys(ex.markets).find((s) => typeof s === 'string' && ex.markets[s] && ex.markets[s].swap && (s.endsWith(':USDT') || s.endsWith(':USDC')));
}
function build(ex, sym, type, side, price, params) {
const fn = typeof ex.createOrderRequest === 'function' ? 'createOrderRequest'
: typeof ex.createContractOrderRequest === 'function' ? 'createContractOrderRequest' : null;
if (!fn) return { _err: 'no request builder' };
try { return ex[fn](sym, type, side, 100, price, params); } catch (e) { return { _err: `${e.constructor.name}: ${e.message.slice(0, 60)}` }; }
}
async function main() {
let failed = 0;
const rows = [];
for (const id of Object.keys(GOLDEN)) {
const g = GOLDEN[id];
let ex, sym, last;
try {
ex = new ccxt[id]({ timeout: 25000, ...(PROXY ? { httpProxy: PROXY } : {}), options: { defaultType: 'swap' } });
ex.has.fetchCurrencies = false;
await ex.loadMarkets();
sym = pickSwapSymbol(ex);
last = (await ex.fetchTicker(sym)).last;
} catch (e) {
rows.push(`${id.padEnd(12)} ⏭ 跳过(连不上/无市场): ${e.message.slice(0, 60)}`);
continue;
}
const dn = last * 0.9, up = last * 1.1;
// Hyperliquid 市价单需参考价(coinos placeOrder 已补; 矩阵也补,才能反映真实行为)。
const mp = id === 'hyperliquid' ? last : undefined;
const cases = [
['stopLoss', () => build(ex, sym, 'market', 'sell', mp, { stopLossPrice: dn })],
['takeProfit', () => build(ex, sym, 'market', 'sell', mp, { takeProfitPrice: up })],
['trigger', () => build(ex, sym, 'market', 'buy', mp, { triggerPrice: up, triggerDirection: 'ascending' })],
['trailing', () => build(ex, sym, 'market', 'sell', mp, { trailingPercent: 1, trailingTriggerPrice: last })],
];
for (const [name, fn] of cases) {
const req = fn();
const got = req._err ? false : hasTrigger(req);
const want = g[name];
const ok = got === want;
if (!ok) failed++;
rows.push(`${id.padEnd(12)} ${name.padEnd(11)} ${ok ? '✅' : '❌'} 期望${want ? '支持' : '不支持'}/实际${got ? '支持' : '不支持'}${req._err ? ' (' + req._err + ')' : ''}`);
}
// 方向回归断言
if (DIR_GOLDEN[id]) {
const d = DIR_GOLDEN[id];
const req = build(ex, sym, 'market', 'sell', undefined, d.params);
const s = JSON.stringify(req).toLowerCase();
const ok = !req._err && s.includes(d.mustInclude);
if (!ok) failed++;
rows.push(`${id.padEnd(12)} ${'dir-close'.padEnd(11)} ${ok ? '✅' : '❌'} ${d.desc}${req._err ? ' (' + req._err + ')' : ''}`);
}
}
console.log('订单形状黄金矩阵(ccxt createOrderRequest, 仅请求体)');
console.log('-'.repeat(72));
for (const r of rows) console.log(r);
console.log('-'.repeat(72));
console.log('注: Hyperliquid 为 USDC 边角所(多走 aicoin-onchain),不纳入严格矩阵,条件单支持以实盘 smoke 为准。');
if (failed) {
console.log(`\n❌ ${failed} 项与黄金预期不符 —— ccxt 行为可能变了。核对后更新 GOLDEN 或修 exchange.mjs(尤其安全网/closeParamsFor)。`);
process.exit(1);
}
console.log('\n✅ 全部符合黄金预期(6 家 CEX 的条件单映射 + bitget/htx hedge 平仓方向未漂移)。');
}
main().catch((e) => { console.error('matrix error:', e.message); process.exit(2); });
Related skills
FAQ
Does aicoin-trading support paper trading?
aicoin-trading covers disciplined workflows for both live and paper crypto strategies. Developers can run signal, sizing, execution-check, and risk-guardrail steps without committing real capital during iteration.
What does aicoin-trading automate for developers?
aicoin-trading automates crypto trading workflow steps: signal evaluation, position sizing, execution validation, and risk guardrails. The skill targets agent-operated strategies rather than one-off discretionary trades.