Published on

寫一個 CLI 小工具 - Stock Reporter

Authors
目錄

前言

嗨,大家好,年底終於拾起筆來寫文章了,由於最近股市慘淡,虧了一屁股的我,決定刪除股票 APP,但這時又想要定期追蹤自己的績效,故決定自建股票 CLI Reporter!

建立套件

第一步不外乎就是 npm init 初始化專案!

> mkdir <your-project>
> npm init
(略)... 填寫專案資訊

在填寫完專案資訊後,在 src 底下建立 index.js

<your-project> % mkdir src && cd "$_"
<your-project> src % touch index.js

並且在 index.js 寫出第一段程式

// <your-project>/src/index.js
console.log('Hello World!!!')

這樣第一步就完成了!

如何在終端機執行程式

當我們建立好專案後,要如何透過 CLI 去執行該專案呢?

而這非常簡單,只需要建立 bin 檔,並且將該路徑用 key-value 的方式新增在 package.jsonbin 欄位。

<your-project> % mkdir bin && cd "$_"
<your-project> bin % touch <command_name>.js

*command_name 將會是未來你在 CLI 輸入的 command, ex: npm, aws, portfolio-reporter ...

並在 <command_name>.js 加上

#!/usr/bin/env node
require('../src/index')

最後將 <command_name>.js 新增在 package.jsonbin 欄位

{
"name": "<your-project>",
// ...
"bin": {
"<your-project>": "./bin/<your-project>.js"
},
// ...
}

現在可以在 terminal 輸入,但會發現噴出 command not found: <your-project>

<your-project> % <your-project>

這時候透過 npm link 就可以解決了!再重新輸入一個 Command 就可以看到 Hello World!!

<your-project> % <your-project>
> Hello World!!

實作

接下來就要進到主菜的部分,

預期功能

Stock reporter 需要使用者建立一份 json 檔 (stock.json) 放到執行 command 的同一層作為輸入

// stock.json
{
"US": ["VOO"],
"TW": ["0050"]
}

Command

<your-command> compare --output=(cli|markdown) --date=<timestamp>

執行完後則會得出以下結果

ExchangeStockCurrent PriceChange (since date)
USVOOxxxxx%

第一步: Install CLI tool & CLI Setup

首先,我們需要一個 CLI 工具讓我可以讀取使用者的輸入,在本篇將使用 yargs 來達成此事

npm install --save yargs@13.3.2

下載好 yargs 後,接下來到 index.js 將定義我們的 CLI

  • commandDir: 根據 command 所執行對應的檔案
  • scriptName: 需與上面提到的 command_name 一致
  • options: nodejs 執行時,監聽使用者是否有中斷程式執行 (q)
// <your-project>/src/index.js
const { boolean } = require('yargs')
const yargs = require('yargs')
const cliSetup = yargs
.commandDir('commands', { exclude: /.test\.js$/ })
.option('quiet', {
alias: 'q',
type: boolean,
description: 'Suppress verbose build output',
default: false,
})
.scriptName('<your-command>')
.version(false).argv
module.exports = cliSetup

第二步: Command setup

接下來需要在輸入 <your-command> compare 時,執行對應的 nodejs, 所以在 <your-project>/src/commands 路徑底下新增 compareStock.js

compareStock.js 需要定義兩個部分

  • API: command 的 API
  • handler: 執行 command 時相對應的 handler 函式
async function compareStock() {
console.log('Hello World!!')
}
const api = {
command: 'compare',
describe: 'compare current value and specific date value results',
handler: compareStock,
}
module.exports = api

此時就可以在 Terminal 執行,看到 Hello World!! 就代表成功將 command 與 handler 串起來了!

<your-project> % <your-command> compare
> Hello World!!

當然我們還需要 output 格式以及特定日期,這部分的設置都會放在 builder

async function compareStock(options) {
const { output, dateTime } = options
console.log(output, dateTime)
}
const api = {
command: 'compare',
describe: 'compare current value and specific date value results',
builder: {
output: {
alias: 'o',
type: 'string',
choices: ['cli', 'markdown'],
description: 'defines a reporter to produce output',
default: 'cli',
},
date: {
alias: 'd',
type: 'number',
description: 'defines a specific date that you want to compare',
},
},
handler: compareStock,
}
module.exports = api

最後,來測試 command 是否可以得到使用者所輸入的資訊!

