数据说话:出国旅游,Visa还是MasterCard?

进入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%的。
结论:

  1. Home Trust的Visa慎用,有可能赔钱;Scotiabank Passport VI可用。
  2. 大部分的MasterCard都不会赔钱。
  3. 无脑刷MC吧,除非不让。

本次研究没能解决的问题:

  1. Visa和MC都有连续几天汇率不变的情况,然而国际汇市是不可能不波动的。Visa的问题更加明显。不知道是Visa的系统抽风还是Visa的交易员比较懒。
  2. AMEX的数据实在没有找到:希望有这部分数据的观众进行补充。
  3. 由于XE的数据太贵了,这次研究使用了openexchangerates的数据作为中间价,有可能精度不如XE:但是应该不会有颠覆性影响。

附:

  1. 原始数据:https://docs.google.com/spreadsheets/d/1uwTFxSuQsJxey_KP3pMO1h2xZmj0buSO5YSP3tzFMMU/edit#gid=0
  2. 爬虫代码:
#!/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

Leave a Reply

Your email address will not be published. Required fields are marked *