前端工程師轉型財經分析大V,就差這個了:Tagged Templates

前端工程師轉型財經分析大V,就差這個了:Tagged Templates

來自專欄文因互聯

文因互聯的問答機器人里有一個小功能,叫「金融小邏輯」,可以利用文因互聯擁有的結構化的財務數據生成財經箴言

利潤,充滿估計和假設;現金,才是實打實的硬通貨。凈利潤不等於掙到的錢,兩者可以有很大的差別。凈利潤一定要和現金流量表的「經營現金流凈額」對照著看,投資者可以直接用「經營現金流凈額 / 凈利潤」來衡量凈利潤的含金量。萬科2017年三季報表明,萬科的2016年現金比率為1.2,比值大於1,凈利潤含金量比較高,現金流入量較大,凈利潤質量較好,推薦關注。

這不禁讓人浮想聯翩,人類擅長定性分析,而機器擅長定量分析,當一位富有智慧的人拿到金融小邏輯給出的各家公司近幾年的CAGR(複合年均增長率),他就可以做很多事了,比如拿著機器給出的箴言去炒股群里指點江山獲得高人一等的裝逼快感。

金融支持小姐姐最近在維護 @財報妹 這個微博賬號,她希望在機器提取完財務數據後,可以用這些數據自動生成金融分析。這樣就可以抓住年報季的熱點,第一時間發布上市公司財務情況。

雖然她在學校的專業是金融學,但她並沒有選擇自己去讀財報來指點江山——機器完成數據提取才不到10秒,金融小姐姐要把數據手動複製到分析模板里,一些指標還需要查找前三年的數值,人工搞完至少要兩個多小時。這樣就無法第一時間搶先發布,一點都不熱乎的財經評論還怎麼漲粉?

所以最好還是讓機器來讀財報,選出有意思的公司,並計算各種導出值,按照小姐姐的「智慧箴言」自動生成分析就行。

但是問答機器人里的金融小邏輯引擎應用場景是實時問答,所以為了效率犧牲了一定的可配置性,有比較多的限制,無法靈活地輸出小姐姐的智慧金句。

她聽說文因互聯有一位前端工程師寫文檔寫得比較靈活,他可能也懂一些技術吧,就來向他請教怎麼解除這些限制,讓她能指點江山的時候能放飛自我。

前端小哥一聽需求,就明白她需要的是一個模板引擎,「那就來學 React 吧」,他建議道。但小姐姐不學 React。前端小哥想,有什麼模板引擎是不用學就會用的呢?存在這樣的模板引擎嗎?

五毛錢特效讓你變身財經大V

