进入2018年,免货转(no-FX)信用卡突然大行其道。毕竟消费降级,大家囊中羞涩,传统信用卡2.5%的货币转换费简直等于给信用卡公司上税。
Brim Financial的Brim (WE)MC靠免货转的旗号,靠PPT收割了十几万个人信息然而发卡还是遥遥无期;Rogers推出了屌丝三宝之一的Rogers WEMC,外币返现4%;Scotiabank的Passport Visa Infinite正式成为丰业银行旗下的主打旅游卡产品;Home Bank的Preferred Visa仗着免年费可刷美国Costco成为了不错的抽屉卡;Prepaid卡的选择也有很多。
常逛小黄网的观众应该都知道,Visa和MasterCard肯定不会好心到按中间价进行兑换。那么到底高多少?交易群普遍认为是0.5%。有这么多吗?
这个问题greedyrates有过研究(https://www.greedyrates.ca/blog/mastercard-or-visa-foreign-purchases-better-canadians/) :但是数据量比较少,只对每周进行了取样,而汇率这个东西是瞬息万变的。这次的研究希望可以解决之前研究的缺陷。
研究方法:
数据源:Visa,MasterCard(下称MC),和中间价数据。文中所有单位都为基点(百分比)。数据的日期范围是报告日(2018年9月8日)前364天(2017年9月10日)至报告日,因为MasterCard只提供一年的历史数据。
- Visa的汇率来自https://usa.visa.com/support/consumer/travel-support/exchange-rate-calculator.html 。其中有9天没有数据:使用后一天的数据填充。
- MasterCard的汇率来自https://www.mastercard.us/en-us/consumers/get-support/convert-currency.html 。
- 中间价数据来自https://openexchangerates.org/ 。数据是当日closing价格的中间价。在处理数据中,可能有小于千万分之一的误差。
所有的数据由爬虫得到。每种数据抽样5次检查爬虫工作情况。爬虫源代码公开。
结论:
先上一张全家福:
眼花缭乱?我们一点点分析:
上图是按时间排列,Visa卡比MasterCard汇率高出的基点数。可以看出,总体而言,Visa的汇率要高于MasterCard。
计算得出,Visa平均比MasterCard高0.209个基点,然而标准差是0.438,意味着差异统计上不显著:因为在去年中,MasterCard只比Visa优秀271天。
下图更加清楚:
大部分情况下,MasterCard的汇率都会比Visa好那么一点的。
Visa比中间价高那么0.449个基点:标准差是0.611。所以单单是免货转的Visa卡是不够的:在极端条件下,1%的返现会被吃光。
比起Visa,MasterCard就没那么心黑:多收0.240个基点,标准差0.454。大部分的免货转MC都不用担心赔钱了:Rogers WEMC即使在最惨的情况会剩下个0.8%的。
结论:
- Home Trust的Visa慎用,有可能赔钱;Scotiabank Passport VI可用。
- 大部分的MasterCard都不会赔钱。
- 无脑刷MC吧,除非不让。
本次研究没能解决的问题:
- Visa和MC都有连续几天汇率不变的情况,然而国际汇市是不可能不波动的。Visa的问题更加明显。不知道是Visa的系统抽风还是Visa的交易员比较懒。
- AMEX的数据实在没有找到:希望有这部分数据的观众进行补充。
- 由于XE的数据太贵了,这次研究使用了openexchangerates的数据作为中间价,有可能精度不如XE:但是应该不会有颠覆性影响。
附:
- 原始数据:https://docs.google.com/spreadsheets/d/1uwTFxSuQsJxey_KP3pMO1h2xZmj0buSO5YSP3tzFMMU/edit#gid=0
- 爬虫代码:
#!/usr/bin/env python
#coding:utf-8
# Author: Beining --<i at cnbeining.com>
# Purpose: Research: Visa vs MC
# Created: 09/08/2018
import requests
import lxml
import re
from multiprocessing.dummy import Pool as ThreadPool
#----------------------------------------------------------------------
def get_visa_usd_cad_exchange_rate_by_date(date_string):
""""""
url = "https://usa.visa.com/support/consumer/travel-support/exchange-rate-calculator.html"
params = (
('amount', '100'),
('fee', '0.0'),
('exchangedate', date_string),
('fromCurr', 'CAD'),
('toCurr', 'USD'),
('submitButton', 'Calculate exchange rate'),
)
headers = {
'authority': "usa.visa.com",
'pragma': "no-cache",
'Cache-Control': "no-cache",
'upgrade-insecure-requests': "1",
'user-agent': "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.81 Safari/537.36",
'dnt': "1",
'accept': "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
'accept-encoding': "gzip, deflate, br",
'accept-language': "en-CA,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,en-GB;q=0.6,en-US;q=0.5",
}
response = requests.get(url, headers=headers, params=params)
if response.ok and 'converted-amount-value' in response.text:
price_find = re.search( r'<strong class="converted-amount-value"> (.+) Canadian Dollar', response.text)
if price_find:
price_find = price_find.groups()
else:
return (date_string, None)
if len(price_find) > 0:
return (date_string, float(price_find[0]))
return (date_string, None)
#----------------------------------------------------------------------
def get_mc_usd_cad_rate_by_date(date_str):
""""""
url = "https://www.mastercard.us/settlement/currencyrate/fxDate={date_str};transCurr=USD;crdhldBillCurr=CAD;bankFee=0;transAmt=100/conversion-rate".format(date_str = date_str)
headers = {
'user-agent': "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.81 Safari/537.36",
'referer': "https://www.mastercard.us/en-us/consumers/get-support/convert-currency.html",
'Cache-Control': "no-cache",
}
response = requests.get(url, headers=headers)
if response.ok:
return float(response.json()['data']['crdhldBillAmt'])
return None
#----------------------------------------------------------------------
def get_middle_usd_cad_rate_by_date(date_str):
""""""
url = "https://openexchangerates.org/api/historical/{date_str}.json?app_id=11ff5e6d97d74131abe05942bae6796e&base=usd&symbols=cad".format(date_str = date_str)
headers = {
'user-agent': "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.81 Safari/537.36",
'Cache-Control': "no-cache",
}
response = requests.get(url, headers=headers)
if response.ok:
return float(response.json()['rates']['CAD'])
return None
#----------------------------------------------------------------------
def execute_multiprocess(func, iterable, thread_num = 8):
pool = ThreadPool(thread_num)
result = pool.map(func, iterable)
pool.close()
pool.join()
return result
mc_date_list = [(datetime.date.today() - datetime.timedelta(days = x)).strftime('%Y-%m-%d') for x in range(0, 364)]
visa_date_list = [(datetime.date.today() - datetime.timedelta(days = x)).strftime('%m/%d/%Y') for x in range(0, 364)]
mc_result = execute_multiprocess(get_mc_usd_cad_rate_by_date, mc_date_list, thread_num = 8)
visa_result = execute_multiprocess(get_visa_usd_cad_exchange_rate_by_date, visa_date_list, thread_num = 16)
middle_result = execute_multiprocess(get_middle_usd_cad_rate_by_date, mc_date_list, thread_num = 8)
visa_result.count(None) # 9