【祿】您希望您的醫療保險 期滿回贈多少保費?

朋友圈當中一位朋友最近買了醫療保險,6年繳的計劃宣稱保障回報兼備,不論有否索償,10年期滿後不僅可獲退還全數保費,更有額外 8% 回贈。朋友自言覺得很吸引,「有病有得賠,又能全數取回保費有凸;有 upside 無 downside,完全無蝕底!」



相信作為本 Blog 的讀者,看到這裏一定覺得有點問題、一定覺得「冇咁大隻蛤乸隨街跳」、一定已經很明白「羊毛出自羊身上」的道理。如果讀者這樣想,你就對了!所謂的「回贈」,就算再高,歸根究底就是退回閣下自己的錢而已。坦白說,就算要回贈不只 8% 而是 18% 的保費,保險公司也能有方法在不提高實際投資回報(內部回報率)的情況下輕鬆達成。

在被 8% 甚至 18% 大大的數字吸引之前,我們必須搞清楚這個回贈背後的內部回報率,並將之與市場上不同的投資工具作比較。重要的事情不妨多講一遍:「保障」才是保險公司擅長處理的風險,我們單憑個人的力量難以自行處理,在市場上也難以尋找代替品;但保險公司在「回報」面前卻是跟其他金融機構甚至自己沒有兩樣,大家基本上都是在同一個股金債匯市場投資買賣金融資產,保險公司投資股票等「高風險資產」甚至需要面對監管機構的資本要求,亦即個人不用面對的額外風險資金成本。所以當搞清楚保險公司能為閣下的儲蓄帶來什麼合理回報後,讀者就不會再被 8% 或者 18% 等吸引眼球的數字所蒙蔽,從而根據自己的理財能力選擇合適的理財產品了。


至於讀者可能有興趣,保險公司是如何創造 8% 期滿保費回贈給筆者的朋友?這回筆者換個方法不直接告訴你,如果讀者小學畢業數學科學過代數,就讓我們一起動手做吧!

現設想一張給予 30歲男性投保人 10 年保障、6年繳清、期滿不論索償狀況退還已繳保費 108% 的保單。假設每年保費為 $X。

投保人的支出:

在保單開立後首 6 年的年初(即 0/1/2/3/4/5 年後),每年繳納保費 $X。為方便計算,假設在保單開立後的 2.5 年(六個時間點的平均)一次過繳納 $6X。

投保人得到的利益:

1. 十年內的基本醫療保障;這個部分可以純保障醫療保險代替。網上隨意估價,35歲男性(30-40歲平均)年保費大概為 $3,000。同樣,為方便計算,假設在 10 年的中間,投保人獲得等值 $30,000 的保障。

2. 十年後得到保費退還兼回贈 108% x $6X,即 $6.48X。

現假設保險公司願意給予投保人每年 3% 的實際回報,以等價交換原則計算 10年末的價值:

支出 = 利益
$6X (1+3%)10-2.5= $30,000 (1+3%)5 + $6.48X
$X = $32,823

即首 6 年共給予保險公司 $196,938,10年期滿取回 $212,693;所得的保障跟 $30,000 買到的醫療保險一樣,另 10 年後僅送 $15,755 即半年保費也沒有的回贈聊表心意。然而這張有回贈的保單,一年所需的保費卻已經可以買足 10 年純保障。


想期滿要 18% 回贈?沒問題,在維持 3% 回報率的前提下,根據高小學生也懂的代數:
$6X (1+3%)10-2.5 = $30,000 (1+3%)5 + $7.08X
$X = $85,009

即首 6 年共給予保險公司 $510,054,10年期滿取回 $601,864,所得的保障同樣跟 $30,000 買到的醫療保險一樣,另 10 年後回贈 $91,810,不計時間值的話勉強保費打個九折,但付的卻是 純保障保險保費的 28 倍以上。


看完這篇,讀者又希望自己的醫療保險,期滿回贈多少保費?筆者自己答案已經不言而喻了。

【福 / 壽】愛在當下 降醣減脂 飯後散步半小時

半桶水爸爸最近文章的啟發,良久未有發文靈感的筆者方才發現,幸福的事早就在自己身邊,現短文一篇跟各位讀者分享 🙂


筆者多篇文章都講述自己每早的健康習慣。培養晨早的習慣是因為筆者正職比較繁忙,今早出門往往不知今晚能否準時下班,所以習慣先將重要的時間和事情先做好,免得晚上晚了下班方才後悔。然而本篇的習慣卻是一個例外——只要晚上是一起吃飯的話,筆者和太太都會飯後在住家附近公園作簡單散步,即使現在太太進入了第三孕期仍然堅持這個習慣。

這個習慣的開始是一個健康的考慮。數年前筆者和太太在明珠台節目《祝君健康》認識到,晚飯後散步 30 分鐘能有助消化,並能有效降低血糖。為了不想中年發福甚至得上糖尿病,從那天起我們便開始了晚飯後散步這個習慣。

筆者和太太沒有像節目主持人那般,散步後去驗血糖驗證散步的功效,但早就能從生活中感受到長期飯後散步帶來的各種健康及健康以外的莫大好處:

第一當然是體重控制:有飯後散步的晚上,第二朝醒來體重變化通常要比沒有散步的晚上要來得健康。

第二是提升睡眠品質:通常散步後的晚上都睡得比較好,較少「扎醒」或夢多難眠的情形。

第三是減壓和增加認知能力:愈來愈多研究顯示,散步能增加腦袋的血液流量,幫助記憶和認知能力、有助減壓;筆者也記得《恐佈醫學》不下一次以「散步」作為對抗腦退化症和抑鬱症的處方。

但其實筆者認為最重要的好處,卻是散步製造了兩夫婦的「優質時間」(Quality Time)。要知道兩夫婦的肉體雖然每日都有十個八個小時在附近,但二人有深度心靈交流的時間和機會卻是少之又少——日間在職場打拚,晚上處理家頭細務,試問兩人不努力爭取的話,還有甚麼時間細心聆聽並了解對方的感受?散步就是我們每天那 30 分鐘深度溝通交流的時間,就是那促進夫妻感情的重要時間。一般我們會一邊在公園走,一邊互相傾談各自的工作、從同事或朋友得到的資訊、講出自己一天的心情和感受;有時我們也利用這個機會,談及一些嚴肅的議題,例如家庭財政、育兒方針,或者是憧憬一下退休之後的生活、發一下六合彩橫財春秋大夢等。

而最能讓晚間飯後散步成為我們優質時間的主因,是公園黑暗的環境有效促使我們放下手機。散步是機不離手的筆者主動願意放下手機的時間,起初是出於對眼睛健康的考慮,後來當散步漸漸成為筆者和太太心靈交流的上佳時間後,更多是處於珍惜眼前人、活在當下的考慮。到最近筆者換了能接聽電話的 Apple Watch,確保了散步時親人能作緊急聯繫筆者的方法,筆者乾脆電話也沒帶就去散步。

自從太太懷了我倆的骨肉,散步的時間變得更加重要。一方面散步能促進太太的血液循環,減低妊娠糖尿病的風險,也更有效將吸收到的營養帶給寶寶;二方面是藉著我們散步時的交流討論,繼續就育兒方針、家務安排等交換意見以準備新生命的降臨;三方面寶寶聽著我們討論的聲音,也就是胎教的一種,讓寶寶早日認識聆聽爸爸媽媽的聲音。孕婦只有少數適合做的運動,當中散步是最方便又最安全的。

很慶幸自己居住的地方附近就有一個大公園,從公園一端走到另一端是一公里,來回走一趟大概就是 30 分鐘。喜歡的時候就多走一點,有要事在身就走短一點,沒有限制和束縛,但卻是我倆每天期待著的時光。有研究指出,吃晚飯後立即散步是最健康的,所以各位不要再用「洗碗」做藉口了,坐言起行去散步拍拖,碗碟待回家再洗吧! 

