File Coverage

blib/lib/App/cryp/arbit.pm
Criterion Covered Total %
statement 31 629 4.9
branch 1 230 0.4
condition 0 60 0.0
subroutine 9 33 27.2
pod 7 7 100.0
total 48 959 5.0


line stmt bran cond sub pod time code
1             package App::cryp::arbit;
2              
3             our $DATE = '2018-11-29'; # DATE
4             our $VERSION = '0.008'; # VERSION
5              
6 1     1   20 use 5.010001;
  1         3  
7 1     1   6 use strict;
  1         1  
  1         22  
8 1     1   5 use warnings;
  1         2  
  1         39  
9 1     1   582 use Devel::Confess;
  1         11375  
  1         3  
10 1     1   67 use Log::ger;
  1         2  
  1         9  
11              
12 1     1   205 use Time::HiRes qw(time);
  1         2  
  1         7  
13              
14             our %SPEC;
15              
16             $SPEC{':package'} = {
17             summary => 'Cryptocurrency arbitrage utility',
18             v => 1.1,
19             };
20              
21             our %args_db = (
22             db_name => {
23             schema => 'str*',
24             req => 1,
25             tags => ['category:database-connection'],
26             },
27             # XXX db_host
28             # XXX db_port
29             db_username => {
30             schema => 'str*',
31             tags => ['category:database-connection'],
32             },
33             db_password => {
34             schema => 'str*',
35             tags => ['category:database-connection'],
36             },
37             );
38              
39             # shared between these subcommands: opportunities, arbit, collect-orderbooks
40             our %args_accounts_and_currencies = (
41             accounts => {
42             summary => 'Cryptoexchange accounts',
43             schema => ['array*', of=>'cryptoexchange::account', min_len=>2],
44             description => <<'_',
45              
46             There should at least be two accounts, on at least two different
47             cryptoexchanges. If not specified, all accounts listed on the configuration file
48             will be included. Note that it's possible to include two or more accounts on the
49             same cryptoexchange.
50              
51             _
52             },
53             base_currencies => {
54             'x.name.is_plural' => 1,
55             'x.name.singular' => 'base_currency',
56             summary => 'Target (crypto)currencies to arbitrate',
57             schema => ['array*', of=>'cryptocurrency*', min_len=>1],
58             description => <<'_',
59              
60             If not specified, will list all supported pairs on all the exchanges and include
61             the base cryptocurrencies that are listed on at least 2 different exchanges (for
62             arbitrage possibility).
63              
64             _
65             },
66             quote_currencies => {
67             'x.name.is_plural' => 1,
68             'x.name.singular' => 'quote_currency',
69             summary => 'The currencies to exchange (buy/sell) the target currencies',
70             schema => ['array*', of=>'fiat_or_cryptocurrency*', min_len=>1],
71             description => <<'_',
72              
73             You can have fiat currencies as the quote currencies, to buy/sell the target
74             (base) currencies during arbitrage. For example, to arbitrage LTC against USD
75             and IDR, `base_currencies` is ['BTC'] and `quote_currencies` is ['USD', 'IDR'].
76              
77             You can also arbitrage cryptocurrencies against other cryptocurrency (usually
78             BTC, "the USD of cryptocurrencies"). For example, to arbitrage XMR and LTC
79             against BTC, `base_currencies` is ['XMR', 'LTC'] and `quote_currencies` is
80             ['BTC'].
81              
82             _
83             },
84             );
85              
86             # shared between these subcommands: opportunities, arbit
87             our %args_arbit_common = (
88             strategy => {
89             summary => 'Which strategy to use for arbitration',
90             schema => ['str*', match=>qr/\A\w+\z/],
91             default => 'merge_order_book',
92             description => <<'_',
93              
94             Strategy is implemented in a `App::cryp::arbit::Strategy::*` perl module.
95              
96             _
97             },
98             %args_accounts_and_currencies,
99             min_net_profit_margin => {
100             summary => 'Minimum net profit margin that will trigger an arbitrage '.
101             'trading, in percentage',
102             schema => 'float*',
103             default => 0,
104             description => <<'_',
105              
106             Below this percentage number, no order pairs will be sent to the exchanges to do
107             the arbitrage. Note that the net profit margin already takes into account
108             trading fees and forex spread (see Glossary section for more details and
109             illustration).
110              
111             Suggestion: If you set this option too high, there might not be any order pairs
112             possible. If you set this option too low, you will be getting too thin profits.
113             Run `cryp-arbit opportunities` or `cryp-arbit arbit --dry-run` for a while to
114             see what the average percentage is and then decide at which point you want to
115             perform arbitrage.
116              
117             _
118             },
119             max_order_quote_size => {
120             summary => 'What is the maximum amount of a single order',
121             schema => 'float*',
122             default => 100,
123             description => <<'_',
124              
125             A single order will be limited to not be above this value (in quote currency,
126             which if fiat will be converted to USD). This is the amount for the buying
127             (because an arbitrage transaction is comprised of a pair of orders, where one
128             order is a selling order at a higher quote currency size than the buying order).
129              
130             For example if you are arbitraging BTC against USD and IDR, and set this option
131             to 75, then orders will not be above 75 USD. If you are arbitraging LTC against
132             BTC and set this to 0.03 then orders will not be above 0.03 BTC.
133              
134             Suggestion: If you set this option too high, a few orders can use up your
135             inventory (and you might not be getting optimal profit percentage). Also, large
136             orders can take a while (or too long) to fill. If you set this option too low,
137             you will hit the exchanges' minimum order size and no orders can be created.
138             Since we want smaller risk of orders not getting filled quickly, we want small
139             order sizes. The optimum number range a little above the exchanges' minimum
140             order size.
141              
142             _
143             },
144             max_order_pairs_per_round => {
145             summary => 'Maximum number of order pairs to create per round',
146             schema => 'posint*',
147             },
148             min_account_balances => {
149             summary => 'What are the minimum account balances',
150             schema => ['hash*', {
151             each_key => 'cryptoexchange::account*',
152             each_value => ['hash*', {
153             each_key => 'fiat_or_cryptocurrency*',
154             each_value => 'float',
155             }],
156             }],
157             },
158             );
159              
160             our %arg_max_order_age = (
161             max_order_age => {
162             summary => 'How long should we wait for orders to be completed '.
163             'before cancelling them (in seconds)',
164             schema => 'posint*',
165             default => 86400,
166             description => <<'_',
167              
168             Sometimes because of rapid trading and price movement, our order might not be
169             filled immediately. This setting sets a limit on how long should an order be
170             left open. After this limit is reached, we cancel the order. The imbalance of
171             the arbitrage transaction will be recorded.
172              
173             _
174             },
175             );
176              
177             our %arg_usd_rates = (
178             usd_rates => {
179             summary => 'Set USD rates',
180             'x.name.is_plural' => 1,
181             'x.name.singular' => 'usd_rate',
182             schema => ['hash*', each_key=>["str*", match=>qr/\A[A-Z]{3}\z/], each_value=>'float*'],
183             description => <<'_',
184              
185             Example:
186              
187             --usd-rate IDR=14500 --usd-rate THB=33.25
188              
189             _
190             },
191             );
192              
193             our $db_schema_spec = {
194             component_name => 'cryp_arbit',
195             latest_v => 2,
196             provides => [qw/exchange account balance tx price order_pair/],
197             install => [
198             # XXX later move to cryp-folio?
199             'CREATE TABLE exchange (
200             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
201             safename VARCHAR(100) NOT NULL, UNIQUE(safename)
202             )',
203              
204             # XXX later move to cryp-folio?
205             'CREATE TABLE account (
206             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
207             exchange_id INT NOT NULL,
208             nickname VARCHAR(64) NOT NULL,
209             UNIQUE(exchange_id,nickname),
210             note VARCHAR(255)
211             )',
212              
213             # XXX later move to cryp-folio?
214             'CREATE TABLE latest_balance (
215             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
216             time DOUBLE NOT NULL,
217             account_id INT NOT NULL,
218             currency VARCHAR(10) NOT NULL,
219             UNIQUE(account_id, currency),
220             available DECIMAL(21,8) NOT NULL
221             )',
222              
223             # XXX later move to cryp-folio?
224             'CREATE TABLE balance_history (
225             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
226             time DOUBLE NOT NULL,
227             account_id INT NOT NULL,
228             currency VARCHAR(10) NOT NULL,
229             UNIQUE(time, account_id, currency),
230             available DECIMAL(21,8) NOT NULL
231             )',
232              
233             # XXX later move to cryp-folio
234             'CREATE TABLE price (
235             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
236             time DOUBLE NOT NULL, INDEX(time),
237             base_currency VARCHAR(10) NOT NULL,
238             quote_currency VARCHAR(10) NOT NULL,
239             type VARCHAR(4) NOT NULL, -- "buy" or "sell"
240             price DECIMAL(21,8) NOT NULL, -- price to buy (or sell) base_currency in quote_currency, e.g. if base_currency = BTC, quote_currency = USD, price = 11150 means 1 BTC is $11150
241             exchange_id INT NOT NULL,
242             note VARCHAR(255)
243             )',
244              
245             'CREATE TABLE arbit_opportunity (
246             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
247             time DOUBLE NOT NULL, INDEX(time),
248             base_currency VARCHAR(10) NOT NULL,
249             quote_currency VARCHAR(10) NOT NULL,
250             -- base_size DECIMAL(21,8),
251             buy_exchange_id INT NOT NULL,
252             buy_price DECIMAL(21,8) NOT NULL,
253             sell_exchange_id INT NOT NULL,
254             sell_price DECIMAL(21,8) NOT NULL,
255             gross_profit_margin DOUBLE NOT NULL,
256             trading_profit_margin DOUBLE NOT NULL,
257             net_profit_margin DOUBLE
258             )',
259              
260             # to collect historical orderbook data
261             'CREATE TABLE orderbook (
262             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
263             time DOUBLE NOT NULL, INDEX(time),
264             exchange_id INT NOT NULL,
265             base_currency VARCHAR(10) NOT NULL,
266             quote_currency VARCHAR(10) NOT NULL,
267             type TEXT NOT NULL -- "buy" or "sell"
268             )',
269              
270             'CREATE TABLE orderbook_item (
271             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
272             orderbook_id INT NOT NULL, INDEX(orderbook_id),
273             amount DECIMAL(21,8) NOT NULL,
274             price DECIMAL(21,8) NOT NULL
275             )',
276              
277             'CREATE TABLE order_pair (
278             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
279             ctime DOUBLE NOT NULL, INDEX(ctime), -- create time in our database
280              
281             base_currency VARCHAR(10) NOT NULL, -- the currency we are arbitraging, e.g. LTC
282             base_size DECIMAL(21,8) NOT NULL, -- amount of "currency" that we are arbitraging (sell on "sell exchange" and buy on "buy exchange")
283              
284             expected_profit_margin DOUBLE NOT NULL, -- expected profit percentage (after trading fees & forex spread)
285             expected_net_profit DOUBLE NOT NULL, -- expected net profit (after trading fees & forex spread) in quote currency (converted to USD if fiat) if fully executed
286              
287             -- we buy "base_size" of "base_currency" on "buy exchange" at
288             -- "buy_gross_price_orig" (in "buy_quote_currency") a.k.a
289             -- "buy_gross_price" (in "buy_quote_currency" converted to USD if
290             -- fiat)
291              
292             -- possible statuses/lifecyle: creating (submitting to exchange),
293             -- open (created and open), cancelling, cancelled, done
294              
295             buy_exchange_id INT NOT NULL,
296             buy_account_id INT NOT NULL,
297             buy_quote_currency VARCHAR(10) NOT NULL,
298             buy_gross_price_orig DECIMAL(21,8) NOT NULL,
299             buy_gross_price DECIMAL(21,8) NOT NULL,
300             buy_status VARCHAR(16) NOT NULL,
301              
302             buy_ctime DOUBLE, -- order create time in "buy_exchange"
303             buy_order_id VARCHAR(80),
304             buy_actual_price DECIMAL(21,8), -- actual price after we create on exchange
305             buy_actual_base_size DECIMAL(21,8), -- actual size after we create on exchange
306             buy_filled_base_size DECIMAL(21,8),
307              
308             -- then sell the same "base_size" of "base_currency"" on "sell
309             -- exchange" (the "base_currency"/"sell_exchange_quote_currency"
310             -- market pair) at "sell_gross_price_orig" (in
311             -- "sell_exchange_quote_currency") a.k.a "sell_gross_price" (in
312             -- "sell_exchange_quote_currency" converted to USD if fiat)
313              
314             sell_exchange_id INT NOT NULL,
315             sell_account_id INT NOT NULL,
316             sell_quote_currency VARCHAR(10) NOT NULL,
317             sell_gross_price_orig DECIMAL(21,8) NOT NULL,
318             sell_gross_price DECIMAL(21,8) NOT NULL,
319             sell_status VARCHAR(16) NOT NULL,
320              
321             sell_ctime DOUBLE, -- create time in "sell exchange"
322             sell_order_id VARCHAR(80),
323             sell_actual_price DECIMAL(21,8), -- actual price after we create on exchange
324             sell_actual_base_size DECIMAL(21,8), -- actual size after we create on exchange
325             sell_filled_base_size DECIMAL(21,8)
326             )',
327              
328             'CREATE TABLE arbit_order_log (
329             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
330             order_pair_id INT NOT NULL,
331             type VARCHAR(4) NOT NULL, -- "buy" or "sell"
332             summary TEXT NOT NULL
333             )',
334             ],
335             upgrade_to_v2 => [
336             # to collect historical orderbook data
337             'CREATE TABLE orderbook (
338             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
339             time DOUBLE NOT NULL, INDEX(time),
340             exchange_id INT NOT NULL,
341             base_currency VARCHAR(10) NOT NULL,
342             quote_currency VARCHAR(10) NOT NULL,
343             type TEXT NOT NULL -- "sell" or "buy"
344             )',
345              
346             'CREATE TABLE orderbook_item (
347             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
348             orderbook_id INT NOT NULL, INDEX(orderbook_id),
349             amount DECIMAL(21,8) NOT NULL,
350             price DECIMAL(21,8) NOT NULL
351             )',
352             ],
353             install_v1 => [
354             # XXX later move to cryp-folio?
355             'CREATE TABLE exchange (
356             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
357             safename VARCHAR(100) NOT NULL, UNIQUE(safename)
358             )',
359              
360             # XXX later move to cryp-folio?
361             'CREATE TABLE account (
362             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
363             exchange_id INT NOT NULL,
364             nickname VARCHAR(64) NOT NULL,
365             UNIQUE(exchange_id,nickname),
366             note VARCHAR(255)
367             )',
368              
369             # XXX later move to cryp-folio?
370             'CREATE TABLE latest_balance (
371             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
372             time DOUBLE NOT NULL,
373             account_id INT NOT NULL,
374             currency VARCHAR(10) NOT NULL,
375             UNIQUE(account_id, currency),
376             available DECIMAL(21,8) NOT NULL
377             )',
378              
379             # XXX later move to cryp-folio?
380             'CREATE TABLE balance_history (
381             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
382             time DOUBLE NOT NULL,
383             account_id INT NOT NULL,
384             currency VARCHAR(10) NOT NULL,
385             UNIQUE(time, account_id, currency),
386             available DECIMAL(21,8) NOT NULL
387             )',
388              
389             # XXX later move to cryp-folio
390             'CREATE TABLE price (
391             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
392             time DOUBLE NOT NULL, INDEX(time),
393             base_currency VARCHAR(10) NOT NULL,
394             quote_currency VARCHAR(10) NOT NULL,
395             type VARCHAR(4) NOT NULL, -- "buy" or "sell"
396             price DECIMAL(21,8) NOT NULL, -- price to buy (or sell) base_currency in quote_currency, e.g. if base_currency = BTC, quote_currency = USD, price = 11150 means 1 BTC is $11150
397             exchange_id INT NOT NULL,
398             note VARCHAR(255)
399             )',
400              
401             'CREATE TABLE arbit_opportunity (
402             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
403             time DOUBLE NOT NULL, INDEX(time),
404             base_currency VARCHAR(10) NOT NULL,
405             quote_currency VARCHAR(10) NOT NULL,
406             -- base_size DECIMAL(21,8),
407             buy_exchange_id INT NOT NULL,
408             buy_price DECIMAL(21,8) NOT NULL,
409             sell_exchange_id INT NOT NULL,
410             sell_price DECIMAL(21,8) NOT NULL,
411             gross_profit_margin DOUBLE NOT NULL,
412             trading_profit_margin DOUBLE NOT NULL,
413             net_profit_margin DOUBLE
414             )',
415              
416             'CREATE TABLE order_pair (
417             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
418             ctime DOUBLE NOT NULL, INDEX(ctime), -- create time in our database
419              
420             base_currency VARCHAR(10) NOT NULL, -- the currency we are arbitraging, e.g. LTC
421             base_size DECIMAL(21,8) NOT NULL, -- amount of "currency" that we are arbitraging (sell on "sell exchange" and buy on "buy exchange")
422              
423             expected_profit_margin DOUBLE NOT NULL, -- expected profit percentage (after trading fees & forex spread)
424             expected_net_profit DOUBLE NOT NULL, -- expected net profit (after trading fees & forex spread) in quote currency (converted to USD if fiat) if fully executed
425              
426             -- we buy "base_size" of "base_currency" on "buy exchange" at
427             -- "buy_gross_price_orig" (in "buy_quote_currency") a.k.a
428             -- "buy_gross_price" (in "buy_quote_currency" converted to USD if
429             -- fiat)
430              
431             -- possible statuses/lifecyle: creating (submitting to exchange),
432             -- open (created and open), cancelling, cancelled, done
433              
434             buy_exchange_id INT NOT NULL,
435             buy_account_id INT NOT NULL,
436             buy_quote_currency VARCHAR(10) NOT NULL,
437             buy_gross_price_orig DECIMAL(21,8) NOT NULL,
438             buy_gross_price DECIMAL(21,8) NOT NULL,
439             buy_status VARCHAR(16) NOT NULL,
440              
441             buy_ctime DOUBLE, -- order create time in "buy_exchange"
442             buy_order_id VARCHAR(80),
443             buy_actual_price DECIMAL(21,8), -- actual price after we create on exchange
444             buy_actual_base_size DECIMAL(21,8), -- actual size after we create on exchange
445             buy_filled_base_size DECIMAL(21,8),
446              
447             -- then sell the same "base_size" of "base_currency"" on "sell
448             -- exchange" (the "base_currency"/"sell_exchange_quote_currency"
449             -- market pair) at "sell_gross_price_orig" (in
450             -- "sell_exchange_quote_currency") a.k.a "sell_gross_price" (in
451             -- "sell_exchange_quote_currency" converted to USD if fiat)
452              
453             sell_exchange_id INT NOT NULL,
454             sell_account_id INT NOT NULL,
455             sell_quote_currency VARCHAR(10) NOT NULL,
456             sell_gross_price_orig DECIMAL(21,8) NOT NULL,
457             sell_gross_price DECIMAL(21,8) NOT NULL,
458             sell_status VARCHAR(16) NOT NULL,
459              
460             sell_ctime DOUBLE, -- create time in "sell exchange"
461             sell_order_id VARCHAR(80),
462             sell_actual_price DECIMAL(21,8), -- actual price after we create on exchange
463             sell_actual_base_size DECIMAL(21,8), -- actual size after we create on exchange
464             sell_filled_base_size DECIMAL(21,8)
465             )',
466              
467             'CREATE TABLE arbit_order_log (
468             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
469             order_pair_id INT NOT NULL,
470             type VARCHAR(4) NOT NULL, -- "buy" or "sell"
471             summary TEXT NOT NULL
472             )',
473             ],
474             };
475              
476             my $fnum2 = [number => {precision=>2}];
477             my $fnum4 = [number => {precision=>4}];
478             my $fnum8 = [number => {precision=>8}];
479              
480             sub _exchange_catalog {
481 0     0   0 state $xcat = do {
482 0         0 require CryptoExchange::Catalog;
483 0         0 CryptoExchange::Catalog->new;
484             };
485 0         0 $xcat;
486             }
487              
488             # to only show currency rate in log when they are different from last log
489             my %rate_mem;
490             sub _convert_to_usd {
491 0     0   0 require Finance::Currency::FiatX;
492              
493 0         0 my ($r, $amount, $cur) = @_;
494              
495 0         0 my $dbh = $r->{_stash}{dbh};
496              
497 0         0 my $fxres;
498 0 0 0     0 if ($r->{args}{usd_rates} && $r->{args}{usd_rates}{$cur}) {
499 0         0 $fxres = [200, "OK (user-set)", {rate=>1 / $r->{args}{usd_rates}{$cur}}];
500             } else {
501 0         0 $fxres = Finance::Currency::FiatX::get_spot_rate(
502             dbh => $dbh, from => $cur, to => 'USD', type => 'sell');
503 0 0 0     0 die "Couldn't get conversion rate from $cur to USD: $fxres->[0] - $fxres->[1]"
504             unless $fxres->[0] == 200 || $fxres->[0] == 304;
505             }
506 0 0 0     0 if (!$rate_mem{$cur} || $rate_mem{$cur} != $fxres->[2]{rate}) {
507 0         0 log_info "Using currency conversion rate for $cur/USD: %s", $fxres;
508 0         0 $rate_mem{$cur} = $fxres->[2]{rate};
509             }
510              
511 0         0 $r->{_stash}{fx}{$cur} = $fxres;
512              
513 0         0 $amount * $fxres->[2]{rate};
514             }
515              
516             # XXX move to App::cryp::Util or folio? given a safename, get or assign exchange
517             # numeric ID from the database
518             sub _get_exchange_id {
519 0     0   0 my ($r, $exchange) = @_;
520              
521             return $r->{_stash}{exchange_ids}{$exchange} if
522 0 0       0 $r->{_stash}{exchange_ids}{$exchange};
523              
524 0         0 my $xcat = _exchange_catalog();
525 0         0 my $rec = $xcat->by_safename($exchange);
526 0 0       0 $rec or die "BUG: Unknown exchange '$exchange'";
527              
528 0         0 my $dbh = $r->{_stash}{dbh};
529              
530 0         0 my ($eid) = $dbh->selectrow_array("SELECT id FROM exchange WHERE safename=?", {}, $exchange);
531 0 0       0 unless ($eid) {
532 0         0 $dbh->do("INSERT INTO exchange (safename) VALUES (?)", {},
533             $exchange);
534 0         0 $eid = $dbh->last_insert_id("","","","");
535             }
536              
537 0         0 $r->{_stash}{exchange_ids}{$exchange} = $eid;
538 0         0 $eid;
539             }
540              
541             sub _get_account_id {
542 0     0   0 my ($r, $exchange, $account) = @_;
543              
544             return $r->{_stash}{account_ids}{$exchange}{$account} if
545 0 0       0 $r->{_stash}{account_ids}{$exchange}{$account};
546              
547 0         0 my $dbh = $r->{_stash}{dbh};
548              
549 0         0 my $eid = _get_exchange_id($r, $exchange);
550              
551 0         0 my ($aid) = $dbh->selectrow_array("SELECT id FROM account WHERE exchange_id=? AND nickname=?", {}, $eid, $account);
552 0 0       0 unless ($aid) {
553 0         0 $dbh->do("INSERT INTO account (exchange_id,nickname) VALUES (?,?)", {}, $eid, $account);
554 0         0 $aid = $dbh->last_insert_id("","","","");
555             }
556              
557 0         0 $r->{_stash}{account_ids}{$exchange}{$account} = $aid;
558 0         0 $aid;
559             }
560              
561             sub _sort_account_balances {
562 9     9   14 my $account_balances = shift;
563              
564 9         25 for my $e (keys %$account_balances) {
565 18         25 my $balances = $account_balances->{$e};
566 18         35 for my $cur (keys %$balances) {
567             $balances->{$cur} = [
568 16         62 grep { $_->{available} >= 1e-8 }
569 2         8 sort { $b->{available} <=> $a->{available} }
570 18         22 @{ $balances->{$cur} }
  18         45  
571             ];
572             }
573             }
574             }
575              
576             sub _get_exchange_client {
577 0     0   0 my ($r, $exchange, $account) = @_;
578              
579             # if account is unspecified (caller doesn't care which account, e.g. he just
580             # wants to get som exchange-related information), then we pick an account
581             # from the configuration
582 0 0       0 unless (defined $account) {
583 0         0 my $h = $r->{_cryp}{exchanges}{$exchange};
584 0 0       0 die "No configuration found for exchange $exchange. ".
585             "Please specify [exchange/$exchange] section in configuration"
586             unless keys %$h;
587 0         0 $account = (keys %$h)[0];
588             }
589              
590             return $r->{_stash}{exchange_clients}{$exchange}{$account} if
591 0 0       0 $r->{_stash}{exchange_clients}{$exchange}{$account};
592              
593 0         0 my $mod = "App::cryp::Exchange::$exchange";
594 0         0 $mod =~ s/-/_/g;
595 0         0 (my $modpm = "$mod.pm") =~ s!::!/!g;
596 0         0 require $modpm;
597              
598 0 0       0 unless ($r->{_cryp}{exchanges}{$exchange}{$account}) {
599 0         0 die "No configuration found for exchange $exchange (account $account). ".
600             "Please specify [exchange/$exchange/$account] section in configuration";
601             }
602              
603 0   0     0 my %client_args = %{ $r->{_cryp}{exchanges}{$exchange}{$account} // {} };
  0         0  
604 0 0       0 unless ($client_args{api_key}) {
605 0         0 $client_args{public_only} = 1;
606             }
607              
608 0         0 my $client = $mod->new(%client_args);
609              
610 0         0 $r->{_stash}{exchange_clients}{$exchange}{$account} = $client;
611             }
612              
613             sub _get_account_balances {
614 0     0   0 my ($r, $no_cache) = @_;
615              
616 0         0 my $dbh = $r->{_stash}{dbh};
617              
618 0         0 $r->{_stash}{account_balances} = {};
619 0         0 for my $e (sort keys %{ $r->{_stash}{account_exchanges} }) {
  0         0  
620 0         0 my $accounts = $r->{_stash}{account_exchanges}{$e};
621             ACC:
622 0         0 for my $acc (sort keys %$accounts) {
623 0         0 my $client = _get_exchange_client($r, $e, $acc);
624 0         0 my $time = time();
625 0         0 my $res = $client->list_balances;
626 0 0       0 unless ($res->[0] == 200) {
627 0         0 log_error "Couldn't list balances for account %s/%s: %s, skipping account",
628             $e, $acc, $res;
629 0         0 next ACC;
630             }
631 0         0 for my $rec (@{ $res->[2] }) {
  0         0  
632 0         0 $rec->{account} = $acc;
633 0         0 $rec->{account_id} = _get_account_id($r, $e, $acc);
634 0         0 push @{ $r->{_stash}{account_balances}{$e}{$rec->{currency}} }, $rec;
  0         0  
635             $dbh->do(
636             "REPLACE INTO latest_balance (time, account_id, currency, available) VALUES (?,?,?,?)",
637             {},
638             $time, $rec->{account_id}, $rec->{currency}, $rec->{available},
639 0         0 );
640             $dbh->do(
641             "INSERT INTO balance_history (time, account_id, currency, available) VALUES (?,?,?,?)",
642             {},
643             $time, $rec->{account_id}, $rec->{currency}, $rec->{available},
644 0         0 );
645             } # for rec
646             } # for account
647             } # for exchange
648              
649             # sort by largest available balance first
650 0         0 _sort_account_balances($r->{_stash}{account_balances});
651              
652             #log_trace "account_balances: %s", $r->{_stash}{account_balances};
653 0         0 $r->{_stash}{account_balances};
654             }
655              
656             sub _get_exchange_pairs {
657 0     0   0 my ($r, $exchange) = @_;
658              
659             return $r->{_stash}{exchange_pairs}{$exchange} if
660 0 0       0 $r->{_stash}{exchange_pairs}{$exchange};
661              
662 0         0 my $client = _get_exchange_client($r, $exchange);
663              
664 0         0 my $res = $client->list_pairs(detail=>1);
665 0 0       0 if ($res->[0] == 200) {
666 0         0 $r->{_stash}{exchange_pairs}{$exchange} = $res->[2];
667             } else {
668 0         0 log_error "Couldn't list pairs on %s: %s, ".
669             "skipping this exchange", $exchange, $res;
670 0         0 $r->{_stash}{exchange_pairs}{$exchange} = [];
671             }
672              
673 0         0 $r->{_stash}{exchange_pairs}{$exchange};
674             }
675              
676             sub _get_trading_fee {
677 0     0   0 my ($r, $exchange, $currency) = @_;
678              
679 0         0 my $fees = $r->{_stash}{trading_fees};
680 0   0     0 my $fees_exchange = $fees->{$exchange} // $fees->{':default'};
681 0   0     0 my $fee = $fees_exchange->{$currency} // $fees_exchange->{':default'};
682             }
683              
684             sub _is_fiat {
685 17     17   762 require Locale::Codes::Currency_Codes;
686 1     1   2317 no warnings 'once';
  1         2  
  1         6934  
687 17         6304 my $code = shift;
688 17 50       120 $Locale::Codes::Data{'currency'}{'code2id'}{alpha}{uc $code} ? 1:0;
689             }
690              
691             # should be used by all subcommands
692             sub _init {
693 0     0     my ($r, $opts) = @_;
694              
695 0   0       $opts //= {};
696              
697 0           my %account_exchanges; # key = exchange safename, value = {account1=>1, account2=>1, ...)
698              
699 0           my $xcat = _exchange_catalog();
700              
701             CHECK_ARGUMENTS:
702             {
703 0           CHECK_ACCOUNTS:
704             {
705 0 0         last CHECK_ACCOUNTS unless exists $r->{args}{accounts};
  0            
706              
707             # accounts: there must be at least two accounts on two different
708             # exchanges
709             return [400, "Please specify at least two accounts"]
710 0 0 0       unless $r->{args}{accounts} && @{ $r->{args}{accounts} } >= 2;
  0            
711 0           for (@{ $r->{args}{accounts} }) {
  0            
712 0 0         m!(.+)/(.+)! or return [400, "Invalid account '$_', please use EXCHANGE/ACCOUNT syntax"];
713 0           my ($xchg, $acc) = ($1, $2);
714 0 0         unless (exists $account_exchanges{$xchg}) {
715 0 0         return [400, "Unknown exchange '$xchg'"]
716             unless $xcat->by_safename($xchg);
717             }
718 0           $account_exchanges{$xchg}{$acc} = 1;
719             }
720 0 0         return [400, "Please specify accounts on at least two ".
721             "cryptoexchanges, you only specify account(s) on " .
722             join(", ", keys %account_exchanges)]
723             unless keys(%account_exchanges) >= 2;
724 0           $r->{_stash}{account_exchanges} = \%account_exchanges;
725             }
726             }
727              
728 0           my $dbh;
729             CONNECT:
730             {
731 0 0         last if $opts->{skip_connect};
  0            
732              
733 0           require DBIx::Connect::MySQL;
734 0           log_trace "Connecting to database ...";
735             $r->{_stash}{dbh} = DBIx::Connect::MySQL->connect(
736             "dbi:mysql:database=$r->{args}{db_name}",
737             $r->{args}{db_username},
738             $r->{args}{db_password},
739 0           {RaiseError => 1},
740             );
741 0           $dbh = $r->{_stash}{dbh};
742             }
743              
744             SETUP_SCHEMA:
745             {
746 0 0         last if $opts->{skip_connect};
  0            
747              
748 0           require SQL::Schema::Versioned;
749             my $res = SQL::Schema::Versioned::create_or_update_db_schema(
750 0           dbh => $r->{_stash}{dbh}, spec => $db_schema_spec,
751             );
752 0 0         die "Cannot run the application: cannot create/upgrade database schema: $res->[1]"
753             unless $res->[0] == 200;
754             }
755              
756 0           [200];
757             }
758              
759             sub _init_arbit {
760 0     0     my $r = shift;
761              
762             DETERMINE_QUOTE_CURRENCIES:
763             {
764 0           my @quotecurs;
  0            
765             my %fiatquotecurs; # key=fiat, value=1
766 0   0       my @quotecurs_arg = @{ $r->{args}{quote_currencies} // [] };
  0            
767 0           my %quotecur_exchanges; # key=(cryptocurrency code or ':fiat'), value={exchange1=>1, ...}
768              
769             # list pairs on all exchanges
770 0           for my $e (sort keys %{ $r->{_stash}{account_exchanges} }) {
  0            
771 0           my $pair_recs = _get_exchange_pairs($r, $e);
772 0           for my $pair_rec (@$pair_recs) {
773 0           my $pair = $pair_rec->{name};
774 0           my ($basecur, $quotecur) = split m!/!, $pair;
775             # consider all fiat currencies as a single ":fiat" because we
776             # assume fiat currencies can be converted from one to the aother
777             # at a stable rate.
778 0           my $key;
779 0 0         if (_is_fiat($quotecur)) {
780 0           $key = ':fiat';
781 0           $fiatquotecurs{$quotecur} = 1;
782             } else {
783 0           $key = $quotecur;
784             }
785 0           $quotecur_exchanges{$key}{$e} = 1;
786             }
787             }
788              
789             # only consider quote currencies that are traded in >1 exchanges, for
790             # arbitrage possibility.
791 0           my @possible_quotecurs = grep { keys(%{$quotecur_exchanges{$_}}) > 1 }
  0            
  0            
792             sort keys %quotecur_exchanges;
793             # convert back fiat currencies back to their original
794 0 0         if (grep {':fiat'} @possible_quotecurs) {
  0            
795 0           @possible_quotecurs = grep {$_ ne ':fiat'} @possible_quotecurs;
  0            
796 0           push @possible_quotecurs, sort keys %fiatquotecurs;
797             }
798              
799 0 0         if (@quotecurs_arg) {
800 0           my @impossible_quotecurs;
801 0           for my $c (@quotecurs_arg) {
802 0 0         if (grep { $c eq $_ } @possible_quotecurs) {
  0            
803 0           push @quotecurs, $c;
804             } else {
805 0           push @impossible_quotecurs, $c;
806             }
807             }
808 0 0         if (@impossible_quotecurs) {
809 0           log_warn "The following quote currencies are not traded on at least two exchanges: %s, excluding these quote currencies",
810             \@impossible_quotecurs;
811             }
812             } else {
813 0           log_warn "Will be arbitraging using these quote currencies: %s",
814             \@possible_quotecurs;
815 0           @quotecurs = @possible_quotecurs;
816             }
817              
818 0           $r->{_stash}{quote_currencies} = \@quotecurs;
819             } # DETERMINE_QUOTE_CURRENCIES
820              
821             # determine possible base currencies to arbitrage against
822             DETERMINE_BASE_CURRENCIES:
823             {
824              
825 0           my @basecurs;
  0            
826 0   0       my @basecurs_arg = @{ $r->{args}{base_currencies} // [] };
  0            
827 0           my %basecur_exchanges; # key=currency code, value={exchange1=>1, ...}
828              
829             # list pairs on all exchanges
830 0           for my $e (sort keys %{ $r->{_stash}{account_exchanges} }) {
  0            
831 0           my $pair_recs = _get_exchange_pairs($r, $e);
832 0           for my $pair_rec (@$pair_recs) {
833 0           my $pair = $pair_rec->{name};
834 0           my ($basecur, $quotecur) = split m!/!, $pair;
835 0 0         next unless grep { $_ eq $quotecur } @{ $r->{_stash}{quote_currencies} };
  0            
  0            
836 0           $basecur_exchanges{$basecur}{$e} = 1;
837             }
838             }
839              
840             # only consider base currencies that are traded in >1 exchanges, for
841             # arbitrage possibility
842 0           my @possible_basecurs = grep { keys(%{$basecur_exchanges{$_}}) > 1 }
  0            
  0            
843             keys %basecur_exchanges;
844              
845 0 0         if (@basecurs_arg) {
846 0           my @impossible_basecurs;
847 0           for my $c (@basecurs_arg) {
848 0 0         if (grep { $c eq $_ } @possible_basecurs) {
  0            
849 0           push @basecurs, $c;
850             } else {
851 0           push @impossible_basecurs, $c;
852             }
853             }
854 0 0         if (@impossible_basecurs) {
855 0           log_warn "The following base currencies are not traded on at least two exchanges: %s, excluding these base currencies",
856             \@impossible_basecurs;
857             }
858             } else {
859 0           log_warn "Will be arbitraging these base currencies that are traded on at least two exchanges: %s",
860             \@possible_basecurs;
861 0           @basecurs = @possible_basecurs;
862             }
863              
864 0 0         return [412, "No base currencies possible for arbitraging"] unless @basecurs;
865 0           $r->{_stash}{base_currencies} = \@basecurs;
866             } # DETERMINE_BASE_CURRENCIES
867              
868             DETERMINE_TRADING_FEES:
869             {
870             # XXX hardcoded for now
871 0           $r->{_stash}{trading_fees} = {
872 0           ':default' => {':default'=>0.3},
873             'indodax' => {':default'=>0.3},
874             'coinbase-pro' => {BTC=>0.25, ':default'=>0.3},
875             };
876             }
877              
878 0           [200];
879             }
880              
881             sub _format_order_pairs_response {
882 0     0     my $order_pairs = shift;
883              
884             # format for table display
885 0           my @res;
886 0           for my $op (@$order_pairs) {
887 0           my $size = $op->{base_size};
888 0           my ($base_currency, $buy_currency) = $op->{buy}{pair} =~ m!(.+)/(.+)!;
889 0           my ($sell_currency) = $op->{sell}{pair} =~ m!/(.+)!;
890 0 0         my $profit_currency = _is_fiat($buy_currency) ? 'USD' : $buy_currency;
891              
892             my $rec = {
893             size => $size,
894             currency => $base_currency,
895             buy_from => $op->{buy}{exchange},
896             buy_currency => $buy_currency,
897             buy_gross_price => $op->{buy}{gross_price_orig},
898             sell_to => $op->{sell}{exchange},
899             sell_currency => $sell_currency,
900             sell_gross_price => $op->{sell}{gross_price_orig},
901             (gross_profit_margin => $op->{gross_profit_margin}) x !!exists($op->{gross_profit_margin}),
902             (trading_profit_margin => $op->{trading_profit_margin}) x !!exists($op->{trading_profit_margin}),
903             (forex_spread => $op->{forex_spread}) x !!exists($op->{forex_spread}),
904             (net_profit_margin => $op->{net_profit_margin}) x !!exists($op->{net_profit_margin}),
905             profit_currency => $profit_currency,
906             profit => $op->{profit},
907 0           };
908 0 0 0       if (_is_fiat($buy_currency) && $buy_currency ne 'USD') {
909 0           $rec->{buy_gross_price_usd} = $op->{buy}{gross_price};
910             #$rec->{buy_net_price_usd} = $op->{buy}{net_price};
911             }
912 0 0 0       if (_is_fiat($sell_currency) && $sell_currency ne 'USD') {
913 0           $rec->{sell_gross_price_usd} = $op->{sell}{gross_price};
914             #$rec->{sell_net_price_usd} = $op->{sell}{net_price};
915             }
916 0           push @res, $rec;
917             }
918              
919 0           my $resmeta = {};
920 0           $resmeta->{'table.fields'} = ['size', 'currency', 'buy_from', 'buy_currency', 'buy_gross_price', 'buy_gross_price_usd', 'sell_to', 'sell_currency', 'sell_gross_price', 'sell_gross_price_usd', 'gross_profit_margin', 'trading_profit_margin', 'forex_spread', 'net_profit_margin', 'profit_currency', 'profit'];
921 0           $resmeta->{'table.field_labels'} = [undef, 'c', 'buyFrom', 'buyC', 'buyGrossP', 'buyGrossP.USD', 'sellTo', 'sellC', 'sellGrossP', 'sellGrossP.USD', 'grossProfitM', 'trdProfitM', 'fxSpread', 'netProfitM', 'profitC', undef];
922 0           $resmeta->{'table.field_formats'} = [$fnum8, undef, undef, undef, $fnum8, $fnum8, undef, undef, $fnum8, $fnum8, $fnum4, $fnum4, $fnum4, $fnum4, undef, $fnum8];
923 0           $resmeta->{'table.field_aligns'} = ['right', 'left', 'left', 'left', 'right', 'right', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right', 'left', 'right'];
924              
925 0           [200, "OK", \@res, $resmeta];
926             }
927              
928             sub _create_orders {
929 0     0     my $r = shift;
930              
931 0           my $dbh = $r->{_stash}{dbh};
932              
933 0           local $dbh->{RaiseError};
934              
935             ORDER_PAIR:
936 0           for my $i (0..$#{ $r->{_stash}{order_pairs} }) {
  0            
937 0           my $op = $r->{_stash}{order_pairs}[$i];
938 0           my $is_err;
939             my $do_cancel_buy_order_on_err;
940 0           my $do_cancel_sell_order_on_err;
941              
942             log_debug "[%d/%d] Creating order pair on the exchanges: %s ...",
943 0           $i+1, scalar(@{ $r->{_stash}{order_pairs} }), $op;
  0            
944 0           my $buy = $op->{buy};
945 0           my $sell = $op->{sell};
946 0           my $buy_eid = _get_exchange_id($r, $buy ->{exchange});
947 0           my $buy_aid = _get_account_id ($r, $buy ->{exchange}, $buy ->{account});
948 0           my ($buy_quotecur) = $buy->{pair} =~ m!/(.+)!;
949 0           my $sell_eid = _get_exchange_id($r, $sell->{exchange});
950 0           my $sell_aid = _get_account_id ($r, $sell->{exchange}, $sell->{account});
951 0           my ($sell_quotecur) = $sell->{pair} =~ m!/(.+)!;
952              
953 0           my $time = time();
954             # first, insert to database with status 'creating'
955             $dbh->do(
956             "INSERT INTO order_pair (
957             ctime,
958             base_currency, base_size,
959             expected_net_profit_margin, expected_net_profit,
960              
961             buy_exchange_id , buy_account_id , buy_quote_currency , buy_gross_price_orig , buy_gross_price , buy_status,
962             sell_exchange_id, sell_account_id, sell_quote_currency, sell_gross_price_orig, sell_gross_price, sell_status
963              
964             ) VALUES (
965             ?,
966             ?, ?,
967             ?, ?,
968              
969             ?, ?, ?, ?, ?, ?,
970             ?, ?, ?, ?, ?, ?
971             )",
972              
973             {},
974              
975             $time,
976             $op->{base_currency}, $op->{base_size},
977             $op->{net_profit_margin}, $op->{net_profit},
978              
979             $buy_eid , $buy_aid , $buy_quotecur , $buy ->{gross_price_orig}, $buy ->{gross_price}, 'creating',
980             $sell_eid, $sell_aid, $sell_quotecur, $sell->{gross_price_orig}, $sell->{gross_price}, 'creating',
981 0 0         ) or do {
982 0           log_error "Couldn't record order_pair in db: %s, skipping this order pair", $dbh->errstr;
983 0           next ORDER_PAIR;
984             };
985 0           my $pair_id = $dbh->last_insert_id("", "", "", "");
986              
987 0           my $buy_client = _get_exchange_client($r, $buy->{exchange}, $buy->{account});
988 0           my $buy_order_id;
989             CREATE_BUY_ORDER:
990             {
991 0           my $res = $buy_client->create_limit_order(
992             pair => $buy->{pair},
993             type => 'buy',
994             price => $buy->{gross_price_orig},
995             base_size => $op->{base_size},
996 0           );
997 0 0         unless ($res->[0] == 200) {
998 0           log_error "Couldn't create buy order: %s", $res;
999 0           $is_err++;
1000 0           goto CLEANUP;
1001             }
1002 0           $buy_order_id = $res->[2]{order_id};
1003 0           $do_cancel_buy_order_on_err++;
1004             my $res2 = $buy_client->get_order(
1005             pair => $buy->{pair},
1006 0           type => 'buy',
1007             order_id => $buy_order_id,
1008             );
1009 0 0         unless ($res2->[0] == 200) {
1010 0           log_error "Couldn't get buy order: %s", $res2;
1011 0           $is_err++;
1012 0           goto CLEANUP;
1013             }
1014             $dbh->do(
1015             "UPDATE order_pair SET buy_ctime=?, buy_order_id=?, buy_actual_price=?, buy_actual_base_size=?, buy_status=? WHERE id=?",
1016             {},
1017             $res2->[2]{create_time}, $buy_order_id, $res->[2]{price}, $res->[2]{base_size}, 'open',
1018             $pair_id,
1019 0 0         ) or do {
1020 0           log_error "Couldn't update order status in db for buy order: %s", $res2;
1021             };
1022             }
1023              
1024 0           my $sell_client = _get_exchange_client($r, $sell->{exchange}, $sell->{account});
1025 0           my $sell_order_id;
1026             CREATE_SELL_ORDER:
1027             {
1028 0           my $res = $sell_client->create_limit_order(
1029             pair => $sell->{pair},
1030             type => 'sell',
1031             price => $sell->{gross_price_orig},
1032             base_size => $op->{base_size},
1033 0           );
1034 0 0         unless ($res->[0] == 200) {
1035 0           log_error "Couldn't create sell order: %s", $res;
1036 0           $is_err++;
1037 0           goto CLEANUP;
1038             }
1039 0           $sell_order_id = $res->[2]{order_id};
1040 0           $do_cancel_sell_order_on_err++;
1041             my $res2 = $sell_client->get_order(
1042             pair => $sell->{pair},
1043 0           type => 'sell',
1044             order_id => $sell_order_id,
1045             );
1046 0 0         unless ($res2->[0] == 200) {
1047 0           log_error "Couldn't get sell order: %s", $res2;
1048 0           $is_err++;
1049 0           goto CLEANUP;
1050             }
1051             $dbh->do(
1052             "UPDATE order_pair SET sell_ctime=?, sell_order_id=?, sell_actual_price=?, sell_actual_base_size=?, sell_status=? WHERE id=?",
1053             {},
1054             $res2->[2]{create_time}, $sell_order_id, $res->[2]{price}, $res->[2]{base_size}, 'open',
1055             $pair_id,
1056 0 0         ) or do {
1057 0           log_error "Couldn't update order status in db for sell order: %s", $res2;
1058             };
1059             }
1060              
1061             CLEANUP:
1062             {
1063 0 0         last unless $is_err;
  0            
1064 0 0         if ($do_cancel_buy_order_on_err) {
1065 0           $dbh->do("UPDATE order_pair SET buy_status='cancelling' WHERE id=?", {}, $pair_id);
1066             my $res = $buy_client->cancel_order(
1067             type => 'buy',
1068             pair => $buy->{pair},
1069 0           order_id => $buy_order_id,
1070             );
1071 0 0         if ($res->[0] != 200) {
1072 0           log_error "Couldn't cancel buy order #%s (order pair ID %d): %s",
1073             $buy_order_id, $pair_id, $res;
1074             } else {
1075 0           $dbh->do("UPDATE order_pair SET buy_status='cancelled' WHERE id=?", {}, $pair_id)
1076             }
1077             }
1078 0 0         if ($do_cancel_sell_order_on_err) {
1079 0           $dbh->do("UPDATE order_pair SET sell_status='cancelling' WHERE id=?", {}, $pair_id);
1080             my $res = $sell_client->cancel_order(
1081             type => 'sell',
1082             pair => $sell->{pair},
1083 0           order_id => $sell_order_id,
1084             );
1085 0 0         if ($res->[0] != 200) {
1086 0           log_error "Couldn't cancel sell order #%s (order pair ID %d): %s",
1087             $sell_order_id, $pair_id, $res;
1088             } else {
1089 0           $dbh->do("UPDATE order_pair SET sell_status='cancelled' WHERE id=?", {}, $pair_id)
1090             }
1091             }
1092             }
1093             } # ORDER_PAIR
1094             }
1095              
1096             $SPEC{dump_cryp_config} = {
1097             v => 1.1,
1098             args => {
1099             },
1100             };
1101             sub dump_cryp_config {
1102 0     0 1   my %args = @_;
1103              
1104 0           my $r = $args{-cmdline_r};
1105 0           my $res;
1106              
1107 0 0         $res = _init($r, {skip_connect=>1}); return $res unless $res->[0] == 200;
  0            
1108              
1109 0           [200, "OK", $r->{_cryp}];
1110             }
1111              
1112             $SPEC{show_opportunities} = {
1113             v => 1.1,
1114             summary => 'Show arbitrage opportunities',
1115             description => <<'_',
1116              
1117             This subcommand, like the `arbit` subcommand, checks prices of cryptocurrencies
1118             on several exchanges for arbitrage possibility; but does not actually perform
1119             the arbitraging.
1120              
1121             _
1122             args => {
1123             %args_db,
1124             %args_arbit_common,
1125             ignore_balance => {
1126             summary => 'Ignore account balances',
1127             schema => 'bool*',
1128             default => 0,
1129             },
1130             ignore_min_order_size => {
1131             summary => 'Ignore minimum order size limitation from exchanges',
1132             schema => 'bool*',
1133             default => 0,
1134             },
1135             },
1136             };
1137             sub show_opportunities {
1138 0     0 1   my %args = @_;
1139              
1140 0           my $r = $args{-cmdline_r};
1141             # XXX schema
1142 0   0       my $strategy = $args{strategy} // 'merge_order_book';
1143              
1144 0           my $res;
1145              
1146 0 0         $res = _init($r); return $res unless $res->[0] == 200;
  0            
1147 0 0         $res = _init_arbit($r); return $res unless $res->[0] == 200;
  0            
1148              
1149 0           my $strategy_mod = "App::cryp::arbit::Strategy::$strategy";
1150 0           (my $strategy_modpm = "$strategy_mod.pm") =~ s!::!/!g;
1151 0           require $strategy_modpm;
1152              
1153 0           $res = $strategy_mod->calculate_order_pairs(r => $r);
1154 0 0         return $res unless $res->[0] == 200;
1155              
1156             #log_trace "order pairs: %s", $res->[2];
1157              
1158 0           _format_order_pairs_response($res->[2]);
1159             }
1160              
1161             $SPEC{arbit} = {
1162             v => 1.1,
1163             summary => 'Perform arbitrage',
1164             description => <<'_',
1165              
1166             This utility monitors prices of several cryptocurrencies ("base currencies",
1167             e.g. LTC) in several cryptoexchanges. The "quote currency" can be fiat (e.g.
1168             USD, all other fiat currencies will be converted to USD) or another
1169             cryptocurrency (usually BTC).
1170              
1171             When it detects a net price difference for a base currency that is large enough
1172             (see `min_net_profit_margin` option), it will perform a buy order on the
1173             exchange that has the lower price and sell the exact same amount of base
1174             currency on the exchange that has the higher price. For example, if on XCHG1 the
1175             buy price of LTC 100.01 USD and on XCHG2 the sell price of LTC is 98.80 USD,
1176             then this utility will buy LTC on XCHG2 for 98.80 USD and sell the same amount
1177             of LTD on XCHG1 for 100.01 USD. The profit is (100.01 - 98.80 - trading fees)
1178             per LTC arbitraged. You have to maintain enough LTC balance on XCHG1 and enough
1179             USD balance on XCHG2.
1180              
1181             The balances are called inventories or your working capital. You fill and
1182             transfer inventories manually to refill balances and/or to collect profits.
1183              
1184             _
1185             args => {
1186             %args_db,
1187             %args_arbit_common,
1188             rounds => {
1189             summary => 'How many rounds',
1190             schema => 'int*',
1191             default => 1,
1192             cmdline_aliases => {
1193             loop => {is_flag=>1, code=>sub { $_[0]{rounds} = -1 }, summary => 'Shortcut for --rounds -1'},
1194             },
1195             description => <<'_',
1196              
1197             -1 means unlimited.
1198              
1199             _
1200             },
1201             frequency => {
1202             summary => 'How many seconds to wait between rounds (in seconds)',
1203             schema => 'posint*',
1204             default => 30,
1205             description => <<'_',
1206              
1207             A round consists of checking prices and then creating arbitraging order pairs.
1208              
1209             _
1210             },
1211             %arg_max_order_age,
1212             },
1213             features => {
1214             dry_run => 1,
1215             },
1216             };
1217             sub arbit {
1218 0     0 1   my %args = @_;
1219              
1220 0           my $r = $args{-cmdline_r};
1221             # XXX schema
1222 0   0       my $strategy = $args{strategy} // 'merge_order_book';
1223 0 0         $args{min_net_profit_margin} > 0 or return [412, "Refusing to do arbitrage with no positive min_net_profit_margin"];
1224              
1225 0           my $res;
1226              
1227 0 0         $res = _init($r); return $res unless $res->[0] == 200;
  0            
1228 0 0         $res = _init_arbit($r); return $res unless $res->[0] == 200;
  0            
1229              
1230 0           log_info "Starting arbitration with '%s' strategy ...", $strategy;
1231              
1232 0           my $strategy_mod = "App::cryp::arbit::Strategy::$strategy";
1233 0           (my $strategy_modpm = "$strategy_mod.pm") =~ s!::!/!g;
1234 0           require $strategy_modpm;
1235              
1236 0           my $round = 0;
1237             ROUND:
1238 0           while (1) {
1239 0           $round++;
1240 0           log_info "Round #%d", $round;
1241              
1242 0           $res = $strategy_mod->calculate_order_pairs(r => $r);
1243              
1244 0 0         if ($res->[0] == 200) {
1245 0           log_debug "Got these order pairs from arbit strategy module: %s",
1246             $res->[2];
1247             } else {
1248 0           log_error "Got error response from arbit strategy module: %s, ".
1249             "skipping this round", $res;
1250 0           goto SLEEP;
1251             }
1252 0           $r->{_stash}{order_pairs} = $res->[2];
1253              
1254 0 0         if ($args{-dry_run}) {
1255 0 0         if ($args{rounds} == 1) {
1256 0           log_info "[DRY-RUN] Will not actually be creating order pairs on the exchanges, showing possible order pairs ...";
1257 0           return _format_order_pairs_response($r->{_stash}{order_pairs});
1258             } else {
1259 0           log_info "[DRY-RUN] Will not actually be creating order pairs on the exchanges, waiting for next round ...";
1260 0           goto SLEEP;
1261             }
1262             }
1263              
1264 0           _create_orders($r);
1265              
1266 0           _check_orders($r);
1267              
1268 0 0 0       last if $args{rounds} > 0 && $round >= $args{rounds};
1269              
1270             SLEEP:
1271             log_trace "Sleeping for %d second(s) before next round ...",
1272 0           $args{frequency};
1273 0           sleep $args{frequency};
1274             }
1275              
1276 0           [200];
1277             }
1278              
1279             $SPEC{collect_orderbooks} = {
1280             v => 1.1,
1281             summary => 'Collect orderbooks into the database',
1282             description => <<'_',
1283              
1284             This utility collect orderbooks from exchanges and put it into the database. The
1285             data can be used later e.g. for backtesting.
1286              
1287             _
1288             args => {
1289             %args_db,
1290             %args_accounts_and_currencies,
1291             frequency => {
1292             summary => 'How many seconds to wait between rounds (in seconds)',
1293             schema => 'posint*',
1294             default => 30,
1295             },
1296             },
1297             };
1298             sub collect_orderbooks {
1299 0     0 1   my %args = @_;
1300              
1301 0           my $r = $args{-cmdline_r};
1302 0           my $res;
1303 0 0         $res = _init($r); return $res unless $res->[0] == 200;
  0            
1304 0 0         $res = _init_arbit($r); return $res unless $res->[0] == 200;
  0            
1305              
1306 0           my $dbh = $r->{_stash}{dbh};
1307              
1308             # this section is borrowed from App::cryp::arbit::Strategy::merge_order_book
1309              
1310 0           my %exchanges_for; # key="base currency"/"quote cryptocurrency or ':fiat'", value => [exchange, ...]
1311             my %fiat_for; # key=exchange safename, val=[fiat currency, ...]
1312 0           my %pairs_for; # key=exchange safename, val=[pair, ...]
1313             DETERMINE_SETS:
1314 0           for my $exchange (sort keys %{ $r->{_stash}{exchange_clients} }) {
  0            
1315 0           my $pair_recs = $r->{_stash}{exchange_pairs}{$exchange};
1316 0           for my $pair_rec (@$pair_recs) {
1317 0           my $pair = $pair_rec->{name};
1318 0           my ($basecur, $quotecur) = $pair =~ m!(.+)/(.+)!;
1319 0 0         next unless grep { $_ eq $basecur } @{ $r->{_stash}{base_currencies} };
  0            
  0            
1320 0 0         next unless grep { $_ eq $quotecur } @{ $r->{_stash}{quote_currencies} };
  0            
  0            
1321              
1322 0           my $key;
1323 0 0         if (App::cryp::arbit::_is_fiat($quotecur)) {
1324 0           $key = "$basecur/:fiat";
1325 0   0       $fiat_for{$exchange} //= [];
1326 0           push @{ $fiat_for{$exchange} }, $quotecur
1327 0 0         unless grep { $_ eq $quotecur } @{ $fiat_for{$exchange} };
  0            
  0            
1328             } else {
1329 0           $key = "$basecur/$quotecur";
1330             }
1331 0   0       $exchanges_for{$key} //= [];
1332 0           push @{ $exchanges_for{$key} }, $exchange;
  0            
1333              
1334 0   0       $pairs_for{$exchange} //= [];
1335 0           push @{ $pairs_for{$exchange} }, $pair
1336 0 0         unless grep { $_ eq $pair } @{ $pairs_for{$exchange} };
  0            
  0            
1337             }
1338             } # DETERMINE_SETS
1339              
1340             ROUND:
1341 0           while (1) {
1342             SET:
1343 0           for my $set (keys %exchanges_for) {
1344 0           my ($base_currency, $quote_currency0) = $set =~ m!(.+)/(.+)!;
1345              
1346             EXCHANGE:
1347 0           for my $exchange (sort keys %{ $r->{_stash}{exchange_clients} }) {
  0            
1348 0           my $eid = App::cryp::arbit::_get_exchange_id($r, $exchange);
1349 0           my $clients = $r->{_stash}{exchange_clients}{$exchange};
1350 0           my $client = $clients->{ (sort keys %$clients)[0] };
1351              
1352 0           my @pairs;
1353 0 0         if ($quote_currency0 eq ':fiat') {
1354 0           push @pairs, map { "$base_currency/$_" } @{ $fiat_for{$exchange} };
  0            
  0            
1355             } else {
1356 0           push @pairs, $set;
1357             }
1358              
1359             PAIR:
1360 0           for my $pair (@pairs) {
1361 0           my ($basecur, $quotecur) = split m!/!, $pair;
1362 0 0         next unless grep { $_ eq $pair } @{ $pairs_for{$exchange} };
  0            
  0            
1363              
1364 0           my $time = time();
1365 0           log_debug "Getting orderbook %s on %s ...", $pair, $exchange;
1366 0           my $res = $client->get_order_book(pair => $pair);
1367 0 0         unless ($res->[0] == 200) {
1368 0           log_error "Couldn't get orderbook %s on %s: %s, skipping this pair",
1369             $pair, $exchange, $res;
1370 0           next PAIR;
1371             }
1372              
1373             # save orderbook to database
1374             TYPE:
1375 0           for my $type ("buy", "sell") {
1376             # sanity checks
1377 0 0 0       unless ($res->[2]{$type} && @{ $res->[2]{$type} }) {
  0            
1378 0           log_warn "No $type orders for %s on %s, skipping",
1379             $pair, $exchange;
1380 0           next;
1381             }
1382 0           $dbh->do("INSERT INTO orderbook (time,exchange_id,base_currency,quote_currency,type) VALUES (?,?,?,?,?)", {}, $time, $eid, $basecur, $quotecur, $type);
1383 0           my $orderbook_id = $dbh->last_insert_id("","","","");
1384 0           my $sth = $dbh->prepare("INSERT INTO orderbook_item (orderbook_id, price, amount) VALUES (?,?,?)");
1385 0           for my $item (@{ $res->[2]{$type} }) {
  0            
1386             #log_trace "item: %s", $item;
1387 0           $sth->execute($orderbook_id, $item->[0], $item->[1]);
1388             }
1389             } # TYPE
1390             } # PAIR
1391             } # EXCHANGE
1392             } # SET
1393              
1394             SLEEP:
1395             log_trace "Sleeping for %d second(s) before next round ...",
1396 0           $args{frequency};
1397 0           sleep $args{frequency};
1398             } # ROUND
1399              
1400 0           [200];
1401             }
1402              
1403             sub _check_orders {
1404 0     0     my $r = shift;
1405              
1406 0           my $dbh = $r->{_stash}{dbh};
1407              
1408             my $code_update_buy_status = sub {
1409 0     0     my ($id, $status, $summary) = @_;
1410 0           local $dbh->{RaiseError};
1411             $dbh->do(
1412             "UPDATE order_pair SET buy_status=? WHERE id=?",
1413             {},
1414             $status,
1415             $id,
1416 0 0         ) or do {
1417 0           log_warn "Couldn't update buy status for order pair #%d: %s",
1418             $id, $dbh->errstr;
1419 0           return;
1420             };
1421 0 0         $dbh->do(
1422             "INSERT INTO arbit_order_log (order_pair_id, type, summary) VALUES (?,?,?)",
1423             {},
1424             $id, 'buy', "status changed to $status" . ($summary ? ": $summary" : ""),
1425             );
1426 0           };
1427              
1428             my $code_update_sell_status = sub {
1429 0     0     my ($id, $status, $summary) = @_;
1430 0           local $dbh->{RaiseError};
1431             $dbh->do(
1432             "UPDATE order_pair SET sell_status=? WHERE id=?",
1433             {},
1434             $status,
1435             $id,
1436 0 0         ) or do {
1437 0           log_warn "Couldn't update sell status for order pair #%d: %s",
1438             $id, $dbh->errstr;
1439 0           return;
1440             };
1441 0 0         $dbh->do(
1442             "INSERT INTO arbit_order_log (order_pair_id, type, summary) VALUES (?,?,?)",
1443             {},
1444             $id, 'sell', "status changed to $status" . ($summary ? ": $summary" : ""),
1445             );
1446 0           };
1447              
1448             my $code_update_buy_filled_base_size = sub {
1449 0     0     my ($id, $size, $summary) = @_;
1450 0           local $dbh->{RaiseError};
1451             $dbh->do(
1452             "UPDATE order_pair SET buy_filled_base_size=? WHERE id=?",
1453             {},
1454             $size,
1455             $id,
1456 0 0         ) or do {
1457 0           log_warn "Couldn't update buy filled base size for order pair #%d: %s",
1458             $id, $dbh->errstr;
1459 0           return;
1460             };
1461 0 0         $dbh->do(
1462             "INSERT INTO arbit_order_log (order_pair_id, type, summary) VALUES (?,?,?)",
1463             {},
1464             $id, 'buy', "filled_base_size changed to $size" . ($summary ? ": $summary" : ""),
1465             );
1466 0           };
1467              
1468             my $code_update_sell_filled_base_size = sub {
1469 0     0     my ($id, $size, $summary) = @_;
1470 0           local $dbh->{RaiseError};
1471             $dbh->do(
1472             "UPDATE order_pair SET sell_filled_base_size=? WHERE id=?",
1473             {},
1474             $size,
1475             $id,
1476 0 0         ) or do {
1477 0           log_warn "Couldn't update sell filled base size for order pair #%d: %s",
1478             $id, $dbh->errstr;
1479 0           return;
1480             };
1481 0 0         $dbh->do(
1482             "INSERT INTO arbit_order_log (order_pair_id, type, summary) VALUES (?,?,?)",
1483             {},
1484             $id, 'sell', "filled_base_size changed to $size" . ($summary ? ": $summary" : ""),
1485             );
1486 0           };
1487              
1488 0           my @open_order_pairs;
1489 0           my $sth = $dbh->prepare(
1490             "SELECT
1491             op.id id,
1492             op.ctime ctime,
1493             CONCAT(op.base_currency, '/', op.buy_quote_currency) buy_pair,
1494             op.buy_status buy_status,
1495             (SELECT safename FROM exchange WHERE id=op.buy_exchange_id) buy_exchange,
1496             (SELECT nickname FROM account WHERE id=op.buy_account_id) buy_account,
1497             op.buy_order_id buy_order_id,
1498              
1499             op.sell_status sell_status,
1500             CONCAT(op.base_currency, '/', op.sell_quote_currency) sell_pair,
1501             (SELECT safename FROM exchange WHERE id=op.sell_exchange_id) sell_exchange,
1502             (SELECT nickname FROM account WHERE id=op.sell_account_id) sell_account,
1503             op.sell_order_id sell_order_id
1504             FROM order_pair op
1505             WHERE
1506             (op.buy_order_id IS NOT NULL AND
1507             op.buy_status NOT IN ('done','filled','cancelled')) OR
1508             (op.sell_order_id IS NOT NULL AND
1509             op.sell_status NOT IN ('done','filled','cancelled'))
1510             ORDER BY op.ctime");
1511 0           $sth->execute;
1512 0           while (my $row = $sth->fetchrow_hashref) {
1513 0           push @open_order_pairs, $row;
1514             }
1515              
1516 0           my $time = time();
1517 0           for my $op (@open_order_pairs) {
1518             log_debug "Checking order pair #%d (buy status=%s, sell status=%s) ...",
1519 0           $op->{id}, $op->{buy_status}, $op->{sell_status};
1520              
1521             CHECK_BUY_ORDER: {
1522 0 0         last if $op->{buy_status} =~ /\A(done|cancelled)\z/;
  0            
1523 0           my $client = _get_exchange_client($r, $op->{buy_exchange}, $op->{buy_account});
1524 0           my $res = $client->get_order(pair=>$op->{buy_pair}, type=>'buy', order_id=>$op->{buy_order_id});
1525 0 0         if ($res->[0] == 404) {
    0          
1526             # assume 404 as order which was never filled and got cancelled.
1527             # some exchanges, e.g. coinbase-pro returns 404 for such orders
1528 0           $code_update_buy_status->($op->{id}, 'cancelled', 'not found via get_order(), assume cancelled without being filled');
1529 0           last;
1530             } elsif ($res->[0] != 200) {
1531             log_error "Couldn't get buy order %s (pair %s): %s",
1532 0           $op->{buy_order_id}, $op->{buy_pair}, $res;
1533 0           last;
1534             } else {
1535 0           my $status = $res->[2]{status};
1536 0           $code_update_buy_filled_base_size->($op->{id}, $res->[2]{filled_base_size});
1537 0           $code_update_buy_status->($op->{id}, $status);
1538              
1539 0 0 0       if ($status eq 'open' && $time - $op->{ctime} > $r->{args}{max_order_age}) {
1540 0           log_info "Order %s (buy) has been open for too long (>%d secs), cancelling ...";
1541 0           my $cancelres = $client->cancel_order(pair=>$op->{buy_pair}, type=>'buy', order_id=>$op->{buy_order_id});
1542 0 0         if ($cancelres->[0] != 200) {
1543 0           log_error "Couldn't cancel order %s (buy): %s", $op->{buy_order_id}, $cancelres;
1544             } else {
1545 0           $code_update_buy_status->($op->{id}, "cancelled");
1546             }
1547             }
1548             }
1549             } # CHECK_BUY_ORDER
1550              
1551             CHECK_SELL_ORDER: {
1552 0 0         last if $op->{sell_status} =~ /\A(done|cancelled)\z/;
  0            
1553 0           my $client = _get_exchange_client($r, $op->{sell_exchange}, $op->{sell_account});
1554 0           my $res = $client->get_order(pair=>$op->{sell_pair}, type=>'sell', order_id=>$op->{sell_order_id});
1555 0 0         if ($res->[0] == 404) {
    0          
1556             # assume 404 as order which was never filled and got cancelled.
1557             # some exchanges, e.g. coinbase-pro returns 404 for such orders
1558 0           $code_update_sell_status->($op->{id}, 'cancelled', 'not found via get_order(), assume cancelled without being filled');
1559 0           last;
1560             } elsif ($res->[0] != 200) {
1561             log_error "Couldn't get sell order %s (pair %s): %s",
1562 0           $op->{sell_order_id}, $op->{sell_pair}, $res;
1563 0           last;
1564             } else {
1565 0           my $status = $res->[2]{status};
1566 0           $code_update_sell_filled_base_size->($op->{id}, $res->[2]{filled_base_size});
1567 0           $code_update_sell_status->($op->{id}, $status);
1568              
1569 0 0 0       if ($status eq 'open' && $time - $op->{ctime} > $r->{args}{max_order_age}) {
1570 0           log_info "Order %s (sell) has been open for too long (>%d secs), cancelling ...";
1571 0           my $cancelres = $client->cancel_order(pair=>$op->{sell_pair}, type=>'sell', order_id=>$op->{sell_order_id});
1572 0 0         if ($cancelres->[0] != 200) {
1573 0           log_error "Couldn't cancel order %s (sell): %s", $op->{sell_order_id}, $cancelres;
1574             } else {
1575 0           $code_update_sell_status->($op->{id}, "cancelled");
1576             }
1577             }
1578             }
1579             } # CHECK_SELL_ORDER
1580             }
1581             }
1582              
1583             $SPEC{check_orders} = {
1584             v => 1.1,
1585             summary => 'Check the orders that have been created',
1586             description => <<'_',
1587              
1588             This subcommand will check the orders that have been created previously by
1589             `arbit` subcommand. It will update the order status and filled size (if still
1590             open). It will cancel (give up) the orders if deemed too old.
1591              
1592             _
1593             args => {
1594             %args_db,
1595             %arg_max_order_age,
1596             },
1597             };
1598             sub check_orders {
1599 0     0 1   my %args = @_;
1600              
1601 0           my $r = $args{-cmdline_r};
1602              
1603 0           my $res;
1604              
1605             # [ux] remove extraneous arguments supplied by config
1606 0           delete $r->{args}{accounts};
1607              
1608 0 0         $res = _init($r); return $res unless $res->[0] == 200;
  0            
1609              
1610 0           _check_orders($r);
1611 0           [200];
1612             }
1613              
1614             $SPEC{list_order_pairs} = {
1615             v => 1.1,
1616             summary => 'List created order pairs',
1617             args => {
1618             %args_db,
1619             time_start => {
1620             schema => 'date*',
1621             tags => ['category:filtering'],
1622             },
1623             time_end => {
1624             schema => 'date*',
1625             tags => ['category:filtering'],
1626             },
1627             open => {
1628             schema => 'bool*',
1629             tags => ['category:filtering'],
1630             },
1631             },
1632             };
1633             sub list_order_pairs {
1634 0     0 1   my %args = @_;
1635              
1636 0           my $r = $args{-cmdline_r};
1637              
1638             # [ux] remove extraneous arguments supplied by config
1639 0           delete $r->{args}{accounts};
1640              
1641 0           my $res;
1642              
1643 0 0         $res = _init($r); return $res unless $res->[0] == 200;
  0            
1644              
1645 0           my $dbh = $r->{_stash}{dbh};
1646              
1647 0           my @wheres;
1648             my @binds;
1649 0 0         if (defined $args{open}) {
1650 0 0         if ($args{open}) {
1651 0           push @wheres, "buy_status NOT IN ('done', 'cancelled', 'filled')";
1652             } else {
1653 0           push @wheres, "buy_status IN ('done', 'cancelled', 'filled')";
1654             }
1655             }
1656 0 0         if ($args{time_start}) {
1657 0           push @wheres, "ctime >= ?";
1658 0           push @binds, $args{time_start};
1659             }
1660 0 0         if ($args{time_end}) {
1661 0           push @wheres, "ctime <= ?";
1662 0           push @binds, $args{time_end};
1663             }
1664 0 0         my $sth = $dbh->prepare(
1665             "SELECT *, eb.safename buy_exchange, es.safename sell_exchange
1666             FROM order_pair op
1667             LEFT JOIN exchange eb ON op.buy_exchange_id=eb.id
1668             LEFT JOIN exchange es ON op.sell_exchange_id=es.id
1669             ".
1670             (@wheres ? "WHERE ".join(" AND ", @wheres)." " : "").
1671             "ORDER BY ctime");
1672 0           $sth->execute(@binds);
1673              
1674 0           my @recs;
1675 0           while (my $op = $sth->fetchrow_hashref) {
1676             my $rec = {
1677             ctime => int $op->{ctime},
1678             base_size => $op->{base_size},
1679             base_currency => $op->{base_currency},
1680              
1681             buy_exchange => $op->{buy_exchange},
1682             buy_quote_currency => $op->{buy_quote_currency},
1683             buy_actual_base_size => $op->{buy_actual_base_size},
1684             buy_actual_price => $op->{buy_actual_price},
1685             buy_filled_pct => defined($op->{buy_filled_base_size}) ? $op->{buy_filled_base_size} / $op->{buy_actual_base_size}*100 : undef,
1686             buy_status => $op->{buy_status},
1687              
1688             sell_exchange => $op->{sell_exchange},
1689             sell_quote_currency => $op->{sell_quote_currency},
1690             sell_actual_base_size => $op->{sell_actual_base_size},
1691             sell_actual_price => $op->{sell_actual_price},
1692             sell_filled_pct => defined($op->{sell_filled_base_size}) ? $op->{sell_filled_base_size} / $op->{sell_actual_base_size}*100 : undef,
1693             sell_status => $op->{sell_status},
1694              
1695 0 0         };
    0          
1696 0           push @recs, $rec;
1697             }
1698              
1699 0           my $resmeta = {};
1700 0           $resmeta->{'table.fields'} = ['ctime' , 'base_size', 'base_currency', 'buy_exchange', 'buy_actual_base_size', 'buy_actual_price', 'buy_quote_currency', 'buy_filled_pct', 'buy_status', 'sell_exchange', 'sell_actual_base_size', 'sell_actual_price', 'sell_quote_currency', 'sell_filled_pct', 'sell_status',];
1701 0           $resmeta->{'table.field_labels'} = [undef , 'amount' , 'c' , 'buyFrom' , 'buyAmount' , 'buyPrice' , 'buyC' , 'buy%' , 'buySt' , 'sellTo' , 'sellAmount' , 'sellPrice' , 'sellC' , 'sell%' , 'sellSt' ,];
1702 0           $resmeta->{'table.field_formats'} = ['iso8601_datetime' , $fnum8 , undef , undef , $fnum8 , $fnum8 , undef , $fnum2 , undef , undef , $fnum8 , $fnum8 , undef , $fnum2 , undef ,];
1703 0           $resmeta->{'table.field_aligns'} = ['left' , 'right' , 'left' , 'left' , 'right' , 'right' , 'left' , 'right' , 'left' , 'left' , 'right' , 'right' , 'left' , 'right' , 'left' ,];
1704              
1705 0           [200, "OK", \@recs, $resmeta];
1706             }
1707              
1708             $SPEC{get_profit_report} = {
1709             v => 1.1,
1710             summary => 'Get profit report',
1711             args => {
1712             %args_db,
1713             time_start => {
1714             schema => 'date*',
1715             tags => ['category:filtering'],
1716             },
1717             time_end => {
1718             schema => 'date*',
1719             tags => ['category:filtering'],
1720             },
1721             detail => {
1722             schema => 'bool*',
1723             cmdline_aliases => {l=>{}},
1724             },
1725             %arg_usd_rates,
1726             },
1727             };
1728             sub get_profit_report {
1729 0     0 1   my %args = @_;
1730              
1731 0           my $r = $args{-cmdline_r};
1732              
1733             # [ux] remove extraneous arguments supplied by config
1734 0           delete $r->{args}{accounts};
1735              
1736 0           my $res;
1737              
1738 0 0         $res = _init($r); return $res unless $res->[0] == 200;
  0            
1739              
1740 0           my $dbh = $r->{_stash}{dbh};
1741              
1742 0           my @wheres;
1743             my @binds;
1744 0 0         if ($args{time_start}) {
1745 0           push @wheres, "ctime >= ?";
1746 0           push @binds, $args{time_start};
1747             }
1748 0 0         if ($args{time_end}) {
1749 0           push @wheres, "ctime <= ?";
1750 0           push @binds, $args{time_end};
1751             }
1752 0 0         my $sth = $dbh->prepare(
1753             "SELECT
1754             *,
1755             eb.safename buy_exchange, es.safename sell_exchange,
1756             acb.nickname buy_account, acs.nickname sell_account
1757             FROM order_pair op
1758             LEFT JOIN exchange eb ON op.buy_exchange_id=eb.id
1759             LEFT JOIN exchange es ON op.sell_exchange_id=es.id
1760             LEFT JOIN account acb ON op.buy_account_id=acb.id
1761             LEFT JOIN account acs ON op.sell_account_id=acs.id
1762             ".
1763             (@wheres ? "WHERE ".join(" AND ", @wheres)." " : "").
1764             "ORDER BY ctime");
1765 0           $sth->execute(@binds);
1766              
1767 0           my @recs;
1768             my %per_currency_sums; # key = currency
1769 0           my %per_currency_sums_usd; # key = currency
1770 0           my %per_account_per_currency_sums; # key = "exchange/account", val = { currency1 => total, ... }
1771 0           while (my $op = $sth->fetchrow_hashref) {
1772             RECORD_BUY: {
1773 0 0 0       last unless defined $op->{buy_filled_base_size} && $op->{buy_filled_base_size};
  0            
1774 0           my $frac_b = $op->{buy_filled_base_size} / $op->{buy_actual_base_size};
1775             my $rec_b = {
1776             time => int $op->{ctime},
1777             currency => $op->{base_currency},
1778             amount => $frac_b * $op->{buy_actual_base_size},
1779 0           summary => "bought on $op->{buy_exchange} \@$op->{buy_actual_price}",
1780             };
1781             my $rec_q = {
1782             time => int $op->{ctime},
1783             currency => $op->{buy_quote_currency},
1784             amount => -$frac_b * $op->{buy_actual_base_size} * $op->{buy_actual_price},
1785 0           summary => "spent on $op->{buy_exchange} for buying $op->{base_currency} \@$op->{buy_actual_price}",
1786             };
1787 0           $per_currency_sums{ $op->{base_currency} } += $rec_b->{amount};
1788 0           $per_currency_sums{ $op->{buy_quote_currency} } += $rec_q->{amount};
1789              
1790 0           $per_currency_sums_usd{ $op->{buy_quote_currency} } += _convert_to_usd($r, $rec_q->{amount}, $op->{buy_quote_currency});
1791              
1792 0 0         my $acckey = $op->{buy_exchange} . ($op->{buy_account} eq 'default' ? '' : "/$op->{buy_account}");
1793 0           $per_account_per_currency_sums{ $acckey }{ $op->{base_currency} } += $rec_b->{amount};
1794 0           $per_account_per_currency_sums{ $acckey }{ $op->{buy_quote_currency} } += $rec_q->{amount};
1795 0 0         push @recs, $rec_b, $rec_q if $args{detail};
1796             }
1797             RECORD_SELL: {
1798 0 0 0       last unless defined $op->{sell_filled_base_size} && $op->{sell_filled_base_size};
  0            
1799 0           my $frac_s = $op->{sell_filled_base_size} / $op->{sell_actual_base_size};
1800             my $rec_b = {
1801             time => int $op->{ctime},
1802             summary => "sold on $op->{sell_exchange} \@$op->{sell_actual_price}",
1803             currency => $op->{base_currency},
1804             amount => -$frac_s * $op->{sell_actual_base_size},
1805 0           };
1806             my $rec_q = {
1807             time => int $op->{ctime},
1808             summary => "received on $op->{sell_exchange} for selling $op->{base_currency} \@$op->{sell_actual_price}",
1809             currency => $op->{sell_quote_currency},
1810             amount => $frac_s * $op->{sell_actual_base_size} * $op->{sell_actual_price},
1811 0           };
1812 0           $per_currency_sums{ $op->{base_currency} } += $rec_b->{amount};
1813 0           $per_currency_sums{ $op->{sell_quote_currency} } += $rec_q->{amount};
1814              
1815 0           $per_currency_sums_usd{ $op->{sell_quote_currency} } += _convert_to_usd($r, $rec_q->{amount}, $op->{sell_quote_currency});
1816              
1817 0 0         my $acckey = $op->{sell_exchange} . ($op->{sell_account} eq 'default' ? '' : "/$op->{sell_account}");
1818 0           $per_account_per_currency_sums{ $acckey }{ $op->{base_currency} } += $rec_b->{amount};
1819 0           $per_account_per_currency_sums{ $acckey }{ $op->{sell_quote_currency} } += $rec_q->{amount};
1820 0 0         push @recs, $rec_b, $rec_q if $args{detail};
1821             }
1822             }
1823              
1824             PER_CURRENCY_PER_ACCOUNT_SUBTOTAL: {
1825 0           for my $acckey (sort keys %per_account_per_currency_sums) {
  0            
1826 0           my $per_currency_sums = $per_account_per_currency_sums{$acckey};
1827 0           my $i = 0;
1828 0           for my $cur (sort keys %$per_currency_sums) {
1829             push @recs, {
1830             summary => $i++ ? '' : "Account $acckey subtotal",
1831             currency => $cur,
1832 0 0         amount => $per_currency_sums->{$cur},
1833             };
1834             }
1835             }
1836             }
1837              
1838             PER_CURRENCY_SUBTOTAL: {
1839 0           my $i = 0;
  0            
1840 0           for my $cur (sort keys %per_currency_sums) {
1841             my $rec = {
1842             summary => $i++ ? '' : "Per-currency subtotal",
1843             currency => $cur,
1844 0 0         amount => $per_currency_sums{$cur},
1845             };
1846             $rec->{amount_usd} = $per_currency_sums_usd{$cur}
1847 0 0         if exists $per_currency_sums_usd{$cur};
1848 0           push @recs, $rec;
1849             }
1850             }
1851              
1852             FIAT_PROFIT: {
1853 0           my $profit = 0;
  0            
1854 0           for my $cur (sort keys %per_currency_sums_usd) {
1855 0           $profit += $per_currency_sums_usd{$cur};
1856             }
1857 0           push @recs, {
1858             summary => 'Profit',
1859             currency => 'USD',
1860             amount => $profit,
1861             amount_usd => $profit,
1862             };
1863 0           for my $cur (sort keys %per_currency_sums) {
1864 0 0         next if _is_fiat($cur);
1865 0 0         next if $per_currency_sums{$cur} == 0;
1866             push @recs, {
1867             currency => $cur,
1868 0           amount => $per_currency_sums{$cur},
1869             };
1870             }
1871             }
1872              
1873 0           my $resmeta = {
1874             'table.fields' => ['time' , 'summary', 'currency', 'amount', 'amount_usd'],
1875             'table.field_labels' => [undef , undef , 'c' , undef , 'amountUSD'],
1876             'table.field_formats' => ['iso8601_datetime', undef , undef , $fnum8 , $fnum8],
1877             'table.field_aligns' => ['left' , 'left' , 'left' , 'right' , 'right'],
1878             };
1879              
1880 0           [200, "OK", \@recs, $resmeta];
1881             }
1882              
1883             1;
1884             # ABSTRACT: Cryptocurrency arbitrage utility
1885              
1886             __END__
1887              
1888             =pod
1889              
1890             =encoding UTF-8
1891              
1892             =head1 NAME
1893              
1894             App::cryp::arbit - Cryptocurrency arbitrage utility
1895              
1896             =head1 VERSION
1897              
1898             This document describes version 0.008 of App::cryp::arbit (from Perl distribution App-cryp-arbit), released on 2018-11-29.
1899              
1900             =head1 SYNOPSIS
1901              
1902             Please see included script L<cryp-arbit>.
1903              
1904             =head1 DESCRIPTION
1905              
1906             =head2 Glossary
1907              
1908             =over
1909              
1910             =item * inventory
1911              
1912             =item * order pair
1913              
1914             =item * gross profit margin
1915              
1916             Price difference percentage of a cryptocurrency between two exchanges, without
1917             taking into account trading fees and foreign exchange spread.
1918              
1919             For example, suppose BTC is being offered (ask price, sell price) at 7010 USD on
1920             exchange1 and is being bidden (bid price, buy price) at 7150 USD on exchange2.
1921             This means there is a (7150-7010)/7010 = 1.997% gross profit margin. We can buy
1922             BTC on exchange1 for 7010 USD then sell the same amout of BTC on exchange2 for
1923             7150 USD and gain (7150-7010) = 140 USD per BTC, before fees.
1924              
1925             =item * trading profit margin
1926              
1927             Price difference percentage of a cryptocurrency between two exchanges, after
1928             taking into account trading fees.
1929              
1930             For example, suppose BTC is being offered (ask price, sell price) at 7010 USD on
1931             exchange1 and is being bidden (bid price, buy price) at 7150 USD on exchange2.
1932             Trading (market maker) fee on exchange1 is 0.3% and on exchange2 is 0.25%. After
1933             trading fees, the ask price becomes 7010 * (1+0.3%) = 7031.03 USD and the bid
1934             price becomes 7150 * (1-0.25%) = 7132.125. The trading profit margin is
1935             (7132.125-7031.03)/7031.03 = 1.438%. We can buy BTC on exchange1 for 7010 USD
1936             then sell the same amout of BTC on exchange2 for 7150 USD and still gain
1937             (7132.125-7031.03) = 101.095 USD per BTC, after trading fees.
1938              
1939             =item * net profit margin
1940              
1941             Price difference percentage of a cryptocurrency between two exchanges, after
1942             taking into account trading fees and foreign exchange spread. If the price on
1943             both exchanges are quoted in the same currency (e.g. USD) then there is no forex
1944             spread and net profit margin is the same as trading profit margin.
1945              
1946             If the quoting currencies are different, e.g. USD on exchange1 and IDR on
1947             exchange2, then first we calculate gross and trading profit margin using prices
1948             converted to USD using average forex rate (highest forex dealer's sell price +
1949             lowest buy price, divided by two). Then we subtract trading profit margin with
1950             forex spread for safety.
1951              
1952             For example, suppose BTC is being offered (ask price, sell price) at 7010 USD on
1953             exchange1 and is being bidden (bid price, buy price) at 99,500,000 IDR on
1954             exchange2. The forex rate for USD/IDR is: buy 13,895, sell 13,925, average
1955             (13,925+13,895)/2 = 13,910, spread (13,925-13,895)/13,895 = 0.216%. The price on
1956             exchange2 in USD is 99,500,000 / 13,910 = 7153.127 USD. Trading (market maker)
1957             fee on exchange1 is 0.3% and on exchange2 is 0.25%. After trading fees, the ask
1958             price becomes 7010 * (1+0.3%) = 7031.03 USD and the bid price becomes 7153.127 *
1959             (1-0.25%) = 7135.244. The trading profit margin is (7135.244-7031.03)/7031.03 =
1960             1.482%. We can buy BTC on exchange1 for 7010 USD then sell the same amout of BTC
1961             on exchange2 for 7150 USD and still gain (7132.125-7031.03) = 101.095 USD per
1962             BTC, after trading fees. The net profit margin is 1.482% - 0.216% = 1.266%.
1963              
1964             =back
1965              
1966             =head1 INTERNAL NOTES
1967              
1968             The cryp app family uses L<Perinci::CmdLine::cryp> which puts cryp-specific
1969             information from the configuration into the $r->{_cryp} hash:
1970              
1971             $r->{_cryp}
1972             {arbit_strategies} # from [arbit-strategy/XXX] config sections
1973             {exchanges} # from [exchange/XXX(/YYY)?] config sections
1974             {masternodes} # from [masternode/XXX(/YYY)?] config sections
1975             {wallet} # from [wallet/COIN]
1976              
1977             Routines inside this module communicate with one another either using the
1978             database (obviously), or by putting stuffs in C<$r> (the request hash/stash) and
1979             passing C<$r> around. The keys that are used by routines in this module:
1980              
1981             $r->{_stash}
1982             {dbh}
1983             {account_balances} # key=exchange safename, value={currency1 => [{account=>account1, account_id=>aid, available=>..., ...}, {...}]}. value->{currency} sorted by largest available balance first
1984             {account_exchanges} # key=exchange safename, value={account1 => 1, ...}
1985             {account_ids} # key=exchange safename, value={account1 => numeric ID from db, ...}
1986             {base_currencies} # target (crypto)currencies to arbitrage
1987             {exchange_clients} # key=exchange safename, value={account1 => $client1, ...}
1988             {exchange_ids} # key=exchange safename, value=exchange (numeric) ID from db
1989             {exchange_recs} # key=exchange safename, value=hash (from CryptoExchange::Catalog)
1990             {exchange_coins} # key=exchange safename, value=[COIN1, COIN2, ...]
1991             {exchange_pairs} # key=exchange safename, value=[{name=>PAIR1, min_base_size=>..., min_quote_size=>...}, ...]
1992             {forex_rates} # key=currency pair (e.g. IDR/USD), val=exchange rate (avg rate)
1993             {forex_spreads} # key=fiat currency pair, e.g. USD/IDR, value=percentage
1994             {fx} # key=currency value=result from get_spot_rate()
1995             {order_pairs} # result from calculate_order_pairs()
1996             {quote_currencies} # what currencies we use to buy/sell the base currencies
1997             {quote_currencies_for} # key=base currency, value={quotecurrency1 => 1, quotecurrency2=>1, ...}
1998             {trading_fees} # key=exchange safename, value={coin1=>num (in percent) market taker fees, ...}, ':default' for all other coins, ':default' for all other exchanges
1999              
2000             =head1 FUNCTIONS
2001              
2002              
2003             =head2 arbit
2004              
2005             Usage:
2006              
2007             arbit(%args) -> [status, msg, payload, meta]
2008              
2009             Perform arbitrage.
2010              
2011             This utility monitors prices of several cryptocurrencies ("base currencies",
2012             e.g. LTC) in several cryptoexchanges. The "quote currency" can be fiat (e.g.
2013             USD, all other fiat currencies will be converted to USD) or another
2014             cryptocurrency (usually BTC).
2015              
2016             When it detects a net price difference for a base currency that is large enough
2017             (see C<min_net_profit_margin> option), it will perform a buy order on the
2018             exchange that has the lower price and sell the exact same amount of base
2019             currency on the exchange that has the higher price. For example, if on XCHG1 the
2020             buy price of LTC 100.01 USD and on XCHG2 the sell price of LTC is 98.80 USD,
2021             then this utility will buy LTC on XCHG2 for 98.80 USD and sell the same amount
2022             of LTD on XCHG1 for 100.01 USD. The profit is (100.01 - 98.80 - trading fees)
2023             per LTC arbitraged. You have to maintain enough LTC balance on XCHG1 and enough
2024             USD balance on XCHG2.
2025              
2026             The balances are called inventories or your working capital. You fill and
2027             transfer inventories manually to refill balances and/or to collect profits.
2028              
2029             This function is not exported.
2030              
2031             This function supports dry-run operation.
2032              
2033              
2034             Arguments ('*' denotes required arguments):
2035              
2036             =over 4
2037              
2038             =item * B<accounts> => I<array[cryptoexchange::account]>
2039              
2040             Cryptoexchange accounts.
2041              
2042             There should at least be two accounts, on at least two different
2043             cryptoexchanges. If not specified, all accounts listed on the configuration file
2044             will be included. Note that it's possible to include two or more accounts on the
2045             same cryptoexchange.
2046              
2047             =item * B<base_currencies> => I<array[cryptocurrency]>
2048              
2049             Target (crypto)currencies to arbitrate.
2050              
2051             If not specified, will list all supported pairs on all the exchanges and include
2052             the base cryptocurrencies that are listed on at least 2 different exchanges (for
2053             arbitrage possibility).
2054              
2055             =item * B<db_name>* => I<str>
2056              
2057             =item * B<db_password> => I<str>
2058              
2059             =item * B<db_username> => I<str>
2060              
2061             =item * B<frequency> => I<posint> (default: 30)
2062              
2063             How many seconds to wait between rounds (in seconds).
2064              
2065             A round consists of checking prices and then creating arbitraging order pairs.
2066              
2067             =item * B<max_order_age> => I<posint> (default: 86400)
2068              
2069             How long should we wait for orders to be completed before cancelling them (in seconds).
2070              
2071             Sometimes because of rapid trading and price movement, our order might not be
2072             filled immediately. This setting sets a limit on how long should an order be
2073             left open. After this limit is reached, we cancel the order. The imbalance of
2074             the arbitrage transaction will be recorded.
2075              
2076             =item * B<max_order_pairs_per_round> => I<posint>
2077              
2078             Maximum number of order pairs to create per round.
2079              
2080             =item * B<max_order_quote_size> => I<float> (default: 100)
2081              
2082             What is the maximum amount of a single order.
2083              
2084             A single order will be limited to not be above this value (in quote currency,
2085             which if fiat will be converted to USD). This is the amount for the buying
2086             (because an arbitrage transaction is comprised of a pair of orders, where one
2087             order is a selling order at a higher quote currency size than the buying order).
2088              
2089             For example if you are arbitraging BTC against USD and IDR, and set this option
2090             to 75, then orders will not be above 75 USD. If you are arbitraging LTC against
2091             BTC and set this to 0.03 then orders will not be above 0.03 BTC.
2092              
2093             Suggestion: If you set this option too high, a few orders can use up your
2094             inventory (and you might not be getting optimal profit percentage). Also, large
2095             orders can take a while (or too long) to fill. If you set this option too low,
2096             you will hit the exchanges' minimum order size and no orders can be created.
2097             Since we want smaller risk of orders not getting filled quickly, we want small
2098             order sizes. The optimum number range a little above the exchanges' minimum
2099             order size.
2100              
2101             =item * B<min_account_balances> => I<hash>
2102              
2103             What are the minimum account balances.
2104              
2105             =item * B<min_net_profit_margin> => I<float> (default: 0)
2106              
2107             Minimum net profit margin that will trigger an arbitrage trading, in percentage.
2108              
2109             Below this percentage number, no order pairs will be sent to the exchanges to do
2110             the arbitrage. Note that the net profit margin already takes into account
2111             trading fees and forex spread (see Glossary section for more details and
2112             illustration).
2113              
2114             Suggestion: If you set this option too high, there might not be any order pairs
2115             possible. If you set this option too low, you will be getting too thin profits.
2116             Run C<cryp-arbit opportunities> or C<cryp-arbit arbit --dry-run> for a while to
2117             see what the average percentage is and then decide at which point you want to
2118             perform arbitrage.
2119              
2120             =item * B<quote_currencies> => I<array[fiat_or_cryptocurrency]>
2121              
2122             The currencies to exchange (buy/sell) the target currencies.
2123              
2124             You can have fiat currencies as the quote currencies, to buy/sell the target
2125             (base) currencies during arbitrage. For example, to arbitrage LTC against USD
2126             and IDR, C<base_currencies> is ['BTC'] and C<quote_currencies> is ['USD', 'IDR'].
2127              
2128             You can also arbitrage cryptocurrencies against other cryptocurrency (usually
2129             BTC, "the USD of cryptocurrencies"). For example, to arbitrage XMR and LTC
2130             against BTC, C<base_currencies> is ['XMR', 'LTC'] and C<quote_currencies> is
2131             ['BTC'].
2132              
2133             =item * B<rounds> => I<int> (default: 1)
2134              
2135             How many rounds.
2136              
2137             -1 means unlimited.
2138              
2139             =item * B<strategy> => I<str> (default: "merge_order_book")
2140              
2141             Which strategy to use for arbitration.
2142              
2143             Strategy is implemented in a C<App::cryp::arbit::Strategy::*> perl module.
2144              
2145             =back
2146              
2147             Special arguments:
2148              
2149             =over 4
2150              
2151             =item * B<-dry_run> => I<bool>
2152              
2153             Pass -dry_run=>1 to enable simulation mode.
2154              
2155             =back
2156              
2157             Returns an enveloped result (an array).
2158              
2159             First element (status) is an integer containing HTTP status code
2160             (200 means OK, 4xx caller error, 5xx function error). Second element
2161             (msg) is a string containing error message, or 'OK' if status is
2162             200. Third element (payload) is optional, the actual result. Fourth
2163             element (meta) is called result metadata and is optional, a hash
2164             that contains extra information.
2165              
2166             Return value: (any)
2167              
2168              
2169             =head2 check_orders
2170              
2171             Usage:
2172              
2173             check_orders(%args) -> [status, msg, payload, meta]
2174              
2175             Check the orders that have been created.
2176              
2177             This subcommand will check the orders that have been created previously by
2178             C<arbit> subcommand. It will update the order status and filled size (if still
2179             open). It will cancel (give up) the orders if deemed too old.
2180              
2181             This function is not exported.
2182              
2183             Arguments ('*' denotes required arguments):
2184              
2185             =over 4
2186              
2187             =item * B<db_name>* => I<str>
2188              
2189             =item * B<db_password> => I<str>
2190              
2191             =item * B<db_username> => I<str>
2192              
2193             =item * B<max_order_age> => I<posint> (default: 86400)
2194              
2195             How long should we wait for orders to be completed before cancelling them (in seconds).
2196              
2197             Sometimes because of rapid trading and price movement, our order might not be
2198             filled immediately. This setting sets a limit on how long should an order be
2199             left open. After this limit is reached, we cancel the order. The imbalance of
2200             the arbitrage transaction will be recorded.
2201              
2202             =back
2203              
2204             Returns an enveloped result (an array).
2205              
2206             First element (status) is an integer containing HTTP status code
2207             (200 means OK, 4xx caller error, 5xx function error). Second element
2208             (msg) is a string containing error message, or 'OK' if status is
2209             200. Third element (payload) is optional, the actual result. Fourth
2210             element (meta) is called result metadata and is optional, a hash
2211             that contains extra information.
2212              
2213             Return value: (any)
2214              
2215              
2216             =head2 collect_orderbooks
2217              
2218             Usage:
2219              
2220             collect_orderbooks(%args) -> [status, msg, payload, meta]
2221              
2222             Collect orderbooks into the database.
2223              
2224             This utility collect orderbooks from exchanges and put it into the database. The
2225             data can be used later e.g. for backtesting.
2226              
2227             This function is not exported.
2228              
2229             Arguments ('*' denotes required arguments):
2230              
2231             =over 4
2232              
2233             =item * B<accounts> => I<array[cryptoexchange::account]>
2234              
2235             Cryptoexchange accounts.
2236              
2237             There should at least be two accounts, on at least two different
2238             cryptoexchanges. If not specified, all accounts listed on the configuration file
2239             will be included. Note that it's possible to include two or more accounts on the
2240             same cryptoexchange.
2241              
2242             =item * B<base_currencies> => I<array[cryptocurrency]>
2243              
2244             Target (crypto)currencies to arbitrate.
2245              
2246             If not specified, will list all supported pairs on all the exchanges and include
2247             the base cryptocurrencies that are listed on at least 2 different exchanges (for
2248             arbitrage possibility).
2249              
2250             =item * B<db_name>* => I<str>
2251              
2252             =item * B<db_password> => I<str>
2253              
2254             =item * B<db_username> => I<str>
2255              
2256             =item * B<frequency> => I<posint> (default: 30)
2257              
2258             How many seconds to wait between rounds (in seconds).
2259              
2260             =item * B<quote_currencies> => I<array[fiat_or_cryptocurrency]>
2261              
2262             The currencies to exchange (buy/sell) the target currencies.
2263              
2264             You can have fiat currencies as the quote currencies, to buy/sell the target
2265             (base) currencies during arbitrage. For example, to arbitrage LTC against USD
2266             and IDR, C<base_currencies> is ['BTC'] and C<quote_currencies> is ['USD', 'IDR'].
2267              
2268             You can also arbitrage cryptocurrencies against other cryptocurrency (usually
2269             BTC, "the USD of cryptocurrencies"). For example, to arbitrage XMR and LTC
2270             against BTC, C<base_currencies> is ['XMR', 'LTC'] and C<quote_currencies> is
2271             ['BTC'].
2272              
2273             =back
2274              
2275             Returns an enveloped result (an array).
2276              
2277             First element (status) is an integer containing HTTP status code
2278             (200 means OK, 4xx caller error, 5xx function error). Second element
2279             (msg) is a string containing error message, or 'OK' if status is
2280             200. Third element (payload) is optional, the actual result. Fourth
2281             element (meta) is called result metadata and is optional, a hash
2282             that contains extra information.
2283              
2284             Return value: (any)
2285              
2286              
2287             =head2 dump_cryp_config
2288              
2289             Usage:
2290              
2291             dump_cryp_config() -> [status, msg, payload, meta]
2292              
2293             This function is not exported.
2294              
2295             No arguments.
2296              
2297             Returns an enveloped result (an array).
2298              
2299             First element (status) is an integer containing HTTP status code
2300             (200 means OK, 4xx caller error, 5xx function error). Second element
2301             (msg) is a string containing error message, or 'OK' if status is
2302             200. Third element (payload) is optional, the actual result. Fourth
2303             element (meta) is called result metadata and is optional, a hash
2304             that contains extra information.
2305              
2306             Return value: (any)
2307              
2308              
2309             =head2 get_profit_report
2310              
2311             Usage:
2312              
2313             get_profit_report(%args) -> [status, msg, payload, meta]
2314              
2315             Get profit report.
2316              
2317             This function is not exported.
2318              
2319             Arguments ('*' denotes required arguments):
2320              
2321             =over 4
2322              
2323             =item * B<db_name>* => I<str>
2324              
2325             =item * B<db_password> => I<str>
2326              
2327             =item * B<db_username> => I<str>
2328              
2329             =item * B<detail> => I<bool>
2330              
2331             =item * B<time_end> => I<date>
2332              
2333             =item * B<time_start> => I<date>
2334              
2335             =item * B<usd_rates> => I<hash>
2336              
2337             Set USD rates.
2338              
2339             Example:
2340              
2341             --usd-rate IDR=14500 --usd-rate THB=33.25
2342              
2343             =back
2344              
2345             Returns an enveloped result (an array).
2346              
2347             First element (status) is an integer containing HTTP status code
2348             (200 means OK, 4xx caller error, 5xx function error). Second element
2349             (msg) is a string containing error message, or 'OK' if status is
2350             200. Third element (payload) is optional, the actual result. Fourth
2351             element (meta) is called result metadata and is optional, a hash
2352             that contains extra information.
2353              
2354             Return value: (any)
2355              
2356              
2357             =head2 list_order_pairs
2358              
2359             Usage:
2360              
2361             list_order_pairs(%args) -> [status, msg, payload, meta]
2362              
2363             List created order pairs.
2364              
2365             This function is not exported.
2366              
2367             Arguments ('*' denotes required arguments):
2368              
2369             =over 4
2370              
2371             =item * B<db_name>* => I<str>
2372              
2373             =item * B<db_password> => I<str>
2374              
2375             =item * B<db_username> => I<str>
2376              
2377             =item * B<open> => I<bool>
2378              
2379             =item * B<time_end> => I<date>
2380              
2381             =item * B<time_start> => I<date>
2382              
2383             =back
2384              
2385             Returns an enveloped result (an array).
2386              
2387             First element (status) is an integer containing HTTP status code
2388             (200 means OK, 4xx caller error, 5xx function error). Second element
2389             (msg) is a string containing error message, or 'OK' if status is
2390             200. Third element (payload) is optional, the actual result. Fourth
2391             element (meta) is called result metadata and is optional, a hash
2392             that contains extra information.
2393              
2394             Return value: (any)
2395              
2396              
2397             =head2 show_opportunities
2398              
2399             Usage:
2400              
2401             show_opportunities(%args) -> [status, msg, payload, meta]
2402              
2403             Show arbitrage opportunities.
2404              
2405             This subcommand, like the C<arbit> subcommand, checks prices of cryptocurrencies
2406             on several exchanges for arbitrage possibility; but does not actually perform
2407             the arbitraging.
2408              
2409             This function is not exported.
2410              
2411             Arguments ('*' denotes required arguments):
2412              
2413             =over 4
2414              
2415             =item * B<accounts> => I<array[cryptoexchange::account]>
2416              
2417             Cryptoexchange accounts.
2418              
2419             There should at least be two accounts, on at least two different
2420             cryptoexchanges. If not specified, all accounts listed on the configuration file
2421             will be included. Note that it's possible to include two or more accounts on the
2422             same cryptoexchange.
2423              
2424             =item * B<base_currencies> => I<array[cryptocurrency]>
2425              
2426             Target (crypto)currencies to arbitrate.
2427              
2428             If not specified, will list all supported pairs on all the exchanges and include
2429             the base cryptocurrencies that are listed on at least 2 different exchanges (for
2430             arbitrage possibility).
2431              
2432             =item * B<db_name>* => I<str>
2433              
2434             =item * B<db_password> => I<str>
2435              
2436             =item * B<db_username> => I<str>
2437              
2438             =item * B<ignore_balance> => I<bool> (default: 0)
2439              
2440             Ignore account balances.
2441              
2442             =item * B<ignore_min_order_size> => I<bool> (default: 0)
2443              
2444             Ignore minimum order size limitation from exchanges.
2445              
2446             =item * B<max_order_pairs_per_round> => I<posint>
2447              
2448             Maximum number of order pairs to create per round.
2449              
2450             =item * B<max_order_quote_size> => I<float> (default: 100)
2451              
2452             What is the maximum amount of a single order.
2453              
2454             A single order will be limited to not be above this value (in quote currency,
2455             which if fiat will be converted to USD). This is the amount for the buying
2456             (because an arbitrage transaction is comprised of a pair of orders, where one
2457             order is a selling order at a higher quote currency size than the buying order).
2458              
2459             For example if you are arbitraging BTC against USD and IDR, and set this option
2460             to 75, then orders will not be above 75 USD. If you are arbitraging LTC against
2461             BTC and set this to 0.03 then orders will not be above 0.03 BTC.
2462              
2463             Suggestion: If you set this option too high, a few orders can use up your
2464             inventory (and you might not be getting optimal profit percentage). Also, large
2465             orders can take a while (or too long) to fill. If you set this option too low,
2466             you will hit the exchanges' minimum order size and no orders can be created.
2467             Since we want smaller risk of orders not getting filled quickly, we want small
2468             order sizes. The optimum number range a little above the exchanges' minimum
2469             order size.
2470              
2471             =item * B<min_account_balances> => I<hash>
2472              
2473             What are the minimum account balances.
2474              
2475             =item * B<min_net_profit_margin> => I<float> (default: 0)
2476              
2477             Minimum net profit margin that will trigger an arbitrage trading, in percentage.
2478              
2479             Below this percentage number, no order pairs will be sent to the exchanges to do
2480             the arbitrage. Note that the net profit margin already takes into account
2481             trading fees and forex spread (see Glossary section for more details and
2482             illustration).
2483              
2484             Suggestion: If you set this option too high, there might not be any order pairs
2485             possible. If you set this option too low, you will be getting too thin profits.
2486             Run C<cryp-arbit opportunities> or C<cryp-arbit arbit --dry-run> for a while to
2487             see what the average percentage is and then decide at which point you want to
2488             perform arbitrage.
2489              
2490             =item * B<quote_currencies> => I<array[fiat_or_cryptocurrency]>
2491              
2492             The currencies to exchange (buy/sell) the target currencies.
2493              
2494             You can have fiat currencies as the quote currencies, to buy/sell the target
2495             (base) currencies during arbitrage. For example, to arbitrage LTC against USD
2496             and IDR, C<base_currencies> is ['BTC'] and C<quote_currencies> is ['USD', 'IDR'].
2497              
2498             You can also arbitrage cryptocurrencies against other cryptocurrency (usually
2499             BTC, "the USD of cryptocurrencies"). For example, to arbitrage XMR and LTC
2500             against BTC, C<base_currencies> is ['XMR', 'LTC'] and C<quote_currencies> is
2501             ['BTC'].
2502              
2503             =item * B<strategy> => I<str> (default: "merge_order_book")
2504              
2505             Which strategy to use for arbitration.
2506              
2507             Strategy is implemented in a C<App::cryp::arbit::Strategy::*> perl module.
2508              
2509             =back
2510              
2511             Returns an enveloped result (an array).
2512              
2513             First element (status) is an integer containing HTTP status code
2514             (200 means OK, 4xx caller error, 5xx function error). Second element
2515             (msg) is a string containing error message, or 'OK' if status is
2516             200. Third element (payload) is optional, the actual result. Fourth
2517             element (meta) is called result metadata and is optional, a hash
2518             that contains extra information.
2519              
2520             Return value: (any)
2521              
2522             =head1 BUGS
2523              
2524             Please report all bug reports or feature requests to L<mailto:stevenharyanto@gmail.com>.
2525              
2526             =head1 SEE ALSO
2527              
2528             =head1 AUTHOR
2529              
2530             perlancar <perlancar@cpan.org>
2531              
2532             =head1 COPYRIGHT AND LICENSE
2533              
2534             This software is copyright (c) 2018 by perlancar@cpan.org.
2535              
2536             This is free software; you can redistribute it and/or modify it under
2537             the same terms as the Perl 5 programming language system itself.
2538              
2539             =cut