@chongqingliangyunzhijing/ssquant-cli 0.1.0 → 0.2.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 CHANGED
@@ -8,4 +8,5 @@
8
8
  > 请完整读取 https://www.pandaaiquant.com/ssquant/openapi/v1/agent-install-guide/cli ,按指引帮我安装 SSQUANT 期货仿真交易 CLI,使用我的 PandaAI 账号登录。请完成安装和环境检查,需要浏览器登录、验证码或授权时提醒我。登录后列出账户和剩余名额。如果我需要开户,问我初始资金(万元,1 至 10000),换算成人民币元,等我确认再创建。最后告诉我账户和就绪状态。本次仅做接入和开户。
9
9
 
10
10
  运行 `ssquant --help` 查看交易命令。查询、开户、交易计划和操作状态支持 `--json`。
11
+ 交易模式默认逐笔确认,适用于同一 PandaAI 用户下的全部 SSQUANT 仿真账户。用 `ssquant trade-mode show` 查询,`ssquant trade-mode set direct` 开启直接交易,`ssquant trade-mode set confirm` 恢复逐笔确认。AI 应按用户的明确指令更改设置。直接交易执行计划时省略 `--yes`。
11
12
  删除使用 `ssquant account delete <账户号>`。确认后释放本地名额,柜台已有委托仍可能成交。
package/bin/ssquant.js CHANGED
@@ -6,6 +6,7 @@ import * as acct from '../src/commands/account.js';
6
6
  import * as trade from '../src/commands/trade.js';
7
7
  import * as mandate from '../src/commands/mandate.js';
8
8
  import * as plans from '../src/commands/plan.js';
9
+ import * as tradeMode from '../src/commands/trade-mode.js';
9
10
  import { CLIENT_VERSION } from '../src/version.js';
10
11
  import * as sim from '../src/commands/sim.js';
11
12
 
@@ -39,6 +40,9 @@ program.command('login')
39
40
  program.command('logout').description('登出并清除本地凭证').action(run(async () => sys.logout()));
40
41
  program.command('whoami').description('查看登录状态与绑定账户').action(run(sys.whoami));
41
42
  program.command('doctor').description('连通性与状态自检').action(run(sys.doctor));
43
+ const mode = program.command('trade-mode').description('当前 PandaAI 用户的 SSQUANT 交易确认设置');
44
+ mode.command('show').description('查看交易模式').action(run(tradeMode.show));
45
+ mode.command('set <mode>').description('confirm 逐笔确认,direct 直接交易').action(run(tradeMode.set));
42
46
  const plan = program.command('plan').description('冻结交易计划');
43
47
  plan.command('create')
44
48
  .requiredOption('--operation <name>', 'place_order/cancel_order/cancel_all/close_all/rollover/target_position')
@@ -48,7 +52,7 @@ plan.command('create')
48
52
  plan.command('show').argument('<planId>').description('查看计划').action(run(plans.show));
49
53
  plan.command('execute').argument('<planId>').description('确认并执行计划')
50
54
  .option('--client-request-id <id>', '网络重试复用的幂等号')
51
- .requiredOption('-y, --yes', '确认执行该冻结计划')
55
+ .option('-y, --yes', '用户已确认执行该冻结计划')
52
56
  .action(run(plans.execute));
53
57
 
54
58
  const operation = program.command('operation').description('交易操作状态');
@@ -132,6 +136,7 @@ program.command('order')
132
136
  program.command('cancel')
133
137
  .argument('<orderId>', '委托号')
134
138
  .description('撤单')
139
+ .option('-y, --yes', '用户已确认撤销该委托')
135
140
  .action(run(trade.cancel));
136
141
 
137
142
  program.command('cancel-all')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chongqingliangyunzhijing/ssquant-cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "SSQUANT 期货仿真交易命令行工具",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,7 +22,9 @@ export async function create(opts) {
22
22
  leg.contractCode || leg.orderId || '-', `${leg.side || ''}/${leg.offset || ''}`,
23
23
  leg.volume ?? leg.quantity ?? '-',
24
24
  ]));
25
- say(' 这是预演计划,尚未真实执行。取得用户明确确认后使用 plan execute。');
25
+ if (data.status === 'prepared') say(data.tradeMode === 'direct'
26
+ ? '当前为直接交易模式,可用 plan execute 执行本计划。'
27
+ : '请展示本计划,用户明确确认后用 plan execute --yes 执行。');
26
28
  }
27
29
 