【家】 贏在起跑線 不如先選對運動場

這幾個月來,筆者發文的速度明顯減慢了。今個夏天過得異常充實,因為筆者和太太都忙於準備數個月後迎接小生命的來臨;在身心、財務和育兒知識上,都忙於學習新世界的一切。


「全家福祿壽」既有「全」、「福」、「祿」、「壽」四卷,豈能沒有「家」卷?筆者也等待了這個時刻很久,終於可以解鎖 「幸福家庭」之卷,分享交流筆者對育兒、建立家庭的一些願望和想法。

系列第一篇,是寫給日後的自己做警惕的,題目是煲到爛的「贏在起跑線」。這篇文章讀起來來可能有點散亂,因為起初想寫議論文,結果卻更似一篇抒情文,還望讀者見諒,哈哈。


筆者覺得,在幾年前,社會的氛圍跟現在的有點不同;在當時「贏在起跑線」是很理所當然的事 —— 超聲波相要用來報讀保證前途的名牌幼稚園、幾個月大的嬰幼兒要上 Playgroup 學三文四語乃等閒事;選不定要讀「本地線」還是「國際線」幼稚園不要緊,上下午各修讀一所就可以,中午在私家車上換校服和午飯,小朋友比上班還要忙。還有當然就是琴棋書畫十八般武藝樣樣皆通、Portfolio 厚過康熙字典,還「沒法解釋怎可報漏古箏」。

這個現象,坦白說,的確嚇怕了當年新婚的筆者夫婦。結婚前後不久開始,我倆就不時均對育兒的期望和價值觀交流討論。我們的看法很一致,就是要讓孩子健康愉快成長,讓孩子自己選擇自己的道路,然後鍛鍊自己的意志,以不傷害別人的方式,去達致孩子自己定義的成功,而前提是:他必須能先自己養活自己。這個期望,跟當時的社會價值,凡事為孩子選擇好,贏在起跑線,再操練成繁忙兒童,的確有點大相徑庭。

然後社會愈加關注兒童患抑鬱症、精神病的個案,然後社會廣泛報導壓力兒童自殺的個案,然後讓筆者印象最深刻的是 UNICEF 也要發廣告,捍衛已發展國家兒童每日一小時的自由遊玩的「權利」⋯⋯ 幸而筆者也看到社會這幾年來也有點兒改變,開始尊重兒童也是一個個體,開始察覺並尊重每個孩子的獨特性,而不是家長用來耀武揚威的工具。

但切身的問題來了,大部分家長在生育之前都會說自己不是「怪獸家長」、「虎媽」,會以孩子的獨特性和需要為養育的大前提,然而真正生育以後,孩子面對的競爭卻是實在的:學位就是那麼多、一班只有一個第一、拉 Curve 之下就是只有這麽多個 5**(但文憑試 5** 不如報 IB),當其他家長「怪獸」起來,如果我不怪獸,豈不很輸蝕?面對這 Game Theory,筆者在幾個月後即為人父之際,又教我如何保持堅定的意志?

筆者目前的答案是:「成功需父幹」。

先別誤會,筆者想說的不是由筆者連子女的錢也賺下來,然後買幾層樓收租給子女作為生活費;前文早就說過筆者希望將來的孩子能夠自給自足自力更生。

筆者想說的是,為人父母,子女成功有賴他們無憂無慮的不斷探索自己內在和四周外在的世界;而父母的責任,就是信任他們,放手讓孩子去嘗試,同時為孩子張開安全網,讓孩子死不掉,因為「沒有致命的東西只會讓人更堅強」(what doesn’t kill you makes you stronger)。

我認為父幹的目的,在於將子女的可能性最大化,同時張開最大的防護網,讓孩子自己選擇如何去成長。


筆者認為,希望自己小朋友贏在起跑線的家長,某程度上是對自己沒有信心,不能給予子女成長所需要的探索和犯錯的空間,只知道要「達標」,達成某些健康科學甚或社會文化強加於小朋友發展身上的所謂指標。沒有犯錯的空間,也許來自父母不知道育兒路上種種權利及選擇(知識上)、也許來自父母沒有足夠的人際關係去為孩子打開某些密門(人脈上)、也許來自父母沒有足夠的財政資源去提供第二選項、Fall Back Plan(財政上)。作為父母,在知識、人脈、財政上裝備自己,才是我所希望的「成功需父幹」。

「贏在起跑線」也表示將子女的人生看成是一條直線的跑步競賽,但殊不知到單是田徑比賽場就有數十種項目可以參與,為什麼不讓小朋友探索選擇自己喜歡的項目之前,就讓他認定了馬拉松長跑?當人人都跑馬拉松,退一步參賽擲鐵餅,不是有更廣闊的天空?在沒有衡量過馬拉松比賽是否適合自己的孩子之前就盲目報名,這跟人人都去買某投資物、自己沒有衡量自己的財政和流動性需求,又跟風去買,有什麼分別?如果有一天,向前一步則死,後退一步則亡,只懂往前跑的馬拉松孩子,又是否知道他擁有「往旁邊走」的人生選項 —— 尤其是如果那個時候自己的身體已經不容許在孩子身旁陪着走、提點他的時候?


還有另一件事:人生馬拉松比賽的獎品,往往是 —— 繼續跑更多、更遠的馬拉松。

港爸港媽要子女贏在起跑線,琴棋書畫三文四語是基本,目標是不是以下這個?

「家長:我認為讀幼稚園的目的是升上名牌小學,接著升上名牌中學,然後順理成章地升上名牌大學。這樣,將來的出路就有了保證,否則孩子將來怎能找到一份好工作呢?」(節錄自本地某升學專家著作,筆者第一次讀到的時候,當場大呼「痴線!」)

然而某大型銀行早前的一則廣告,就告訴你往後的故事:

「我個囡細細個就好叻(小女孩在彈鋼琴),依家… 最叻係加班(上班女在打鍵盤)。有邊個老豆想個囡嫁咗畀份工?(下班女在梳化累得倒下來)」

將兩件事放在一起,就會明白… 是誰讓女兒嫁了給工作。

讓孩子們醉心讀書考試一心一意疊埋心水(也只懂)做專業人士再開行 OT 賣命,30 歲時孩子方才發現,做狀元其實跟成功沒有甚麼關係,讓他們後悔從前浪費了這麼多時間在跟別人拚成績、拚職級、拚人工,活在社會強加於自己身上的價值觀,而忘記去尋找自己的價值觀,試問這是我們作為家長樂於見到的嗎?還是這只是容易、方便自己教養的方法?

跑馬拉松,縱使沿路風景多美麗,總不能停下來駐足觀賞。如果我們的孩子可以有條件、有權利選擇,在喜歡的地方停下來看看,欣賞路旁的鮮花、遠處的景色、無雲的晴空,甚至帶同朋友或是爸爸媽媽一同野餐、同歡同樂⋯⋯ 為孩子報名跑馬拉松,你清楚這真的能為孩子奪回人生的選擇權?

筆者認為,成功的人生,是有選擇權的人生;而打從一開始為孩子報名參加賽跑,父母已為孩子做了決定,已經將他的人生選擇權剝削掉。

筆者認為,作為父母,責任在於帶孩子去不同的運動場,告訴他、讓他看見這個世界上除了賽跑,還有各式各樣不同的競技可以參與。甚至如果他喜歡而有能力,自己創立一個全新的運動也行。(題外話:早前在電視上見到一種新運動「Golfball」,就是用踢的打高爾夫,如果將來孩子是足球小將,這運動應該會是我們的交匯點 :) )

