The official figure of credit card transaction fee for foreign currency transactions (Foreign Transaction Fees) is mostly 1.63%, but the actual figure is somewhat different. Especially, "I feel" the difference between VISA and Master is unexpectedly big. To produce statistically meaningful numbers instead of "I feel" -- to a very limited extent with personal data (The only one who can do that with many sorts of analysis axis is Money Forward, Inc. and so on.) -- I've get them in order as much as we can./カードを外貨建てで使った場合の手数料(海外決済事務取扱手数料/Foreign transaction fees)は公式では1.63%の数字が多いが、実際の数字はなぜか違う。特にVISAとMasterの差が意外と大きい「感触がある」。「感触がある」ではなく、統計的に意味のある数字を出すには――それが可能な切り口は――、個人の手持ちデータではかなり限られるが(縦横無尽の切り口でそれができるのはマネーフォワードなど限られた位置にある者だけ)、できる範囲で整理して見た。
There is another way to compare. I tried to use multiple cards at the same time at the same store to evaluate each other. I charged my gift cards on Amazon USA. This makes it easy to increase the number of observations./もう一つ、比較方法がある。同じ時刻に同じ店で複数のカードを使って相対評価する方式を試した。米国amazonでギフトカードをチャージして試している。これなら観測個数を増やすのが簡単だ。
Right: Comparison with TTM (In other words, absolute evaluation. Conversion date basis). Left: A relative evaluation with ANA Master. Compared with "Exchange Rate" on each card's invoice. (This is always there.)/右はTTMとの比較(いわば絶対評価。換算日基準)。左はANA Masterとの相対評価。各カードの請求書記載の「換算レート」(これは必ず載っている)を比較している。
とりあえず、ANA MasterとANA VISA Suicaの比較では、VISAの方が手数料が高いと言える。どちらも三井住友カードで、換算日が明記されているので、TTM比で比べても無理がないカードだ。JCBの数字は誤差がまだ大きめであり、何とも言えない。
換算日のTTMで比較する(絶対評価) | カード発行会社の上乗せ分を見る。 |
利用日のTTMで比較する(絶対評価) | カード発行会社とブランドの上乗せ分を見る。しかし誤差が大きく(為替変動や料率の変動や、その他未知の要因など全ての変動が一緒くたになっているためだろう)、カード毎の差も小さいため、解釈が難しい。 |
相対評価 | 為替変動リスクを含む消費者の負担を見る。誤差は為替変動リスクを見るのに適する。 |
自動化しないと誤記する。また、自動化し、巡回精度を保たないと統計精度を保証できない。
速報はCSVでダウンロードできないカードが意外とある。そういうのは勝手にボタンを付ける。Amexは単純なスクレイプでは取得できない(ページがJavaScriptで動的に作られる)ので、headless Chrome + Puppeteer (Node.js)で取得している。
const puppeteer = require('puppeteer'); const fs = require('fs'); (async() => { const browser = await puppeteer.launch({ //headless: false, args: [ '--no-sandbox', ] }); const page = await browser.newPage(); await page.setViewport({width: 1024, height: 768}); await page.goto('https://global.americanexpress.com/myca/logon/japa/action/LogonHandler?request_type=LogonHandler&Face=ja_JP&inav=jp_utility_login', { waitUntil: 'networkidle2'}); await page.waitForSelector('input#lilo_userName', {waitUntil: 'networkidle2', timeout: 10000}); await page.focus("input#lilo_userName"); await page.type("input#lilo_userName", 'your ID'); await page.focus("input#lilo_password"); await page.type("input#lilo_password", 'your passwd'); await page.click('#lilo_formSubmit'); await page.waitForNavigation({waitUntil: 'load'}); await page.waitForSelector('#balance-summary-table', {waitUntil: 'networkidle2', timeout: 2500}); // カードによって違うので要修正 await page.click('#balance-summary-table tr.recent-debits > td.description > a'); await page.waitForNavigation({waitUntil: 'load'}); await page.waitForSelector('#transaction-table', {waitUntil: 'networkidle2', timeout: 7000}); //tr要素のidを取得。transaction id let tid = await page.$$eval('#transaction-table tr[id]', list => { return list.map(date => date.id); }); let lines = []; // transaction id毎に // 各決済のtrは2段からなる。 // クリックによる展開前と展開後の詳細 // idが違う。 for (let i in tid) { await page.click("#transaction-table tr#" + tid[i] + " td.date"); await page.waitForSelector("#transaction-table tr#" + tid[i] + " + tr p.amount.ng-binding", {waitUntil: 'networkidle2', timeout: 7000}); // 詳細欄のtransaction id取得 // 例: #etd-details-content-AT181580... let dom = '#transaction-table tr#' + tid[i] + ' + tr[id]'; let tidd = await page.$eval(dom, e => e.id); // dateと請求額は1段目のtr dom = 'tr#' + tid[i] + ' td.date span.ng-binding'; let date = await page.$eval(dom, e => e.innerText); //#trans-... > td.date > div > span.ng-binding let shop = await page.$eval("tr#" + tidd + " p.header.ng-binding", e => e.innerText); //#etd-details-content-AT1815... > div > div.etdContent.ng-scope > div.etdBlock.ng-scope > p.header.ng-binding let local = await page.$eval("tr#" + tidd + " p.amount > span:nth-child(1)", e => e.innerText); //#etd-details-content-AT1815... > div > div.etdDetails.ng-scope > div > div:nth-child(2) > p.amount > span:nth-child(1) let curr = await page.$eval("tr#" + tidd + " p.amount > span:nth-child(2)", e => e.innerText); //#etd-details-content-AT181... > div > div.etdDetails.ng-scope > div > div:nth-child(2) > p.amount > span:nth-child(2) dom = 'tr#' + tid[i] + ' td.amount > div > span'; let jpy = await page.$eval(dom, e => e.innerText); //#trans-AT18158... > td.amount > div > span let rate_date = await page.$eval("tr#" + tidd + " div.etdContent.ng-scope > div:nth-child(2) > div > p.ng-binding.ng-scope", e => e.innerText); //#etd-details-content-AT18156... > div > div.etdContent.ng-scope > div:nth-child(2) > div > p.ng-binding.ng-scope let rate = await page.$eval("tr#" + tidd + " p.amount.ng-binding", e => e.innerText); // #etd-details-content-AT181... > div > div.etdDetails.ng-scope > div > div:nth-child(3) > p.amount.ng-binding let line = [date,shop,local,curr,jpy,rate_date,rate].join(','); console.log(line); lines.push(line); } fs.writeFile('amex.csv', lines.join("\n"),(err) => { if (err) throw err; }); //ログアウト await page.click('#jp_utility_login'); await browser.close(); })();
TTMの入手先:
自動化しないと、誤記する。
#!/usr/bin/perl use utf8; use strict; use warnings; my $url = 'http://www.smbc.co.jp/market/backnumber/fixing/'; # 今月 my $thisMonth = '//*[@id="tabbox1"]/div//li/a/@href'; # 前月 my $lastMonth = '//*[@id="tabbox2"]/div//li/a/@href'; use URI; my $u = URI->new($url); use HTML::TreeBuilder::XPath; my $tree = HTML::TreeBuilder->new_from_url($url); my @href = $tree->findvalues($lastMonth); push(@href, $tree->findvalues($thisMonth)); use LWP::UserAgent (); my $ua = LWP::UserAgent->new; for my $pdfpath (@href) { my $uri = sprintf qq(http://%s%s),$u->host,$pdfpath; my $response = $ua->get($uri); if ($response->is_success) { my $pdfbody = $response->decoded_content; my $tmpfile = "/tmp/msbcrate.$$"; open(my $fh, '>', "$tmpfile") or die "$tmpfile:$!"; print $fh $pdfbody; close $fh; open($fh, "pdftotext -layout $tmpfile - |") or die "pipe:$!"; my @content = <$fh>; close $fh; unlink $tmpfile; if (my ($date, %rate) = &msbcrate_pdf(@content)) { my $ttm = ($rate{'usd'}->{'tts'} + $rate{'usd'}->{'ttb'})/2; printf qq(%s\t%.2f\n),$date,$ttm; #my $insert = #"INSERT IGNORE INTO usd VALUES ('$date','SM','$ttm')"; #$db->do($insert) or die $db->errstr; } else { print STDERR "failed in $pdfpath\n"; } } } #$db->disconnect; exit; sub msbcrate_pdf { my @lines = @_; my $date = ''; my %rate = (); my %month = qw( Jan 1 Feb 2 Mar 3 Apr 4 May 5 Jun 6 Jul 7 Aug 8 Sept 9 Sep 9 Oct 10 Nov 11 Dec 12 ); for (@lines) { # Date: May.25, 2018 if (my @temp = (/Date:\s+([a-zA-Z]+)\D+(\d+)\D+(\d+)/)) { my $month = $month{$temp[0]} // 'fail'; $date = "$temp[2]/$month/$temp[1]"; next; } if (/U\.S\.A\.\s+USD\s+1\s+([\d\.]+)\s+([\d\.]+)\s+([\d\.]+)\s+([\d\.]+)\s+([\d\.]+)\s+([\d\.]+)\s+([\d\.]+)/) { $rate{'usd'}->{'tts'} = $1; $rate{'usd'}->{'ttb'} = $3; next; } } if ($date eq '' || !defined $rate{'usd'}->{'tts'}) { return; } else { return $date, %rate; } }
#!/usr/bin/perl use utf8; use strict; use warnings; use Time::Piece; use Time::Seconds; my $t = localtime; my $days = 20; # 過去○日分取得する。 use LWP::UserAgent (); use HTML::TreeBuilder::XPath; my $baseurl = 'http://www.murc-kawasesouba.jp/fx/past/index.php'; # http://www.murc-kawasesouba.jp/fx/past/index.php?id=180419 for (1..$days) { my $date = sprintf qq(%02d%02d%02d),$t->yy,$t->mon,$t->mday; my $url = $baseurl . '?id=' . $date; my $ua = LWP::UserAgent->new; # 営業日以外は「本日の為替相場」に転送される。 # http://www.murc-kawasesouba.jp/fx/index.php $ua->max_redirect(0); my $response = $ua->get($url); my $root = HTML::TreeBuilder->new(); if ($response->is_success) { $root->parse_content($response->decoded_content); my $tts = $root->findvalue('//*[@id="main"]/table[1]/tr[2]/td[4]'); my $ttb = $root->findvalue('//*[@id="main"]/table[1]/tr[2]/td[5]'); my $ttm = ($tts+$ttb)/2; my $date = $t->ymd("/"); printf qq(%s\t%.2f\n),$date,$ttm; #my $insert = #"INSERT IGNORE INTO usd VALUES ('$date','MU','$ttm')"; #$db->do($insert) or die $db->errstr; } $t -= ONE_DAY; } #$db->disconnect; exit;
Amexの決済インフラは他社に比べ圧倒的に強いかもしれない。Amexの自社発行カード(日本法人)は、試した範囲では、他のイシュアー(発行会社。三井住友カードや楽天カード、DCカードなど)やブランド(VISA、Master、JCBなど)に比べ、カードを利用してから明細に出るまでの時間が、圧倒的に速い。(今回、明細の確認はロボットで自動化しているので、傾向を統計的に把握できる。)クレジットカードの仕組みはもう何十年も前に作られたもので、本来リアルタイム決済を想定したものでは全くない。時代の趨勢に答えてリアルタイムで処理するには、多大なシステム投資が必要だ。しかも、自社だけの投資で終わらず、販売店との連携も必要になる。また、クロスボーダーの決済はどんどん増えるだろう。Amexでは、日本発行カードでも日本の営業時間に制約されないことも見て取れる。
Amexの他では、三井住友カード(VISA、Master)が結構健闘していた。それでも、日曜はデータが動かないことが見て取れるなど、システム設計の古さを推知できる。日本時間の午前中か否かで処理のバッチが切り替わる(Master)ことも推知できる。VISAかMasterかでバッチの挙動も異なる。自社ブランドでないので自分でコントロール出来ないのだろう。今回のカードでは、JCBは自社ブランド・自社発行のカードだが、速度に特に目立った特徴はない。三井住友トラストクラブ株式会社(三井住友信託銀行の100%子会社。旧、シティカードジャパン株式会社)のDinersは、今回、システム更新後のみで試しているが、かなり遅い。
こうしたことも、個人で調べるのは時間も金もかかるが、マネーフォワードなら簡単に分析できる。研究者には垂涎の的だろう。