File Coverage

blib/lib/Finance/MtGox.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Finance::MtGox;
2              
3 1     1   28312 use warnings;
  1         3  
  1         35  
4 1     1   6 use strict;
  1         2  
  1         43  
5 1     1   6 use Carp qw( croak );
  1         7  
  1         76  
6 1     1   1051 use JSON::Any;
  1         36888  
  1         7  
7 1     1   11026 use WWW::Mechanize;
  0            
  0            
8             use URI;
9             use Time::HiRes qw( gettimeofday );
10             use Digest::SHA qw( hmac_sha512 );
11             use MIME::Base64;
12              
13             =head1 NAME
14              
15             Finance::MtGox - trade Bitcoin with the MtGox API
16              
17             =head1 VERSION
18              
19             Version 0.50
20              
21             =cut
22              
23             our $VERSION = '0.50';
24              
25              
26             =head1 SYNOPSIS
27              
28             use Finance::MtGox;
29             my $mtgox = Finance::MtGox->new({
30             key => 'api key',
31             secret => 'api secret'
32             });
33              
34             # unauthenticated API calls
35             my $depth = $mtgox->call('getDepth');
36              
37             # authenticated API calls
38             my $funds = $mtgox->call_auth('generic/info');
39              
40             # convenience methods built on the core API
41             my ( $btcs, $usds ) = $mtgox->balances;
42             my $rate = $mtgox->clearing_rate( 'asks', 200, 'BTC' );
43             $rate = $mtgox->clearing_rate( 'bids', 42, 'USD' );
44              
45             =head1 BASIC METHODS
46              
47             =head2 new
48              
49             Create a new C object with your MtGox credentials provided
50             in the C and C arguments.
51              
52             =cut
53              
54             sub new {
55             my ( $class, $args ) = @_;
56              
57             $args->{key} && $args->{secret}
58             or croak "You must provide 'key' and 'secret' credentials.";
59              
60             $args->{json} = JSON::Any->new;
61             $args->{mech} = WWW::Mechanize->new(stack_depth => 0);
62              
63             return bless $args, $class;
64             }
65              
66             =head2 call( $name )
67              
68             Run the API call named C<$name>. Returns a Perl data structure
69             representing the JSON returned from MtGox.
70              
71             =cut
72              
73             sub call {
74             my ( $self, $name ) = @_;
75             croak "You must provide an API method" if not $name;
76              
77             my $version = $self->_version_from_name($name);
78             my $req = $self->_build_api_method_request( 'GET',
79             $version,
80             $name,
81             $version == 0 ? 'data': '' );
82             $self->_mech->request($req);
83             return $self->_decode;
84             }
85              
86             =head2 call_auth( $name, $args )
87              
88             Run the API call named C<$name> with arguments provided by the hashref
89             C<$args>. Returns a Perl data structure representing the JSON returned
90             from MtGox
91              
92             =cut
93              
94             sub call_auth {
95             my ( $self, $name, $args ) = @_;
96             croak "You must provide an API method" if not $name;
97              
98             my $version = $self->_version_from_name($name);
99             $args ||= {};
100             my $req = $self->_build_api_method_request( 'POST', $version, $name, '', $args );
101             $self->_mech->request($req);
102             return $self->_decode;
103             }
104              
105             =head1 CONVENIENCE METHODS
106              
107             =head2 balances
108              
109             Returns a list with current BTC and C<$currency> account balances,
110             respectively. If C<$currency> is not specified it defaults to USD.
111              
112             =cut
113              
114             sub balances {
115             my ($self, $currency) = @_;
116             $currency ||= 'USD';
117              
118             my $result = $self->call_auth('info');
119             return ( $result->{Wallets}{BTC}, $result->{Wallets}{$currency} );
120             }
121              
122             =head2 clearing_rate( $side, $amount, $currency )
123              
124             Traverse the current "asks" or "bids" (C<$side>) on the order book until the
125             given amount of currency has been consumed.
126             Returns the resulting market clearing rate.
127             This method is useful when trying to determine how much you'd have to pay
128             to purchase $40 worth of BTC:
129              
130             my $rate = $mtgox->clearing_rate( 'asks', 40, 'USD' );
131              
132             Similar code for determining the rate to sell 40 BTC:
133              
134             my $rate = $mtgox->clearing_rate( 'bids', 40, 'BTC' );
135              
136             Dark pool orders are not considered since they're not visible on the order
137             book.
138              
139             =cut
140              
141             sub clearing_rate {
142             my ( $self, $side, $amount, $currency ) = @_;
143             croak "You must specify a side" if not defined $side;
144             $side = lc $side;
145             croak "Invalid side: $side" if not $side =~ /^(asks|bids)$/;
146             croak "You must specify an amount" if not defined $amount;
147             croak "You must specify a currency" if not defined $currency;
148             $currency = uc $currency;
149              
150             # make sure we traverse offers in the right order
151             my @offers =
152             sort { $a->[0] <=> $b->[0] }
153             @{ $self->call('getDepth')->{$side} };
154             @offers = reverse @offers if $side eq 'bids';
155              
156             # how much will we pay to purchase the desired quantity of BTC?
157             my $bought_btc = 0;
158             my $paid_usd = 0;
159             for my $offer (@offers) {
160             my ( $price_usd, $volume_btc ) = @$offer;
161             my $trade_btc = $currency eq 'BTC' ? $amount-$bought_btc
162             : $currency eq 'USD' ? ($amount-$paid_usd)/$price_usd
163             : croak "Invalid currency: $currency"
164             ;
165             $trade_btc = $volume_btc if $volume_btc < $trade_btc;
166             $paid_usd += $trade_btc * $price_usd;
167             $bought_btc += $trade_btc;
168             last if $currency eq 'BTC' && $bought_btc >= $amount;
169             last if $currency eq 'USD' && $paid_usd >= $amount;
170             }
171              
172             return $paid_usd / $bought_btc;
173             }
174              
175             =head2 market_price
176              
177             Returns a volume-weighted USD price per BTC based on MtGox trades within
178             the last 24 hours. Returns C if there have been no trades in the
179             last 24 hours.
180              
181             =cut
182              
183             sub market_price {
184             my ($self) = @_;
185             my $trades = $self->call('getTrades');
186             my $threshold = time - 86400; # last 24 hours
187              
188             my $trade_count = 0;
189             my $trade_volume_btc = 0;
190             my $trade_volume_usd = 0;
191             for my $trade (@$trades) {
192             next if $trade->{date} < $threshold;
193             $trade_count++;
194             $trade_volume_btc += $trade->{amount};
195             $trade_volume_usd += $trade->{price} * $trade->{amount};
196             }
197              
198             return if $trade_count == 0;
199             return if $trade_volume_btc == 0;
200             return $trade_volume_usd / $trade_volume_btc;
201             }
202              
203             ### Private methods below here
204              
205             sub _decode {
206             my ($self) = @_;
207             return $self->_json->decode( $self->_mech->content );
208             }
209              
210             sub _json {
211             my ($self) = @_;
212             return $self->{json};
213             }
214              
215             sub _mech {
216             my ($self) = @_;
217             return $self->{mech};
218             }
219              
220             sub _key {
221             my ($self) = @_;
222             return $self->{key};
223             }
224              
225             sub _secret {
226             my ($self) = @_;
227             return $self->{secret};
228             }
229              
230             # build a URI object for the endpoint of an API call
231             sub _build_api_method_uri {
232             my ( $self, $version, $name, $prefix ) = @_;
233             my $version_url_token = "api/" . $version;
234              
235             $prefix = $prefix ? "$prefix/" : '';
236             return URI->new($version == 0 ?
237             "https://mtgox.com/$version_url_token/$prefix$name.php"
238             : "https://data.mtgox.com/$version_url_token/$prefix$name");
239             }
240              
241             # builds an HTTP::Request object for making an API call
242             sub _build_api_method_request {
243             my ( $self, $method, $version, $name, $prefix, $params ) = @_;
244             $method = uc $method;
245             $params ||= {};
246              
247             # prepare for authentication
248             if ( $method eq 'POST') {
249             $params->{nonce} = $self->_generate_nonce;
250             }
251              
252             my $uri = $self->_build_api_method_uri( $version, $name, $prefix );
253             if ( $method eq 'GET') {
254             # since March 19, 2013 no-auth requests need this hostname
255             $uri->scheme('http');
256             $uri->host('data.mtgox.com');
257             }
258              
259             my $req = HTTP::Request->new( $method, $uri );
260             if ( keys %$params ) {
261             $uri->query_form($params);
262             if ( $method eq 'POST' ) {
263              
264             # move params to the request body
265             my $query = $uri->query;
266             my $message = $version == 2 ? "$name\0$query" : $query;
267             $req->header( 'Content-Type' => 'application/x-www-form-urlencoded' );
268             $req->content($query);
269             $uri->query(undef);
270              
271             # include a signature
272             $req->header( 'Rest-Key', $self->_key );
273             $req->header( 'Rest-Sign', $self->_sign($message) );
274             }
275             }
276             return $req;
277             }
278              
279             # Returns an ever-increasing nonce value
280             # (Fails to increase when the system clock adjusts backwards)
281             sub _generate_nonce {
282             return sprintf '%s%06s', gettimeofday()
283             }
284              
285             # Returns a signature for the given message (using the API secret)
286             sub _sign {
287             my ( $self, $message ) = @_;
288             my $secret = decode_base64( $self->_secret );
289             return encode_base64( hmac_sha512( $message, $secret ) );
290             }
291              
292             # Returns the version of the api from the method name
293             sub _version_from_name {
294             my ( $self, $name ) = @_;
295             $name =~ /^BTC[A-Z]{3}\/(money|stream|security)/ and return 2;
296             $name =~ /^(BTC[A-Z]{3}|generic)\/(\w*)/ and return 1;
297             return 0;
298             }
299              
300             =head1 AUTHOR
301              
302             Michael Hendricks, C<< >>
303              
304             =head1 BUGS
305              
306             Please report any bugs or feature requests through
307             the web interface at L.
308              
309             =head1 SUPPORT
310              
311             You can find documentation for this module with the perldoc command.
312              
313             perldoc Finance::MtGox
314              
315              
316             You can also look for information at:
317              
318             =over 4
319              
320             =item * AnnoCPAN: Annotated CPAN documentation
321              
322             L
323              
324             =item * CPAN Ratings
325              
326             L
327              
328             =item * Search CPAN
329              
330             L
331              
332             =back
333              
334              
335             =head1 ACKNOWLEDGEMENTS
336              
337              
338             =head1 LICENSE AND COPYRIGHT
339              
340             Copyright 2011 Michael Hendricks.
341              
342             This program is distributed under the MIT (X11) License:
343             L
344              
345             Permission is hereby granted, free of charge, to any person
346             obtaining a copy of this software and associated documentation
347             files (the "Software"), to deal in the Software without
348             restriction, including without limitation the rights to use,
349             copy, modify, merge, publish, distribute, sublicense, and/or sell
350             copies of the Software, and to permit persons to whom the
351             Software is furnished to do so, subject to the following
352             conditions:
353              
354             The above copyright notice and this permission notice shall be
355             included in all copies or substantial portions of the Software.
356              
357             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
358             EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
359             OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
360             NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
361             HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
362             WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
363             FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
364             OTHER DEALINGS IN THE SOFTWARE.
365              
366              
367             =cut
368              
369             1; # End of Finance::MtGox