@chongqingliangyunzhijing/ssquant-cli 0.2.0 → 0.2.1
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 +2 -0
- package/package.json +1 -1
- package/src/auth.js +27 -19
- package/src/commands/sim.js +1 -1
- package/src/commands/system.js +1 -1
- package/src/http.js +14 -1
package/README.md
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
## 发给 AI
|
|
7
7
|
|
|
8
|
+
开户完成后,账户列表显示柜台分配的实际交易账号,可直接用于 `account use` 和 `account delete`。开户中的请求可按操作号查询进度,已有内部编号仍可用于选择账户。
|
|
9
|
+
|
|
8
10
|
> 请完整读取 https://www.pandaaiquant.com/ssquant/openapi/v1/agent-install-guide/cli ,按指引帮我安装 SSQUANT 期货仿真交易 CLI,使用我的 PandaAI 账号登录。请完成安装和环境检查,需要浏览器登录、验证码或授权时提醒我。登录后列出账户和剩余名额。如果我需要开户,问我初始资金(万元,1 至 10000),换算成人民币元,等我确认再创建。最后告诉我账户和就绪状态。本次仅做接入和开户。
|
|
9
11
|
|
|
10
12
|
运行 `ssquant --help` 查看交易命令。查询、开户、交易计划和操作状态支持 `--json`。
|
package/package.json
CHANGED
package/src/auth.js
CHANGED
|
@@ -61,7 +61,7 @@ export async function loginBrowser({ scope = 'futures:read futures:trade', timeo
|
|
|
61
61
|
const { verifier, challenge } = makePkce();
|
|
62
62
|
const state = b64url(crypto.randomBytes(16));
|
|
63
63
|
|
|
64
|
-
const { server, port, waitForCode } = await startCallbackServer(timeout);
|
|
64
|
+
const { server, port, waitForCode, finish } = await startCallbackServer(timeout, state);
|
|
65
65
|
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
66
66
|
|
|
67
67
|
const url = apiUrl('/oauth/authorize') + '?' + new URLSearchParams({
|
|
@@ -93,42 +93,49 @@ export async function loginBrowser({ scope = 'futures:read futures:trade', timeo
|
|
|
93
93
|
client_id: clientId(),
|
|
94
94
|
},
|
|
95
95
|
});
|
|
96
|
-
|
|
96
|
+
const saved = persist(data);
|
|
97
|
+
finish('success');
|
|
98
|
+
return saved;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
finish('error');
|
|
101
|
+
throw error;
|
|
97
102
|
} finally {
|
|
98
103
|
server.close();
|
|
99
104
|
}
|
|
100
105
|
}
|
|
101
106
|
|
|
102
|
-
function startCallbackServer(timeoutSec) {
|
|
107
|
+
export function startCallbackServer(timeoutSec, expectedState) {
|
|
103
108
|
return new Promise((resolve, reject) => {
|
|
104
109
|
let settle;
|
|
105
110
|
const waitForCode = new Promise((res, rej) => {
|
|
106
111
|
settle = { res, rej };
|
|
107
112
|
});
|
|
108
113
|
|
|
114
|
+
let browserResponse;
|
|
115
|
+
const finish = (result) => {
|
|
116
|
+
if (!browserResponse || browserResponse.writableEnded) return;
|
|
117
|
+
browserResponse.writeHead(303, {
|
|
118
|
+
Location: LOGIN_HOME_URL + '?result=' + result,
|
|
119
|
+
'Cache-Control': 'no-store',
|
|
120
|
+
'Referrer-Policy': 'no-referrer',
|
|
121
|
+
}).end();
|
|
122
|
+
};
|
|
109
123
|
const server = http.createServer((req, res) => {
|
|
110
124
|
const u = new URL(req.url, 'http://127.0.0.1');
|
|
111
|
-
if (
|
|
125
|
+
if (req.method !== 'GET' || u.pathname !== '/callback') {
|
|
112
126
|
res.writeHead(404).end();
|
|
113
127
|
return;
|
|
114
128
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
129
|
+
if (u.searchParams.get('state') !== expectedState || browserResponse ||
|
|
130
|
+
(!u.searchParams.get('code') && !u.searchParams.get('error'))) {
|
|
131
|
+
res.writeHead(400, { 'Cache-Control': 'no-store' }).end();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
browserResponse = res;
|
|
135
|
+
if (u.searchParams.get('error')) {
|
|
122
136
|
settle.rej(new ApiError('access_denied', '你在浏览器中拒绝了授权'));
|
|
123
137
|
return;
|
|
124
138
|
}
|
|
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
139
|
settle.res({
|
|
133
140
|
code: u.searchParams.get('code'),
|
|
134
141
|
state: u.searchParams.get('state'),
|
|
@@ -144,7 +151,7 @@ function startCallbackServer(timeoutSec) {
|
|
|
144
151
|
|
|
145
152
|
// 端口交给系统分配:固定端口会在多开时冲突
|
|
146
153
|
server.listen(0, '127.0.0.1', () => {
|
|
147
|
-
resolve({ server, port: server.address().port, waitForCode });
|
|
154
|
+
resolve({ server, port: server.address().port, waitForCode, finish });
|
|
148
155
|
});
|
|
149
156
|
server.on('error', reject);
|
|
150
157
|
});
|
|
@@ -158,6 +165,7 @@ export function persist(d) {
|
|
|
158
165
|
expiresAt: Date.now() + (d.expires_in || 2592000) * 1000,
|
|
159
166
|
scope: d.scope,
|
|
160
167
|
accountId: d.account_id,
|
|
168
|
+
tradingAccount: d.trading_account ?? null,
|
|
161
169
|
market: d.market,
|
|
162
170
|
clientId: clientId(),
|
|
163
171
|
savedAt: new Date().toISOString(),
|
package/src/commands/sim.js
CHANGED
|
@@ -34,7 +34,7 @@ export async function create(opts) {
|
|
|
34
34
|
export async function use(id) {
|
|
35
35
|
const { data } = await request('/sim/accounts/' + encodeURIComponent(id) + '/select', { method:'POST' });
|
|
36
36
|
persist(data);
|
|
37
|
-
show({ accountId:
|
|
37
|
+
show({ accountId: data.trading_account ?? null, ...(!data.trading_account && { accountRef: data.account_id }) });
|
|
38
38
|
}
|
|
39
39
|
export async function remove(id, opts) {
|
|
40
40
|
if (!opts.yes && !/^y(es)?$/i.test(await ask(`确认删除 ${id} 并释放名额?柜台已有委托仍可能成交。[y/N] `))) return;
|
package/src/commands/system.js
CHANGED
|
@@ -5,7 +5,7 @@ import { emit, say } from '../output.js';
|
|
|
5
5
|
|
|
6
6
|
export async function login() {
|
|
7
7
|
const info = await loginBrowser();
|
|
8
|
-
emit({ authenticated: true, accountId: info.
|
|
8
|
+
emit({ authenticated: true, accountId: info.tradingAccount || null });
|
|
9
9
|
say('已登录 PandaAI。运行 ssquant account list 查看账户和名额。');
|
|
10
10
|
}
|
|
11
11
|
export function logout() {
|
package/src/http.js
CHANGED
|
@@ -89,10 +89,23 @@ async function rawFetch(url, {
|
|
|
89
89
|
return { res, json };
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// API 保留稳定的内部关联键,CLI 的账户号始终用柜台分配的交易账号。
|
|
93
|
+
function forDisplay(value, selectedAccount) {
|
|
94
|
+
if (Array.isArray(value)) return value.map(item => forDisplay(item, selectedAccount));
|
|
95
|
+
if (!value || typeof value !== 'object') return value;
|
|
96
|
+
const result = Object.fromEntries(Object.entries(value).map(([key, item]) => [key, forDisplay(item, selectedAccount)]));
|
|
97
|
+
if (typeof result.accountId === 'string' && result.accountId.startsWith('sq_')) {
|
|
98
|
+
const number = result.tradingAccount ?? selectedAccount ?? null;
|
|
99
|
+
if (!number) result.accountRef = result.accountId;
|
|
100
|
+
result.accountId = number;
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
92
105
|
/** 服务端信封 → 结果或抛错 */
|
|
93
106
|
function unwrap({ res, json }) {
|
|
94
107
|
if (res.ok && json && json.code === '0') {
|
|
95
|
-
return { data: json.data, meta: json.meta, requestId: json.request_id };
|
|
108
|
+
return { data: forDisplay(json.data, res.headers.get('X-SSQUANT-Trading-Account')), meta: json.meta, requestId: json.request_id };
|
|
96
109
|
}
|
|
97
110
|
const code = (json && json.code) || `http_${res.status}`;
|
|
98
111
|
const message = (json && json.message) || `HTTP ${res.status}`;
|