先選擇了幾個孩子喜歡的運動場,一邊訓練再觀察,根據孩子的年紀和能力而參與,慢慢轉為從旁協助,再而只是場邊鼓勵;讓孩子在自己適合的時間選擇自己要走的道路、要做的運動、要作的比賽。如果我們相信並認定孩子是未來世界的主人翁,將來也會成為別人的配偶、家長、老闆、同事、合夥人,我們就得信任他、訓練他去做選擇、做決定,而不是自己在遼闊的世界裏畫兩條線作為賽道,然後訓練孩子在這個狹窄的、被定義的「世界」裏邊成為「『世界』第一」。要孩子將來「Play by the rules」還是「Play with the rules」,父母的心態起決定性作用。

這才是筆者所說的「成功需父幹」。



說到底,家長們搞這麼多動作,都是想孩子,甚至自己,有能力選擇做自己喜歡做的事。簡單點說,就是做回自己;簡單點說,我們都在贖身。

做人⋯ 其實簡單些不可以嗎?

如果自己真心喜歡平凡生活,做父母就要跟小朋友講,在能養活自己的前提下,做平凡人、做平凡事,但每天在過自己喜歡的生活,這個絕對可以係人生的 Happily everafter。

如果小朋友自己不喜歡,就鼓勵他用自己雙手去尋找自己的未來。

父母能做的,就是投射不同的可能性給小朋友知道;但小朋友要認定哪一條路去走、如何走,就自己去選擇、解決。

其實,在香港,要平凡生活,也不容易,是吧?

【祿】邁向財務「自由」之路 II — 育成收發 Telegram 的智能秘書(下篇)

本篇繼續我們開發收發 Telegram 短訊彙報牛記匯率的智能秘書之旅!由於這兩篇的技術含量較高,如果讀者是初次接觸編程,建議由上篇或者系列第一篇 Google Sheet 智能秘書開始閱讀。



上回講到我們已從牛記匯率網頁擱取到所有外幣的匯率,並儲存在一個名叫 FXDatabase 的 Python 字典物件內。本篇我們會討論:

  • 資訊的展現,即如何將 FXDatabase 內的資訊,換成秘書在 Telegram 發送的訊息;
  • 利用另一個 Python 程序,將已開發的程序跟 Telegram 聯繫,建立聊天機械人(Chatbot);
  • 將程序放上雲端運算,讓程序 24/7 執行,讓智能秘書隨時隨地為你效勞。

我們會討論兩種訊息: (1) 發送主要貨幣的匯率一覽,以及 (2) 根據我們輸入的貨幣代碼,回覆相應貨幣的匯率。


4. 以喜歡的形式  將字典裏的資訊換成短訊息

這個步驟是整個開發過程中最開心、最自由度大、最考創意的,因為我們可以通過資訊的展現,決定私人智能秘書的性格、做人物角色設定。當然,性格這回事,並沒有對錯的,喜歡就好 :)

這一步我們先來編寫秘書所回覆的訊息,下一步我們再想如何讓秘書連線上 Telegram,以及秘書如何看懂我們在 Telegram 發出的訊息。

現在假設筆者希望得到的訊息有兩款:

  1. 主要貨幣的匯率買賣價一覽,其中「主要貨幣」自定義為美元、英鎊、歐羅、日圓和人幣。效果如下圖:

  1. 當自己在 Telegram 輸入貨幣的代碼,顯示該貨幣的買賣價。效果如下圖:


為了方便 Telegram 程序的存取,我們將所需要的訊息寫成是函數的回傳。待會我們就能看到這個方法的好處——利用函數直接得到所需要的訊息,而不用將製作訊息和顯示訊息的程式碼混在一起。

在此之前,讓我們也先把上回得到 FXDatabase 的程序,以函數形式表示:

   def DownloadFXDB():
       r = requests.get(url).content
       Soup = BeautifulSoup(r, "html.parser")
       TargetRows = Soup.find("table").find_all("tr")[2:]

       FXDatabase = {}

       for Row in TargetRows:
           Currency = Row.find_all("td")[1].text.encode('utf-8')
           TDBuy = Row.find_all("td")[2]
           TDSell = Row.find_all("td")[3]
           Buy = GetFX(TDBuy)
           Sell = GetFX(TDSell)   
           FXDatabase[Currency] = { 'Buy' : Buy, 'Sell' : Sell }
    
       return FXDatabase

這樣每次呼叫 DownloadFXDB,Python 就會從網際網路下載最新的牛記匯率,並製作匯率字典了。


4a. 回傳訊息內容函數:主要貨幣的匯率買賣價一覽

第一行當然是定義一個函數:

   def MajorCurrFXMsg():

在函數中,首先讓我們從網際網路下載最新的匯率資訊:

   FXDatabase = DownloadFXDB()

然後,告訴 Python 我們需要的主要貨幣列表:

   MajorCurr = ['USD','GBP','EUR','JPY','RMB']

接下來,由於我們也是用 for 迴圈去累積訊息的內容,所以也得先建立一個空字串:

   Message = ""

當然,如果想為私人秘書加添一些個人性格,可以在這一步加上⋯⋯

  • 通告風: Message = "💹 主要貨幣匯率一覽" + "\n"
  • 客服風: Message = "👩🏻‍💻 以下是為尊貴閣下從網路實時取得的匯率報價" + "\n"
  • 女僕風: Message = "💁🏻‍♀️ 主人,這是您需要的匯率資訊 ☺️" + "\n"
  • 外星生物風: Message = "👽 本星球列強貨幣等價 🌍" + "\n"

其中 "\n" 是換行符號。

來到靈魂一步,寫一個 For 迴圈,將所有主要貨幣跑一遍,並將匯率資訊以自己喜歡的形式展現出來。這裏筆者只展現基本形式:

   for Currency in MajorCurr:
       Message = Message + Currency + " 買 " + 
                 str(FXDatabase[Currency]['Buy']) + " 沽 " + 
                 str(FXDatabase[Currency]['Sell']) + "\n"

其中 str() 函數是將匯率數字變成字串便於與訊息其餘部份連繫在一起。最後將 Message 作為函數的回傳:

return Message




4b. 回傳訊息內容函數:按照輸入顯示該貨幣匯價

假設使用者在 Telegram 輸入的貨幣代號名稱為 InputCurr (我們將在下節討論智能秘書如何接收我們的指令)。我們輸出訊息內容的函數可以直接以此為參數:

   def PromptedCurrFXMsg(InputCurr):

同樣的,我們先下載最新的匯率資訊:

   FXDatabase = DownloadFXDB()

然後在顯示匯率資訊之前,讓我們先檢查輸入的代號是否存在在從牛記取得的字典內。要取得 FXDatabase 內所有貨幣,亦即該字典的「索引」列表,方法是 .keys():

   if InputCurr not in FXDatabase.keys():
       Message = "對不起,沒有提供" + 
                 InputCurr.encode('utf-8') + "的匯率"

如果存在,則顯示相應的匯率:

   else:
       Message = InputCurr.encode('utf-8') + " 買 " + 
                 str(FXDatabase[InputCurr]['Buy']) + " 沽 " + 
                 str(FXDatabase[InputCurr]['Sell'])

   return Message

留意筆者在 InputCurr 用上了 .encode('utf-8'),這是因為使用者從 Telegram 輸入的訊息,也會被視之為 UTF-8 編碼的。




4c. 小結

至此,整個輸入輸出匯率的程序碼檔 NgauKeeFX.py 已開發完成。整個檔案應該有四個程序函數(內容從略):

   ## NgauKeeFX.py

   import requests
   from bs4 import BeautifulSoup

   def GetFX(TD):
       ...

   def DownloadFXDB():
       ...

   def MajorCurrFXMsg():
       ...

   def PromptedCurrFXMsg(InputCurr):
       ...


5.  建立 Telegram 聊天機械人  並將已開發的程序聯繫

說到底,我們的 Telegram 智能秘書就是一個聊天機械人(Chatbot)。我們現在就要建立一個新的 Chatbot,並以 Python 程序賦予這個 Chatbot 靈魂。

5a. 建立一個新 Chatbot

