@chongqingliangyunzhijing/ssquant-cli 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/bin/ssquant.js +192 -0
- package/package.json +26 -4
- package/src/auth.js +167 -0
- package/src/commands/account.js +190 -0
- package/src/commands/mandate.js +92 -0
- package/src/commands/plan.js +68 -0
- package/src/commands/sim.js +45 -0
- package/src/commands/system.js +25 -0
- package/src/commands/trade.js +268 -0
- package/src/config.js +115 -0
- package/src/http.js +217 -0
- package/src/output.js +133 -0
- package/src/version.js +7 -0
package/README.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# SSQUANT 期货仿真 CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
使用 Node.js 18 或更高版本安装 `npm install -g @chongqingliangyunzhijing/ssquant-cli`。
|
|
4
|
+
执行 `ssquant login` 登录 PandaAI,再执行 `ssquant account list --json` 查看账户名额。`ssquant account create` 交互输入初始资金,单位万元,范围 1 至 10000。`ssquant account use <账户号>` 选择交易账户。
|
|
5
|
+
|
|
6
|
+
## 发给 AI
|
|
7
|
+
|
|
8
|
+
> 请完整读取 https://www.pandaaiquant.com/ssquant/openapi/v1/agent-install-guide/cli ,按指引帮我安装 SSQUANT 期货仿真交易 CLI,使用我的 PandaAI 账号登录。请完成安装和环境检查,需要浏览器登录、验证码或授权时提醒我。登录后列出账户和剩余名额。如果我需要开户,问我初始资金(万元,1 至 10000),换算成人民币元,等我确认再创建。最后告诉我账户和就绪状态。本次仅做接入和开户。
|
|
9
|
+
|
|
10
|
+
运行 `ssquant --help` 查看交易命令。查询、开户、交易计划和操作状态支持 `--json`。
|
|
11
|
+
删除使用 `ssquant account delete <账户号>`。确认后释放本地名额,柜台已有委托仍可能成交。
|
package/bin/ssquant.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { EXIT_BIZ, fail, setJsonMode, setUpdateNotice } from '../src/output.js';
|
|
4
|
+
import * as sys from '../src/commands/system.js';
|
|
5
|
+
import * as acct from '../src/commands/account.js';
|
|
6
|
+
import * as trade from '../src/commands/trade.js';
|
|
7
|
+
import * as mandate from '../src/commands/mandate.js';
|
|
8
|
+
import * as plans from '../src/commands/plan.js';
|
|
9
|
+
import { CLIENT_VERSION } from '../src/version.js';
|
|
10
|
+
import * as sim from '../src/commands/sim.js';
|
|
11
|
+
|
|
12
|
+
const program = new Command();
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.name('ssquant')
|
|
16
|
+
.description('SSQUANT 期货仿真交易')
|
|
17
|
+
.version(CLIENT_VERSION)
|
|
18
|
+
.option('--json', '以 JSON 输出,便于 Agent 解析')
|
|
19
|
+
.hook('preAction', async (thisCmd, actionCmd) => {
|
|
20
|
+
setJsonMode(!!thisCmd.opts().json);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/** 统一异常出口:把 ApiError 的 code/suggestion 透出给 Agent */
|
|
24
|
+
function run(fn) {
|
|
25
|
+
return async (...args) => {
|
|
26
|
+
try {
|
|
27
|
+
await fn(...args);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
fail(e, e.exitCode || EXIT_BIZ);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── 认证与环境 ──
|
|
35
|
+
program.command('login')
|
|
36
|
+
.description('授权登录(浏览器)')
|
|
37
|
+
.action(run(sys.login));
|
|
38
|
+
|
|
39
|
+
program.command('logout').description('登出并清除本地凭证').action(run(async () => sys.logout()));
|
|
40
|
+
program.command('whoami').description('查看登录状态与绑定账户').action(run(sys.whoami));
|
|
41
|
+
program.command('doctor').description('连通性与状态自检').action(run(sys.doctor));
|
|
42
|
+
const plan = program.command('plan').description('冻结交易计划');
|
|
43
|
+
plan.command('create')
|
|
44
|
+
.requiredOption('--operation <name>', 'place_order/cancel_order/cancel_all/close_all/rollover/target_position')
|
|
45
|
+
.option('--parameters <json>', '计划参数 JSON', '{}')
|
|
46
|
+
.description('生成计划,不执行真实交易')
|
|
47
|
+
.action(run(plans.create));
|
|
48
|
+
plan.command('show').argument('<planId>').description('查看计划').action(run(plans.show));
|
|
49
|
+
plan.command('execute').argument('<planId>').description('确认并执行计划')
|
|
50
|
+
.option('--client-request-id <id>', '网络重试复用的幂等号')
|
|
51
|
+
.requiredOption('-y, --yes', '确认执行该冻结计划')
|
|
52
|
+
.action(run(plans.execute));
|
|
53
|
+
|
|
54
|
+
const operation = program.command('operation').description('交易操作状态');
|
|
55
|
+
operation.command('show').argument('<operationId>').description('查询操作回报')
|
|
56
|
+
.action(run(plans.operationShow));
|
|
57
|
+
operation.command('watch').argument('<operationId>').description('按间隔轮询操作回报')
|
|
58
|
+
.option('--interval <seconds>', '轮询间隔,至少1秒', '2')
|
|
59
|
+
.option('--max-polls <n>', '最多轮询次数', '10')
|
|
60
|
+
.action(run(plans.operationWatch));
|
|
61
|
+
|
|
62
|
+
// ── 账户 ──
|
|
63
|
+
program.command('varieties')
|
|
64
|
+
.description('列出全部品种,或按中文、品种码搜索候选')
|
|
65
|
+
.option('-q, --query <text>', '搜索中文品种名、别名或品种码')
|
|
66
|
+
.option('--exchange <code>', '按交易所筛选,如 SHFE、DCE')
|
|
67
|
+
.option('--prefix', '仅按前缀联想', false)
|
|
68
|
+
.action(run(acct.varieties));
|
|
69
|
+
program.command('resolve')
|
|
70
|
+
.argument('<query>', '品种名称、代码或指定月份合约')
|
|
71
|
+
.description('识别品种并返回歧义候选,不查询主力或下单')
|
|
72
|
+
.action(run(acct.resolveVariety));
|
|
73
|
+
const accounts = program.command('account').description('仿真账户管理与资金查询').action(run(acct.account));
|
|
74
|
+
accounts.command('list').description('账户列表与可用名额').action(run(sim.list));
|
|
75
|
+
accounts.command('create').option('--capital-wan <amount>', '初始资金(万元)').option('--request-id <key>', '开户幂等号').option('-y, --yes', '确认开户').action(run(sim.create));
|
|
76
|
+
accounts.command('use <id>').description('选择当前交易账户').action(run(sim.use));
|
|
77
|
+
accounts.command('delete <id>').option('-y, --yes', '确认本地删除').action(run(sim.remove));
|
|
78
|
+
accounts.command('status <id>').description('查看开户进度').action(run(sim.status));
|
|
79
|
+
program.command('equity').action(run(sim.equity));
|
|
80
|
+
program.command('settlement').option('--date <yyyymmdd>').action(run(sim.settlement));
|
|
81
|
+
program.command('position').description('持仓').action(run(acct.position));
|
|
82
|
+
program.command('positions').description('持仓(position 的别名)').action(run(acct.position));
|
|
83
|
+
program.command('snapshot').description('资金 + 持仓 + 挂单,一次拿齐').action(run(acct.snapshot));
|
|
84
|
+
program.command('quote')
|
|
85
|
+
.argument('<symbol>', '中文品种名、品种码或实际合约,如 黄金、AU、au2610')
|
|
86
|
+
.description('查询单品种最新行情')
|
|
87
|
+
.action(run(acct.quote));
|
|
88
|
+
|
|
89
|
+
program.command('contract-spec')
|
|
90
|
+
.argument('<symbol>', '中文品种名、品种码或实际合约,如 纯碱、SA、SA2701.CZC')
|
|
91
|
+
.description('查询合约乘数、参考保证金和最后交易日')
|
|
92
|
+
.action(run(acct.contractSpec));
|
|
93
|
+
|
|
94
|
+
program.command('orders')
|
|
95
|
+
.description('委托查询')
|
|
96
|
+
.option('-s, --status <status>', 'active=在途挂单 / filled / cancelled / all')
|
|
97
|
+
.option('-d, --date <yyyymmdd>', '交易日')
|
|
98
|
+
.option('--last-id <id>', '游标:上一页最后一条委托的 id')
|
|
99
|
+
.option('--order-id <id>', '按柜台委托号精确查询')
|
|
100
|
+
.option('--client-order-id <id>', '按幂等号精确查询')
|
|
101
|
+
.option('-c, --count <n>', '条数(历史订单默认最近 20 条;挂单默认全部)')
|
|
102
|
+
.action(run(acct.orders));
|
|
103
|
+
|
|
104
|
+
program.command('trades')
|
|
105
|
+
.description('成交查询')
|
|
106
|
+
.option('-d, --date <yyyymmdd>', '交易日')
|
|
107
|
+
.option('--last-id <id>', '游标:上一页最后一条成交的 id')
|
|
108
|
+
.option('-c, --count <n>', '条数,默认最近 20 条')
|
|
109
|
+
.action(run(acct.trades));
|
|
110
|
+
|
|
111
|
+
program.command('settlements')
|
|
112
|
+
.description('每日结算明细,默认最近 5 个交易日,最多 30 个交易日')
|
|
113
|
+
.option('--limit <n>', '交易日数量', '5')
|
|
114
|
+
.option('--from-date <date>', '起始交易日 YYYY-MM-DD 或 YYYYMMDD')
|
|
115
|
+
.option('--to-date <date>', '结束交易日 YYYY-MM-DD 或 YYYYMMDD')
|
|
116
|
+
.action(run(acct.settlements));
|
|
117
|
+
|
|
118
|
+
// ── 交易 ──
|
|
119
|
+
program.command('order')
|
|
120
|
+
.description('下单')
|
|
121
|
+
.requiredOption('--symbol <code-or-name>', '实际合约代码,或开仓品种名/品种码,例如 rb2610、原油、SC')
|
|
122
|
+
.requiredOption('--direction <buy|sell>', '买卖方向')
|
|
123
|
+
.requiredOption('--offset <open|close>', '开平仓')
|
|
124
|
+
.option('--type <limit|market>', '兼容参数;传价格自动限价 GFD,不传价格自动市价 IOC')
|
|
125
|
+
.option('--price <price>', '传入即按 GFD 限价单;不传即按 IOC 市价单')
|
|
126
|
+
.requiredOption('--volume <n>', '手数')
|
|
127
|
+
.option('--dry-run', '预演:走完全部校验但不真下单', false)
|
|
128
|
+
.option('--client-order-id <id>', '幂等号')
|
|
129
|
+
.option('-y, --yes', '跳过交互确认(Agent 用)', false)
|
|
130
|
+
.action(run(trade.order));
|
|
131
|
+
|
|
132
|
+
program.command('cancel')
|
|
133
|
+
.argument('<orderId>', '委托号')
|
|
134
|
+
.description('撤单')
|
|
135
|
+
.action(run(trade.cancel));
|
|
136
|
+
|
|
137
|
+
program.command('cancel-all')
|
|
138
|
+
.description('撤掉当前全部挂单(服务端编排)')
|
|
139
|
+
.option('--symbol <code>', '只撤指定合约')
|
|
140
|
+
.option('--client-request-id <id>', '批次幂等号,重试时复用')
|
|
141
|
+
.option('--dry-run', '只预演并列出将撤销的委托号', false)
|
|
142
|
+
.option('-y, --yes', '跳过交互确认', false)
|
|
143
|
+
.action(run(trade.cancelAll));
|
|
144
|
+
|
|
145
|
+
program.command('close-all')
|
|
146
|
+
.description('一键平仓(Agent 止损常用)')
|
|
147
|
+
.option('--symbol <code>', '只平该合约')
|
|
148
|
+
.option('--client-request-id <id>', '批次幂等号,重试时复用')
|
|
149
|
+
.option('--dry-run', '预演', false)
|
|
150
|
+
.option('-y, --yes', '跳过交互确认', false)
|
|
151
|
+
.action(run(trade.closeAll));
|
|
152
|
+
|
|
153
|
+
program.command('rollover')
|
|
154
|
+
.description('按手数移仓到当前主力合约')
|
|
155
|
+
.option('--symbol <name>', '品种别名或品种码;不传则扫描全部持仓')
|
|
156
|
+
.option('--volume <n>', '移仓手数')
|
|
157
|
+
.option('--direction <long|short>', '多头或空头')
|
|
158
|
+
.option('--from-contract <code>', '多个旧合约时指定旧合约')
|
|
159
|
+
.option('--client-request-id <id>', '批次幂等号,真实执行重试时复用')
|
|
160
|
+
.option('--dry-run', '只预演,不执行', false)
|
|
161
|
+
.option('-y, --yes', '跳过交互确认', false)
|
|
162
|
+
.action(run(trade.rollover));
|
|
163
|
+
|
|
164
|
+
program.command('target-position')
|
|
165
|
+
.description('把指定品种调到目标多空仓位(默认预演)')
|
|
166
|
+
.requiredOption('--targets <json>', 'JSON 数组,例如 [{"symbol":"黄金","direction":"long","volume":5}]')
|
|
167
|
+
.option('--client-request-id <id>', '批次幂等号,重试时复用')
|
|
168
|
+
.option('--dry-run', '只预演,不执行', false)
|
|
169
|
+
.option('-y, --yes', '跳过交互确认', false)
|
|
170
|
+
.action(run(trade.targetPosition));
|
|
171
|
+
|
|
172
|
+
// ── 授权边界 ──
|
|
173
|
+
const md = program.command('mandate').description('查看自动交易授权状态(安装后自动启用)');
|
|
174
|
+
md.command('show').description('查看当前授权状态').action(run(mandate.show));
|
|
175
|
+
md.command('create')
|
|
176
|
+
.description('兼容旧版:手动声明自定义授权边界(正常安装无需执行)')
|
|
177
|
+
.requiredOption('--symbols <list>', '允许交易的合约,逗号分隔')
|
|
178
|
+
.option('--direction <long|short|both>', '允许方向', 'both')
|
|
179
|
+
.requiredOption('--max-volume-per-order <n>', '单笔最大手数')
|
|
180
|
+
.requiredOption('--max-orders-per-day <n>', '单日最大委托笔数')
|
|
181
|
+
.requiredOption('--max-position <n>', '总持仓手数上限')
|
|
182
|
+
.option('--max-margin-ratio <r>', '保证金占用比上限,如 0.6')
|
|
183
|
+
.option('--stop-loss <r>', '权益回撤触及即冻结授权,如 0.15')
|
|
184
|
+
.requiredOption('--expires <iso>', '失效时间,如 2026-08-10T15:00:00')
|
|
185
|
+
.option('-y, --yes', '跳过交互确认(仅供选手自己写脚本,skill 禁止 Agent 使用)', false)
|
|
186
|
+
.action(run(mandate.create));
|
|
187
|
+
md.command('revoke').description('紧急撤销授权(立即生效)')
|
|
188
|
+
.action(run(mandate.revoke));
|
|
189
|
+
|
|
190
|
+
program.parseAsync(process.argv).catch((e) => {
|
|
191
|
+
fail(e, e.exitCode || EXIT_BIZ);
|
|
192
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chongqingliangyunzhijing/ssquant-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "SSQUANT 期货仿真交易命令行工具",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ssquant": "bin/ssquant.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public",
|
|
19
|
+
"registry": "https://registry.npmjs.org/"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "node --test test/*.mjs"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"commander": "^12.1.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 登录:唯一入口是浏览器 OAuth2.1 + PKCE。
|
|
3
|
+
*
|
|
4
|
+
* 身份始终在 PandaAI 官网浏览器页面完成,CLI 只拿到本服务签发的 pt_ token。
|
|
5
|
+
* 当前不提供设备码或其他远程授权方式。
|
|
6
|
+
*/
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import http from 'node:http';
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import { apiUrl, clientId, saveCredentials } from './config.js';
|
|
11
|
+
import { ApiError, requestAnon } from './http.js';
|
|
12
|
+
import { say } from './output.js';
|
|
13
|
+
|
|
14
|
+
export const LOGIN_HOME_URL = 'https://www.pandaaiquant.com/ssquant/openapi/v1/login-complete';
|
|
15
|
+
|
|
16
|
+
function b64url(buf) {
|
|
17
|
+
return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function makePkce() {
|
|
21
|
+
const verifier = b64url(crypto.randomBytes(48));
|
|
22
|
+
const challenge = b64url(crypto.createHash('sha256').update(verifier).digest());
|
|
23
|
+
return { verifier, challenge };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function browserLaunchSpec(url, platform = process.platform) {
|
|
27
|
+
// Windows 不使用 `cmd /c start` 或 explorer.exe:前者会解析 URL 中的
|
|
28
|
+
// `&`,后者在部分系统会打开资源管理器。系统 URL 协议处理器能保留完整参数。
|
|
29
|
+
const cmd = platform === 'win32' ? 'rundll32.exe'
|
|
30
|
+
: platform === 'darwin' ? 'open' : 'xdg-open';
|
|
31
|
+
const args = platform === 'win32'
|
|
32
|
+
? ['url.dll,FileProtocolHandler', url]
|
|
33
|
+
: [url];
|
|
34
|
+
return { cmd, args };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function openBrowser(url) {
|
|
38
|
+
const { cmd, args } = browserLaunchSpec(url);
|
|
39
|
+
try {
|
|
40
|
+
const p = spawn(cmd, args, { detached: true, stdio: 'ignore' });
|
|
41
|
+
p.unref();
|
|
42
|
+
return true;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function hasDisplay() {
|
|
49
|
+
if (process.platform === 'win32' || process.platform === 'darwin') return true;
|
|
50
|
+
// Linux 上没有 DISPLAY / WAYLAND_DISPLAY 基本就是无头服务器
|
|
51
|
+
return !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** ── 浏览器授权 ────────────────────────────────────────── */
|
|
55
|
+
export async function loginBrowser({ scope = 'futures:read futures:trade', timeout = 300 } = {}) {
|
|
56
|
+
if (!hasDisplay()) {
|
|
57
|
+
throw new ApiError('no_browser',
|
|
58
|
+
'当前 CLI 登录仅支持本机浏览器 OAuth,请在有图形界面的电脑上执行 ssquant login',
|
|
59
|
+
{ suggestion: '请在桌面环境运行 ssquant login,不要使用其他授权方式' });
|
|
60
|
+
}
|
|
61
|
+
const { verifier, challenge } = makePkce();
|
|
62
|
+
const state = b64url(crypto.randomBytes(16));
|
|
63
|
+
|
|
64
|
+
const { server, port, waitForCode } = await startCallbackServer(timeout);
|
|
65
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
66
|
+
|
|
67
|
+
const url = apiUrl('/oauth/authorize') + '?' + new URLSearchParams({
|
|
68
|
+
response_type: 'code',
|
|
69
|
+
client_id: clientId(),
|
|
70
|
+
redirect_uri: redirectUri,
|
|
71
|
+
code_challenge: challenge,
|
|
72
|
+
code_challenge_method: 'S256',
|
|
73
|
+
scope,
|
|
74
|
+
state,
|
|
75
|
+
}).toString();
|
|
76
|
+
|
|
77
|
+
say('正在打开浏览器完成授权…');
|
|
78
|
+
say(`若没有自动打开,请手动访问:\n ${url}\n`);
|
|
79
|
+
openBrowser(url);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const { code, state: got } = await waitForCode;
|
|
83
|
+
if (got !== state) {
|
|
84
|
+
throw new ApiError('state_mismatch', '授权回调 state 不匹配,可能遭到 CSRF,已中止');
|
|
85
|
+
}
|
|
86
|
+
const { data } = await requestAnon('/oauth/token', {
|
|
87
|
+
form: true,
|
|
88
|
+
body: {
|
|
89
|
+
grant_type: 'authorization_code',
|
|
90
|
+
code,
|
|
91
|
+
code_verifier: verifier,
|
|
92
|
+
redirect_uri: redirectUri,
|
|
93
|
+
client_id: clientId(),
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
return persist(data);
|
|
97
|
+
} finally {
|
|
98
|
+
server.close();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function startCallbackServer(timeoutSec) {
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
let settle;
|
|
105
|
+
const waitForCode = new Promise((res, rej) => {
|
|
106
|
+
settle = { res, rej };
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const server = http.createServer((req, res) => {
|
|
110
|
+
const u = new URL(req.url, 'http://127.0.0.1');
|
|
111
|
+
if (!u.pathname.startsWith('/callback')) {
|
|
112
|
+
res.writeHead(404).end();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const err = u.searchParams.get('error');
|
|
116
|
+
const html = (msg) =>
|
|
117
|
+
`<!doctype html><meta charset="utf-8"><body style="font-family:system-ui;padding:40px">
|
|
118
|
+
<h2>${msg}</h2><p>可以关闭本页面,回到命令行继续。</p></body>`;
|
|
119
|
+
if (err) {
|
|
120
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
|
|
121
|
+
.end(html('✗ 授权被拒绝'));
|
|
122
|
+
settle.rej(new ApiError('access_denied', '你在浏览器中拒绝了授权'));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// 回调只负责交付一次性 code;交付后离开 localhost 页面,避免把 code
|
|
126
|
+
// 留在地址栏或让用户看到一个简陋的本地确认页。
|
|
127
|
+
res.writeHead(303, {
|
|
128
|
+
Location: LOGIN_HOME_URL,
|
|
129
|
+
'Cache-Control': 'no-store',
|
|
130
|
+
'Referrer-Policy': 'no-referrer',
|
|
131
|
+
}).end();
|
|
132
|
+
settle.res({
|
|
133
|
+
code: u.searchParams.get('code'),
|
|
134
|
+
state: u.searchParams.get('state'),
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const timer = setTimeout(() => {
|
|
139
|
+
settle.rej(new ApiError('timeout', `等待授权超时(${timeoutSec}s)`,
|
|
140
|
+
{ suggestion: '请重新执行 ssquant login,并在浏览器中完成授权' }));
|
|
141
|
+
server.close();
|
|
142
|
+
}, timeoutSec * 1000);
|
|
143
|
+
waitForCode.finally(() => clearTimeout(timer)).catch(() => {});
|
|
144
|
+
|
|
145
|
+
// 端口交给系统分配:固定端口会在多开时冲突
|
|
146
|
+
server.listen(0, '127.0.0.1', () => {
|
|
147
|
+
resolve({ server, port: server.address().port, waitForCode });
|
|
148
|
+
});
|
|
149
|
+
server.on('error', reject);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function persist(d) {
|
|
154
|
+
const saved = {
|
|
155
|
+
accessToken: d.access_token,
|
|
156
|
+
refreshToken: d.refresh_token,
|
|
157
|
+
tokenType: d.token_type || 'Bearer',
|
|
158
|
+
expiresAt: Date.now() + (d.expires_in || 2592000) * 1000,
|
|
159
|
+
scope: d.scope,
|
|
160
|
+
accountId: d.account_id,
|
|
161
|
+
market: d.market,
|
|
162
|
+
clientId: clientId(),
|
|
163
|
+
savedAt: new Date().toISOString(),
|
|
164
|
+
};
|
|
165
|
+
saveCredentials(saved);
|
|
166
|
+
return saved;
|
|
167
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/** 账户 / 持仓 / 委托 / 成交查询。 */
|
|
2
|
+
import { qs, request } from '../http.js';
|
|
3
|
+
import { emit, kv, num, pct, say, table } from '../output.js';
|
|
4
|
+
|
|
5
|
+
export async function varieties(opts) {
|
|
6
|
+
const { data } = await request(`/futures/varieties${qs({ q: opts.query, exchange: opts.exchange,
|
|
7
|
+
prefix: opts.prefix ? 'true' : undefined })}`);
|
|
8
|
+
emit(data);
|
|
9
|
+
table(['品种码', '名称', '交易所', '目录状态'], data.items.map(item =>
|
|
10
|
+
[item.code, item.name, item.exchange, item.enabled ? '启用' : '未启用']));
|
|
11
|
+
say(`共 ${data.total} 项;品种目录不代表当前合约已经通过交易校验。`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function resolveVariety(query) {
|
|
15
|
+
const { data } = await request(`/futures/resolve${qs({ q: query })}`);
|
|
16
|
+
emit(data);
|
|
17
|
+
kv([['识别状态', data.status], ['输入', data.input]]);
|
|
18
|
+
if (data.variety) kv([['品种', `${data.variety.name} ${data.variety.code}`],
|
|
19
|
+
['交易所', data.variety.exchange], ['指定合约', data.specifiedContract || '未指定月份']]);
|
|
20
|
+
if (data.candidates?.length) {
|
|
21
|
+
table(['候选品种码', '名称', '交易所'], data.candidates.map(item => [item.code, item.name, item.exchange]));
|
|
22
|
+
say('请明确选择候选品种的名称或代码;此次识别不生成交易委托。');
|
|
23
|
+
}
|
|
24
|
+
if (data.message) say(data.message);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function account() {
|
|
28
|
+
const { data } = await request('/futures/account');
|
|
29
|
+
emit(data);
|
|
30
|
+
kv([
|
|
31
|
+
['账户', data.accountId ?? '-'],
|
|
32
|
+
['动态权益', num(data.totalProfit ?? data.equity)],
|
|
33
|
+
['可用资金', num(data.availableFunds)],
|
|
34
|
+
['占用保证金', num(data.margin)],
|
|
35
|
+
['风险度', pct(data.riskRate)],
|
|
36
|
+
['当日盈亏', num(data.dailyPnl)],
|
|
37
|
+
['持仓盈亏', num(data.holdingPnl)],
|
|
38
|
+
['累计盈亏', num(data.addProfit)],
|
|
39
|
+
['手续费(当日)', num(data.cost)],
|
|
40
|
+
]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function showQuote(data) {
|
|
44
|
+
if (!data?.ready) {
|
|
45
|
+
say(`最新行情:${data?.message || '暂无行情快照'}`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
kv([
|
|
49
|
+
['品种', `${data.name || data.variety || '-'} ${data.symbol || data.contractCode}`],
|
|
50
|
+
['最新价', num(data.latestPrice ?? data.last)],
|
|
51
|
+
['涨跌', `${num(data.change)} (${pct(data.changeRate)})`],
|
|
52
|
+
['今开 / 高 / 低', `${num(data.open)} / ${num(data.high)} / ${num(data.low)}`],
|
|
53
|
+
['买一', `${num(data.bidPrice1)} / ${data.bidVolume1 ?? 0} 手`],
|
|
54
|
+
['卖一', `${num(data.askPrice1)} / ${data.askVolume1 ?? 0} 手`],
|
|
55
|
+
['成交量 / 持仓量', `${data.volume ?? 0} / ${data.openInterest ?? 0}`],
|
|
56
|
+
['行情时间(上海)', data.quoteTime || '-'],
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function quote(symbol) {
|
|
61
|
+
const { data } = await request(`/futures/quote${qs({ symbol })}`);
|
|
62
|
+
emit(data);
|
|
63
|
+
showQuote(data);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function contractSpec(symbol) {
|
|
67
|
+
const { data } = await request(`/futures/contract-spec${qs({ symbol })}`);
|
|
68
|
+
emit(data);
|
|
69
|
+
showContractSpec(data);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function showContractSpec(data) {
|
|
73
|
+
const value = (v, scale = 1, unit = '') => v == null ? '暂无数据'
|
|
74
|
+
: new Intl.NumberFormat('zh-CN', { useGrouping: false, maximumFractionDigits: 8 }).format(v * scale) + unit;
|
|
75
|
+
const margin = data.margin;
|
|
76
|
+
const day = data.lastTradeDate;
|
|
77
|
+
kv([
|
|
78
|
+
['合约', data.symbol],
|
|
79
|
+
['合约乘数', value(data.contractMultiplier)],
|
|
80
|
+
['多头保证金率', value(margin.longMarginRatioByMoney, 100, '%')],
|
|
81
|
+
['空头保证金率', value(margin.shortMarginRatioByMoney, 100, '%')],
|
|
82
|
+
['多头每手保证金', value(margin.longMarginByVolume, 1, ' 元/手')],
|
|
83
|
+
['空头每手保证金', value(margin.shortMarginByVolume, 1, ' 元/手')],
|
|
84
|
+
['最后交易日', day ? `${day.slice(0, 4)}-${day.slice(4, 6)}-${day.slice(6)}` : '暂无数据'],
|
|
85
|
+
]);
|
|
86
|
+
if (Object.hasOwn(data, 'deliveryDate')) {
|
|
87
|
+
const delivery = data.deliveryDate;
|
|
88
|
+
kv([['交割日', delivery ? `${delivery.slice(0, 4)}-${delivery.slice(4, 6)}-${delivery.slice(6)}` : '暂无数据']]);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function position() {
|
|
93
|
+
const { data } = await request('/futures/positions');
|
|
94
|
+
emit(data);
|
|
95
|
+
const rows = (data || []).map((p) => [
|
|
96
|
+
p.contractCode,
|
|
97
|
+
p.direction === 'long' ? '多' : '空',
|
|
98
|
+
p.volume ?? p.position ?? 0,
|
|
99
|
+
p.todayVolume ?? p.tdPosition ?? 0,
|
|
100
|
+
p.closable ?? p.sellable ?? 0,
|
|
101
|
+
num(p.openPrice ?? p.avgPrice),
|
|
102
|
+
num(p.lastPrice),
|
|
103
|
+
num(p.holdingPnl),
|
|
104
|
+
num(p.openMarketValue),
|
|
105
|
+
pct(p.holdingPnlRate),
|
|
106
|
+
num(p.margin),
|
|
107
|
+
]);
|
|
108
|
+
table(['合约', '方向', '持仓', '今仓', '可平', '开仓价', '最新价', '浮盈', '开仓市值', '持仓收益率', '保证金'], rows);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function orders(opts) {
|
|
112
|
+
const status = opts.status;
|
|
113
|
+
const path = status === 'active' || status === 'open'
|
|
114
|
+
? `/futures/orders/open${qs({ count: opts.count })}`
|
|
115
|
+
: `/futures/orders${qs({ status, tradeDate: opts.date, lastId: opts.lastId,
|
|
116
|
+
orderId: opts.orderId, clientOrderId: opts.clientOrderId, count: opts.count })}`;
|
|
117
|
+
const { data, meta } = await request(path);
|
|
118
|
+
emit(data, meta);
|
|
119
|
+
const rows = (data || []).map((o) => [
|
|
120
|
+
o.orderId,
|
|
121
|
+
o.contractCode,
|
|
122
|
+
o.exchange || '',
|
|
123
|
+
o.tradeDirectionText || `${o.sideText || o.side}${o.offsetText || o.offset}`,
|
|
124
|
+
o.volume ?? o.quantity,
|
|
125
|
+
o.filledVolume ?? o.filledQuantity ?? 0,
|
|
126
|
+
num(o.price),
|
|
127
|
+
o.statusText || o.status,
|
|
128
|
+
o.orderTime || '',
|
|
129
|
+
o.executionSourceText || '',
|
|
130
|
+
o.message || '',
|
|
131
|
+
]);
|
|
132
|
+
table(['委托号', '合约', '交易所', '方向', '手数', '已成', '价格', '状态', '委托时间', '来源', '说明'], rows);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function trades(opts) {
|
|
136
|
+
const { data, meta } = await request(
|
|
137
|
+
`/futures/trades${qs({ tradeDate: opts.date, lastId: opts.lastId, count: opts.count })}`);
|
|
138
|
+
emit(data, meta);
|
|
139
|
+
const rows = (data || []).map((t) => [
|
|
140
|
+
t.tradeId,
|
|
141
|
+
t.orderId || '',
|
|
142
|
+
t.contractCode,
|
|
143
|
+
t.exchange || '',
|
|
144
|
+
t.tradeDirectionText || `${t.sideText || t.side}${t.offsetText || t.offset}`,
|
|
145
|
+
t.volume,
|
|
146
|
+
num(t.price),
|
|
147
|
+
num(t.cost ?? t.commission),
|
|
148
|
+
t.tradeTime || '',
|
|
149
|
+
t.executionSourceText || '',
|
|
150
|
+
]);
|
|
151
|
+
table(['成交号', '委托号', '合约', '交易所', '方向', '手数', '成交价', '手续费', '成交时间', '来源'], rows);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function snapshot() {
|
|
155
|
+
const { data } = await request('/futures/snapshot');
|
|
156
|
+
emit(data);
|
|
157
|
+
const a = data.account || {};
|
|
158
|
+
say('【资金】');
|
|
159
|
+
kv([
|
|
160
|
+
['动态权益', num(a.totalProfit ?? a.equity)],
|
|
161
|
+
['可用资金', num(a.availableFunds)],
|
|
162
|
+
['占用保证金', num(a.margin)],
|
|
163
|
+
['风险度', pct(a.riskRate)],
|
|
164
|
+
]);
|
|
165
|
+
say('\n【持仓】');
|
|
166
|
+
table(['合约', '方向', '持仓', '可平', '开仓价', '浮盈', '开仓市值', '持仓收益率'],
|
|
167
|
+
(data.positions || []).map((p) => [
|
|
168
|
+
p.contractCode, p.direction === 'long' ? '多' : '空',
|
|
169
|
+
p.volume ?? p.position, p.closable ?? p.sellable,
|
|
170
|
+
num(p.openPrice ?? p.avgPrice), num(p.holdingPnl), num(p.openMarketValue), pct(p.holdingPnlRate),
|
|
171
|
+
]));
|
|
172
|
+
say('\n【挂单】');
|
|
173
|
+
table(['委托号', '合约', '方向', '手数', '未成'],
|
|
174
|
+
(data.openOrders || []).map((o) => [
|
|
175
|
+
o.orderId, o.contractCode, `${o.sideText || o.side}${o.offsetText || o.offset}`,
|
|
176
|
+
o.volume ?? o.quantity, o.unfilledVolume ?? o.unfilledQuantity,
|
|
177
|
+
]));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function settlements(opts) {
|
|
181
|
+
const { data } = await request(`/futures/settlements${qs({
|
|
182
|
+
limit: opts.limit, fromDate: opts.fromDate, toDate: opts.toDate,
|
|
183
|
+
})}`);
|
|
184
|
+
emit(data);
|
|
185
|
+
table(['结算日', '当日盈亏', '收益率', '累计净值', '最大回撤', '手续费', '累计盈亏'],
|
|
186
|
+
(data.items || []).map((r) => [
|
|
187
|
+
r.settleDate || '-', num(r.dailyPnl), pct(r.dailyReturn), num(r.cumNav),
|
|
188
|
+
pct(r.maxDrawdown), num(r.commission), num(r.cumulativePnl),
|
|
189
|
+
]));
|
|
190
|
+
}
|