Guest User

KrakenTrader.php

a guest
Feb 20th, 2014
150
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 22.74 KB | None | 0 0
  1. <?php
  2.  
  3.    
  4. class KrakenTraderException extends Exception{}  
  5.  
  6. class KrakenTrader{
  7.  
  8.     protected $kraken;
  9.     protected $set;
  10.     protected $currentRate;
  11.     protected $lastPrivateApiCallMicrotime;
  12.     protected $totalMargin;
  13.     protected $totalProfit;
  14.     protected $totalFees;
  15.    
  16.    
  17.     const BUY  = 'buy';
  18.     const SELL = 'sell';
  19.  
  20.     public function __construct($key, $secret){
  21.        
  22.         $url = 'https://api.kraken.com';
  23.         $sslverify = true;
  24.         $version = 0;
  25.  
  26.         $this->kraken = new KrakenAPI($key, $secret, $url, $version, $sslverify);
  27.        
  28.         $this->set = array();
  29.         $this->lastPrivateApiCallMicrotime = 0;
  30.         $this->totalMargin = 0;
  31.         $this->totalProfit = 0;
  32.         $this->totalFees = 0;
  33.        
  34.     }
  35.    
  36.     protected function apiLog($res){
  37.         $this->log($res, '-api');
  38.     }
  39.     protected function objectLog($res){
  40.         $this->log($res, '-object');
  41.     }
  42.    
  43.     protected function log($message, $suffix = ''){
  44.         $filename = 'kraken-trade' . $suffix . '.log';
  45.         if (!$handle = fopen(dirname(__FILE__) . DIRECTORY_SEPARATOR . $filename, 'a+')){
  46.             throw new KrakenTraderException('can\'t open file ' . dirname(__FILE__) . DIRECTORY_SEPARATOR . $filename . '. Make sure it\'s writable for PHP.');
  47.         }
  48.         if (fwrite($handle, json_encode(array('date' => date('Y-m-d H:i:s'), 'msg' => $message)) . PHP_EOL) === FALSE) {
  49.             throw new KrakenTraderException('can\'t write to file ' . dirname(__FILE__) . DIRECTORY_SEPARATOR . $filename . '. Make sure it\'s writable for PHP.');
  50.         }
  51.        
  52.         fclose($handle);
  53.        
  54.     }
  55.    
  56.    
  57.     /**
  58.     * Get the fee charged by Kraken
  59.     * @todo get current fee from API
  60.     *
  61.     * @param float $price
  62.     */
  63.     public static function getFee($price){
  64.         return $price / 100 * 0.11;
  65.     }
  66.    
  67.  
  68.     /**
  69.     * Add a buy/sell set
  70.     *
  71.     * @param float $buyRate   in Euros
  72.     * @param float $margin    percentage (1 = 1%, 10 = 10%, 0.5 = 0.5%)
  73.     * @param float $bitcoins  in BTC (0.01 minimum)
  74.     * @param string $buySell  buy|sell constant  
  75.     * @param bool $placeOrderAtAnyRate
  76.     */
  77.     public function addSet($buyRate, $margin, $bitcoins, $buySell = self::BUY, $placeOrderAtAnyRate = false){
  78.            
  79.         $trade = new KrakenTradeSet($buyRate, $margin, $bitcoins, $buySell, count($this->set)+1, $placeOrderAtAnyRate);
  80.         $this->set[] = $trade;
  81.        
  82.         $this->log(array(
  83.             'index'        => $trade->getIndex(),
  84.             'startWith'    => $trade->getBuySell(),
  85.             'margin'       => $trade->getMargin(),
  86.             'bitcoins'     => $trade->getBitcoins(),
  87.             'buyRate'      => $trade->getBuyRate(),
  88.             'sellAt'       => round($trade->getBuyRate() + ($trade->getBuyRate() / 100 * $trade->getMargin()), 5),
  89.             'atAnyRate'    => $trade->getPlaceOrderAtAnyRate()
  90.         ));
  91.     }
  92.    
  93.     /**
  94.     * Get the the price at which last trade was made
  95.     * And log changes in the current rate
  96.     *
  97.     */
  98.     public function getCurrentRate(){
  99.                
  100.         if (!$res = KrakenApiCache::get('Ticker')){
  101.             // cache for 1 second
  102.             $res = $this->QueryPublic('Ticker', array('pair' => 'XXBTZEUR'));
  103.            
  104.             $this->apiLog($res);  
  105.             KrakenApiCache::set('Ticker', $res, 1);
  106.         }
  107.        
  108.         if (!isset($res['result']['XXBTZEUR']['c'][0])){
  109.             throw new KrakenTraderException('Not a valid API response for current rate');
  110.         }
  111.         $rate = $res['result']['XXBTZEUR']['c'][0];
  112.        
  113.         if ($rate !== $this->currentRate){
  114.             // only log the rate at a change
  115.             $this->log('current rate: ' . $rate);
  116.         }
  117.         $this->currentRate = $rate;
  118.        
  119.         return $rate;
  120.     }
  121.    
  122.    
  123.    
  124.     /**
  125.     * return trade data since given id (exclusive)
  126.     *
  127.     * @param string $sinceId  
  128.     */
  129.     public function getRecentTrades($sinceId = null){
  130.                
  131.         if (!$res = KrakenApiCache::get('Trades')){
  132.             // cache for 1 second
  133.                
  134.             $options = array('pair' => 'XXBTZEUR');
  135.             if (!is_null($sinceId)){
  136.                 $options['since'] = $sinceId;
  137.             }
  138.             $res = $this->QueryPublic('Trades', $options);
  139.                
  140.             $this->apiLog($res);  
  141.             KrakenApiCache::set('Trades', $res, 1);
  142.         }
  143.                                              
  144.         if (!isset($res['result']['XXBTZEUR'])){
  145.             throw new KrakenTraderException('Not a valid API response for recent trades');
  146.         }
  147.         $trades = array();
  148.         foreach($res['result']['XXBTZEUR'] as $trade){
  149.             $trades[] = array(
  150.            
  151.                 'price'         => $trade[0],
  152.                 'volume'        => $trade[1],
  153.                 'time'          => date('Y-m-d H:i:s', $trade[2]),
  154.                 'buySell'       => $trade[3],
  155.                 'marketLimit'   => $trade[4],
  156.            
  157.             );
  158.         }
  159.        
  160.        
  161.         return $trades;
  162.        
  163.     }
  164.    
  165.     /**
  166.     * interface API with 5 tries before exit
  167.     *
  168.     * @param string $method
  169.     * @param array $request
  170.     * @return array
  171.     */
  172.     private function queryPublic($method, array $request = array()){
  173.        
  174.         static $apiErrors = 0;
  175.        
  176.         try {
  177.             $res = $this->kraken->QueryPublic($method, $request);
  178.         } catch(Exception $e){    
  179.             $apiErrors++;
  180.             $this->log('Kraken queryPublic Error #' . $apiErrors . ' on ' . $method . ': ' . print_r($e->getMessage(), true));
  181.             if($apiErrors >= 5){
  182.                 throw new KrakenTraderException('>=5 API errors queryPublic on ' . $method .  ' ' . $e->getMessage());
  183.             }
  184.             sleep(1);
  185.             // try again
  186.             return $this->queryPublic($method, $request);
  187.         }
  188.         $apiErrors = 0;
  189.        
  190.         return $res;
  191.     }
  192.    
  193.     /**
  194.     * interface API with 3 tries before exit
  195.     *
  196.     * @param string $method
  197.     * @param array $request
  198.     * @return array
  199.     */
  200.     private function queryPrivate($method, array $request = array()){
  201.        
  202.         static $apiErrors = 0;
  203.        
  204.         try {
  205.             $res = $this->kraken->queryPrivate($method, $request);
  206.         } catch(Exception $e){    
  207.             $apiErrors++;
  208.             $this->log('Kraken queryPrivate Error #' . $apiErrors . ' on ' . $method . ': ' . print_r($e->getMessage(), true));
  209.             if($apiErrors >= 3){
  210.                 throw new KrakenTraderException('>=3 API errors queryPrivate on ' . $method .  ' ' . $e->getMessage());
  211.             }
  212.             sleep(5);
  213.             // try again
  214.             return $this->queryPrivate($method, $request);
  215.         }
  216.         $apiErrors = 0;
  217.        
  218.         return $res;
  219.     }
  220.    
  221.    
  222.     public function analyseTrades($margin = 0.52, $step = 0.05){
  223.        
  224.         $trades  = $this->getRecentTrades();
  225.         $lowest  = $trades[0]['price'];
  226.         $highest = $trades[0]['price'];
  227.        
  228.         foreach($trades as $trade){
  229.             if ($trade['price'] < $lowest){
  230.                 $lowest = $trade['price'];
  231.             }
  232.             if ($trade['price'] > $highest){
  233.                 $highest = $trade['price'];
  234.             }
  235.         }
  236.         echo date('Y-m-d H:i:s') . PHP_EOL;
  237.         echo 'lowest :' . $lowest . PHP_EOL;
  238.         echo 'highest :' . $highest . PHP_EOL;
  239.         flush();
  240.         ob_flush();
  241.        
  242.         $result = array();
  243.        
  244.         for ($current = $lowest; $current < $highest - ($highest / 100 * $margin); $current += $step){
  245.             $buyAt = round($current, 3);
  246.             $sellAt =  round($current + ($current / 100 * $margin), 3);
  247.            
  248.             //echo $buyAt . ' ' . $sellAt . PHP_EOL;
  249.             flush();
  250.             ob_flush();
  251.            
  252.             $buyCounter = 0;
  253.             $sellCounter = 0;
  254.             $volumeBelowBuy  = 0;
  255.             $volumeAboveSell = 0;
  256.            
  257.             foreach($trades as $trade){
  258.                
  259.                 if ($trade['price'] <= $buyAt){
  260.                    
  261.                     $volumeBelowBuy += $trade['volume'];
  262.                    
  263.                     $buyCounter++;  
  264.                 }
  265.                
  266.                 if ($trade['price'] >= $sellAt){
  267.                    
  268.                     $volumeAboveSell += $trade['volume'];
  269.                    
  270.                     $sellCounter++;  
  271.                 }
  272.                
  273.             }
  274.            
  275.             $result[] = array(
  276.            
  277.                 'buyAt'             => $buyAt,
  278.                 'sellAt'            => $sellAt,
  279.                 'sellCounter'       => $sellCounter,
  280.                 'buyCounter'        => $buyCounter,
  281.                 'volumeBelowBuy'    => $volumeBelowBuy,
  282.                 'volumeAboveSell'   => $volumeAboveSell
  283.            
  284.             );
  285.            
  286.         }
  287.        
  288.         usort($result, array('self', 'sortTradeResult'));
  289.        
  290.         echo '<pre>';
  291.         print_r($result);
  292.         exit;
  293.        
  294.        
  295.        
  296.         return $result;
  297.        
  298.     }
  299.    
  300.     /* This is the static comparing function: */
  301.     protected static function sortTradeResult($a, $b){
  302.        
  303.         $aMax = max(array($a['sellCounter'], $a['buyCounter']));
  304.         $aMin = min(array($a['sellCounter'], $a['buyCounter']));
  305.        
  306.         $result = $aMax - $aMin;
  307.         if ($result == 0){
  308.             $result = 0.1;
  309.         }
  310.         $a = $aMax / $result;
  311.        
  312.        
  313.         $bMax = max(array($b['sellCounter'], $b['buyCounter']));
  314.         $bMin = min(array($b['sellCounter'], $b['buyCounter']));
  315.        
  316.         $result = $bMax - $bMin;
  317.         if ($result == 0){
  318.             $result = 0.1;
  319.         }
  320.         $b = $bMax / $result;
  321.        
  322.         return ($a < $b) ? 1 : -1;
  323.        
  324.        
  325.        
  326.     }
  327.    
  328.    
  329.    
  330.     /**
  331.     * Check if order is in open Orders list
  332.     * Log partial executed orders (still in open orders)
  333.     *
  334.     * @param KrakenTradeSet $trade
  335.     * @param string $txid
  336.     */
  337.     protected function inOpenOrders(KrakenTradeSet &$trade, $txid){
  338.        
  339.         if (!$res = KrakenApiCache::get('OpenOrders')){
  340.            
  341.             if (microtime(true) - $this->lastPrivateApiCallMicrotime <= 5){
  342.                 // sleep for 5 seconds
  343.                 // kraken does not allow fast orders :(
  344.                 usleep(5 * 1000000);
  345.             }
  346.             $this->lastPrivateApiCallMicrotime = microtime(true);
  347.            
  348.             // cache the output of the Kraken API for 5 seconds to prevent rate limit errors
  349.             $res = $this->QueryPrivate('OpenOrders', array('trades' => true));
  350.             $this->apiLog($res);    
  351.             KrakenApiCache::set('OpenOrders', $res, 5);
  352.         }
  353.        
  354.         if (!isset($res['result']['open'])){
  355.             throw new KrakenTraderException('Not a valid API result on OpenOrders');
  356.         }
  357.        
  358.         if (isset($res['result']['open'][$txid])){
  359.             $order = $res['result']['open'][$txid];
  360.             if ($trade->getVolumeExecuted() != $order['vol_exec'] && $order['vol_exec'] > 0 && $order['vol_exec'] < $trade->getBitcoins()){
  361.                 // log partial executed orders. Only log a change in execution
  362.                 $trade->setVolumeExecuted($order['vol_exec']);
  363.                 $this->log('#' . $trade->getIndex() . ' partial order executed ' . $order['vol_exec'] . ' of ' . $trade->getBitcoins() . ' XBTEUR @ limit ' . $order['price']);    
  364.             }
  365.         } else {
  366.             // open order is gone, reset volume
  367.             $trade->setVolumeExecuted(0);
  368.         }
  369.        
  370.        
  371.         return isset ($res['result']['open'][$txid]);
  372.        
  373.        
  374.     }
  375.    
  376.     protected function sell(KrakenTradeSet &$trade, $bitcoins, $rate){
  377.        
  378.         $trade->setSellAt($rate);
  379.         $this->transaction($trade, self::SELL, $bitcoins, $rate);    
  380.     }
  381.    
  382.     protected function buy(KrakenTradeSet &$trade, $bitcoins, $rate){
  383.        
  384.         $trade->setBuyRate($rate);
  385.         $this->transaction($trade, self::BUY, $bitcoins, $rate);
  386.     }
  387.    
  388.    
  389.     /**
  390.     * Place BUY or SELL order
  391.     *
  392.     * @param KrakenTradeSet $trade
  393.     * @param string $action  buy|sell
  394.     * @param float $bitcoins
  395.     * @param float $rate
  396.     */
  397.     protected function transaction(KrakenTradeSet &$trade, $action, $bitcoins, $rate){
  398.        
  399.         if (microtime(true) - $this->lastPrivateApiCallMicrotime <= 5){
  400.             // sleep for 5 seconds
  401.             // kraken does not allow fast orders :(
  402.             usleep(5 * 1000000);
  403.         }
  404.         $this->lastPrivateApiCallMicrotime = microtime(true);
  405.        
  406.         $bitcoins   = round($bitcoins, 10);
  407.         $rate       = round($rate, 5);
  408.         $price      = round($rate * $bitcoins, 5);
  409.                    
  410.         $this->log('#' . $trade->getIndex() . ' ' . $action . ' ' . $bitcoins . ' XBTEUR @ limit ' . $rate . ' (' . $price . ')');
  411.        
  412.         $res = $this->QueryPrivate('AddOrder', array(
  413.             'pair' =>       'XXBTZEUR',
  414.             'type' =>       $action,
  415.             'ordertype' =>  'limit',
  416.             'price' =>      $rate,
  417.             'volume' =>     $bitcoins
  418.         ));
  419.         $this->apiLog($res);
  420.        
  421.         if (!isset($res['result']['txid'][0])){
  422.             throw new KrakenTraderException('API response on ' . $action . ' is not valid. ' . print_r($res, true));    
  423.         }
  424.         $txid = $res['result']['txid'][0];
  425.        
  426.         // assign the transaction ID to this trade
  427.         $trade->setTxid($txid);
  428.         $this->log('#' . $trade->getIndex() . ' made order ' . $txid);
  429.        
  430.     }
  431.    
  432.     protected function validateSet(){
  433.        
  434.         $sellRates = array();
  435.         foreach($this->set as $trade){
  436.             $sellRates[] = $trade->getBuyRate() + ($trade->getBuyRate() / 100 * $trade->getMargin());
  437.         }
  438.        
  439.         foreach($sellRates as $sellRate){
  440.             foreach($this->set as $trade){
  441.                 if ($sellRate <= $trade->getBuyRate()){
  442.                     throw new KrakenTraderException('buyRate + margin is lower then buyrate of another set. You would sell to yourself.');
  443.                 }
  444.             }
  445.         }
  446.        
  447.     }
  448.    
  449.     /**
  450.     * Start trading
  451.     *
  452.     * @param int $timeout exit after X seconds
  453.     */
  454.     public function start($timeout = null){
  455.        
  456.         if (count($this->set) == 0){
  457.             throw new KrakenTraderException('No sets available');
  458.         }
  459.         $this->validateSet();
  460.        
  461.         $start = time();
  462.        
  463.         while(true){
  464.            
  465.             if (!is_null($timeout) && time() > $start + $timeout){
  466.                 exit('Killed because of timeout (' . $timeout . ' seconds)');
  467.             }
  468.            
  469.             $currentRate = $this->getCurrentRate();
  470.            
  471.             foreach($this->set as $index => &$trade){
  472.                 // foreach by reference
  473.                  
  474.                 if (!$trade->isProcessing()){
  475.                     // don't do anything with trades that are currently being processed
  476.                                                      
  477.                     if ($trade->getPlaceOrderAtAnyRate() !== true && $trade->getBuySell() == self::BUY && $currentRate <= $trade->getBuyRate()){    
  478.                         // diff between current rate and buy rate
  479.                         $diff = abs($trade->getBuyRate() - $currentRate);
  480.                         // add 15% of the diff to the price
  481.                         $this->buy($trade, $trade->getBitcoins(), ($currentRate + ($diff * 0.15)));    
  482.                        
  483.                         // set a flag
  484.                         $trade->setProcessing(true);
  485.                     }
  486.                    
  487.                     if ($trade->getPlaceOrderAtAnyRate() !== true && $trade->getBuySell() == self::SELL && $currentRate >= ($trade->getBuyRate() + ($trade->getBuyRate() / 100 * $trade->getMargin()))){
  488.                         // diff between current rate and sell rate
  489.                         $diff = abs($currentRate - ($trade->getBuyRate() + ($trade->getBuyRate() / 100 * $trade->getMargin())));
  490.                         // lower the price with 15% of the diff
  491.                         $this->sell($trade, $trade->getBitcoins(), ($currentRate - ($diff * 0.15)));  
  492.                        
  493.                         // set a flag
  494.                         $trade->setProcessing(true);  
  495.                          
  496.                     }
  497.                    
  498.                     // Any rate buying / selling
  499.                     if ($trade->getPlaceOrderAtAnyRate() === true){
  500.                        
  501.                         if ($trade->getBuySell() == self::BUY){
  502.                             $this->buy($trade, $trade->getBitcoins(), $trade->getBuyRate());  
  503.                             // set a flag
  504.                             $trade->setProcessing(true);  
  505.                         }
  506.                        
  507.                         if ($trade->getBuySell() == self::SELL){
  508.                             $this->sell($trade, $trade->getBitcoins(), ($trade->getBuyRate() + ($trade->getBuyRate() / 100 * $trade->getMargin())));  
  509.                             // set a flag
  510.                             $trade->setProcessing(true);  
  511.                         }
  512.                     }
  513.                    
  514.                 } else {
  515.                    
  516.                     // check if these orders are fullfilled
  517.                     if ($trade->getTxid() !== null){
  518.                         // if a transaction ID is set, the transaction must be made
  519.                         // check if it's still in the open Orders
  520.                         if(!$this->inOpenOrders($trade, $trade->getTxid())){
  521.                             // it's complete.
  522.                            
  523.                             if ($trade->getBuySell() == self::SELL){
  524.                                 $logtext = 'sold';    
  525.                                 $atLimit = $trade->getSellAt();
  526.                             } else {
  527.                                 $logtext = 'bought';
  528.                                 $atLimit = $trade->getBuyRate();
  529.                             }
  530.                            
  531.                             $price      = round($atLimit * $trade->getBitcoins(), 5);
  532.                            
  533.                             $this->log('#' . $trade->getIndex() . ' ' . $logtext . ' ' . $trade->getBitcoins() . ' XBTEUR @ limit ' . round($atLimit, 5) . ' (' . $price . ')');
  534.                            
  535.                            
  536.                             // logging
  537.                             if ($trade->getBuySell() == self::SELL){
  538.                                
  539.                                 $trade->addSell();                                
  540.  
  541.                                 if ($trade->getNumberOfSells() > 0 && $trade->getNumberOfBuys() > 0){
  542.                                     // calculate profit
  543.                                     $feeOnBuy  = $this->getFee($trade->getBuyRate() * $trade->getBitcoins());
  544.                                     $feeOnSell = $this->getFee($trade->getSellAt() * $trade->getBitcoins());
  545.                                     $totalFee  = $feeOnBuy + $feeOnSell;
  546.                                     $margin    = ($trade->getSellAt() * $trade->getBitcoins()) - ($trade->getBuyRate() * $trade->getBitcoins());
  547.                                     $profit    = $margin - $totalFee;
  548.                                    
  549.                                     $this->log(array(
  550.                                         'index'     => $trade->getIndex(),
  551.                                         'sells'     => $trade->getNumberOfSells(),
  552.                                         'buys'      => $trade->getNumberOfBuys(),
  553.                                         'margin'    => round($margin, 5),
  554.                                         'fee'       => round($totalFee, 5),
  555.                                         'profit'    => round($profit, 5)
  556.                                     ));  
  557.                                    
  558.                                     // log profit totals
  559.                                     $this->totalMargin += $margin;
  560.                                     $this->totalFees   += $totalFee;
  561.                                     $this->totalProfit += $profit;
  562.                                    
  563.                                     $this->log(array(
  564.                                         'totalMargin'    => round($this->totalMargin, 5),
  565.                                         'totalFee'       => round($this->totalFees, 5),
  566.                                         'totalProfit'    => round($this->totalProfit, 5)
  567.                                     ));  
  568.                                    
  569.                                 }
  570.                                
  571.                                 // calculate new buy rate
  572.                                 $newRate = $trade->getSellAt() / (1 + ($trade->getMargin() / 100));
  573.                                 $trade->setBuyRate(round($newRate, 5));
  574.                                 $this->log('#' . $trade->getIndex() . ' set new buy rate @ ' . $trade->getBuyRate());  
  575.                                
  576.                             } else {
  577.                                 $trade->addBuy();
  578.                                 // log the new sell rate
  579.                                 $this->log('#' . $trade->getIndex() . ' set new sell rate @ ' . round(($trade->getBuyRate() + ($trade->getBuyRate() / 100 * $trade->getMargin())), 5));
  580.                             }
  581.                            
  582.                             // remove the flag
  583.                             $trade->setTxid(null);
  584.                             $trade->setProcessing(false);
  585.                            
  586.                             // reverse buySell
  587.                             $buySell = ($trade->getBuySell() == self::SELL) ? self::BUY : self::SELL;
  588.                             $trade->setBuySell($buySell);                              
  589.                            
  590.                         }
  591.                        
  592.                     }
  593.                    
  594.                    
  595.                 }
  596.                
  597.                 // sleep for 1 second in the total set    
  598.                 usleep(1 / count($this->set) * 1000000);
  599.                 $this->objectLog(print_r($trade, true));
  600.             }
  601.             // remove reference
  602.             unset($trade);
  603.            
  604.             // sleep for 1 second
  605.             usleep(1 * 1000000);
  606.         }
  607.        
  608.        
  609.        
  610.        
  611.     }
  612.  
  613. }
Advertisement
Add Comment
Please, Sign In to add comment