quote-observer 2.0.1 → 2.0.3
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/core.js +41 -26
- package/index.js +1 -0
- package/logger.js +3 -3
- package/package.json +1 -1
- package/services/futu.js +2 -2
- package/services/kline.js +16 -10
package/core.js
CHANGED
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
* @Last Modified time: 2021-11-09 10:43:03
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
const fs = require('fs');
|
|
8
9
|
const logger = require('./logger')();
|
|
9
10
|
const Futu = require('./services/futu');
|
|
10
11
|
const KLine = require('./services/kline');
|
|
11
12
|
|
|
13
|
+
// Update KLine every 10 minutes
|
|
14
|
+
const KLINE_UPDATE_PERIOD = 10 * 60 * 1000;
|
|
15
|
+
|
|
12
16
|
// https://github.com/Marak/colors.js/blob/56de9f0983f68cd0a08c5b76d10a783e4b881716/lib/styles.js
|
|
13
17
|
const styleCodes = {
|
|
14
18
|
black: [30, 39],
|
|
@@ -23,13 +27,13 @@ const styleCodes = {
|
|
|
23
27
|
};
|
|
24
28
|
|
|
25
29
|
function colors(string, name) {
|
|
26
|
-
|
|
30
|
+
const [open, close] = styleCodes[name];
|
|
27
31
|
return `\u001b[${open}m${string}\u001b[${close}m`;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
function moving_average(datas, days = 3) {
|
|
31
35
|
if (!Array.isArray(datas)) return 0;
|
|
32
|
-
|
|
36
|
+
const total = datas
|
|
33
37
|
.slice(0, days)
|
|
34
38
|
.map(v => parseFloat(v[2]))
|
|
35
39
|
.reduce((sum, v) => sum += v, 0);
|
|
@@ -44,7 +48,7 @@ function moving_average(datas, days = 3) {
|
|
|
44
48
|
// => 2tm0 = 3tm4 + 3tm3 - 2tm2 - 2tm1
|
|
45
49
|
// => tm0 = 1.5tm4 + 1.5tm3 - tm2 - tm1
|
|
46
50
|
function marginal_value(datas) {
|
|
47
|
-
|
|
51
|
+
const [tm4, tm3, tm2, tm1, tm0] = datas.slice(0, 5).map(v => parseFloat(v[2])).reverse();
|
|
48
52
|
return {
|
|
49
53
|
now: tm0,
|
|
50
54
|
curr: 1.5 * tm4 + 1.5 * tm3 - tm2 - tm1,
|
|
@@ -65,9 +69,9 @@ const padding = [
|
|
|
65
69
|
];
|
|
66
70
|
const pad = (items) => {
|
|
67
71
|
return items.map((item, index) => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
const conf = padding[index];
|
|
73
|
+
const size = conf.size || conf;
|
|
74
|
+
const text = item[index ? 'padStart' : 'padEnd'](size - item.split(/[^\x00-\xff]/).length - 1);
|
|
71
75
|
return conf.color ? colors(text, conf.color(item)) : text;
|
|
72
76
|
}).join('');
|
|
73
77
|
};
|
|
@@ -76,13 +80,13 @@ const LINE = new Array(128).fill('=').join('');
|
|
|
76
80
|
|
|
77
81
|
const STOCKS_MAP = new Map();
|
|
78
82
|
function holding(code) {
|
|
79
|
-
|
|
83
|
+
const stock = STOCKS_MAP.get(code);
|
|
80
84
|
return (stock && stock.market_val) ? '*' : '';
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
function roc(MV) {
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
const { now, next } = MV;
|
|
89
|
+
const v = 100 * (next - now) / now;
|
|
86
90
|
return (v > 0 ? '+' : '') + v.toFixed(2) + '%';
|
|
87
91
|
}
|
|
88
92
|
|
|
@@ -91,33 +95,37 @@ function ceil(v, d = 2) {
|
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
function sale_signal(code, now) {
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
const stock = STOCKS_MAP.get(code);
|
|
99
|
+
const { cost_price } = stock;
|
|
96
100
|
if (!cost_price) return '[=====][00]';
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
const irate = 100 * (now - cost_price) / cost_price;
|
|
102
|
+
const srate = [-25, -15, -8, -5, -2].map(signal => irate < signal ? '-' : ' ').join('');
|
|
99
103
|
return `[${srate}][${ceil(irate)}]`;
|
|
100
104
|
}
|
|
101
105
|
|
|
106
|
+
function random_delay() {
|
|
107
|
+
return new Promise(r => setTimeout(r, Math.random() * 60 * 1000));
|
|
108
|
+
}
|
|
109
|
+
|
|
102
110
|
function start() {
|
|
103
|
-
|
|
111
|
+
const quotes = [...STOCKS_MAP.keys()].map(code => random_delay().then(() => KLine(code, 10, STOCKS_MAP.get(code)?.tid)));
|
|
104
112
|
Promise.all(quotes).then(resps => {
|
|
105
113
|
console.reset();
|
|
106
114
|
console.log((new Date()).toTimeString().split(' ')[0]);
|
|
107
115
|
console.log(LINE);
|
|
108
|
-
console.log(pad(['ID', '
|
|
116
|
+
console.log(pad(['ID', 'NAME', 'NOW', 'MA3', 'MA5', 'MVC', 'MVN', 'MVN-ROC', 'S-SIG']));
|
|
109
117
|
console.log(LINE);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
118
|
+
const rows = [[], LINE.replace(/=/g, '-'), []];
|
|
119
|
+
const errors = resps.filter((data, index) => {
|
|
120
|
+
const { fid, id, info, data: resp } = data;
|
|
113
121
|
if (!id || !info || !resp) return false;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
122
|
+
const name = String(info[1] || '').replace(/(ETF|\s).+$/i, '');
|
|
123
|
+
const MA3 = moving_average(resp, 3);
|
|
124
|
+
const MA5 = moving_average(resp, 5);
|
|
125
|
+
const MV = marginal_value(resp);
|
|
118
126
|
if (!MV.now) return global.MARKET[id] = `us${info[2]}`;
|
|
119
|
-
|
|
120
|
-
|
|
127
|
+
const hold_star = holding(fid);
|
|
128
|
+
const row = pad([
|
|
121
129
|
fid + hold_star, String(name).replace(/-\w+$/, ''),
|
|
122
130
|
MV.now.toFixed(3), MA3.toFixed(3), MA5.toFixed(3),
|
|
123
131
|
MV.curr.toFixed(3), MV.next.toFixed(3), roc(MV),
|
|
@@ -128,10 +136,9 @@ function start() {
|
|
|
128
136
|
console.log(rows.flat().join('\n'));
|
|
129
137
|
console.log(LINE);
|
|
130
138
|
if (errors.length) throw new Error('Errors occur.');
|
|
131
|
-
setTimeout(() => start(),
|
|
139
|
+
setTimeout(() => start(), KLINE_UPDATE_PERIOD);
|
|
132
140
|
}).catch((error) => {
|
|
133
141
|
logger.error(error.message);
|
|
134
|
-
setTimeout(() => start(), 1 * 1000);
|
|
135
142
|
});
|
|
136
143
|
}
|
|
137
144
|
|
|
@@ -143,6 +150,14 @@ function update_stocks() {
|
|
|
143
150
|
// SH,SZ,HK,US => [ 'SH', 'SZ', 'HK', 'US' ]
|
|
144
151
|
const markets = String(global.config?.opts?.market || '').trim()
|
|
145
152
|
.split(',').map(v => v.trim().toUpperCase()).filter(v => v);
|
|
153
|
+
const codes = global.config?.opts?.codes;
|
|
154
|
+
if (codes && fs.existsSync(codes)) {
|
|
155
|
+
fs.readFileSync(codes, 'utf8').toString().trim().split('\n').map(v => {
|
|
156
|
+
const stock = JSON.parse(v.trim());
|
|
157
|
+
STOCKS_MAP.set(stock.code, stock);
|
|
158
|
+
});
|
|
159
|
+
return Promise.resolve();
|
|
160
|
+
}
|
|
146
161
|
return Promise.all([Futu.position(), Futu.following()]).then(([holders, follows]) => {
|
|
147
162
|
STOCKS_MAP.clear();
|
|
148
163
|
[
|
package/index.js
CHANGED
|
@@ -15,6 +15,7 @@ const pkg = require('./package.json');
|
|
|
15
15
|
program
|
|
16
16
|
.version(pkg.version)
|
|
17
17
|
.option('-S, --save <root>', `save response into root`, '')
|
|
18
|
+
.option('-C, --codes <root>', `load codes from file`, '')
|
|
18
19
|
.option('-H, --holder', `show stock holder`, true)
|
|
19
20
|
.option(' --no-holder', `hide stock holder`)
|
|
20
21
|
.option('-F, --follow', `show stock follow`, false)
|
package/logger.js
CHANGED
|
@@ -19,9 +19,9 @@ const Logger = new Console(
|
|
|
19
19
|
fs.writeFileSync(logfile, '');
|
|
20
20
|
|
|
21
21
|
module.exports = () => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
const error = (new Error()).stack.toString().split('\n')[2] || '';
|
|
23
|
+
const filename = (error.match(/[\\\/\(]([-\w\.]+.\w+):\d+:\d+\)$/) || [])[1] || 'unknown';
|
|
24
|
+
const logger = {};
|
|
25
25
|
'trace,debug,info,warn,error,fatal,mark'.split(',').forEach(name => {
|
|
26
26
|
logger[name] = (...args) => {
|
|
27
27
|
Logger.info(`[${(new Date()).toISOString()}] [${filename}] [${name.toUpperCase()}]`, ...args);
|
package/package.json
CHANGED
package/services/futu.js
CHANGED
|
@@ -10,8 +10,8 @@ const conf = global.config.futu;
|
|
|
10
10
|
const logger = require('../logger')();
|
|
11
11
|
|
|
12
12
|
function apply(name) {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const headers = { Authorization: conf.auth };
|
|
14
|
+
const url = `${conf.base}${conf[name]}`;
|
|
15
15
|
logger.info(`Query ${name}: ${url}`);
|
|
16
16
|
return axios
|
|
17
17
|
.get(url, { headers })
|
package/services/kline.js
CHANGED
|
@@ -39,27 +39,33 @@ const save_resp = (data) => {
|
|
|
39
39
|
}
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (!code)
|
|
42
|
+
const convert2tid = (mkt, code, tid) => {
|
|
43
|
+
if (tid) return tid;
|
|
44
|
+
if (!code) return '';
|
|
45
|
+
return fix_id(`${mkt.toLowerCase()}${code}${'US' === mkt ? '.OQ' : ''}`);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function kline(id, days = 10, tid = '') {
|
|
49
|
+
const [mkt, code] = id.split('.');
|
|
50
|
+
const _id = convert2tid(mkt, code, tid);
|
|
51
|
+
if (!_id) {
|
|
45
52
|
logger.info(`[${id}] INDEX SKIP`);
|
|
46
53
|
return {};
|
|
47
54
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
let params = {
|
|
55
|
+
const _var = 'kline_dayqfq';
|
|
56
|
+
const params = {
|
|
51
57
|
_var,
|
|
52
58
|
_r: Math.random(),
|
|
53
59
|
param: `${_id},day,,,${days},qfq`,
|
|
54
60
|
};
|
|
55
|
-
|
|
61
|
+
const url = `${base_url}${path_mkt[mkt]}?${querystring.stringify(params)}`;
|
|
56
62
|
logger.info(`[${id}] -> ${url}`);
|
|
57
63
|
return axios.get(url)
|
|
58
64
|
.then((raw = '') => {
|
|
59
65
|
try {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
const resp = JSON.parse(raw.data.toString().replace(`${_var}=`, ''));
|
|
67
|
+
const data = (resp || {}).data[_id] || [];
|
|
68
|
+
const info = (data.qt || {})[_id] || [];
|
|
63
69
|
return save_resp({ fid: id, id: _id, info, data: (data.day || data.qfqday || []).reverse() });
|
|
64
70
|
} catch (error) {
|
|
65
71
|
logger.error(`[${id}] PARSE RESPONSE ERROR:`, error.message);
|