quote-observer 2.0.0 → 2.0.2
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 +32 -26
- package/logger.js +3 -3
- package/package.json +1 -1
- package/services/futu.js +2 -2
- package/services/kline.js +17 -14
package/core.js
CHANGED
|
@@ -9,6 +9,9 @@ const logger = require('./logger')();
|
|
|
9
9
|
const Futu = require('./services/futu');
|
|
10
10
|
const KLine = require('./services/kline');
|
|
11
11
|
|
|
12
|
+
// Update KLine every 10 minutes
|
|
13
|
+
const KLINE_UPDATE_PERIOD = 10 * 60 * 1000;
|
|
14
|
+
|
|
12
15
|
// https://github.com/Marak/colors.js/blob/56de9f0983f68cd0a08c5b76d10a783e4b881716/lib/styles.js
|
|
13
16
|
const styleCodes = {
|
|
14
17
|
black: [30, 39],
|
|
@@ -23,13 +26,13 @@ const styleCodes = {
|
|
|
23
26
|
};
|
|
24
27
|
|
|
25
28
|
function colors(string, name) {
|
|
26
|
-
|
|
29
|
+
const [open, close] = styleCodes[name];
|
|
27
30
|
return `\u001b[${open}m${string}\u001b[${close}m`;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
function moving_average(datas, days = 3) {
|
|
31
34
|
if (!Array.isArray(datas)) return 0;
|
|
32
|
-
|
|
35
|
+
const total = datas
|
|
33
36
|
.slice(0, days)
|
|
34
37
|
.map(v => parseFloat(v[2]))
|
|
35
38
|
.reduce((sum, v) => sum += v, 0);
|
|
@@ -44,7 +47,7 @@ function moving_average(datas, days = 3) {
|
|
|
44
47
|
// => 2tm0 = 3tm4 + 3tm3 - 2tm2 - 2tm1
|
|
45
48
|
// => tm0 = 1.5tm4 + 1.5tm3 - tm2 - tm1
|
|
46
49
|
function marginal_value(datas) {
|
|
47
|
-
|
|
50
|
+
const [tm4, tm3, tm2, tm1, tm0] = datas.slice(0, 5).map(v => parseFloat(v[2])).reverse();
|
|
48
51
|
return {
|
|
49
52
|
now: tm0,
|
|
50
53
|
curr: 1.5 * tm4 + 1.5 * tm3 - tm2 - tm1,
|
|
@@ -65,9 +68,9 @@ const padding = [
|
|
|
65
68
|
];
|
|
66
69
|
const pad = (items) => {
|
|
67
70
|
return items.map((item, index) => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
const conf = padding[index];
|
|
72
|
+
const size = conf.size || conf;
|
|
73
|
+
const text = item[index ? 'padStart' : 'padEnd'](size - item.split(/[^\x00-\xff]/).length - 1);
|
|
71
74
|
return conf.color ? colors(text, conf.color(item)) : text;
|
|
72
75
|
}).join('');
|
|
73
76
|
};
|
|
@@ -76,13 +79,13 @@ const LINE = new Array(128).fill('=').join('');
|
|
|
76
79
|
|
|
77
80
|
const STOCKS_MAP = new Map();
|
|
78
81
|
function holding(code) {
|
|
79
|
-
|
|
82
|
+
const stock = STOCKS_MAP.get(code);
|
|
80
83
|
return (stock && stock.market_val) ? '*' : '';
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
function roc(MV) {
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
const { now, next } = MV;
|
|
88
|
+
const v = 100 * (next - now) / now;
|
|
86
89
|
return (v > 0 ? '+' : '') + v.toFixed(2) + '%';
|
|
87
90
|
}
|
|
88
91
|
|
|
@@ -91,33 +94,37 @@ function ceil(v, d = 2) {
|
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
function sale_signal(code, now) {
|
|
94
|
-
|
|
95
|
-
|
|
97
|
+
const stock = STOCKS_MAP.get(code);
|
|
98
|
+
const { cost_price } = stock;
|
|
96
99
|
if (!cost_price) return '[=====][00]';
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
const irate = 100 * (now - cost_price) / cost_price;
|
|
101
|
+
const srate = [-25, -15, -8, -5, -2].map(signal => irate < signal ? '-' : ' ').join('');
|
|
99
102
|
return `[${srate}][${ceil(irate)}]`;
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
function random_delay() {
|
|
106
|
+
return new Promise(r => setTimeout(r, Math.random() * 60 * 1000));
|
|
107
|
+
}
|
|
108
|
+
|
|
102
109
|
function start() {
|
|
103
|
-
|
|
110
|
+
const quotes = [...STOCKS_MAP.keys()].map(code => random_delay().then(() => KLine(code, 10)));
|
|
104
111
|
Promise.all(quotes).then(resps => {
|
|
105
112
|
console.reset();
|
|
106
113
|
console.log((new Date()).toTimeString().split(' ')[0]);
|
|
107
114
|
console.log(LINE);
|
|
108
|
-
console.log(pad(['ID', '
|
|
115
|
+
console.log(pad(['ID', 'NAME', 'NOW', 'MA3', 'MA5', 'MVC', 'MVN', 'MVN-ROC', 'S-SIG']));
|
|
109
116
|
console.log(LINE);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
const rows = [[], LINE.replace(/=/g, '-'), []];
|
|
118
|
+
const errors = resps.filter((data, index) => {
|
|
119
|
+
const { fid, id, info, data: resp } = data;
|
|
113
120
|
if (!id || !info || !resp) return false;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
121
|
+
const name = String(info[1] || '').replace(/(ETF|\s).+$/i, '');
|
|
122
|
+
const MA3 = moving_average(resp, 3);
|
|
123
|
+
const MA5 = moving_average(resp, 5);
|
|
124
|
+
const MV = marginal_value(resp);
|
|
118
125
|
if (!MV.now) return global.MARKET[id] = `us${info[2]}`;
|
|
119
|
-
|
|
120
|
-
|
|
126
|
+
const hold_star = holding(fid);
|
|
127
|
+
const row = pad([
|
|
121
128
|
fid + hold_star, String(name).replace(/-\w+$/, ''),
|
|
122
129
|
MV.now.toFixed(3), MA3.toFixed(3), MA5.toFixed(3),
|
|
123
130
|
MV.curr.toFixed(3), MV.next.toFixed(3), roc(MV),
|
|
@@ -128,10 +135,9 @@ function start() {
|
|
|
128
135
|
console.log(rows.flat().join('\n'));
|
|
129
136
|
console.log(LINE);
|
|
130
137
|
if (errors.length) throw new Error('Errors occur.');
|
|
131
|
-
setTimeout(() => start(),
|
|
138
|
+
setTimeout(() => start(), KLINE_UPDATE_PERIOD);
|
|
132
139
|
}).catch((error) => {
|
|
133
140
|
logger.error(error.message);
|
|
134
|
-
setTimeout(() => start(), 1 * 1000);
|
|
135
141
|
});
|
|
136
142
|
}
|
|
137
143
|
|
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
|
@@ -33,39 +33,42 @@ const save_resp = (data) => {
|
|
|
33
33
|
fs.writeFileSync(`${root}/stock-quote-${data.fid}.json`, JSON.stringify(data));
|
|
34
34
|
}
|
|
35
35
|
} catch (error) {
|
|
36
|
-
logger.error(`
|
|
36
|
+
logger.error(`[${data.fid}] SAVE RESP ERROR:`, error.message);
|
|
37
37
|
} finally {
|
|
38
38
|
return data;
|
|
39
39
|
}
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
function kline(id, days = 10) {
|
|
43
|
-
|
|
43
|
+
const [mkt, code] = id.split('.');
|
|
44
44
|
if (!code) {
|
|
45
|
-
logger.info(`
|
|
45
|
+
logger.info(`[${id}] INDEX SKIP`);
|
|
46
46
|
return {};
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
const _id = fix_id(`${mkt.toLowerCase()}${code}${'US' === mkt ? '.OQ' : ''}`);
|
|
49
|
+
const _var = 'kline_dayqfq';
|
|
50
|
+
const params = {
|
|
51
51
|
_var,
|
|
52
52
|
_r: Math.random(),
|
|
53
53
|
param: `${_id},day,,,${days},qfq`,
|
|
54
54
|
};
|
|
55
|
-
|
|
56
|
-
logger.info(`
|
|
57
|
-
return axios
|
|
58
|
-
.get(url)
|
|
55
|
+
const url = `${base_url}${path_mkt[mkt]}?${querystring.stringify(params)}`;
|
|
56
|
+
logger.info(`[${id}] -> ${url}`);
|
|
57
|
+
return axios.get(url)
|
|
59
58
|
.then((raw = '') => {
|
|
60
59
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
const resp = JSON.parse(raw.data.toString().replace(`${_var}=`, ''));
|
|
61
|
+
const data = (resp || {}).data[_id] || [];
|
|
62
|
+
const info = (data.qt || {})[_id] || [];
|
|
64
63
|
return save_resp({ fid: id, id: _id, info, data: (data.day || data.qfqday || []).reverse() });
|
|
65
64
|
} catch (error) {
|
|
66
|
-
logger.error(id
|
|
65
|
+
logger.error(`[${id}] PARSE RESPONSE ERROR:`, error.message);
|
|
67
66
|
return {};
|
|
68
67
|
}
|
|
68
|
+
})
|
|
69
|
+
.catch((error) => {
|
|
70
|
+
logger.error(`[${id}] FETCH ERROR:`, error.message);
|
|
71
|
+
return {};
|
|
69
72
|
});
|
|
70
73
|
}
|
|
71
74
|
|