- Published on
寫一個 CLI 小工具 - Stock Reporter
- Authors
- Name
- Jing-Huang Su
- @Jing19794341
目錄
前言
嗨,大家好,年底終於拾起筆來寫文章了,由於最近股市慘淡,虧了一屁股的我,決定刪除股票 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.jsconsole.log('Hello World!!!')
這樣第一步就完成了!
如何在終端機執行程式
當我們建立好專案後,要如何透過 CLI 去執行該專案呢?
而這非常簡單,只需要建立 bin
檔,並且將該路徑用 key-value 的方式新增在 package.json
的 bin
欄位。
<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 noderequire('../src/index')
最後將 <command_name>.js
新增在 package.json
的 bin
欄位
{"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>
執行完後則會得出以下結果
Exchange | Stock | Current Price | Change (since date) |
---|---|---|---|
US | VOO | xxx | xx% |
第一步: 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.jsconst { 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).argvmodule.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 } = optionsconsole.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 } = optionsconst 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 getStockDataconst 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 } = optionsconst path = path.resolve(process.cwd(), 'stock.json')const painStockData = await fs.readFile(path, 'utf-8')const stockData = JSON.parse(painStockData)// TODO: GET STOCK INFOconst results = await Object.keys(stockData).reduce(async (result, exchange) => {let resolvedResult = await resultresolvedResult[exchange] = await stockData[exchange].reduce(async (acc, market) => {let resolvedAcc = await accconst 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.jsconst 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 } = stockconst { delta, percent } = difftable.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.greenreturn colorFn(getDirectionSymbol(delta))}
最後將 cliReporter
引入到 compareStock.js 就大功告成了
// TODO2: MAKE REPORTswitch (output) {case 'cli': {cliReporter(results, date)break}default:break}console.log(`${chalk.green('[✔]')} Finished!`)
結語
想說趁這個假日學一下 CLI,並且把想法實踐出來,雖然功能並不完善且沒有錯誤處理,但這種學習的過程中真的很有趣!