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 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
- let [open, close] = styleCodes[name];
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
- let total = datas
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
- let [tm4, tm3, tm2, tm1, tm0] = datas.slice(0, 5).map(v => parseFloat(v[2])).reverse();
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
- let conf = padding[index];
69
- let size = conf.size || conf;
70
- let text = item[index ? 'padStart' : 'padEnd'](size - item.split(/[^\x00-\xff]/).length - 1);
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
- let stock = STOCKS_MAP.get(code);
83
+ const stock = STOCKS_MAP.get(code);
80
84
  return (stock && stock.market_val) ? '*' : '';
81
85
  }
82
86
 
83
87
  function roc(MV) {
84
- let { now, next } = MV;
85
- let v = 100 * (next - now) / now;
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
- let stock = STOCKS_MAP.get(code);
95
- let { cost_price } = stock;
98
+ const stock = STOCKS_MAP.get(code);
99
+ const { cost_price } = stock;
96
100
  if (!cost_price) return '[=====][00]';
97
- let irate = 100 * (now - cost_price) / cost_price;
98
- let srate = [-25, -15, -8, -5, -2].map(signal => irate < signal ? '-' : ' ').join('');
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
- let quotes = [...STOCKS_MAP.keys()].map(code => KLine(code, 10));
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', '名称', 'NOW', 'MA3', 'MA5', 'MVC', 'MVN', 'MVN-ROC', 'S-SIG']));
116
+ console.log(pad(['ID', 'NAME', 'NOW', 'MA3', 'MA5', 'MVC', 'MVN', 'MVN-ROC', 'S-SIG']));
109
117
  console.log(LINE);
110
- let rows = [[], LINE.replace(/=/g, '-'), []];
111
- let errors = resps.filter((data, index) => {
112
- let { fid, id, info, data: resp } = data;
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
- let name = String(info[1] || '').replace(/(ETF|\s).+$/i, '');
115
- let MA3 = moving_average(resp, 3);
116
- let MA5 = moving_average(resp, 5);
117
- let MV = marginal_value(resp);
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
- let hold_star = holding(fid);
120
- let row = pad([
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(), 10 * 1000);
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
- let error = (new Error()).stack.toString().split('\n')[2] || '';
23
- let filename = (error.match(/[\\\/\(]([-\w\.]+.\w+):\d+:\d+\)$/) || [])[1] || 'unknown';
24
- let logger = {};
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quote-observer",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "quote observer",
5
5
  "main": "index.js",
6
6
  "scripts": {
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
- let headers = { Authorization: conf.auth };
14
- let url = `${conf.base}${conf[name]}`;
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
- function kline(id, days = 10) {
43
- let [mkt, code] = id.split('.');
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
- let _id = fix_id(`${mkt.toLowerCase()}${code}${'US' === mkt ? '.OQ' : ''}`);
49
- let _var = 'kline_dayqfq';
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
- let url = `${base_url}${path_mkt[mkt]}?${querystring.stringify(params)}`;
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
- let resp = JSON.parse(raw.data.toString().replace(`${_var}=`, ''));
61
- let data = (resp || {}).data[_id] || [];
62
- let info = (data.qt || {})[_id] || [];
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);