不學就會的模板引擎還是存在的,那就是 Tagged Template Literals,前端圈裡叫它模板字元串標籤(或者更隨意地簡稱為標籤模板),而在財經圈子裡它卻被尊稱為「股神引擎」。只要在兩個 ``` 之間寫上一些公式和濫竽充數的廢話,再加上一點魔法,網頁上就會自動顯示出充滿智慧的財經評論,你就能指點江山當財經大V!就像這樣:

其中非經常收益、凈利潤扣非等變數是從一個財經常用變數表裡引入的,底下模板中用到哪個的話得先在上面引入。

「充滿智慧的結論1」是通過對經濟學的多年學習,加上對市場的深刻理解,還有前端工程師一分鐘的語法指導得來的。大概長這樣:

小姐姐表示這還蠻好理解的,因為兩個花括弧里的字元串就是會填到文本里的內容,而前面的 if else 也不難理解,別的部分都是一模一樣的,不需要深入理解也沒事。因此她很快就寫出了一大堆邏輯和模板,準備要開始發微博了。

但作為提供那「一點魔法」的前端,還是得深入理解 template 和 logic 這兩個標籤里到底發生了啥。

模板標籤簡介

經常使用 styled-components 或者 graphql-tag 的人,可能都已經熟悉模板字元串標籤的工作原理。此處僅用《 The magic behind ?? styled-components 》中的例子簡單概括一下。

以這樣一個函數為例:

我們可以直接把它當做模板字元串標籤來使用:

這時候模板中的字面量(「I like 」和「.」)會被傳到 logArgs 的第一個參數 templates 里,而把字面量隔開的那些模板變數會被收集進第二個參數 args 里:

那麼如果我們想把字面量和變數拼起來,就得像拉拉鏈一樣把它們拉到一起,做這種事用 reduce 最方便:

這樣,把 result 返回之後,調用模板標籤就能拿到 I like pizza. 了。

所以這個項目非常簡單,用 create-react-app 新建一個項目,往 app.js 里加一個模板字元串就搞定了。

以上就是股神引擎的全部內容。

收集數據依賴

當然,還有一個也有一點重要的問題要解決,就是「凈利潤扣非」其實等於「凈利潤 - 非經常收益」,而非經常收益又是「營業外收入 - 營業外支出」,其中凈利潤、營業外收入和營業外支出三個基本數據是數據提取組能從財報 PDF 中提取得到的

但「凈利潤扣非」不是,所以要得到「凈利潤扣非」這個導出值,就得向後端一次性請求所有這些基本數據,在前端進行計算後返回再填入模板。

構造公式圖

注意到所有公式中的變數其實組成了一個有向無環圖(DAG),只要收集所有葉子節點的名字,就可以一次性向後端請求它們對應的值了。後端返回數據後,把葉子節點的值填入 DAG 中,就能一級級算出根部的凈利潤扣非的值了。

因此我們把公式表示成這樣的數據結構:

然後在 template 模板字元串標籤里把這個 JSON 中的每一項都變成一個節點,最終把這些節點根據公式連接起來就變成一個 DAG 啦:

連接節點的過程就是簡單地遍歷 formulas.json 把每個公式中的右值對應的 DAG Node 用addChild 塞進左值對應的 DAG Node。

用 VM 做計算

而當節點連接完畢,值也填進了每個葉子節點,就可以用 value 這個 getter 來遞歸計算父節點的值。其中遞歸步驟特別簡單,就是一句:

vm 可以看做是 NodeJS 上一個沙箱化的 eval,它的第二個參數接受一個對象,鍵會變成沙箱中的全局變數名,值就是這些全局變數的值。所以此處 vm 會直接根據公式算出結果。

Dataloader

那麼怎麼向後端請求葉子節點所需的值呢?如果模板中只用了一個變數,那麼這個變數所依賴的基本變數的名字列表可以用relatedName getter 來遞歸地拿到。

但模板中可能會涉及到十幾個變數,他們很可能會交叉依賴共同的基本變數,所以我們需要先去重,然後再一次性向後端請求所有基本變數。

為什麼一直強調「一次性qu」

因為模板是可以嵌入邏輯模板的,邏輯模板中也可以使用變數,然後返回一個結果。但我們不希望每個邏輯模板自己會產生一個非同步的數據請求,金融支持小姐姐想要一次性看到結果,還有後端目前的設計中一次性取數據更快,所以我們期待整個模板加上所有邏輯模板里的變數依賴收集完後一次性向後端請求。

一見「一次性向後端請求」,立刻想到 GraphQL,立刻想到 DataLoader。前端工程師的想像惟在這一層能夠如此躍進。

GraphQL 是 Facebook 提出的為前端組件一次性載入所有所需數據的方案,而 DataLoader 一開始是用於合併 GraphQL 服務端向資料庫的請求的,其原理是在內部維護一個緩存數組,每個需要數據的函數去調用 DataLoader 的時候,其實是把請求參數緩存在數組裡,然後在 process``.nextTick 中就能拿到上個事件循環中所有數據請求的請求參數了,此時再把它們拼接起來,一次性請求掉。

比如下面這個用於去抖載入所有變數值的 DataLoader,DataLoader 接受一個函數,它會把它在上個 tick 收集到的所有請求參數傳給這個函數,此處我們就能拿到所有地方調用這個 variableValueLoader 時傳入的參數,它們被合併成了數組 variableNames

就像用了開塞露一樣,所有的變數一次性湧出,無比地順滑!

當然這也帶來了一個問題,就是我們的每個變數都變成了一個函數調用:

這也的話寫模板的時候就得這樣寫 ↓,多一個莫名其妙的 v,而且變數名還得是字元串,得多打一對引號很累很傷手:

解決了數據獲取和計算的問題,我們還需要實現當初的諾言:「這個模板引擎用起來很簡單的,你把要填在那的變數填在 import 後面,然後填進模板就好了」,所以我打算這樣預先把每個變數都用上面的函數調用一下,幫小姐姐保養她的玉手:

生成可引用的變數

然而公式中的變數實在是有點多,因為它是文因互聯代代相傳的傳家公式表,像一個種馬家族綿延五百年產生的族譜一樣長:

(圖:經馬賽克處理,公式表的二十分之一,顯示在 VsCode 的小地圖裡)

一個個搞起來太累了,而且手寫這些 export const 的話還得兩頭維護,沒有一個 single source of truth。

這時候一句廣告語響了起來:「工作勞累,身心疲憊,何不趁這春光明媚,把 codegen 學會。」

babel-plugin-codegen 和 babel-plugin-preval 是前端在編譯期運行業務代碼的兩個途徑,一般可以使用它們的宏版本, codegen.macro 和 preval.macro 。用它們可以直接在模板字元串里寫代碼,並返回一個值,前者會用返回的字元串直接生成代碼,後者則能把返回的值賦給某個運行期變數等。

此處我們簡單使用 codegen.macro,在模板字元串里引入 formulas.json,對於其中的每一個鍵,都生成一個 export const:

在編譯後,這段代碼將展開成巨長的 export 列表。

然後在別的地方就能直接 import 處理後的變數了:

可惜由於 babel 和 ESModule 的限制,目前還沒法實現一次性自動 import 所有的變數,所以還得手寫 import,當然,這種限制也是有其道理的。

簡化的邏輯

在我們的模板中除了可以嵌入變數以外,還可以嵌入邏輯,邏輯就是任意的一段 JS 表達式,表達式的值會插入到模板中。

目前需要寫的模板都比較簡單,只需要簡單條件判斷的表達能力,所以一開始我們推薦使用三目操作符(? : ),但金融支持小姐姐說這是亂碼看不懂。

那就只好換一種表達式寫法了,在 ECMAScript 中表達式就那麼幾種,其中比較易讀的應該就是 do-expression 了:

我們用 @babel/standalone 來在運行時轉換 do-expression,再交給 vm 來執行。

其中 resultCode 來自

可以看到這段代碼也是一個 reduce 的過程,把字面量數組 strings 和變數數組 resultValuesOfVariables 像拉拉鏈一樣拼在一起。

只不過在 reduce 過程的開頭,我們放上了一個 ""+ ,這是什麼意思呢?

就像我們也偶爾會看到 +function(){}() ,JS 里加號的含義就像中文裡的「搞」一樣多,此處 ""+ 的意思就是「搞後面的東西」,也就是「把後面的東西當做表達式」,不然 babel 會認為這是一個 do…while statement,而認不出它是一個 do-expression,會報錯:

當然,do-expression 的語法還不穩定,在其 Github 倉庫的 issue 里還進行著激烈的討論,此處也僅是在小項目中為了簡化判斷邏輯的輸入,因地制宜地使用了一下。

至此,一個收集模板中的所有變數加上邏輯中用到的所有變數,一次性請求數據,並根據公式計算結果填入模板的小項目就可以投入使用了。金融支持小姐姐也在不知不覺中用上了 ES7 。

但是小姐姐還有別的需求

文因互聯有自己的金融知識圖譜,裡面很多數據還需要找到有創意的使用方式和工程實現,在前端呈現出來。

如果你熟悉前端目前的技術棧,不但能快速閱讀已有項目代碼,了解涉及到的 npm 包,也能在現有數據上提出有創意的工程方案,歡迎來找金融支持小姐姐聽新的好玩的需求

簡歷請投遞:hr@memect.co 綻放你的工程能力吧!

職位細節可以點擊此處鏈接查看喲。

推薦閱讀:

怎樣看待小米 Note 3 現貨發售後,迄今為止官網仍供貨充足?
瀏覽器為什麼要開放網頁源代碼?
在當代中國如何做一個仙女?
「植物人」是如何促醒的?| 詳解植物人促醒綜合治療體系
黑科技酒店正「在路上」 不理大數據的酒店已經打烊

TAG:前端工程師 | 科技 | 財經媒體 |