要建立一個新的 Telegram Chatbot,我們得去拜訪眾 Bot 之父 —— BotFather。(你猜對了!BotFather 本身就是一個 Bot。)要把他加進你的 Telegram 通訊表,網址是:


BotFather 能給你清晰的指示如何建立和管理你的聊天機械人(們)。例如,要建立一個新的聊天機械人,可以輸入「/newbot」指令(或直接按 BotFather 訊息上的相關連結),並按指示先後輸入顯示名稱(日後可更改)以及系統名稱(日後不可更改,並需以 bot 字結尾):



BotFather 會給予這個新 Bot 一個 Token,請務必妥善儲存及處理,因為不幸落入別人手中,別人就能騎劫你的秘書。

現在我們也可以利用「/setuserpic」指令,為秘書助理上載一幅個人玉照:


效果如下圖:


是否已經很有血肉的真實感覺了?且慢,現在我們的 Telegram Chatbot 還需要你為他/她賦予智慧,就是那顆有能力跟你對答的靈魂,而這就是本篇的重點所在。

5b. 為 Chatbot 賦予靈魂

Telegram 本身提供以 HTTP 為基礎的 API,所以要賦予 Telegram Bot 靈魂可以有很多方法,Python、JavaScript,甚至 Excel VBA 也可以,只要讀者能找到這些程式語言如何與 Telegram 提供的 API 溝通便可。

本篇筆者將會利用一個蠻好用的 Telegram API 的 Python Wrapper「Python Telegram Bot」。而為避免本文成為一篇真正的「宅文」,太技術性的部分筆者在解釋上或會有所從略。各位編程員如有興趣,可以拜讀其 GitHub 介紹:https://github.com/python-telegram-bot/python-telegram-bot/wiki/Introduction-to-the-API

要使用這個 Wrapper,第一件事當然是安裝進系統(Google Colab 的朋友這回對不起了,Google 還沒有支援,所以請還是回到 Terminal 去⋯⋯):

$ pip install python-telegram-bot --upgrade

接著,讓我們新增一個靈魂程式碼檔 TelegramBot.py,並將之放在與 NgauKeeFX.py 同一個資料夾內。第一句我們從 Python Telegram Bot 匯入需要的部件:

   from telegram.ext import Updater, CommandHandler, MessageHandler, Filters

還有要匯入的當然是擱取匯率資訊的自家建設的函數群:

   import NgauKeeFX

接下來幾句,筆者先不作解釋,請照著打就好;除了 Token 的位置,請換上 BotFather 親授閣下的智能秘書 Chatbot 的私人 Token(字串格式)。

   Token = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
   updater = Updater(token=Token)
   dispatcher = updater.dispatcher

至此準備功夫已做好了。接下來就可以即席揮毫,就著使用者所有可能的輸入,逐一書寫智能秘書的回應!

首先,有否留意在上面截圖中,當跟新的智能秘書打招呼時,系統會強制執行一個叫做 /start 的指令?這個是讓智能秘書跟你 Say 個 Hi 的機會。上圖中的秘書尚未有反應,就是因為他/她的靈魂還沒有裝上。要編寫智能秘書在你給予 /start 指令的反應,可以新增以下幾句:

   def start(bot, update):
       Message = "福祿壽全兄您好!很高興能為您效勞!"
       bot.send_message(chat_id=update.message.chat_id, text=Message)
   start_handler = CommandHandler('start', start)
   dispatcher.add_handler(start_handler)
   updater.start_polling()


現在,我們可以呼喚一下這個小小靈魂,讓它叫動我們的智能秘書了!儲存好 TelegramBot.py 之後,在 Terminal 移動到放有兩個 .py 檔案的資料夾,然後執行:

   $ python TelegramBot.py

接著在 Telegram 試著再次開展跟秘書的對話。神奇的事… 就… 此… 發… 生…



不過這個時候如果我們向秘書發送其他的訊息,他/她還是會木頭木腦毫無反應的;這就要靠我們後續的努力了!另外,留意 Python 程序仍然在執行以不斷接收並處理使用者的輸入,一但我們中止了程序(Mac Terminal 可按 control + z)或是把電腦關掉,秘書的靈魂就會熄滅掉。這就是我們最後一步需要考慮將靈魂放上雲端運算的原因。


下一步就是牛記匯率的問答。我們希望秘書能回應「/fx」的指令。如果指令後有其他參數,將參數視為所需貨幣,並嘗試按照輸入顯示該貨幣匯價;否則就視指令為顯示主要貨幣的匯率買賣價一覽。輸入如下並再執行靈魂程式:

   def FX(bot, update, args):
       Currency = " ".join(args)
       if Currency == "":
           Message = NgauKeeFX.MajorCurrFXMsg()
       else:
           Message = NgauKeeFX.PromptedCurrFXMsg(Currency)
       bot.send_message(chat_id=update.message.chat_id, text=Message)

   fx_handler = CommandHandler('FX', FX, pass_args=True)
   dispatcher.add_handler(fx_handler)


建構一個 Chatbot 是否要比想像中簡單?

再來下一段,就是如果用家不是用「/fx」指令的方式提出要求,我們還是盡量希望能配合的。如果用家能輸入「FX」、「匯率」、「牛記」等字眼,我們也會視之為對牛記匯率的請求,顯示主要貨幣的匯率買賣價一覽。否則,我們的秘書還是會禮貌地(或不禮貌地,視乎讀者對其角色設定)告訴使用者自己看不明白:

   def echo(bot, update):
       if update.message.text.encode('utf-8') in ["FX","匯率","牛記"]:
           Message = NgauKeeFX.MajorCurrFXMsg()
       else:
           Message = "福祿壽全兄,恕我未能明白何謂「" + 
                     update.message.text.encode('utf-8') + "」。"
       bot.send_message(chat_id=update.message.chat_id, text=Message)
   echo_handler = MessageHandler(Filters.text, echo)
   dispatcher.add_handler(echo_handler)

告訴使用者自己看不明白,除了訊息外,同樣適用於看不明白的指令:

   def unknown(bot, update):
       Message = "福祿壽全兄,恕我未能明白何謂「" + 
                  update.message.text.encode('utf-8') + "」。"
       bot.send_message(chat_id=update.message.chat_id, text=Message)
   unknown_handler = MessageHandler(Filters.command, unknown)
   dispatcher.add_handler(unknown_handler)




5c. 小結

至此,整個控制智能秘書的靈魂程序碼檔 TelegramBot.py 也開發完成。整個檔案應該有四個程序函數及其相應新增至 Handler 及 Dispatcher 之指令(內容從略):

   ## TelegramBot.py

   from telegram.ext import Updater, CommandHandler, MessageHandler, Filters
   import NgauKeeFX

   Token = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
   updater = Updater(token=Token)
   dispatcher = updater.dispatcher

   def start(bot, update):
       ...

   def FX(bot, update, args):
       ...

   def echo(bot, update):
       ...

   def unknown(bot, update):
       ...


如果讀者打算長開個人電腦,為秘書提供永恆的靈魂,以下的最後一步可以省略。否則,請繼續完成最後一節……


6.  將智能秘書的靈魂程式放上雲端運算

網上支援 Python 雲端運算的服務可謂包羅萬有,從山寨免費服務到企業級的 Google Cloud Platform / Amazon AWS 都有,豐儉由人,讀者不妨多做比較再決定讓秘書的靈魂在何處落地生根。

