├── .gitignore ├── src ├── TIException.php ├── TICommission.php ├── TISiteEnum.php ├── TIAccount.php ├── TIIntervalEnum.php ├── TIOperationTrade.php ├── TIPortfolioCurrency.php ├── TICurrencyEnum.php ├── TIInstrument.php ├── TICandle.php ├── TIInstrumentInfo.php ├── TIPortfolio.php ├── TIOrder.php ├── TICandleIntervalEnum.php ├── TIResponse.php ├── TIOperation.php ├── TIPortfolioInstrument.php ├── TIOperationEnum.php ├── TIOrderBook.php └── TIClient.php ├── .github └── workflows │ └── php.yml ├── composer.json ├── LICENSE ├── tests └── TIClientTest.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | nbproject 2 | 3 | ### Composer ### 4 | composer.phar 5 | vendor/ 6 | .idea/ -------------------------------------------------------------------------------- /src/TIException.php: -------------------------------------------------------------------------------- 1 | currency = $currency; 17 | $this->value = $value; 18 | } 19 | 20 | public function getCurrency() 21 | { 22 | return $this->currency; 23 | } 24 | 25 | public function getValue() 26 | { 27 | return $this->value; 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /src/TISiteEnum.php: -------------------------------------------------------------------------------- 1 | brokerAccountType = $type; 17 | $this->brokerAccountId = $id; 18 | } 19 | 20 | function getBrokerAccountType(){ 21 | return $this->brokerAccountType; 22 | } 23 | 24 | function getBrokerAccountId(){ 25 | return $this->brokerAccountId; 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push,pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | 13 | - name: Validate composer.json and composer.lock 14 | run: composer validate 15 | 16 | - name: Install dependencies 17 | run: composer install --prefer-dist --no-progress --no-suggest 18 | 19 | # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit" 20 | # Docs: https://getcomposer.org/doc/articles/scripts.md 21 | 22 | - name: Run unit tests 23 | run: ./vendor/bin/phpunit --bootstrap vendor/autoload.php tests/* 24 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "james.rus52/tinkoffinvest", 3 | "description": "PHP client Tinkoff Invest", 4 | "keywords": [ 5 | "php", 6 | "rest", 7 | "client", 8 | "tinkoff", 9 | "invest" 10 | ], 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Anton V. Davydov", 15 | "email": "james.rus52@gmail.com", 16 | "homepage": "http://www.jamesr52.ru" 17 | } 18 | ], 19 | "autoload": { 20 | "psr-4": { 21 | "jamesRUS52\\TinkoffInvest\\": "src" 22 | } 23 | }, 24 | "require": { 25 | "php": ">=5.6", 26 | "james.rus52/websocket": "^1.3", 27 | "ext-json": "*", 28 | "ext-curl": "*" 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "jamesRUS52\\TinkoffInvest\\Tests\\": "tests/" 33 | } 34 | }, 35 | "require-dev": { 36 | "phpunit/phpunit": "^9.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/TIIntervalEnum.php: -------------------------------------------------------------------------------- 1 | tradeId = $tradeId; 18 | $this->date = $date; 19 | $this->price = $price; 20 | $this->quantity = $quantity; 21 | } 22 | 23 | /** 24 | * @return mixed 25 | */ 26 | public function getTradeId() 27 | { 28 | return $this->tradeId; 29 | } 30 | 31 | /** 32 | * @return mixed 33 | */ 34 | public function getDate() 35 | { 36 | return $this->date; 37 | } 38 | 39 | /** 40 | * @return mixed 41 | */ 42 | public function getPrice() 43 | { 44 | return $this->price; 45 | } 46 | 47 | /** 48 | * @return mixed 49 | */ 50 | public function getQuantity() 51 | { 52 | return $this->quantity; 53 | } 54 | } -------------------------------------------------------------------------------- /src/TIPortfolioCurrency.php: -------------------------------------------------------------------------------- 1 | balance = $balance; 24 | $this->currency = $currency; 25 | $this->blocked = $blocked; 26 | } 27 | 28 | function getBalance() { 29 | return $this->balance; 30 | } 31 | 32 | function getCurrency() { 33 | return $this->currency; 34 | } 35 | 36 | /** 37 | * @return mixed 38 | */ 39 | public function getBlocked() 40 | { 41 | return $this->blocked; 42 | } 43 | 44 | 45 | 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 jamesRUS52 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/TICurrencyEnum.php: -------------------------------------------------------------------------------- 1 | figi = $figi; 29 | $this->currency = $currency; 30 | $this->ticker = $ticker; 31 | $this->isin = $isin; 32 | $this->minPriceIncrement = $minPriceIncrement; 33 | $this->lot = $lot; 34 | $this->name = $name; 35 | $this->type = $type; 36 | } 37 | 38 | function getFigi() { 39 | return $this->figi; 40 | } 41 | 42 | function getTicker() { 43 | return $this->ticker; 44 | } 45 | 46 | function getIsin() { 47 | return $this->isin; 48 | } 49 | 50 | function getMinPriceIncrement() { 51 | return $this->minPriceIncrement; 52 | } 53 | 54 | function getLot() { 55 | return $this->lot; 56 | } 57 | 58 | function getCurrency() { 59 | return $this->currency; 60 | } 61 | 62 | function getName() { 63 | return $this->name; 64 | } 65 | 66 | function getType() { 67 | return $this->type; 68 | } 69 | 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/TICandle.php: -------------------------------------------------------------------------------- 1 | open = $open; 37 | $this->close = $close; 38 | $this->high = $high; 39 | $this->low = $low; 40 | $this->volume = $volume; 41 | $this->time = $time; 42 | $this->interval = $interval; 43 | $this->figi = $figi; 44 | } 45 | 46 | function getOpen() { 47 | return $this->open; 48 | } 49 | 50 | function getClose() { 51 | return $this->close; 52 | } 53 | 54 | function getHigh() { 55 | return $this->high; 56 | } 57 | 58 | function getLow() { 59 | return $this->low; 60 | } 61 | 62 | function getVolume() { 63 | return $this->volume; 64 | } 65 | 66 | function getTime() { 67 | return $this->time; 68 | } 69 | 70 | function getInterval() { 71 | return $this->interval; 72 | } 73 | 74 | function getFigi() { 75 | return $this->figi; 76 | } 77 | 78 | 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/TIInstrumentInfo.php: -------------------------------------------------------------------------------- 1 | trade_status = $trade_status; 28 | $this->min_price_increment = $min_price_increment; 29 | $this->lot = $lot; 30 | $this->figi = $figi; 31 | } 32 | 33 | function getTrade_status() { 34 | return $this->trade_status; 35 | } 36 | 37 | function getMin_price_increment() { 38 | return $this->min_price_increment; 39 | } 40 | 41 | function getLot() { 42 | return $this->lot; 43 | } 44 | 45 | function getAccrued_interest() { 46 | return $this->accrued_interest; 47 | } 48 | 49 | function getLimit_up() { 50 | return $this->limit_up; 51 | } 52 | 53 | function getLimit_down() { 54 | return $this->limit_down; 55 | } 56 | 57 | function getFigi() { 58 | return $this->figi; 59 | } 60 | 61 | function setAccrued_interest($accrued_interest) { 62 | $this->accrued_interest = $accrued_interest; 63 | } 64 | 65 | function setLimit_up($limit_up) { 66 | $this->limit_up = $limit_up; 67 | } 68 | 69 | function setLimit_down($limit_down) { 70 | $this->limit_down = $limit_down; 71 | } 72 | 73 | 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/TIPortfolio.php: -------------------------------------------------------------------------------- 1 | currencies = $currencies; 31 | $this->instruments = $instruments; 32 | } 33 | 34 | /** 35 | * Get balance of currency 36 | * @param TICurrencyEnum $currency 37 | * @return double or false 38 | */ 39 | public function getCurrencyBalance($currency) 40 | { 41 | foreach ($this->currencies as $curr) 42 | { 43 | if ($currency === $curr->getCurrency()) 44 | return $curr->getBalance(); 45 | } 46 | return false; 47 | } 48 | 49 | /** 50 | * Get Lots count of ticker 51 | * @param string $ticker 52 | * @return integer or false 53 | */ 54 | public function getInstrumentLots($ticker) 55 | { 56 | foreach ($this->instruments as $instr) 57 | { 58 | if ($ticker === $instr->getTicker()) 59 | return $instr->getLots(); 60 | } 61 | return false; 62 | } 63 | 64 | /** 65 | * Get all currencies in portfolio 66 | * @return TIPortfolioCurrency[] 67 | */ 68 | public function getAllCurrencies() 69 | { 70 | return $this->currencies; 71 | } 72 | 73 | /** 74 | * Get all instruments in portfolio 75 | * @return TIPortfolioInstrument[] 76 | */ 77 | public function getAllinstruments() 78 | { 79 | return $this->instruments; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/TIOrder.php: -------------------------------------------------------------------------------- 1 | orderId = $orderId; 33 | $this->operation = $operation; 34 | $this->status = $status; 35 | $this->rejectReason = $rejectReason; 36 | $this->requestedLots = $requestedLots; 37 | $this->executedLots = $executedLots; 38 | $this->commission = $commission; 39 | $this->figi = $figi; 40 | $this->type = $type; 41 | $this->message = $message; 42 | $this->price = $price; 43 | } 44 | 45 | function getOrderId() { 46 | return $this->orderId; 47 | } 48 | 49 | function getOperation() { 50 | return $this->operation; 51 | } 52 | 53 | function getStatus() { 54 | return $this->status; 55 | } 56 | 57 | function getRejectReason() { 58 | return $this->rejectReason; 59 | } 60 | 61 | function getRequestedLots() { 62 | return $this->requestedLots; 63 | } 64 | 65 | function getExecutedLots() { 66 | return $this->executedLots; 67 | } 68 | 69 | /** 70 | * @return TICommission 71 | */ 72 | function getCommission() { 73 | return $this->commission; 74 | } 75 | 76 | function getFigi() { 77 | return $this->figi; 78 | } 79 | 80 | function getType() { 81 | return $this->type; 82 | } 83 | 84 | function getMessage(){ 85 | return $this->message; 86 | } 87 | 88 | function getPrice() { 89 | return $this->price; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/TICandleIntervalEnum.php: -------------------------------------------------------------------------------- 1 | trackingId) && isset($result->payload) && isset($result->status)) { 42 | $this->payload = $result->payload; 43 | $this->trackingId = $result->trackingId; 44 | $this->status = $result->status; 45 | } else { 46 | throw new TIException('Required fields are empty'); 47 | } 48 | if ($this->status == 'Error') { 49 | throw new TIException($this->payload->message . ' [' . $this->payload->code . ']'); 50 | } 51 | } 52 | catch (TIException $e) { 53 | throw $e; 54 | } 55 | catch (\Exception $e) { 56 | switch ($curlStatusCode) { 57 | case 401: 58 | $error_message = "Authorization error"; 59 | break; 60 | case 429: 61 | $error_message = "Too Many Requests"; 62 | break; 63 | default: 64 | $error_message = "Unknown error: ".$e->getMessage()."; Http code: ".$curlStatusCode."; Code: ".$e->getCode(); 65 | break; 66 | } 67 | throw new TIException($error_message); 68 | } 69 | } 70 | 71 | 72 | /** 73 | * @return stdClass 74 | */ 75 | public function getPayload() 76 | { 77 | return $this->payload; 78 | } 79 | 80 | /** 81 | * @return string 82 | */ 83 | public function getStatus() 84 | { 85 | return $this->status; 86 | } 87 | 88 | } -------------------------------------------------------------------------------- /src/TIOperation.php: -------------------------------------------------------------------------------- 1 | id = $id; 35 | $this->status = $status; 36 | $this->trades = $trades; 37 | $this->commission = $commission; 38 | $this->currency = $currency; 39 | $this->payment = $payment; 40 | $this->price = $price; 41 | $this->quantity = $quantity; 42 | $this->figi = $figi; 43 | $this->instrumentType = $instrumentType; 44 | $this->isMarginCall = $isMarginCall; 45 | $this->date = $date; 46 | $this->operationType = $operationType; 47 | } 48 | 49 | function getId() { 50 | return $this->id; 51 | } 52 | 53 | function getStatus() { 54 | return $this->status; 55 | } 56 | 57 | function getTrades() { 58 | return $this->trades; 59 | } 60 | 61 | function getCommission() { 62 | return $this->commission; 63 | } 64 | 65 | function getCurrency() { 66 | return $this->currency; 67 | } 68 | 69 | function getPayment() { 70 | return $this->payment; 71 | } 72 | 73 | function getPrice() { 74 | return $this->price; 75 | } 76 | 77 | function getQuantity() { 78 | return $this->quantity; 79 | } 80 | 81 | function getFigi() { 82 | return $this->figi; 83 | } 84 | 85 | function getInstrumentType() { 86 | return $this->instrumentType; 87 | } 88 | 89 | function getIsMarginCall() { 90 | return $this->isMarginCall; 91 | } 92 | 93 | function getDate() { 94 | return $this->date; 95 | } 96 | 97 | function getOperationType() { 98 | return $this->operationType; 99 | } 100 | 101 | 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/TIPortfolioInstrument.php: -------------------------------------------------------------------------------- 1 | figi = $figi; 33 | $this->ticker = $ticker; 34 | $this->isin = $isin; 35 | $this->instrumentType = $instrumentType; 36 | $this->balance = $balance; 37 | $this->blocked = $blocked; 38 | $this->lots = $lots; 39 | $this->expectedYieldValue = $expectedYieldValue; 40 | $this->expectedYieldCurrency = $expectedYieldCurrency; 41 | $this->name = $name; 42 | $this->averagePositionPrice = $averagePositionPrice; 43 | $this->averagePositionPriceNoNkd = $averagePositionPriceNoNkd; 44 | } 45 | 46 | function getFigi() { 47 | return $this->figi; 48 | } 49 | 50 | function getTicker() { 51 | return $this->ticker; 52 | } 53 | 54 | function getIsin() { 55 | return $this->isin; 56 | } 57 | 58 | function getInstrumentType() { 59 | return $this->instrumentType; 60 | } 61 | 62 | function getBalance() { 63 | return $this->balance; 64 | } 65 | 66 | /** 67 | * @return mixed 68 | */ 69 | public function getBlocked() 70 | { 71 | return $this->blocked; 72 | } 73 | 74 | function getLots() { 75 | return $this->lots; 76 | } 77 | 78 | function getExpectedYieldValue() { 79 | return $this->expectedYieldValue; 80 | } 81 | 82 | function getExpectedYieldCurrency() { 83 | return $this->expectedYieldCurrency; 84 | } 85 | 86 | /** 87 | * @return mixed 88 | */ 89 | public function getName() 90 | { 91 | return $this->name; 92 | } 93 | 94 | /** 95 | * @return mixed 96 | */ 97 | public function getAveragePositionPrice() 98 | { 99 | return $this->averagePositionPrice; 100 | } 101 | 102 | /** 103 | * @return mixed 104 | */ 105 | public function getAveragePositionPriceNoNkd() 106 | { 107 | return $this->averagePositionPriceNoNkd; 108 | } 109 | 110 | 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/TIOperationEnum.php: -------------------------------------------------------------------------------- 1 | depth = $depth; 63 | $this->asks = $asks; 64 | $this->bids = $bids; 65 | $this->figi = $figi; 66 | $this->tradeStatus = $tradeStatus; 67 | $this->minPriceIncrement = $minPriceIncrement; 68 | $this->faceValue = $faceValue; 69 | $this->lastPrice = $lastPrice; 70 | $this->closePrice = $closePrice; 71 | $this->limitUp = $limitUp; 72 | $this->limitDown = $limitDown; 73 | } 74 | 75 | function getDepth() 76 | { 77 | return $this->depth; 78 | } 79 | 80 | function getBestPricesToBuy() 81 | { 82 | return $this->asks; 83 | } 84 | 85 | function getBestPricesToSell() 86 | { 87 | return $this->bids; 88 | } 89 | 90 | function getFigi() 91 | { 92 | return $this->figi; 93 | } 94 | 95 | function getBestPriceToBuy() 96 | { 97 | return (count($this->asks) > 0) ? $this->asks[0]->price : null; 98 | } 99 | 100 | function getBestPriceToBuyLotCount() 101 | { 102 | return (count($this->asks) > 0) ? $this->asks[0]->quantity : null; 103 | } 104 | 105 | function getBestPriceToSell() 106 | { 107 | return (count($this->bids) > 0) ? $this->bids[0]->price : null; 108 | } 109 | 110 | function getBestPriceToSellLotCount() 111 | { 112 | return (count($this->bids) > 0) ? $this->bids[0]->quantity : null; 113 | } 114 | 115 | /** 116 | * @return string 117 | */ 118 | public function getTradeStatus() 119 | { 120 | return $this->tradeStatus; 121 | } 122 | 123 | /** 124 | * @return int 125 | */ 126 | public function getMinPriceIncrement() 127 | { 128 | return $this->minPriceIncrement; 129 | } 130 | 131 | /** 132 | * @return int 133 | */ 134 | public function getFaceValue() 135 | { 136 | return $this->faceValue; 137 | } 138 | 139 | /** 140 | * @return int 141 | */ 142 | public function getLastPrice() 143 | { 144 | return $this->lastPrice; 145 | } 146 | 147 | /** 148 | * @return int 149 | */ 150 | public function getClosePrice() 151 | { 152 | return $this->closePrice; 153 | } 154 | 155 | /** 156 | * @return int 157 | */ 158 | public function getLimitUp() 159 | { 160 | return $this->limitUp; 161 | } 162 | 163 | /** 164 | * @return int 165 | */ 166 | public function getLimitDown() 167 | { 168 | return $this->limitDown; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /tests/TIClientTest.php: -------------------------------------------------------------------------------- 1 | fixture = new TIClient("t.RJiVzRcOKf5h0eUJBDsy9aggjgX3FGU82O-4j_Cu2qpM-_yPYwjJMsBDQWObCagCKFNwCvFl-iDlFtBK9KwK_w",TISiteEnum::SANDBOX); 37 | $this->fixture->setIgnoreSslPeerVerification(true); 38 | } 39 | 40 | protected function tearDown(): void 41 | { 42 | $this->fixture->sbClear(); 43 | $this->fixture = null; 44 | } 45 | 46 | 47 | public function testsbRegister() 48 | { 49 | $account = $this->fixture->sbRegister(); 50 | $this->assertInstanceOf(TIAccount::class, $account); 51 | } 52 | 53 | public function testsbClear() 54 | { 55 | $status = $this->fixture->sbClear(); 56 | $this->assertEquals("Ok", $status); 57 | } 58 | 59 | public function testsbPositionBalance() 60 | { 61 | $status = $this->fixture->sbPositionBalance(100, "BBG004730N88"); 62 | $this->assertEquals("Ok", $status); 63 | } 64 | 65 | public function testsbCurrencyBalance() 66 | { 67 | $status = $this->fixture->sbCurrencyBalance(5000000, TICurrencyEnum::RUB); 68 | $this->assertEquals("Ok", $status); 69 | } 70 | 71 | public function testgetStocks() 72 | { 73 | $stocks = $this->fixture->getStocks(); 74 | $this->assertGreaterThan(1, count($stocks)); 75 | $this->assertInstanceOf(TIInstrument::class, $stocks[0]); 76 | 77 | $stock = $this->fixture->getStocks(["SBER"]); 78 | $this->assertCount(1, $stock); 79 | } 80 | 81 | public function testgetBonds() 82 | { 83 | $bonds = $this->fixture->getBonds(); 84 | $this->assertGreaterThan(1, count($bonds)); 85 | $this->assertInstanceOf(TIInstrument::class, $bonds[0]); 86 | 87 | $bond = $this->fixture->getBonds(["SU26227RMFS7"]); 88 | $this->assertCount(1, $bond); 89 | } 90 | 91 | public function testgetEtfs() 92 | { 93 | $etfs = $this->fixture->getEtfs(); 94 | $this->assertGreaterThan(1, count($etfs)); 95 | $this->assertInstanceOf(TIInstrument::class, $etfs[0]); 96 | 97 | $etf = $this->fixture->getEtfs(["FXTB"]); 98 | $this->assertCount(1, $etf); 99 | } 100 | 101 | public function testgetCurrencies() 102 | { 103 | $etfs = $this->fixture->getCurrencies(); 104 | $this->assertContainsOnlyInstancesOf(TIInstrument::class,$etfs); 105 | 106 | $etf = $this->fixture->getCurrencies(["EUR_RUB__TOM"]); 107 | $this->assertCount(1, $etf); 108 | } 109 | 110 | public function testgetInstrumentByTicker() 111 | { 112 | $instrument = $this->fixture->getInstrumentByTicker("SBER"); 113 | $this->assertInstanceOf(TIInstrument::class, $instrument); 114 | } 115 | 116 | public function testgetInstrumentByFigi() 117 | { 118 | $instrument = $this->fixture->getInstrumentByFigi("BBG004730N88"); 119 | $this->assertInstanceOf(TIInstrument::class, $instrument); 120 | } 121 | 122 | public function testgetPortfolio() 123 | { 124 | $portfolio = $this->fixture->getPortfolio(); 125 | $this->assertInstanceOf(TIPortfolio::class, $portfolio); 126 | } 127 | 128 | public function testsendOrder() 129 | { 130 | $this->fixture->sbCurrencyBalance(5000000, TICurrencyEnum::RUB); 131 | 132 | $order = $this->fixture->sendOrder("BBG004RVFCY3", 11, TIOperationEnum::BUY, 100); 133 | $this->assertInstanceOf(TIOrder::class, $order); 134 | $this->assertEquals("Fill", $order->getStatus()); 135 | 136 | $portfolio = $this->fixture->getPortfolio(); 137 | $lots = $portfolio->getInstrumentLots($this->fixture->getInstrumentByFigi("BBG004RVFCY3")->getTicker()); 138 | $this->assertEquals(11, $lots); 139 | 140 | /* we can't check this in sandbox 141 | $orders = $this->fixture->getOrders([$order->getOrderId()]); 142 | $this->assertCount(1, $orders); 143 | 144 | $statusCancel = $this->fixture->cancelOrder($order->getOrderId()); 145 | $this->assertEquals("Ok", $statusCancel); 146 | 147 | $orders = $this->fixture->getOrders([$order->getOrderId()]); 148 | $this->assertCount(0, $orders); 149 | */ 150 | } 151 | 152 | public function testgetOperations() 153 | { 154 | $from = new \DateTime('-3 day'); 155 | $to = new \DateTime(); 156 | $operations = $this->fixture->getOperations($from, $to,'BBG004RVFCY3'); 157 | $this->assertGreaterThan(1, $operations); 158 | 159 | } 160 | 161 | public function testgetBestPriceToBuy() 162 | { 163 | $ordBook = $this->fixture->getHistoryOrderBook("BBG004RVFCY3"); 164 | $this->assertIsNumeric($ordBook->getBestPriceToBuy()); 165 | $this->assertIsInt($ordBook->getBestPriceToBuyLotCount()); 166 | } 167 | 168 | public function testgetAccounts() 169 | { 170 | $accounts = $this->fixture->getAccounts(); 171 | $this->assertGreaterThan(1,$accounts); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP client for Tinkoff invest API (PHP клиент для API Тинькофф инвестиции) 2 | 3 | ![](https://github.com/jamesRUS52/tinkoff-invest/workflows/Tests/badge.svg) 4 | 5 | ## How to install 6 | ``` 7 | composer require james.rus52/tinkoffinvest 8 | ``` 9 | or 10 | add to your compose.json 11 | ```json 12 | { 13 | "require": { 14 | "james.rus52/tinkoffinvest": "^1.*" 15 | } 16 | } 17 | ``` 18 | and then 19 | ``` 20 | composer install 21 | ``` 22 | ## How to use 23 | Include classes via autoloader 24 | ```php 25 | require_once 'vendor/autoload.php'; 26 | 27 | use \jamesRUS52\TinkoffInvest\TIClient; 28 | use \jamesRUS52\TinkoffInvest\TISiteEnum; 29 | use \jamesRUS52\TinkoffInvest\TICurrencyEnum; 30 | use \jamesRUS52\TinkoffInvest\TIInstrument; 31 | use \jamesRUS52\TinkoffInvest\TIPortfolio; 32 | use \jamesRUS52\TinkoffInvest\TIOperationEnum; 33 | use \jamesRUS52\TinkoffInvest\TIIntervalEnum; 34 | use \jamesRUS52\TinkoffInvest\TICandleIntervalEnum; 35 | use \jamesRUS52\TinkoffInvest\TICandle; 36 | use \jamesRUS52\TinkoffInvest\TIOrderBook; 37 | use \jamesRUS52\TinkoffInvest\TIInstrumentInfo; 38 | 39 | ``` 40 | create token to use tinkoff invest on [Tinkoff invest setting page](https://www.tinkoff.ru/invest/settings/) 41 | 42 | Create client instance for sandbox 43 | ```php 44 | $client = new TIClient("TOKEN",TISiteEnum::SANDBOX); 45 | ``` 46 | or real exchange 47 | ```php 48 | $client = new TIClient("TOKEN",TISiteEnum::EXCHANGE); 49 | ``` 50 | Put money to your sandbox account (sandbox only) 51 | ```php 52 | $client->sbCurrencyBalance(500,TICurrencyEnum::USD); 53 | ``` 54 | Client register on sandbox (sandbox only) 55 | ```php 56 | $client->sbRegister(); 57 | ``` 58 | Client remove account on sandbox (sandbox only) 59 | ```php 60 | $client->sbRemove(); 61 | ``` 62 | Put stocks to your sandbox account (sandbox only) 63 | ```php 64 | $client->sbPositionBalance(10.4,"BBG000BR37X2"); 65 | ``` 66 | Clear all positions on sandbox (sandbox only) 67 | ```php 68 | $client->sbClear(); 69 | ``` 70 | Get all stocks/bonds/etfs/currencies from market 71 | ```php 72 | $stockes = $client->getStocks(); 73 | $instr = $client->getBonds(); 74 | $instr = $client->getEtfs(); 75 | $instr = $client->getCurrencies(); 76 | ``` 77 | or with filter 78 | ```php 79 | $stockes = $client->getStocks(["V","LKOH"]); 80 | $instr = $client->getBonds(["RU000A0JX3X7"]); 81 | $instr = $client->getEtfs(["FXRU"]); 82 | $instr = $client->getCurrencies(["USD000UTSTOM"]); 83 | ``` 84 | Get instrument by ticker 85 | ```php 86 | $instr = $client->getInstrumentByTicker("AMZN"); 87 | ``` 88 | or by figi 89 | ```php 90 | $instr = $client->getInstrumentByFigi("BBG000BR37X2"); 91 | ``` 92 | 93 | Get history OrderBook 94 | ```php 95 | $book = $client->getHistoryOrderBook("BBG000BR37X2", 1); 96 | ``` 97 | 98 | Get historical Candles 99 | ```php 100 | $from = new \DateTime(); 101 | $from->sub(new \DateInterval("P7D")); 102 | $to = new \DateTime(); 103 | $candles = $client->getHistoryCandles("BBG000BR37X2", $from, $to, TIIntervalEnum::MIN15); 104 | ``` 105 | 106 | Get accounts 107 | ```php 108 | $accounts = $client->getAccounts(); 109 | ``` 110 | 111 | Get portfolio (if null, used default Tinkoff account) 112 | ```php 113 | $port = $client->getPortfolio(TIAccount $account = null); 114 | ``` 115 | Get portfolio balance 116 | ```php 117 | print $port->getCurrencyBalance(TICurrencyEnum::RUB); 118 | ``` 119 | Get instrument lots count 120 | ```php 121 | print $port->getinstrumentLots("PGR"); 122 | ``` 123 | Send limit order (default brokerAccountId = Tinkoff) 124 | ```php 125 | $order = $client->sendOrder("BBG000BVPV84", 1, TIOperationEnum::BUY, 1.2); 126 | print $order->getOrderId(); 127 | ``` 128 | Send market order (default brokerAccountId = Tinkoff) 129 | ```php 130 | $order = $client->sendOrder("BBG000BVPV84", 1, TIOperationEnum::BUY); 131 | print $order->getOrderId(); 132 | ``` 133 | 134 | 135 | Cancel order 136 | ```php 137 | $client->cancelOrder($order->getOrderId()); 138 | ``` 139 | List of operations from 10 days ago to 30 days period 140 | ```php 141 | $from = new \DateTime(); 142 | $from->sub(new \DateInterval("P7D")); 143 | $to = new \DateTime(); 144 | $operations = $client->getOperations($from, $to); 145 | foreach ($operations as $operation) 146 | print $operation->getId ().' '.$operation->getFigi (). ' '.$operation->getPrice ().' '.$operation->getOperationType().' '.$operation->getDate()->format('d.m.Y H:i')."\n"; 147 | 148 | ``` 149 | Getting instrument status 150 | ```php 151 | $status = $client->getInstrumentInfo($sber->getFigi()); 152 | print 'Instrument status: '. $status->getTrade_status()."\n"; 153 | ``` 154 | 155 | Get Candles and Order books 156 | ```php 157 | if ($status->getTrade_status()=="normal_trading") 158 | { 159 | $candle = $client->getCandle($sber->getFigi(), TICandleIntervalEnum::DAY); 160 | print 'Low: '.$candle->getLow(). ' High: '.$candle->getHigh().' Open: '.$candle->getOpen().' Close: '.$candle->getClose().' Volume: '.$candle->getVolume()."\n"; 161 | 162 | $orderbook = $client->getOrderBook($sber->getFigi(),2); 163 | print 'Price to buy: '.$orderbook->getBestPriceToBuy().' Available lots: '.$orderbook->getBestPriceToBuyLotCount().' Price to Sell: '.$orderbook->getBestPriceToSell().' Available lots: '.$orderbook->getBestPriceToSellLotCount()."\n"; 164 | } 165 | ``` 166 | 167 | You can also to subscribe on changes order books, candles or instrument info: 168 | First of all, make a callback function to manage events: 169 | ```php 170 | function action($obj) 171 | { 172 | print "action\n"; 173 | if ($obj instanceof TICandle) 174 | print 'Time: '.$obj->getTime ()->format('d.m.Y H:i:s').' Volume: '.$obj->getVolume ()."\n"; 175 | if ($obj instanceof TIOrderBook) 176 | print 'Price to Buy: '.$obj->getBestPriceToBuy().' Price to Sell: '.$obj->getBestPriceToSell()."\n"; 177 | } 178 | ``` 179 | Then subscribe to events 180 | ```php 181 | $client->subscribeGettingCandle($sber->getFigi(), TICandleIntervalEnum::MIN1); 182 | $client->subscribeGettingOrderBook($sber->getFigi(), 2); 183 | ``` 184 | and finaly start listening new events 185 | ```php 186 | $client->startGetting("action",20,60); 187 | ``` 188 | in this example we awaiting max 20 respnse and max for 60 seconds 189 | if you want no limits, you should make 190 | ```php 191 | $client->startGetting("action"); 192 | $client->startGetting("action",null,600); 193 | $client->startGetting("action",1000,null); 194 | ``` 195 | to stop listening do 196 | ```php 197 | $client->stopGetting(); 198 | ``` 199 | 200 | ###CAUTION 201 | If you use subscriptions you should check figi on response, because you getting all subscribed instruments in one queue 202 | 203 | ## Donation 204 | Please support my project 205 | 206 | [![](https://img.shields.io/badge/Donate-PayPal-green)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=4WEWSZPBUBSVJ&source=url) 207 | [![](https://img.shields.io/badge/Donate-Yandex-green)](https://money.yandex.ru/quickpay/shop-widget?writer=seller&targets=Project%20support&targets-hint=&default-sum=100&button-text=14&payment-type-choice=on&mobile-payment-type-choice=on&hint=&successURL=&quickpay=shop&account=41001102505770) 208 | [![](https://img.shields.io/badge/Donate-WebMoney-green)](https://funding.webmoney.ru/widgets/horizontal/f892576d-1ce5-4046-abd7-7c947a81b398?hs=1&bt=0&sum=100) 209 | 210 | ## Licence 211 | MIT 212 | -------------------------------------------------------------------------------- /src/TIClient.php: -------------------------------------------------------------------------------- 1 | token = $token; 73 | $this->url = $site; 74 | $this->brokerAccountId = $account; 75 | $this->wsConnect(); 76 | } 77 | 78 | /** 79 | * @return null 80 | */ 81 | public function getBrokerAccount() 82 | { 83 | return $this->brokerAccountId; 84 | } 85 | 86 | /** 87 | * @param null $brokerAccountId 88 | */ 89 | public function setBrokerAccount($account) 90 | { 91 | $this->brokerAccountId = $account; 92 | } 93 | 94 | 95 | 96 | /** 97 | * Удаление всех позиций в песочнице 98 | * 99 | * @param null $accountId 100 | * @return string status 101 | * @throws TIException 102 | */ 103 | public function sbClear() 104 | { 105 | $response = $this->sendRequest("/sandbox/clear", 106 | "POST", 107 | ["brokerAccountId" => $this->brokerAccountId] 108 | ); 109 | return $response->getStatus(); 110 | } 111 | 112 | /** Remove sandbox Account 113 | * @param null $accountId 114 | * @return string 115 | * @throws TIException 116 | */ 117 | public function sbRemove() 118 | { 119 | $response = $this->sendRequest("/sandbox/remove", 120 | "POST", 121 | ["brokerAccountId" => $this->brokerAccountId] 122 | ); 123 | return $response->getStatus(); 124 | } 125 | 126 | /** 127 | * Регистрация клиента в sandbox 128 | * 129 | * @return TIAccount 130 | * @throws TIException 131 | */ 132 | public function sbRegister() 133 | { 134 | $response = $this->sendRequest("/sandbox/register", "POST"); 135 | return new TIAccount($response->getPayload()->brokerAccountType, 136 | $response->getPayload()->brokerAccountId); 137 | 138 | } 139 | 140 | /** 141 | * Выставление баланса по инструментным позициям 142 | * 143 | * @param double $balance 144 | * @param string $figi 145 | * 146 | * @return string status 147 | * @throws TIException 148 | */ 149 | public function sbPositionBalance($balance, $figi) 150 | { 151 | $request = ["figi" => $figi, "balance" => $balance]; 152 | $request_body = json_encode($request, JSON_NUMERIC_CHECK); 153 | $response = $this->sendRequest( 154 | "/sandbox/positions/balance", 155 | "POST", 156 | ["brokerAccountId" => $this->brokerAccountId], 157 | $request_body 158 | ); 159 | return $response->getStatus(); 160 | } 161 | 162 | /** 163 | * Выставление баланса по инструментным позициям 164 | * 165 | * @param double $balance 166 | * @param string $currency 167 | * 168 | * @param null $accountId 169 | * @return string status 170 | * @throws TIException 171 | */ 172 | public function sbCurrencyBalance($balance, $currency = TICurrencyEnum::RUB) 173 | { 174 | $request = ["currency" => $currency, "balance" => $balance]; 175 | $request_body = json_encode($request, JSON_NUMERIC_CHECK); 176 | $response = $this->sendRequest( 177 | "/sandbox/currencies/balance", 178 | "POST", 179 | ["brokerAccountId" => $this->brokerAccountId], 180 | $request_body 181 | ); 182 | return $response->getStatus(); 183 | } 184 | 185 | /** 186 | * Получение списка акций 187 | * 188 | * @param array $tickers Ticker Filter 189 | * 190 | * @return TIInstrument[] Список инструментов 191 | * @throws TIException 192 | */ 193 | public function getStocks($tickers = null) 194 | { 195 | $response = $this->sendRequest("/market/stocks", "GET"); 196 | return $this->setUpLists($response, $tickers); 197 | } 198 | 199 | /** 200 | * Получение списка облигаций 201 | * 202 | * @param array $tickers filter tickers 203 | * 204 | * @return TIInstrument[] 205 | * @throws TIException 206 | */ 207 | public function getBonds($tickers = null) 208 | { 209 | $response = $this->sendRequest("/market/bonds", "GET"); 210 | return $this->setUpLists($response, $tickers); 211 | } 212 | 213 | /** 214 | * Получение списка ETF 215 | * 216 | * @param array $tickers filter ticker 217 | * 218 | * @return TIInstrument[] 219 | * @throws TIException 220 | */ 221 | public function getEtfs($tickers = null) 222 | { 223 | $response = $this->sendRequest("/market/etfs", "GET"); 224 | return $this->setUpLists($response, $tickers); 225 | } 226 | 227 | /** 228 | * Получение списка валют 229 | * 230 | * @param array $tickers filter ticker 231 | * 232 | * @return TIInstrument[] 233 | * @throws TIException 234 | */ 235 | public function getCurrencies($tickers = null) 236 | { 237 | $currencies = []; 238 | $response = $this->sendRequest("/market/currencies", "GET"); 239 | 240 | foreach ($response->getPayload()->instruments as $instrument) { 241 | if ($tickers === null || in_array($instrument->ticker, $tickers)) { 242 | $currency = TICurrencyEnum::getCurrency($instrument->currency); 243 | 244 | $curr = new TIInstrument( 245 | $instrument->figi, 246 | $instrument->ticker, 247 | null, 248 | $instrument->minPriceIncrement, 249 | $instrument->lot, 250 | $currency, 251 | $instrument->name, 252 | $instrument->type 253 | ); 254 | $currencies[] = $curr; 255 | } 256 | } 257 | return $currencies; 258 | } 259 | 260 | 261 | /** 262 | * Получение инструмента по тикеру 263 | * 264 | * @param string $ticker 265 | * 266 | * @return TIInstrument 267 | * @throws TIException 268 | */ 269 | public function getInstrumentByTicker($ticker) 270 | { 271 | $response = $this->sendRequest( 272 | "/market/search/by-ticker", 273 | "GET", 274 | ["ticker" => $ticker] 275 | ); 276 | 277 | if ($response->getPayload()->total == 0) 278 | throw new TIException("Cannot find instrument by ticker {$ticker}"); 279 | 280 | $currency = TICurrencyEnum::getCurrency( 281 | $response->getPayload()->instruments[0]->currency 282 | ); 283 | $isin = (isset($response->getPayload()->instruments[0]->isin)) ? $response->getPayload( 284 | )->instruments[0]->isin : null; 285 | return new TIInstrument( 286 | $response->getPayload()->instruments[0]->figi, 287 | $response->getPayload()->instruments[0]->ticker, 288 | $isin, 289 | $response->getPayload()->instruments[0]->minPriceIncrement, 290 | $response->getPayload()->instruments[0]->lot, 291 | $currency, 292 | $response->getPayload()->instruments[0]->name, 293 | $response->getPayload()->instruments[0]->type 294 | ); 295 | } 296 | 297 | /** 298 | * Получение инструмента по FIGI 299 | * 300 | * @param string $figi 301 | * 302 | * @return TIInstrument 303 | * @throws TIException 304 | */ 305 | public function getInstrumentByFigi($figi) 306 | { 307 | $response = $this->sendRequest( 308 | "/market/search/by-figi", 309 | "GET", 310 | ["figi" => $figi] 311 | ); 312 | 313 | $currency = TICurrencyEnum::getCurrency($response->getPayload()->currency); 314 | 315 | $isin = (isset($response->getPayload()->isin)) ? $response->getPayload()->isin : null; 316 | return new TIInstrument( 317 | $response->getPayload()->figi, 318 | $response->getPayload()->ticker, 319 | $isin, 320 | $response->getPayload()->minPriceIncrement, 321 | $response->getPayload()->lot, 322 | $currency, 323 | $response->getPayload()->name, 324 | $response->getPayload()->type 325 | ); 326 | } 327 | 328 | /** 329 | * Получение исторического стакана 330 | * 331 | * @param string $figi 332 | * @param int $depth 333 | * @return TIOrderBook 334 | * @throws TIException 335 | */ 336 | public function getHistoryOrderBook($figi, $depth = 1) 337 | { 338 | if ($depth < 1) { 339 | $depth = 1; 340 | } 341 | if ($depth > 20) { 342 | $depth = 20; 343 | } 344 | $response = $this->sendRequest( 345 | "/market/orderbook", 346 | "GET", 347 | [ 348 | 'figi' => $figi, 349 | 'depth' => $depth, 350 | ] 351 | ); 352 | 353 | return $this->setUpOrderBook($response->getPayload()); 354 | } 355 | 356 | /** 357 | * Получение исторических свечей 358 | * default figi = AAPL 359 | * default from 7Days ago 360 | * default to now 361 | * default interval 15 min 362 | * 363 | * @param string $figi 364 | * @param \DateTime $from 365 | * @param \DateTime $to 366 | * @param string $interval 367 | * @return TICandle[] 368 | * @throws TIException 369 | */ 370 | public function getHistoryCandles($figi, $from = null, $to = null, $interval = null) 371 | { 372 | $fromDate = new DateTime(); 373 | $fromDate->sub(new DateInterval('P7D')); 374 | $toDate = new DateTime(); 375 | 376 | $response = $this->sendRequest( 377 | "/market/candles", 378 | "GET", 379 | [ 380 | 'figi' => $figi, 381 | 'from' => empty($from) ? $fromDate->format('c') : $from->format('c'), 382 | 'to' => empty($to) ? $toDate->format('c') : $to->format('c'), 383 | 'interval' => empty($interval) ? TIIntervalEnum::MIN15 : $interval 384 | ] 385 | ); 386 | $array = []; 387 | foreach ($response->getPayload()->candles as $candle) { 388 | $array [] = $this->setUpCandle($candle); 389 | } 390 | return $array; 391 | } 392 | 393 | 394 | /** 395 | * Получение текущих аккаунтов пользователя 396 | * 397 | * @return TIAccount[] 398 | * @throws TIException 399 | */ 400 | public function getAccounts() 401 | { 402 | $response = $this->sendRequest("/user/accounts", "GET"); 403 | $accounts = []; 404 | foreach ($response->getPayload()->accounts as $index => $account) { 405 | $accounts [] = new TIAccount( 406 | $account->brokerAccountType, 407 | $account->brokerAccountId 408 | ); 409 | } 410 | return $accounts; 411 | } 412 | 413 | 414 | /** 415 | * Получить портфель клиента 416 | * 417 | * @return TIPortfolio 418 | * @throws TIException 419 | */ 420 | public function getPortfolio() 421 | { 422 | $currs = []; 423 | $params = [ 424 | 'brokerAccountId' => $this->brokerAccountId 425 | ]; 426 | 427 | $response = $this->sendRequest( 428 | "/portfolio/currencies", 429 | "GET", 430 | $params 431 | ); 432 | 433 | foreach ($response->getPayload()->currencies as $currency) { 434 | $ticurrency = TICurrencyEnum::getCurrency($currency->currency); 435 | $blocked = (isset($currency->blocked)) ? $currency->blocked : 0; 436 | 437 | $curr = new TIPortfolioCurrency( 438 | $currency->balance, 439 | $ticurrency, 440 | $blocked 441 | ); 442 | $currs[] = $curr; 443 | } 444 | 445 | $instrs = []; 446 | $response = $this->sendRequest("/portfolio", "GET", $params); 447 | 448 | foreach ($response->getPayload()->positions as $position) { 449 | $expectedYeildCurrency = null; 450 | $expectedYeildValue = null; 451 | if (isset($position->expectedYield)) { 452 | $expectedYeildCurrency = TICurrencyEnum::getCurrency( 453 | $position->expectedYield->currency 454 | ); 455 | $expectedYeildValue = $position->expectedYield->value; 456 | } 457 | 458 | $isin = (isset($position->isin)) ? $position->isin : null; 459 | $blocked = (isset($position->blocked)) ? $position->blocked : 0; 460 | $averagePositionPrice = (isset($position->averagePositionPrice)) ? $position->averagePositionPrice : null; 461 | $averagePositionPriceNoNkd = (isset($position->averagePositionPriceNoNkd)) ? $position->averagePositionPriceNoNkd : null; 462 | 463 | $instr = new TIPortfolioInstrument( 464 | $position->figi, 465 | $position->ticker, 466 | $isin, 467 | $position->instrumentType, 468 | $position->balance, 469 | $blocked, 470 | $position->lots, 471 | $expectedYeildValue, 472 | $expectedYeildCurrency, 473 | $position->name, 474 | $averagePositionPrice, 475 | $averagePositionPriceNoNkd 476 | ); 477 | $instrs[] = $instr; 478 | } 479 | 480 | return new TIPortfolio($currs, $instrs); 481 | } 482 | 483 | /** 484 | * Создание лимитной заявки 485 | * 486 | * @param string $figi 487 | * @param int $lots 488 | * @param TIOperationEnum $operation 489 | * @param double $price 490 | * 491 | * @return TIOrder 492 | * @throws TIException 493 | */ 494 | public function sendOrder($figi, $lots, $operation, $price = null) 495 | { 496 | $req_body = json_encode( 497 | (object)[ 498 | "lots" => $lots, 499 | "operation" => $operation, 500 | "price" => $price, 501 | ] 502 | ); 503 | 504 | $order_type = empty($price) ? "market-order" : "limit-order"; 505 | 506 | $response = $this->sendRequest( 507 | "/orders/" . $order_type, 508 | "POST", 509 | [ 510 | "figi" => $figi, 511 | "brokerAccountId" => $this->brokerAccountId 512 | ], 513 | $req_body 514 | ); 515 | 516 | return $this->setUpOrder($response, $figi); 517 | } 518 | 519 | /** 520 | * Отменить заявку 521 | * 522 | * @param string $orderId Номер заявки 523 | * 524 | * @return string status 525 | * @throws TIException 526 | */ 527 | public function cancelOrder($orderId) 528 | { 529 | $response = $this->sendRequest( 530 | "/orders/cancel", 531 | "POST", 532 | [ 533 | "orderId" => $orderId, 534 | "brokerAccountId" => $this->brokerAccountId 535 | ] 536 | ); 537 | 538 | return $response->getStatus(); 539 | } 540 | 541 | /** 542 | * @param null $orderIds 543 | * @return array 544 | * @throws TIException 545 | */ 546 | public function getOrders($orderIds = null) 547 | { 548 | $orders = []; 549 | $response = $this->sendRequest("/orders", "GET"); 550 | foreach ($response->getPayload() as $order) { 551 | if ($orderIds === null || in_array($order->orderId, $orderIds)) { 552 | $ord = new TIOrder( 553 | $order->orderId, 554 | TIOperationEnum::getOperation($order->operation), 555 | $order->status, 556 | null, // rejected 557 | $order->requestedLots, 558 | $order->executedLots, 559 | null, // comm currency 560 | $order->figi, 561 | $order->type, 562 | '', 563 | $order->price 564 | ); 565 | $orders[] = $ord; 566 | } 567 | } 568 | return $orders; 569 | } 570 | 571 | /** 572 | * 573 | * @param DateTime $fromDate 574 | * @param DateTime $toDate 575 | * @param string $figi 576 | * @return TIOperation[] 577 | * @throws TIException 578 | */ 579 | public function getOperations($fromDate, $toDate, $figi = null) 580 | { 581 | $operations = []; 582 | $response = $this->sendRequest( 583 | "/operations", 584 | "GET", 585 | [ 586 | "from" => $fromDate->format("c"), 587 | "to" => $toDate->format("c"), 588 | "figi" => $figi, 589 | "brokerAccountId" => $this->brokerAccountId, 590 | ] 591 | ); 592 | 593 | foreach ($response->getPayload()->operations as $operation) { 594 | $trades = []; 595 | foreach ((empty($operation->trades) ? [] : $operation->trades) as $operationTrade) 596 | { 597 | $trades[] = new TIOperationTrade( 598 | empty($operationTrade->tradeId) ? null : $operationTrade->tradeId, 599 | empty($operationTrade->date) ? null : $operationTrade->date, 600 | empty($operationTrade->price) ? null : $operationTrade->price, 601 | empty($operationTrade->quantity) ? null : $operationTrade->quantity 602 | ); 603 | } 604 | $commissionCurrency = (isset($operation->commission)) ? TICurrencyEnum::getCurrency( 605 | $operation->commission->currency 606 | ) : null; 607 | $commissionValue = (isset($operation->commission)) ? $operation->commission->value : null; 608 | try { 609 | $dateTime = new DateTime($operation->date); 610 | } catch (Exception $e) { 611 | throw new TIException('Can not create DateTime from operations'); 612 | } 613 | $opr = new TIOperation( 614 | $operation->id, 615 | $operation->status, 616 | $trades, 617 | new TICommission($commissionCurrency, $commissionValue), 618 | TICurrencyEnum::getCurrency($operation->currency), 619 | $operation->payment, 620 | empty($operation->price) ? null : $operation->price, 621 | empty($operation->quantity) ? null : $operation->quantity, 622 | empty($operation->figi) ? null : $operation->figi, 623 | empty($operation->instrumentType) ? null : $operation->instrumentType, 624 | $operation->isMarginCall, 625 | $dateTime, 626 | TIOperationEnum::getOperation( 627 | empty($operation->operationType) ? null : $operation->operationType 628 | ) 629 | ); 630 | $operations[] = $opr; 631 | } 632 | return $operations; 633 | } 634 | 635 | /** 636 | * @param bool $debug 637 | */ 638 | public function setDebug($debug) 639 | { 640 | $this->debug = $debug; 641 | } 642 | 643 | /** 644 | * @param bool $ignore_ssl_peer_verification 645 | */ 646 | public function setIgnoreSslPeerVerification($ignore_ssl_peer_verification) 647 | { 648 | $this->ignore_ssl_peer_verification = $ignore_ssl_peer_verification; 649 | } 650 | 651 | 652 | 653 | /** 654 | * Отправка запроса на API 655 | * 656 | * @param string $action 657 | * @param string $method 658 | * @param array $req_params 659 | * @param string $req_body 660 | * 661 | * @return TIResponse 662 | * @throws TIException 663 | */ 664 | private function sendRequest( 665 | $action, 666 | $method, 667 | $req_params = [], 668 | $req_body = null 669 | ) { 670 | $curl = curl_init(); 671 | curl_setopt($curl, CURLOPT_URL, $this->url . $action); 672 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); 673 | curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method); 674 | 675 | if (count($req_params) > 0) { 676 | curl_setopt( 677 | $curl, 678 | CURLOPT_URL, 679 | $this->url . $action . '?' . http_build_query( 680 | $req_params 681 | ) 682 | ); 683 | } 684 | 685 | if ($method !== "GET") { 686 | curl_setopt($curl, CURLOPT_POST, true); 687 | curl_setopt($curl, CURLOPT_POSTFIELDS, $req_body); 688 | } 689 | 690 | curl_setopt( 691 | $curl, 692 | CURLOPT_HTTPHEADER, 693 | [ 694 | 'Content-Type:application/json', 695 | 'Authorization: Bearer ' . $this->token, 696 | ] 697 | ); 698 | 699 | if ($this->debug) { 700 | curl_setopt($curl, CURLOPT_VERBOSE, true); 701 | curl_setopt($curl, CURLOPT_CERTINFO, true); 702 | } 703 | 704 | if ($this->ignore_ssl_peer_verification) { 705 | curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); 706 | } 707 | 708 | $out = curl_exec($curl); 709 | $res = curl_getinfo($curl, CURLINFO_HTTP_CODE); 710 | 711 | $error = curl_error($curl); 712 | curl_close($curl); 713 | 714 | if ($res === 0) { 715 | throw new \Exception($error); 716 | } 717 | 718 | return new TIResponse($out, $res); 719 | } 720 | 721 | /** 722 | * @throws TIException 723 | */ 724 | private function wsConnect() 725 | { 726 | try { 727 | $this->wsClient = new Client( 728 | "wss://api-invest.tinkoff.ru/openapi/md/v1/md-openapi/ws", 729 | [ 730 | "timeout" => 60, 731 | "headers" => ["authorization" => "Bearer {$this->token}"], 732 | ] 733 | ); 734 | } catch (Exception $e) { 735 | throw new TIException( 736 | "Can't connect to stream API. " . $e->getCode() . ' ' . $e->getMessage() 737 | ); 738 | } 739 | } 740 | 741 | 742 | /** 743 | * @param $figi 744 | * @param $interval 745 | * @param string $action 746 | * @throws TIException 747 | */ 748 | private function candleSubscribtion($figi, $interval, $action = "subscribe") 749 | { 750 | $request = '{ 751 | "event": "candle:' . $action . '", 752 | "figi": "' . $figi . '", 753 | "interval": "' . $interval . '" 754 | }'; 755 | if (!$this->wsClient->isConnected()) { 756 | $this->wsConnect(); 757 | } 758 | try { 759 | $this->wsClient->send($request); 760 | } catch (BadOpcodeException $e) { 761 | throw new TIException('Can not send websocket request errorMessage' . $e->getMessage()); 762 | } 763 | } 764 | 765 | /** 766 | * Получить свечу 767 | * 768 | * @param string $figi 769 | * @param string $interval 770 | * 771 | * @return TICandle 772 | * @throws TIException 773 | */ 774 | public function getCandle($figi, $interval) 775 | { 776 | $this->candleSubscribtion($figi, $interval); 777 | $response = $this->wsClient->receive(); 778 | $this->candleSubscribtion($figi, $interval, "unsubscribe"); 779 | $json = json_decode($response); 780 | if (empty($json)) { 781 | throw new TIException('Got empty response for Candle'); 782 | } 783 | return $this->setUpCandle($json->payload); 784 | } 785 | 786 | /** 787 | * @param $figi 788 | * @param $depth 789 | * @param string $action 790 | * @throws TIException 791 | */ 792 | private function orderbookSubscribtion($figi, $depth, $action = "subscribe") 793 | { 794 | $request = '{ 795 | "event": "orderbook:' . $action . '", 796 | "figi": "' . $figi . '", 797 | "depth": ' . $depth . ' 798 | }'; 799 | if (!$this->wsClient->isConnected()) { 800 | $this->wsConnect(); 801 | } 802 | try { 803 | $this->wsClient->send($request); 804 | } catch (BadOpcodeException $e) { 805 | throw new TIException('Can not send websocket request errorMessage' . $e->getMessage()); 806 | } 807 | } 808 | 809 | /** 810 | * Получить стакан 811 | * 812 | * @param string $figi 813 | * @param int $depth 814 | * 815 | * @return TIOrderBook 816 | * @throws TIException 817 | */ 818 | public function getOrderBook($figi, $depth = 1) 819 | { 820 | if ($depth < 1) { 821 | $depth = 1; 822 | } 823 | if ($depth > 20) { 824 | $depth = 20; 825 | } 826 | $this->orderbookSubscribtion($figi, $depth); 827 | $response = $this->wsClient->receive(); 828 | $this->orderbookSubscribtion($figi, $depth, "unsubscribe"); 829 | $json = json_decode($response); 830 | if (empty($json)) { 831 | throw new TIException('Got empty response for OrderBook'); 832 | } 833 | if (isset($json->payload->bids) && is_array($json->payload->bids)) { 834 | foreach ($json->payload->bids as &$bid) { 835 | $bid = (object)[ 836 | 'price' => $bid[0], 837 | 'quantity' => $bid[1] 838 | ]; 839 | } 840 | } 841 | if (isset($json->payload->asks) && is_array($json->payload->asks)) { 842 | foreach ($json->payload->asks as &$ask) { 843 | $ask = (object)[ 844 | 'price' => $ask[0], 845 | 'quantity' => $ask[1] 846 | ]; 847 | } 848 | } 849 | return $this->setUpOrderBook($json->payload); 850 | } 851 | 852 | /** 853 | * @param $figi 854 | * @param string $action 855 | * @throws TIException 856 | */ 857 | private function instrumentInfoSubscribtion($figi, $action = "subscribe") 858 | { 859 | $request = '{ 860 | "event": "instrument_info:' . $action . '", 861 | "figi": "' . $figi . '" 862 | }'; 863 | if (!$this->wsClient->isConnected()) { 864 | $this->wsConnect(); 865 | } 866 | try { 867 | $this->wsClient->send($request); 868 | } catch (BadOpcodeException $e) { 869 | throw new TIException('Can not send websocket request errorMessage' . $e->getMessage()); 870 | } 871 | } 872 | 873 | /** 874 | * Get Instrument info 875 | * 876 | * @param string $figi 877 | * 878 | * @return TIInstrumentInfo 879 | * @throws TIException 880 | */ 881 | public function getInstrumentInfo($figi) 882 | { 883 | $this->instrumentInfoSubscribtion($figi); 884 | $response = $this->wsClient->receive(); 885 | $this->instrumentInfoSubscribtion($figi, "unsubscribe"); 886 | $json = json_decode($response); 887 | if (empty($json)) { 888 | throw new TIException('Got empty response for InstrumentInfo'); 889 | } 890 | 891 | return $this->setUpInstrumentInfo($json->payload); 892 | } 893 | 894 | 895 | /** 896 | * @param $figi 897 | * @param $interval 898 | * @throws TIException 899 | */ 900 | public function subscribeGettingCandle($figi, $interval) 901 | { 902 | $this->candleSubscribtion($figi, $interval); 903 | } 904 | 905 | /** 906 | * @param $figi 907 | * @param $depth 908 | * @throws TIException 909 | */ 910 | public function subscribeGettingOrderBook($figi, $depth) 911 | { 912 | $this->orderbookSubscribtion($figi, $depth); 913 | } 914 | 915 | /** 916 | * @param $figi 917 | * @throws TIException 918 | */ 919 | public function subscribeGettingInstrumentInfo($figi) 920 | { 921 | $this->instrumentInfoSubscribtion($figi); 922 | } 923 | 924 | /** 925 | * @param $figi 926 | * @param $interval 927 | * @throws TIException 928 | */ 929 | public function unsubscribeGettingCandle($figi, $interval) 930 | { 931 | $this->candleSubscribtion($figi, $interval, "unsubscribe"); 932 | } 933 | 934 | /** 935 | * @param $figi 936 | * @param $depth 937 | * @throws TIException 938 | */ 939 | public function unsubscribeGettingOrderBook($figi, $depth) 940 | { 941 | $this->orderbookSubscribtion($figi, $depth, "unsubscribe"); 942 | } 943 | 944 | /** 945 | * @param $figi 946 | * @throws TIException 947 | */ 948 | public function unsubscribeGettingInstrumentInfo($figi) 949 | { 950 | $this->instrumentInfoSubscribtion($figi, "unsubscribe"); 951 | } 952 | 953 | 954 | /** 955 | * @param $callback 956 | * @param int $max_response 957 | * @param int $max_time_sec 958 | */ 959 | public function startGetting( 960 | $callback, 961 | $max_response = 10, 962 | $max_time_sec = 60 963 | ) { 964 | $this->startGetting = true; 965 | $this->response_now = 0; 966 | $this->response_start_time = time(); 967 | while (true) { 968 | $response = $this->wsClient->receive(); 969 | $json = json_decode($response); 970 | if (!isset($json->event) || $json === null) { 971 | continue; 972 | } 973 | try { 974 | switch ($json->event) { 975 | case "candle" : 976 | $object = $this->setUpCandle($json->payload); 977 | break; 978 | case "orderbook" : 979 | $object = $this->setUpOrderBook($json->payload); 980 | break; 981 | case "instrument_info" : 982 | $object = $this->setUpInstrumentInfo($json->payload); 983 | break; 984 | } 985 | if (!empty($object)) { 986 | call_user_func($callback, $object); 987 | } 988 | } catch (TIException $e) { 989 | //TODO: add Exception to logger 990 | } 991 | $this->response_now++; 992 | if ($this->startGetting === false || ($max_response !== null && $this->response_now >= $max_response) || ($max_time_sec !== null && time( 993 | ) > $this->response_start_time + $max_time_sec)) { 994 | break; 995 | } 996 | } 997 | } 998 | 999 | 1000 | /** 1001 | * 1002 | */ 1003 | public function stopGetting() 1004 | { 1005 | $this->startGetting = false; 1006 | } 1007 | 1008 | 1009 | /** 1010 | * @param $payload 1011 | * @return TIOrderBook 1012 | */ 1013 | private function setUpOrderBook($payload) 1014 | { 1015 | return new TIOrderBook( 1016 | empty($payload->depth) ? null : $payload->depth, 1017 | empty($payload->bids) ? null : $payload->bids, 1018 | empty($payload->asks) ? null : $payload->asks, 1019 | empty($payload->figi) ? null : $payload->figi, 1020 | empty($payload->tradeStatus) ? null : $payload->tradeStatus, 1021 | empty($payload->minPriceIncrement) ? null : $payload->minPriceIncrement, 1022 | empty($payload->faceValue) ? null : $payload->faceValue, 1023 | empty($payload->lastPrice) ? null : $payload->lastPrice, 1024 | empty($payload->closePrice) ? null : $payload->closePrice, 1025 | empty($payload->limitUp) ? null : $payload->limitUp, 1026 | empty($payload->limitDown) ? null : $payload->limitDown 1027 | ); 1028 | } 1029 | 1030 | /** 1031 | * @param $payload 1032 | * @return TIInstrumentInfo 1033 | */ 1034 | private function setUpInstrumentInfo($payload) 1035 | { 1036 | $object = new TIInstrumentInfo( 1037 | $payload->trade_status, 1038 | $payload->min_price_increment, 1039 | $payload->lot, 1040 | $payload->figi 1041 | ); 1042 | if (isset($payload->accrued_interest)) { 1043 | $object->setAccrued_interest( 1044 | $payload->accrued_interest 1045 | ); 1046 | } 1047 | if (isset($payload->limit_up)) { 1048 | $object->setLimit_up($payload->limit_up); 1049 | } 1050 | if (isset($payload->limit_down)) { 1051 | $object->setLimit_down($payload->limit_down); 1052 | } 1053 | return $object; 1054 | } 1055 | 1056 | 1057 | /** 1058 | * @param $payload 1059 | * @return TICandle 1060 | * @throws TIException 1061 | */ 1062 | private function setUpCandle($payload) 1063 | { 1064 | try { 1065 | $datetime = new DateTime($payload->time); 1066 | } catch (Exception $e) { 1067 | throw new TIException('Can not create DateTime for Candle'); 1068 | } 1069 | return new TICandle( 1070 | $payload->o, 1071 | $payload->c, 1072 | $payload->h, 1073 | $payload->l, 1074 | $payload->v, 1075 | $datetime, 1076 | TICandleIntervalEnum::getInterval( 1077 | $payload->interval 1078 | ), 1079 | $payload->figi 1080 | ); 1081 | } 1082 | 1083 | /** 1084 | * @param TIResponse $response 1085 | * @param null|array $tickers 1086 | * @return array 1087 | */ 1088 | private function setUpLists($response, $tickers = null) 1089 | { 1090 | $array = []; 1091 | foreach ($response->getPayload()->instruments as $instrument) { 1092 | if ($tickers === null || in_array($instrument->ticker, $tickers)) { 1093 | $currency = TICurrencyEnum::getCurrency($instrument->currency); 1094 | $minPriceIncrement = (isset($instrument->minPriceIncrement)) ? $instrument->minPriceIncrement : null; 1095 | 1096 | $stock = new TIInstrument( 1097 | empty($instrument->figi) ? null : $instrument->figi, 1098 | empty($instrument->ticker) ? null : $instrument->ticker, 1099 | empty($instrument->isin) ? null : $instrument->isin, 1100 | $minPriceIncrement, 1101 | empty($instrument->lot) ? null : $instrument->lot, 1102 | $currency, 1103 | empty($instrument->name) ? null : $instrument->name, 1104 | empty($instrument->type) ? null : $instrument->type 1105 | ); 1106 | $array[] = $stock; 1107 | } 1108 | } 1109 | return $array; 1110 | } 1111 | 1112 | /** 1113 | * @param TIResponse $response 1114 | * @param string $figi 1115 | * @return TIOrder 1116 | */ 1117 | private function setUpOrder($response, $figi) 1118 | { 1119 | $payload = $response->getPayload(); 1120 | $commissionValue = (isset($payload->commission)) ? $payload->commission->value : null; 1121 | $commissionCurrency = (isset($payload->commission)) ? TICurrencyEnum::getCurrency( 1122 | $payload->commission->currency 1123 | ) : null; 1124 | $rejectReason = (isset($payload->rejectReason)) ? $payload->rejectReason : null; 1125 | 1126 | return new TIOrder( 1127 | empty($payload->orderId) ? null : $payload->orderId, 1128 | TIOperationEnum::getOperation($payload->operation), 1129 | empty($payload->status) ? null : $payload->status, 1130 | $rejectReason, 1131 | empty($payload->requestedLots) ? null : $payload->requestedLots, 1132 | empty($payload->executedLots) ? null : $payload->executedLots, 1133 | new TICommission($commissionCurrency, $commissionValue), 1134 | $figi, 1135 | null, // type 1136 | empty($payload->message) ? null : $payload->message, 1137 | empty($payload->price) ? null : $payload->price 1138 | ); 1139 | } 1140 | 1141 | } 1142 | --------------------------------------------------------------------------------