<your-project> % <your-command> compare --output=cli --date=2022/01/01
> cli 2022/01/01

第三步: 串接股票 API

這一步的目標是將 stock.json 檔的 input, 轉換成想要的結構

// input
{
"US": ["VOO"],
"TW": ["0050"]
}
// expected
{
"US": {
"VOO": {
"symbol": "VOO",
"diff": {
"delta": "-92.67",
"percent": "-21.23%"
}
}
},
"TW": {
"0050": {
"symbol": "0050.TW",
"diff": {
"delta": "-47.50",
"percent": "-32.45%"
}
}
}
}

首先 npm install yahoo-finance ,該套件不用進行多餘的設置,且提供了 API 可以快速的取得股票資訊,

<your-project> % npm install --save yahoo-finance

接下來需要先抓取使用者建立 stock.json 檔

async function compareReports(options) {
const { output, date, quiet } = options
const path = path.resolve(process.cwd(), 'stock.json')
const painStockData = await fs.readFile(path, 'utf-8')
const stockData = JSON.parse(painStockData)
// TODO: GET STOCK INFO
// TODO2: MAKE REPORT
}

得到 stock.json 後,根據給定股票抓取相對應的股價以及算出價差

// 建立 helper function getStockData
const getStockData = (exchange, date, stock) =>
new Promise((resolve, reject) => {
yahooFinance.historical(
{
symbol: exchange === 'US' ? stock : `${stock}.${exchange}`,
from: new Date(date),
to: '2022-10-23',
},
function (err, quotes) {
if (err) {
reject(err)
}
resolve(quotes)
}
)
})
async function compareReports(options) {
const { output, date, quiet } = options
const path = path.resolve(process.cwd(), 'stock.json')
const painStockData = await fs.readFile(path, 'utf-8')
const stockData = JSON.parse(painStockData)
// TODO: GET STOCK INFO
const results = await Object.keys(stockData).reduce(async (result, exchange) => {
let resolvedResult = await result
resolvedResult[exchange] = await stockData[exchange].reduce(async (acc, market) => {
let resolvedAcc = await acc
const stockData = await getStockData(exchange, date, market)
const lastestData = stockData[0]
const targeteData = stockData[stockData.length - 1]
resolvedAcc[market] = {
symbol: lastestData.symbol,
currentValue: lastestData.close,
diff: {
delta: financial(lastestData.close - targeteData.close),
percent: `${financial(
((lastestData.close - targeteData.close) / targeteData.close) * 100
)}%`,
},
}
return resolvedAcc
}, Promise.resolve({}))
return resolvedResult
}, {})
// TODO2: MAKE REPORT
}

最後就是產出 CLI Report 了!

第四步: CLI Table

最後一步我們需要將 Data 轉換成 Table,讓使用者快速的閱讀

這一步需要 chalk 跟 cli-table3 處理 UI

<your-project> % npm install --save chalk@4.1.0 cli-table3

建立 Table

// <your-project>/reporters/cliReporter.js
const chalk = require('chalk')
const Table = require('cli-table3')
module.exports = async function cliReporter(result, date) {
const table = new Table({
colAligns: ['right', 'right', 'right', 'right'],
head: ['Market', 'Stock', 'Current Value', `Change (Percent) - Since ${date}`],
})
Object.keys(result).forEach((market) => {
Object.keys(result[market]).forEach((target) => {
const stock = result[market][target]
const { diff, currentValue } = stock
const { delta, percent } = diff
table.push([
market,
target,
currentValue,
formatDelta(+delta) + ' ' + delta + ' (' + percent + ')',
])
})
})
if (table.length > 0) {
console.log(table.toString())
return
}
}
function getDirectionSymbol(value) {
if (value < 0) {
return '↓'
}
if (value > 0) {
return '↑'
}
return ''
}
function formatDelta(delta) {
if (delta === 0) {
return ''
}
const colorFn = delta > 0 ? chalk.red : chalk.green
return colorFn(getDirectionSymbol(delta))
}

最後將 cliReporter 引入到 compareStock.js 就大功告成了

// TODO2: MAKE REPORT
switch (output) {
case 'cli': {
cliReporter(results, date)
break
}
default:
break
}
console.log(`${chalk.green('[✔]')} Finished!`)

結語

想說趁這個假日學一下 CLI,並且把想法實踐出來,雖然功能並不完善且沒有錯誤處理,但這種學習的過程中真的很有趣!