筆者選用的是 Python Anywhere (https://www.pythonanywhere.com),貪其方便易用,系統 Up Time 和穩定性也不錯。網站雖然容許免費戶口,但免費戶口能「瀏覽」的只限此表(https://www.pythonanywhere.com/whitelist/)註明的網站;要擱取牛記匯率等特定網站,筆者用的是最低級的收費戶口,每個月美帝幣 5 枚。

開戶後,在 Files 新增一個資料夾(假設叫「TelegramBot」),並把 NgauKeeFX.py 及 TelegramBot.py 上載至資料夾內。

在 Consoles 新增一個 Bash(就是 Terminal / Command Prompt)。在執行靈魂程序 TelegramBot.py 之前,別忘了安裝我們會用到的 程序包。注意 Python Anywhere 支援不同版本的 Python,我們用到的是 2.7,所以應利用 「pip2.7」;另外我們只有替自己而沒有替其他用家安裝的權限,所以亦需註明「--user」:

   $ pip2.7 install --user requests
   $ pip2.7 install --user beautifulsoup4
   $ pip2.7 install --user python-telegram-bot --upgrade

之後便可以執行靈魂程序,其中 username 是閣下 Python Anywhere 的用戶名稱:

   $ python TelegramBot/TelegramBot.py

這就完成將秘書靈魂放到網路雲端處運算的工序了。

留意理論上靈魂程序的執行是不會自己終止的,但雲端伺服器偶爾也會有維護而強行殺掉靈魂的時候。Python Anywhere 提供了一個繞路方案,讓程序保持接近 24/7 執行,詳情可以參考:https://help.pythonanywhere.com/pages/LongRunningTasks

設定好後,靈魂程序便能(接近) 24/7 執行,讓智能秘書隨時隨地為我們效勞了!



總結

前後花了兩個月時間才將這幾個月來學到的技巧整理成文,雖然愈寫愈覺得是在「趕客」,希望仍然對某部份理科底子比較強的讀者群、有興趣研究自動化、以及以最皮毛的方式初窺一下下一波「中年危機」即人工智能如何取代我們飯碗的朋友,有所裨益和幫助吧!

書於山竹襲港全城戒備的十號風球前夕,明天既然大家都只能待在家裏,何不一起動手來,在 Telegram 砌個虛擬男女朋友 / 閨蜜 / 颱風預報科學主任,甚至懂報股價和分析股票的大師? 



【祿】邁向財務「自由」之路 II — 育成收發 Telegram 的智能秘書(上篇)

筆者的智能秘書已從只懂發出電郵,進化到懂得接收及發出 Telegram 短訊了 :D

筆者去年發表 Step-by-step 開發 Google Sheet 智能秘書的教學文章,利用 ImportXML 和簡單兩句發送電郵的程序,自動定時提示六合彩下期獎金,幸獲不少讀者的好評。

然而 Google Sheets + Email 這個組合也有不少短處和限制,也許嘗試過一起動手做的讀者也曾感受一二:
  • ImportXML 函數擱取資訊的表現不很穩定,對某些網站的標的偶爾會傳回 #N/A
  • 基於試算表格式的限制,只能擱取資訊的原型,如要進一步處理資訊會頗為麻煩,例如參考筆者回覆讀者 Nelia 的提問,在擱取牛記匯率時需要將幾幅圖片蘊含的數字以及小數點併湊在一起,要在 Google Sheets 處理便很費時失事
  • 資訊的長度不一(也可能是動態),例如不同股票的派息紀錄,長度和所載資訊也不一,很難用統一的試算表格式處理
  • Google Script 每日有頗低的運算資源上限,筆者曾嘗試開發股價數據資料庫,但每 15 分鐘處理一隻股票也爆錶
誠如筆者在半桶水爸爸網誌留言,程式碼是現代語言,編程是現代人不能不認識的技巧,所以筆者把心一橫,將智能秘書的核心技術由 Google Sheets + Script 改寫為 Python 並放上雲端運算,同時賦予秘書收發 Telegram 短訊的能力,一次過解決以上問題。

這回讓筆者以牛記匯率為例,實戰如何用幾句簡單的程式碼擱取網路資訊,並以自己喜歡的方式呈現出來。上篇主要集中討論擱取資訊的技巧,而下篇則集中討論資訊的展現、與 Telegram 的聯繫,以及如何將程序放上雲端運算。

跟去年文章一樣,擱取的對象只是示例,筆者旨在刺激讀者思考,一理通百理明。另外,請注意網絡資訊的版權屬於相關資訊提供者,本篇及下篇文章只作教學及示範用途。


上篇:利用 Python 及其程序包 Beautiful Soup 輕鬆取得並整理匯率資訊


1. 準備開發環境

首先需要準備的是 Python 的開發環境。現在的開發者有福了,因為 Google 最近支援雲端 Python Notebook,也 Pre-install 了一系列常用的程序包(包括下文提及的 requests 和 BeautifulSoup),甚麼安裝也不用做已經可以著手開發自己的程序。Google 用戶只需到 Co-laboratory (colab.research.google.com),然後新增一個 Python 2 Notebook 便可。稍後開發完成,只需把整個程序碼檔案下載(「File」->「Download .py」)便行,方便得不得了。

如果希望在自己的電腦上進行開發,Mac 的用家會比較方便,因為 MacOS 已內置 Python 2.7,直接在 Terminal 輸入 Python 便能進入環境。Windows 的用家也許需要 Google 一下如何安裝 Python 2.7,反正網路上有海量資源,就當成是第一個練習吧!另外,我們會用到兩個程序包(Package),分別是 requests 和 BeautifulSoup(留意在 Python 中,大小寫是有分別的)。安裝 Python 後,我們可以在 Mac Terminal / Windows Command Prompt (往後簡單以 Terminal 代表)用 pip 命令安裝這兩個程序包(需要網路連線): 

  $ pip install requests
  $ pip install beautifulsoup4 

留意 $ 號並不是輸入,而是 Terminal 自動生成的提示碼。至於關於這兩個程序包的詳細資訊,可以參考: 

進入 Python 開發環境(在 Google Co-laboratory 新增 Python 2 Notebook,或者在 Terminal 輸入 $ python,此時提示碼變成 >>> 代表進入成功,否則安裝可能出了問題),輸入以下的程序碼以載入兩個剛下載了的程序包。 

  >>> import requests 
  >>> from bs4 import BeautifulSoup 

留意在 Google Colaboratory 需要按 Shift + Enter 去執行程式碼。 

如果沒有輸出(Terminal 的話,提示碼維持 >>>),代表成功安裝,可以進入下一個步驟。

往後的討論主要假設以 Google Colaboratory 作為開發環境,因為相信有能力操作 Terminal 的讀者應該有能力找到相應的處理。 





2. 探索牛記匯率網頁原始碼 

這一個步驟所用到的技巧,跟系列上一篇探索馬會六合彩網頁的技巧類似。 

讓我們先在瀏覽器探索一下。打開牛記發佈匯率的網頁(http://www.nkcl.com.hk/change-ch.php),利用 Inspect Element 並從原始碼檔了解匯率的排列方式,不難發現其頁面的結構相對簡單: 
  • 所有的匯率資訊都放在頁面上第一個 <table> 裏面
  • <table>的首兩行(<tr>)是標題,並沒有匯率資訊的存在
  • 從第三個 <tr> 開始,每行有四格( <td>)。第一格是國旗和貨幣全名,第二格是貨幣國際通用代碼,第三格是買入價,最後一格是賣出價。
  • 買入價和賣出價以圖片而非文字表示。圖片的名稱(即 <img src="xxx" /> 的 xxx)與其標示數字相符,例如 1.png 代表數字「1」。小數點的圖片名稱則是 vir.png。
  • 我們希望取得的是貨幣代碼、買入價和賣出價,當中後兩者以數字而非圖片表示,並以有條理的方式(例如列表)儲存好。
有了以上對目標擱取資料的認識,已經足夠完成任務了。接下來,只剩下將以上各點編寫成程序了!




3. 將對標的物的描述換成程式碼 

3a. 下載目標網頁並建立結構方便資訊擱取 

回到 Python 開發頁面。第一步是讓程序「瀏覽」即下載目標網頁。先定義我們的目標網頁網址:

  url = "http://www.nkcl.com.hk/change-ch.php" 


按 Shift + Enter (下文略),此時應該沒有輸出。

利用 requests 程序包,將網頁內容從互聯網下載並讀進 r 物件內(r 的名稱可以自定義,另外這一步很明顯需要網絡連線):

  r = requests.get(url).content

此時如果我們檢視 r 的內容(可以用 print r),就會發現 r 只是目標網頁的原始純文字檔;此時對於 Python 來說,文字檔只是一堆沒有意義的符號的集合。




要為 r 賦予意義,即要讓 Python 懂得閱讀這純文字檔的結構,例如讓它知道哪一個 <table> 裏面有哪一些 <tr>,<tr> 裏面又有 <td> 這些從屬關係,而每一個 tag 各自有其標示的屬性,就是簡單而強大的工具包 BeautifulSoup 的工作了。只消以下一句簡單程式碼,即能為 r 賦予意義,回傳的是經過整理、蘊含所有屬性和從屬關係的 BeautifulSoup 物件「Soup」:

  Soup = BeautifulSoup(r, "html.parser")

此時如果讀者用 print Soup 的話,見到的也只是同一個原始碼檔;但請放心,只要沒有 Error 跑出來的話,Python 已經明白了整個 HTML 檔的結構了!有了這個經整理的 Soup ,我們就可以就著對所要求標的物的認識,輕鬆從中尋找並擱取所需資訊。

(至此可能對沒有編程經驗的讀者來說是有點抽象,但只要繼續閱讀下去相信讀者必定能找到端倪,加油!)



3b. 鎖定網頁的資訊目標

就着第 2 節擱取要求的第一點「所有的匯率資訊都放在頁面上第一個 <table> 裏面」,我們要尋找的是 Soup 內第一個 <table> 物件。要取得第一個物件,方法是利用 find 方法:

  TargetTable = Soup.find("table")

所回傳的 TargetTable 其實是一個較小的 BeautifulSoup 物件,內含網頁中第一個 <table> 至 </table> 內所有的屬性和子物件。用一下 print TargetTable 看看,應該就能讓讀者安心了。由於我們需要的匯率資訊都在這個 <table> 內,有了 TargetTable 便能方便收窄其後程式碼的搜索範圍。 





就著要求第二點「<table> 的首兩行(<tr>)是標題,並沒有匯率資訊的存在」,我們要尋找 TargetTable 內所有 <tr> 物件,並去除首兩行。

要取得所有物件,方法是利用 find_all 方法:

  AllRows = TargetTable.find_all("tr")

所回傳的 AllRows 是一個 Python 的列表(List)物件,這個 List 的每一個元素(Elememt)各自是一個 BeautifulSoup 物件,每一個 BeautifulSoup 物件對應在 TargetTable 內找到的一個 <tr>...</tr>。即在概念上,AllRows = [ Soup_tr1, Soup_tr2, ..., Soup_trN ]。

至於除去首兩行,即在 Python List 中除去首兩個 Elements,則是輕而易舉的事:

  TargetRows = AllRows[2:]

留意上述數句的程式碼其實可以簡單一行輕鬆完成: 


  TargetRows = Soup.find("table").find_all("tr")[2:]

這是由於 find 方法接受一個 BeautifulSoup 物件後,回傳的又是另一個BeautifulSoup 物件。這種連鎖寫法,簡單方便直接之餘,又能方便人類閱讀(很容易就能理解為「TargetRows 就是在網頁 Soup 內找第一個 <table>,然後在內找所有 <tr>,再回傳除首兩項外所有的 <tr> 項目」),這就是 Python 之美。



3c. 建立「字典」儲存匯率資訊


接下來我們就可以從每一個 <tr> 行(即 TargetRows 裏面每一個 Element),擱取需要的貨幣代碼、買入價和賣出價。但在討論下去之前,我們先要考慮一下在擱取得實際資料後,我們打算如何有系統、有條理地儲存起來,方便發送 Telegram 短訊的部分使用。

我們的目的,是建立一個「字典」,可以讓我們利用外幣的代號,查詢其買入和賣出價。如果這個字典叫做 FXDatabase,我們希望它讀畢網頁的資訊後,效果如下:

  print FXDatabase['GBP']['Buy']
  10.01

  print FXDatabase['GBP']['Sell']
  10.06

  print FXDatabase['CHF']['Buy']
  7.84

  print FXDatabase['CHF']['Sell']
  7.96

在 Python 裏的確有一種叫做「字典」(Dictionary)的物件,能不費吹灰之力就能做到上述效果。如果我們一早知道英鎊和瑞士法郎的匯價,以上的字典可以如此定義:

  FXDatabase = {
    'GBP' : {'Buy' : 10.01 , 'Sell' : 10.06 },
    'CHF' : {'Buy' : 7.84 , 'Sell' : 7.96 }
    }


留意在 Python 中,字典是以花括號 { } 定義的;另嚴格來說,FXDictionary 是「字典的字典」(留意兩層花括號結構)。

但現在的問題是,我們未讓程式「閱讀」網頁前,並不知道每一種外幣的匯價,甚至連有甚麼外幣提供也不知道(為增加程式的可用性,請盡可能不要將外幣列表手動抄下來然後手動的對號入座,試想像牛記可以隨時更改其可供兌換的貨幣種類)。所以,接下來我們得讓程式從每一個 <tr> 行擱取所需資訊,並續一寫進我們的字典。

要這樣做,我們得先建立一部「空字典」:

  FXDatabase = {}

然後就可以開始命令程式從每一個 <tr> 行進行閱讀並寫進字典裏。要指令程式將 TargetRows 的每個 Element 執行一遍,先要定義一個 for 迴圈:

  for Row in TargetRows:

此時請不要按 Shift + Enter,改用 Enter 直至整個 for 迴圈編寫完畢。同時請留意,Python 是用縮排(Indentation)去區隔不同程序碼的集合的,所以由下一行起,所輸入的在 for 迴圈內的程序碼,請謹記在前面加相同數目的空格。

好了,第一個我們希望取得的資訊是「貨幣國際通用代碼」,存在每一行的第二個 <td> 裏。由於 TargetRows 是一堆 BeautifulSoup 物件的表列,所以 Row 就是一個 BeautifulSoup 物件,可以用我們之前使用過的方法處理。聰明的讀者應該已經想到,可以利用 find_all 函數將所有 <td> 找出來:

  TDs = Row.find_all("td")

然後告訴 Python 需要 TDs 的第二個 Element (注意第一個 Element 的 Index 為 0、第二個 Element 的 Index 為 1,如類推):

  TDCurrency = TDs[1]

此時,TDCurrency 所包含的就是我們目標的那一個 <td> 格的 BeautifulSoup 物件,所以當然也包含了定義該 TD 相關屬性和其他子物件。例如,在英鎊匯價的那一行,我們得到的將會是(部分從略):

  TDCurrency = '<td style="..." width="10%">GBP</td>'

我們需要的,就只是 TDCurrency 裏顯示出來的文字部分,即上面的「GBP」三個字。從 BeautifulSoup 物件取得顯示文字的方法是 text。筆者再後加一個 .encode(‘utf-8’),因為網頁是用 UTF-8 編碼寫成,如此便能讓 Python 對擱取的文字進行相應的解碼:

  Currency = TDCurrency.text.encode('utf-8')

當然,以上的幾句程序可以簡潔的用一行表達:

  Currency = Row.find_all("td")[1].text.encode('utf-8')

至此貨幣代碼擱取完成。之後便輪到買入和賣出價,分別是 <tr> 的第三和第四個 <td>:

  TDBuy = Row.find_all("td")[2]
  TDSell = Row.find_all("td")[3]

由於牛記匯率很聰明地利用圖片而非文字表示匯率,所以我們不可以直接用 text 方法取得匯率資訊。我們得額外做一些功夫,將 <td>...</td> 內的資訊換成是匯率數字。為了不打亂我們的邏輯思考,現在讓我們先假設有一個叫做「GetFX」的自定義的函數,能做出這樣的轉換:

  Buy = GetFX(TDBuy)
  Sell = GetFX(TDSell)

至此我們已經取得所需要的三項資訊,即「貨幣代碼」Currency、「買入價」Buy 和「賣出價」Sell,可以將資料寫入預先建立的字典:

  FXDatabase[Currency] = { 'Buy' : Buy, 'Sell' : Sell }

好了, for 迴圈要執行的動作已編寫完成。但請先不要按 Shift + Enter 執行,因為不要忘記我們其中一步假設了「GetFX」自定義函數的存在,強行執行只會招致程式錯誤:



(在 Terminal 開發的朋友,對不起了 :P)




3d. 建立函數將圖片換成文字再換成數字匯率

現在讓我們先離開 for 迴圈的編寫,在 Google Colaboratory 建立一個新的 Code Cell 去定義 GetFX 函數。前文說過, GetFX 的作用,是將擱取到的 <td>...</td> 內的資訊換成是匯率數字。例如,行文之日,英鎊買入價為 10.01,從上面的程序我們將會讀到(部份從略):

  TDBuy = '<td align="center" style=...>
           <img src="images/1.png"/>
           <img src="images/0.png"/>
           <img src="images/vir.png"/>
           <img src="images/0.png"/>
           <img src="images/1.png"/></td>'

而現在我們就要編寫一個小函數,將 TDBuy 中間的 <img> 煎皮拆骨,使得:

  print GetFX(TDBuy)
  10.01

這回請容許筆者先展示完成的函數,再逐行解釋:

  def GetFX(TD):
      FX = ""
      for ImgTag in TD.find_all('img'):
          Src = ImgTag['src']
          ImgName = Src[(Src.find("/")+1):Src.find(".png")]
          Digit = "." if ImgName == "vir" else ImgName
          FX = FX + Digit
      return float(FX)

  • def GetFX(TD):
    在這行我們定義一個名為 GetFX 的函數,接受一個叫做 TD 的 BeautifulSoup 物件作為參數。TD 就是我們在上面 for 迴圈中傳入蘊含匯率圖片的 <td>...</td> BeautifulSoup 物件。
  • FX = ""
    由於我們要將每一張圖片逐一幻化成文字(每一張圖片將得到一個數字符或小數點),繼而合併再變成匯率數字,而我們不知道 <td> 中圖片的數量,所以我們又得利用 for 迴圈,而在迴圈之前我們先定義一個空字串去裝著逐一得到的文字。
  • for ImgTag in TD.find_all('img'):
    建立 for 迴圈,讓程序將 <td> 內找到的所有 <img> 也逐一執行一次以下的程式碼,就是從 <img> 中抽取所需的字符。
  • Src = ImgTag['src']
    字符存在 <img> 的檔案名稱內,所以我們感興趣的是 <img> 的 src 屬性。Src 執行的結果就是字串 "images/X.png",而 "X就是我們希望得到的字符。
  • ImgName = Src[(Src.find("/")+1):Src.find(".png")]
    要從 "images/X.png" 抽出 "X",方法就是找出字符 "/" 和 ".png" 在 Src 的位置,然後在 Src 本身取得相應位置的字符。這方法跟 Excel 中的 MID() 和 FIND() 混合使用的原理是一樣的。
  • Digit = "." if ImgName == "vir" else ImgName
    這步很容易明白,如果讀到的圖片檔名是 "vir",代表它是小數點,應以 "." 取代之;否則所需字符就是圖片檔名本身。
  • FX = FX + Digit
    將新得到的字符置於代表整個匯率的 FX 最後方,然後重新跑一次 for 迴圈去閱讀下一個字符,直至 <td> 內所有 <img> 完成為止。
  • return float(FX)
    最後,將得到的整個匯率 FX(字串)利用 float 函數轉為數字,並作為整個 GetFX 函數的結果回傳(return)。

現在我們只需先執行這個函數的 Code Cell 將其定義,然後再返回上面的 for 迴圈再執行一次,Error 就會消失。雖然 for 迴圈沒有回傳,但我們的匯率字典已經在瞬間編寫完成了!

如果不相信的話,可以試著執行以下的指令:

  print FXDatabase
  print FXDatabase['CHF']
  print FXDatabase['GBP']['Sell']


雖然開發過程好像很漫長,但回首一看,我們只是用了短短 22 行的程式碼,已經取得了所需的資訊,並有系統地儲存好。 

以下列出本篇訖今整個程序碼,Google Colaboratory 的使用者可以下載 .py 文檔再加以修飾得到;使用 Terminal 的開發者可以另開新純文字檔,將我們的 Python 程序碼儲存成以下 Python Script 檔案:

  ## NgauKeeFX.py

  import requests
  from bs4 import BeautifulSoup

  def GetFX(TD):
      FX = ""
      for ImgTag in TD.find_all('img'):
          Src = ImgTag['src']
          ImgName = Src[(Src.find("/")+1):Src.find(".png")]
          Digit = "." if ImgName == "vir" else ImgName
          FX = FX + Digit
      return float(FX)

  url = "http://www.nkcl.com.hk/change-ch.php"
  r = requests.get(url).content
  Soup = BeautifulSoup(r, "html.parser")
  TargetRows = Soup.find("table").find_all("tr")[2:]

  FXDatabase = {}

  for Row in TargetRows:
      Currency = Row.find_all("td")[1].text.encode('utf-8')
      TDBuy = Row.find_all("td")[2]
      TDSell = Row.find_all("td")[3]
      Buy = GetFX(TDBuy)
      Sell = GetFX(TDSell)
      FXDatabase[Currency] = { 'Buy' : Buy, 'Sell' : Sell }


至此,上篇要討論的,從網際網路擱取資訊的技巧已完成討論。下篇我們會將擱取到的匯率資訊,以喜歡的方式在 Telegram 短訊呈現。敬請期待!



【祿】政府年金之我見

這兩天多位 Blogger 都在討論政府年金,讓筆者也湊湊熱鬧。政府年金的產品特色、償付條件、各方評價相信大家已經聽得太多,容許筆者不再贅述,直入正題。

(圖片來源:香港 01)


首先筆者必須指出,年金不是純投資的產品。說得白一點,年金其實是在生的年金持有人「發死人財」的遊戲。年金持有人所得到的「年金回報」,背後其實是年金公司資產管理者從資產所賺取的「投資回報」,加上「死亡回報」製作出來的:

個人「年金回報」 = 整體「投資回報」+ 「死亡回報」

容許筆者以例子說明。為求簡化例子,本例假設投資回報為零。

假設一群 100 名 70 歲的長者成立了一個互助社,每人貢獻 $300 萬出來成立「自製年金信託」,即成立日基金總額為 $3 億。從即日起,每年之初,信託管理人跟據這班長者的指示,從信託基金取出 $1,000萬,並平分給每年年初所有在生的長者。由於本例假設投資回報為零,基金一共可以取出款項 30次,直至這群老人家 100 歲。

  • 第 1 年,100 名老人家均健在,每人分得 $10萬。
  • 第 2 年,有 1 位老人家在第 1 年內不幸過身了。餘下在生人數只有 99 位,每人分到 $101,010。
  • 第 3 年,有另外兩位老人家不在,餘下在生人數只有 97 位,每人分到 $103,092。
  • ...
  • 第 30 年,只有一名老人家仍然生還,他該年獨吞 $1,000萬。


可以看見,即使支持「信託基金」背後的資產回報為零,每一名在生的老人家所得到的「年金」收入仍然可以遞增,因為在生的人瓜分了死去的同伴最初的貢獻。這便是所謂的「死亡回報」,學名是 Survivorship Accumulation。

留意政府年金的給付是每個月定額的,並不如上述例子每年遞增,這只政府年金公司的產品定價部門將現金流搓圓按扁再排得整整齊齊,但背後產生年金回報的來源概念上是一樣的。(註:上面這個信託安排的學名叫做「唐提」Tontine,有興趣的讀者不妨研究一下。)

政府年金之所以能做到 6.3%-7%「粗略回報率」(Crude Return Rate,即男性年取 $69,600、女性年取 $63,600 除以 $100萬本金),粗算概念就是由政府保證 4% 的「投資回報」,加上每年平均有 2-3% 老友記離開而為在生的年金持有人提供「死亡回報」製造出來的。但由於投保人在投保時並不知道誰人會先行離去,所以年金投保人有機會會是第一年交保費後立即過身「蝕盡」的那位,也有機會是上例守候 30 年的「最後武士」(或「最後無事」)。



當筆者說到這裏,或者說到政府年金在過早身故或者提早贖回會有賠本機會(有些人的「賠本」可以指「比內部回報率 4% 回報為低」,縱使政府已經在死亡給付上包了 105% 底),有些朋友或者老友記就會說,「如果我早死,那豈不很蝕底?」「如果我過早死了,為甚麼後人一筆過償付死亡保障要被『懲罰』?」

要拆解這個謎思,請容許筆者將上面的例子換一換。假設那 100 名長者均有上述老友記「早死好蝕」的擔心,而大家在開盤之初都不知道自己是「賺了」還是「蝕了」那位,於是在成立「自製年金信託」之時,指示信託人在有人離世時必須返還該位長者所貢獻的金額,減去未曾獲分發的年金。結果?


  • 第 1 年,100 名老人家均健在,每人分得 $10萬。
  • 第 2 年,有 1 位老人家在第 1 年內不幸過身了。由於該老人付出了 $300萬但只曾獲發 $10萬年金,他獲得了 $290 萬的賠償。基金現在只剩下 $2億 8710萬。由於預算分 29 年,如果繼續每年攤分 $1,000萬基金到最後便入不敷支,所以管理人今年只分發 $990萬 = $28,710萬 / 29。餘下在生人數只有 99 位,所以每人分得 $10萬,不多不少。
  • 第 3 年,有另外兩位老人家不在,該兩位各得到 $280萬賠償。餘下在生人數只有 97 位,不用多說,每人也只是分到 $10萬。
  • ...
  • 第 30 年,只有一名老人家仍然生還,但基金此時只剩下 $10萬。這位仁兄 30年來不多不少取回當初貢獻出來的 $300萬。


結果就是,每一位老人家都要公平,所以人人「得個吉」!每人分不到「死亡回報」,只能理論上從基金得到「投資回報」(如有的話,上例假設為零);信託當中沒有分擔長壽風險的機制,那就每位老友記自己去投資好了,不用搞甚麼年金。很多人覺得「早死會蝕」就對年金「耍手擰頭」,因為這班人不明白年金的真締就是跟其他年金投保人賭長命「發死人財」,而「賭」就是有「賭贏」和「賭輸」的機會。如果閣下不願意冒上「賭輸」的風險,想魚(死亡給付)與熊掌(生存年金)兼得,那年金產品並不適合閣下。

所以分析年金,我們不應該單看年金的「粗略回報率」或者「內部回報率」,因為當中包含了保險保障的成份,而這回所保障的是「長壽風險」。如果單看回報高低就不買,那麼倒轉過來筆者極力推崇的定期人壽保險也一定不要買了,因為不死就一毛錢都取不到,保險公司「袋晒」;而筆者相信除了有心「呃保險」的極小眾外,應該沒有人希望能取到壽險賠償吧?



筆者認為,考慮應否購買年金,應該同時考慮回報和保障兩個部份。
  1. 年金的保障部份的確要看當事人有多擔心長命的。以同一個資產內部回報率來計,因著「死亡回報」,將本金年金化(Annuitize)所得到的每期回報(即上述的粗略回報率)定必有所提高。如果一名 65 歲男士無人又無物,健康無病痛,手頭上只有 $100萬現金,讀者如果是他,會選擇 (a) 將投資於在保證年回 4% 的收息物,年取 $40,000,還是 (b) 購買政府保證內部回報率 4% 的年金,年取 $69,600?

    當然上述選擇假設了 (1) 他只有兩項回報相同的投資可供選擇(回報的問題在下面討論)、(2) 故事主角無人又無物,沒有遺產需要留給後人的考慮、(3) 他確實擔心長壽,即某程度上他有自信「賭贏」。留意如果他明知自己多病痛、食煙飲酒三高,筆者就會勸他不要學人賭長命,就算賭都應該考慮留番個錢睇醫生。


  2. 年金的回報部份,在比較時也要留意,應該與擁有相似特性的投資物作比較。政府年金「投資」的特性是長線、流動性低但政府保證回報,因此筆者會採用跟政府長期債券去作比較,而非像某些評論員般以股票作點評。需知道 4 厘保證回報,現市況來說真的多長的政府債券 / 銀髮債也做不到。當然日後加息幅度和時間也很難說,但至少就現市況來說,4% Guaranteed IRR 簡直就是政府在派錢。

    在私人市場,筆者敢寫包單一般人壽保險公司不可能會開出這個盤。其實 10 年前金融海嘯以前,年金產品也曾經掀起過一輪熱潮,不過當時賣的是「變額年金」Variable Annuities,年金給付跟投資相連掛勾之上還有各式各樣的保證回報,甚麼 GMDB、GMAB,百花齊放,好像金融市場上的衍生工具、Accumulator 玩法一樣五花百門。這些產品在海嘯後「連影都無」,為甚麼?因為一場海嘯,令監管機構真正意識到「保證回報」是有成本的,而且在零息時代成本之鉅大高得很多人都不相信。海嘯後監管機構、會計準則紛紛修例,要求保險公司更準確反映保證回報的資本成本(Capital Cost)和準備金(Reserves)。長期 4% 保證回報,資本成本高高在天,要不是我們港府儲備雄厚,保監根本不可能批准推出。這也正說明了為甚麼政府要為年金產品設有 100 / 200 億元的額度。

    所以 4% 保證回報是高是低?見仁見智吧。

最後筆者有兩點補充評語:
  • 公共年金的設計上有個「死位」最惹人詬病,就是年金給付「追不上通脹」。筆者也認為政府日後在設計類似年金產品時可以考慮加入通脹的元素,例如年金給付能隨通脹遞增,或者以固定某個百分率增長。但大家也明白「羊毛出自羊身上」的道理,在保證回報大約不變的前提下,要日後通脹派多些,自不然現在要派少些。到時買 $100萬 而年金金額每年遞增 4%,但第一年每個月只取得 $3,000 左右,讀者又會買還是不買?要知道大眾、傳媒都在看表面回報、回本期、「有無賺蝕」的時候,政府現行的年金設計也許已經在迎合大眾口味而犧生某些真正有用的產品特色。
  • 筆者在網上見到不少評論員以 81-82 歲做香港男性平均死亡年齡,然後以此說平均男士只能做到保本(因為 65-80歲收 15 年錢後才大約收回 $100萬);這點其實有個統計謬誤。需知道 81-82 歲是香港所有男性在出生時起計的平均壽命,當中包括嬰兒時期夭折、青年壯年時意外死亡、或者上回講到在海洋公園被棺材壓死的青年。但是,買得起年金的,資格是首先要活到 65 歲,所以年金的參與者已經排除了一班「拉底整體平均壽命」的早死者(統計學上這是「條件概率」 Conditional Probability 的問題)。

    所以真正相關的平均,應該以香港 65 歲在生男士的「平均餘命」去計算。根據筆者以 2014 年香港人口生命表的計算,男性平均餘命 20 年,即活到 85 歲;女性平均餘命 24 年,即活到 89 歲。如能活到這些歲數,年金投保人的個人內部回報率,應該能與整體回報 4% 更為類近。

熱門文章