28
30
  export async function show(planId) {
@@ -33,12 +35,8 @@ export async function show(planId) {
33
35
  }
34
36
 
35
37
  export async function execute(planId, opts) {
36
- if (!opts.yes) {
37
- const err = new Error('执行冻结计划必须显式传 --yes,表示用户已经确认该 planId');
38
- err.code = 'confirmation_required'; throw err;
39
- }
40
38
  const { data } = await request(`/futures/plans/${encodeURIComponent(planId)}/execute`, {
41
- method: 'POST', body: { confirmed: true, clientRequestId: opts.clientRequestId },
39
+ method: 'POST', body: { confirmed: !!opts.yes, clientRequestId: opts.clientRequestId },
42
40
  });
43
41
  emit(data);
44
42
  kv([['操作号', data.operationId], ['计划号', data.planId], ['状态', data.status],
@@ -0,0 +1,31 @@
1
+ import { request } from '../http.js';
2
+ import { emit, isJson, say } from '../output.js';
3
+
4
+ const path = '/settings/trade-mode';
5
+
6
+ function display(data) {
7
+ emit(data);
8
+ say(data.mode === 'direct' ? '当前为直接交易模式。' : '当前为逐笔确认模式。');
9
+ }
10
+
11
+ export async function show() {
12
+ display((await request(path)).data);
13
+ }
14
+
15
+ export async function set(mode) {
16
+ if (!['confirm', 'direct'].includes(mode)) throw Error('模式必须为 confirm 或 direct');
17
+ display((await request(path, { method: 'PUT', body: { mode } })).data);
18
+ }
19
+
20
+ // 跳过提示不等同于用户确认,直接模式执行仍向服务端发送 confirmed=false。
21
+ export async function needsPrompt(opts, mode) {
22
+ if (opts.yes) return false;
23
+ const current = mode ?? (await request(path)).data.mode;
24
+ if (current === 'direct') return false;
25
+ if (!process.stdin.isTTY || isJson()) {
26
+ const error = new Error('当前为逐笔确认模式,请在用户确认本次交易后使用 --yes');
27
+ error.code = 'confirmation_required';
28
+ throw error;
29
+ }
30
+ return true;
31
+ }
@@ -3,12 +3,13 @@
3
3
  *
4
4
  * ★ 开仓允许传中文品种名/品种码,服务端会解析为当前主力实际合约;平仓仍必须传持仓实际合约。
5
5
  * ① 传 --price 自动按 GFD 限价单;不传价格自动按 IOC 市价单
6
- * ② 真实下单前若未加 --yes,交互确认(Agent 场景用 --yes 跳过)
6
+ * ② 按用户当前的交易模式确认后执行或直接执行
7
7
  */
8
8
  import readline from 'node:readline/promises';
9
9
  import { request } from '../http.js';
10
10
  import { emit, kv, num, say, table } from '../output.js';
11
11
  import { showQuote, showContractSpec } from './account.js';
12
+ import { needsPrompt } from './trade-mode.js';
12
13
 
13
14
  export async function order(opts) {
14
15
  const code = opts.symbol;
@@ -30,13 +31,12 @@ export async function order(opts) {
30
31
  orderType,
31
32
  volume: Number(opts.volume),
32
33
  dryRun: !!opts.dryRun,
34
+ confirmed: !!opts.yes,
33
35
  };
34
36
  if (opts.price !== undefined && opts.price !== null) body.price = Number(opts.price);
35
37
  if (opts.clientOrderId) body.clientOrderId = opts.clientOrderId;
36
38
 
37
- if (!opts.dryRun && !opts.yes && !process.stdin.isTTY) throw Error('非交互下单需要 --dry-run 预演或 --yes 明确确认');
38
- // 预演永远直接执行;真实下单在交互模式下要确认
39
- if (!opts.dryRun && !opts.yes && process.stdin.isTTY) {
39
+ if (!opts.dryRun && await needsPrompt(opts)) {
40
40
  const { data: preview } = await request('/futures/orders', {
41
41
  method: 'POST', body: { ...body, dryRun: true },
42
42
  });
@@ -61,6 +61,7 @@ export async function order(opts) {
61
61
  say('已取消,未下单。');
62
62
  return;
63
63
  }
64
+ body.confirmed = true;
64
65
  }
65
66
 
66
67
  const { data } = await request('/futures/orders', { method: 'POST', body });
@@ -101,8 +102,16 @@ export function showOrderPreview(data) {
101
102
  if (data.suggestion) say(` 建议:${data.suggestion}`);
102
103
  }
103
104
 
104
- export async function cancel(orderId) {
105
- const { data } = await request(`/futures/orders/${encodeURIComponent(orderId)}`,
105
+ export async function cancel(orderId, opts) {
106
+ let confirmed = !!opts.yes;
107
+ if (await needsPrompt(opts)) {
108
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
109
+ const answer = await rl.question(`确认撤销委托 ${orderId}?(yes/N) `);
110
+ rl.close();
111
+ if (!/^y(es)?$/i.test(answer.trim())) { say('已取消。'); return; }
112
+ confirmed = true;
113
+ }
114
+ const { data } = await request(`/futures/orders/${encodeURIComponent(orderId)}?confirmed=${confirmed}`,
106
115
  { method: 'DELETE' });
107
116
  emit(data);
108
117
  kv([['委托号', data.orderId || orderId], ['状态', data.statusText || data.status || '-']]);
@@ -124,14 +133,16 @@ export async function cancelAll(opts) {
124
133
  return;
125
134
  }
126
135
  if (!rows.length) { emit(plan); say('当前没有活动挂单。'); return; }
127
- if (!opts.yes && process.stdin.isTTY) {
136
+ let confirmed = !!opts.yes;
137
+ if (await needsPrompt(opts)) {
128
138
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
129
139
  const ans = await rl.question(`即将撤销 ${rows.length} 笔挂单,确认?(yes/N) `);
130
140
  rl.close();
131
141
  if (!/^y(es)?$/i.test(ans.trim())) { say('已取消。'); return; }
142
+ confirmed = true;
132
143
  }
133
144
  const { data } = await request('/futures/orders/cancel-all', {
134
- method: 'POST', body: { ...body, dryRun: false },
145
+ method: 'POST', body: { ...body, dryRun: false, confirmed },
135
146
  });
136
147
  emit(data);
137
148
  table(['委托号', '结果'], (data.results || []).map((r) => [
@@ -166,11 +177,8 @@ export async function closeAll(opts) {
166
177
  say(' 没有可平的持仓。');
167
178
  return;
168
179
  }
169
- if (!opts.yes && !process.stdin.isTTY) {
170
- say(' 非交互环境执行移仓必须显式传入 --yes。');
171
- return;
172
- }
173
- if (!opts.yes) {
180
+ body.confirmed = !!opts.yes;
181
+ if (await needsPrompt(opts)) {
174
182
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
175
183
  say('即将由服务端提交以下平仓腿:');
176
184
  table(['合约', '方向', '手数'], plan.legs.map((p) => [
@@ -181,6 +189,7 @@ export async function closeAll(opts) {
181
189
  say('已取消。');
182
190
  return;
183
191
  }
192
+ body.confirmed = true;
184
193
  }
185
194
  const { data } = await request('/futures/positions/close-all', {
186
195
  method: 'POST', body,
@@ -196,29 +205,27 @@ async function frozenBatch(operation, parameters, opts) {
196
205
  const { data: plan } = await request('/futures/plans', {
197
206
  method: 'POST', body: { operation, parameters },
198
207
  });
199
- emit(plan);
200
208
  say(plan.summary || '');
201
209
  table(['合约', '方向', '手数'], (plan.legs || []).map(leg =>
202
210
  [leg.contractCode, `${leg.side}/${leg.offset}`, leg.volume]));
203
- if (plan.status !== 'prepared' || !plan.requiresConfirmation) {
211
+ if (plan.status !== 'prepared') {
212
+ emit(plan);
204
213
  if (plan.preview?.message) say(plan.preview.message);
205
214
  if (plan.preview?.candidates?.length) table(['旧合约', '方向', '可平手数'],
206
215
  plan.preview.candidates.map(item => [item.contractCode, item.direction, item.closable]));
207
216
  say('预演未通过或需要补充参数,未发送真实订单。');
208
217
  return;
209
218
  }
210
- if (!opts.yes) {
211
- if (!process.stdin.isTTY) {
212
- const err = new Error('非交互执行需要用户明确确认后传 --yes');
213
- err.code = 'confirmation_required'; throw err;
214
- }
219
+ let confirmed = !!opts.yes;
220
+ if (await needsPrompt(opts, plan.tradeMode)) {
215
221
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
216
222
  const answer = await rl.question(`确认执行以上冻结计划 ${plan.planId}?(yes/N) `);
217
223
  rl.close();
218
224
  if (!/^y(es)?$/i.test(answer.trim())) { say('已取消。'); return; }
225
+ confirmed = true;
219
226
  }
220
227
  const { data: operationResult } = await request(`/futures/plans/${encodeURIComponent(plan.planId)}/execute`, {
221
- method: 'POST', body: { confirmed: true, clientRequestId: opts.clientRequestId },
228
+ method: 'POST', body: { confirmed, clientRequestId: opts.clientRequestId },
222
229
  });
223
230
  const data = { status: operationResult.status, ...operationResult.result,
224
231
  planId: plan.planId, operationId: operationResult.operationId };