├── .gitignore ├── composer.json ├── README.md ├── phpunit.xml ├── tests ├── data │ ├── data1.json │ ├── data2.json │ └── data3.json └── InvestorTest.php ├── calculate.php └── app ├── CurlHelper.php └── Investor.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | composer.lock 3 | vendor -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pawlox/nbp-gold-investor", 3 | "type": "project", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Paweł Marciniak", 8 | "email": "pawmarci@gmail.com" 9 | } 10 | ], 11 | "require": {}, 12 | "autoload": { 13 | "psr-4": { 14 | "App\\": "app/" 15 | } 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "^8.1.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nbp best investment 2 | 3 | Simple application to calculate the best investment in gold during given time period. 4 | 5 | It depends on NBP Web Api “http://api.nbp.pl” and returns the best moment to buy and sell gold to get the highest profit. 6 | 7 | ##### Usage: 8 | 9 | `php calculate.php [money] [startDate] [endDate]` 10 | 11 | ##### Parameters: 12 | 13 | `money` - amount of money to invest, float value, 14 | `startDate` - time range start date in format Y-m-d, 15 | `endDate` - time range end date in format Y-m-d. 16 | 17 | ##### Example of usage: 18 | 19 | `php calculate.php 600000 2013-01-02 2017-08-28` 20 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./tests/ 7 | 8 | 9 | 10 | 11 | ./app 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/data/data1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "data": "2018-02-01", 4 | "cena": 160.23 5 | }, 6 | { 7 | "data": "2018-02-02", 8 | "cena": 165.35 9 | }, 10 | { 11 | "data": "2018-02-03", 12 | "cena": 176.76 13 | }, 14 | { 15 | "data": "2018-02-04", 16 | "cena": 171.90 17 | }, 18 | { 19 | "data": "2018-02-05", 20 | "cena": 161.43 21 | }, 22 | { 23 | "data": "2018-02-06", 24 | "cena": 167.77 25 | }, 26 | { 27 | "data": "2018-02-07", 28 | "cena": 159.23 29 | }, 30 | { 31 | "data": "2018-02-08", 32 | "cena": 169.99 33 | }, 34 | { 35 | "data": "2018-02-09", 36 | "cena": 173.33 37 | }, 38 | { 39 | "data": "2018-02-10", 40 | "cena": 172.22 41 | } 42 | ] -------------------------------------------------------------------------------- /tests/data/data2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "data": "2018-02-01", 4 | "cena": 167.23 5 | }, 6 | { 7 | "data": "2018-02-02", 8 | "cena": 165.35 9 | }, 10 | { 11 | "data": "2018-02-03", 12 | "cena": 176.76 13 | }, 14 | { 15 | "data": "2018-02-04", 16 | "cena": 171.90 17 | }, 18 | { 19 | "data": "2018-02-05", 20 | "cena": 161.43 21 | }, 22 | { 23 | "data": "2018-02-06", 24 | "cena": 167.77 25 | }, 26 | { 27 | "data": "2018-02-07", 28 | "cena": 159.23 29 | }, 30 | { 31 | "data": "2018-02-08", 32 | "cena": 169.99 33 | }, 34 | { 35 | "data": "2018-02-09", 36 | "cena": 173.33 37 | }, 38 | { 39 | "data": "2018-02-10", 40 | "cena": 172.22 41 | } 42 | ] -------------------------------------------------------------------------------- /tests/data/data3.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "data": "2018-02-01", 4 | "cena": 177.23 5 | }, 6 | { 7 | "data": "2018-02-02", 8 | "cena": 175.35 9 | }, 10 | { 11 | "data": "2018-02-03", 12 | "cena": 174.76 13 | }, 14 | { 15 | "data": "2018-02-04", 16 | "cena": 173.90 17 | }, 18 | { 19 | "data": "2018-02-05", 20 | "cena": 172.43 21 | }, 22 | { 23 | "data": "2018-02-06", 24 | "cena": 171.77 25 | }, 26 | { 27 | "data": "2018-02-07", 28 | "cena": 170.23 29 | }, 30 | { 31 | "data": "2018-02-08", 32 | "cena": 169.99 33 | }, 34 | { 35 | "data": "2018-02-09", 36 | "cena": 168.33 37 | }, 38 | { 39 | "data": "2018-02-10", 40 | "cena": 167.22 41 | } 42 | ] -------------------------------------------------------------------------------- /calculate.php: -------------------------------------------------------------------------------- 1 | bestGoldProfit($money, $startDate, $endDate); 16 | 17 | if ($goldInvest['profit'] > 0) { 18 | echo sprintf("Best buy price on: %s (price %.2f zl) \n", $goldInvest['buyDate'], $goldInvest['buyPrice']); 19 | echo sprintf("Best sell price on: %s (price %.2f zl) \n", $goldInvest['saleDate'], $goldInvest['salePrice']); 20 | echo sprintf("Best profit: %.2f zl (percentage profit %.2f %%) \n", $goldInvest['profit'], $goldInvest['profitPercent']); 21 | } else { 22 | echo 'In given range of time there was not possible to get any profit from gold buy/sell process.'; 23 | } 24 | -------------------------------------------------------------------------------- /app/CurlHelper.php: -------------------------------------------------------------------------------- 1 | 1, 26 | CURLOPT_URL => $url, 27 | CURLOPT_FAILONERROR => true, 28 | CURLOPT_HTTPHEADER => [ 29 | 'Accept: application/json' 30 | ] 31 | ]); 32 | 33 | $result = curl_exec($curl); 34 | 35 | if (!$result) { 36 | throw new \Exception('Error: "' . curl_error($curl) . '" - Code: ' . curl_errno($curl)); 37 | } 38 | 39 | curl_close($curl); 40 | 41 | return json_decode($result); 42 | } 43 | } -------------------------------------------------------------------------------- /tests/InvestorTest.php: -------------------------------------------------------------------------------- 1 | investor = new Investor('http://api.nbp.pl/api/cenyzlota/'); 12 | } 13 | 14 | public function testBasicGettingTheBestProfit() 15 | { 16 | $mockData = json_decode(file_get_contents(__DIR__ . '/data/data1.json')); 17 | 18 | $goldInvest = $this->investor->getBestProfit($mockData); 19 | 20 | $this->assertEquals("2018-02-01", $goldInvest['buyDate']); 21 | $this->assertEquals("2018-02-03", $goldInvest['saleDate']); 22 | $this->assertEquals(160.23, $goldInvest['buyPrice']); 23 | $this->assertEquals(176.76, $goldInvest['salePrice']); 24 | } 25 | 26 | public function testOtherGettingTheBestProfit() 27 | { 28 | $mockData = json_decode(file_get_contents(__DIR__ . '/data/data2.json')); 29 | 30 | $goldInvest = $this->investor->getBestProfit($mockData); 31 | 32 | $this->assertEquals("2018-02-07", $goldInvest['buyDate']); 33 | $this->assertEquals("2018-02-09", $goldInvest['saleDate']); 34 | $this->assertEquals(159.23, $goldInvest['buyPrice']); 35 | $this->assertEquals(173.33, $goldInvest['salePrice']); 36 | } 37 | 38 | public function testNoProfit() 39 | { 40 | $mockData = json_decode(file_get_contents(__DIR__ . '/data/data3.json')); 41 | 42 | $goldInvest = $this->investor->getBestProfit($mockData); 43 | 44 | $this->assertEquals(null, $goldInvest['buyDate']); 45 | $this->assertEquals(null, $goldInvest['saleDate']); 46 | $this->assertEquals(null, $goldInvest['buyPrice']); 47 | $this->assertEquals(null, $goldInvest['salePrice']); 48 | $this->assertEquals(0, $goldInvest['finalProfit']); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/Investor.php: -------------------------------------------------------------------------------- 1 | url = $url; 37 | } 38 | 39 | /** 40 | * Calculate optimal buy and sell prices of gold in given time period 41 | * for get the best profit 42 | * 43 | * @param float $money Invested amount of money 44 | * @param string $startDate Period start date 45 | * @param string $endDate Period end date 46 | * @return array|null 47 | */ 48 | public function bestGoldProfit(float $money, string $startDate, string $endDate) 49 | { 50 | $goldCosts = $this->getGoldCostData($startDate, $endDate); 51 | 52 | $bestProfit = $this->getBestProfit($goldCosts); 53 | 54 | return [ 55 | 'buyDate' => $bestProfit['buyDate'], 56 | 'buyPrice' => $bestProfit['buyPrice'], 57 | 'saleDate' => $bestProfit['saleDate'], 58 | 'salePrice' => $bestProfit['salePrice'], 59 | 'profitPercent' => $bestProfit['finalProfit'] * 100, 60 | 'profit' => ($money * $bestProfit['finalProfit']) - $money 61 | ]; 62 | } 63 | 64 | /** 65 | * Helper method to get gold cost data from given time range 66 | * 67 | * NBP gold cost API is limited to certain number of days per one request 68 | * so we need multimple requests to get data for period longer than that 69 | * limit. 70 | * 71 | * @param string $startDate Start date 72 | * @param string $endDate End date 73 | * @return array 74 | */ 75 | public function getGoldCostData(string $startDate, string $endDate): array 76 | { 77 | $dStart = new DateTime($startDate); 78 | $dEnd = new DateTime($endDate); 79 | 80 | $dDiff = $dStart->diff($dEnd); 81 | $data = []; 82 | 83 | try { 84 | if ($dDiff->days < self::DAYS_LIMIT) { 85 | // If requested time range is lower that the API limit 86 | $data = CurlHelper::get($this->url . $startDate . '/' . $endDate); 87 | } else { 88 | // If requested time period is larger than the API limit we must divide the given 89 | // range to smaller parts, get data for them separately and merge them in one data set 90 | $start = $end = new DateTime($startDate); 91 | 92 | while ($start->diff($dEnd)->days > 0) { 93 | $partial = CurlHelper::get($this->url . $start->format('Y-m-d') . '/' . $end->format('Y-m-d')); 94 | $data = array_merge($data, $partial); 95 | 96 | $start = clone $end; 97 | 98 | if (($start->diff($dEnd))->days > self::DAYS_LIMIT) { 99 | $end->modify('+' . self::DAYS_LIMIT . ' day'); 100 | } else { 101 | $end = new DateTime($endDate); 102 | } 103 | } 104 | } 105 | } catch (Exception $e) { 106 | echo $e->getMessage() . "\n"; 107 | } 108 | 109 | return $data; 110 | } 111 | 112 | /** 113 | * Recursive method implementing divide and conquer strategy to find the best 114 | * buy and sell points in prices dataset 115 | * 116 | * @param array $prices Dataset/array containing gold prices 117 | * @return array|mixed 118 | */ 119 | public function getBestProfit(array $prices) 120 | { 121 | $length = count($prices); 122 | 123 | // If dataset contains one or zero elements then the profit is zero 124 | if ($length <= 1) { 125 | return [ 126 | 'buyDate' => null, 127 | 'buyPrice' => null, 128 | 'saleDate' => null, 129 | 'salePrice' => null, 130 | 'finalProfit' => 0 131 | ]; 132 | } 133 | 134 | // Divide dataset to two parts 135 | $leftHalf = array_slice($prices, 0, $length / 2); 136 | $rightHalf = array_slice($prices, $length / 2); 137 | 138 | // Calculate the best profit separately for "left" and "right" part 139 | $leftBest = $this->getBestProfit($leftHalf); 140 | $rightBest = $this->getBestProfit($rightHalf); 141 | 142 | // Get the lowest price from left part and highest from right part and calculate best profit for them 143 | $leftMin = array_reduce($leftHalf, function ($prev, $next) { 144 | return ($prev === null || $next->cena < $prev->cena) ? $next : $prev; 145 | }); 146 | 147 | $rightMax = array_reduce($rightHalf, function ($prev, $next) { 148 | return ($prev === null || $next->cena > $prev->cena) ? $next : $prev; 149 | }); 150 | 151 | if ($rightMax->cena > $leftMin->cena) { 152 | $bothBest = [ 153 | 'buyDate' => $leftMin->data, 154 | 'buyPrice' => $leftMin->cena, 155 | 'saleDate' => $rightMax->data, 156 | 'salePrice' => $rightMax->cena, 157 | 'finalProfit' => $rightMax->cena / $leftMin->cena 158 | ]; 159 | } else { 160 | $bothBest = [ 161 | 'buyDate' => null, 162 | 'buyPrice' => null, 163 | 'saleDate' => null, 164 | 'salePrice' => null, 165 | 'finalProfit' => 0 166 | ]; 167 | } 168 | 169 | // Choose the best profit from three results and return it 170 | $bestProfit = array_reduce([$leftBest, $rightBest, $bothBest], function ($first, $second) { 171 | return ($first === null || $second['finalProfit'] > $first['finalProfit']) ? $second : $first; 172 | }); 173 | 174 | return $bestProfit; 175 | } 176 | } 177 | --------------------------------------------------------------------------------