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