@@ -12,6 +12,7 @@ | |||||
"symfony/dotenv": "5.0.*", | "symfony/dotenv": "5.0.*", | ||||
"symfony/flex": "^1.3.1", | "symfony/flex": "^1.3.1", | ||||
"symfony/framework-bundle": "5.0.*", | "symfony/framework-bundle": "5.0.*", | ||||
"symfony/http-client": "5.0.*", | |||||
"symfony/yaml": "5.0.*" | "symfony/yaml": "5.0.*" | ||||
}, | }, | ||||
"require-dev": { | "require-dev": { | ||||
@@ -4,7 +4,7 @@ | |||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", | ||||
"This file is @generated automatically" | "This file is @generated automatically" | ||||
], | ], | ||||
"content-hash": "ed2d4bb6d2a2abe584b5f0867281d0ab", | |||||
"content-hash": "4e5589a47c5b0dc0e0cd3d924e0621aa", | |||||
"packages": [ | "packages": [ | ||||
{ | { | ||||
"name": "myclabs/php-enum", | "name": "myclabs/php-enum", | ||||
@@ -1413,6 +1413,131 @@ | |||||
"homepage": "https://symfony.com", | "homepage": "https://symfony.com", | ||||
"time": "2020-03-30T11:42:42+00:00" | "time": "2020-03-30T11:42:42+00:00" | ||||
}, | }, | ||||
{ | |||||
"name": "symfony/http-client", | |||||
"version": "v5.0.7", | |||||
"source": { | |||||
"type": "git", | |||||
"url": "https://github.com/symfony/http-client.git", | |||||
"reference": "14d386ae55b699ea9a0ddb872fa5f3e35219bba8" | |||||
}, | |||||
"dist": { | |||||
"type": "zip", | |||||
"url": "https://api.github.com/repos/symfony/http-client/zipball/14d386ae55b699ea9a0ddb872fa5f3e35219bba8", | |||||
"reference": "14d386ae55b699ea9a0ddb872fa5f3e35219bba8", | |||||
"shasum": "" | |||||
}, | |||||
"require": { | |||||
"php": "^7.2.5", | |||||
"psr/log": "^1.0", | |||||
"symfony/http-client-contracts": "^1.1.8|^2", | |||||
"symfony/polyfill-php73": "^1.11", | |||||
"symfony/service-contracts": "^1.0|^2" | |||||
}, | |||||
"provide": { | |||||
"php-http/async-client-implementation": "*", | |||||
"php-http/client-implementation": "*", | |||||
"psr/http-client-implementation": "1.0", | |||||
"symfony/http-client-implementation": "1.1" | |||||
}, | |||||
"require-dev": { | |||||
"guzzlehttp/promises": "^1.3.1", | |||||
"nyholm/psr7": "^1.0", | |||||
"php-http/httplug": "^1.0|^2.0", | |||||
"psr/http-client": "^1.0", | |||||
"symfony/dependency-injection": "^4.4|^5.0", | |||||
"symfony/http-kernel": "^4.4|^5.0", | |||||
"symfony/process": "^4.4|^5.0" | |||||
}, | |||||
"type": "library", | |||||
"extra": { | |||||
"branch-alias": { | |||||
"dev-master": "5.0-dev" | |||||
} | |||||
}, | |||||
"autoload": { | |||||
"psr-4": { | |||||
"Symfony\\Component\\HttpClient\\": "" | |||||
}, | |||||
"exclude-from-classmap": [ | |||||
"/Tests/" | |||||
] | |||||
}, | |||||
"notification-url": "https://packagist.org/downloads/", | |||||
"license": [ | |||||
"MIT" | |||||
], | |||||
"authors": [ | |||||
{ | |||||
"name": "Nicolas Grekas", | |||||
"email": "p@tchwork.com" | |||||
}, | |||||
{ | |||||
"name": "Symfony Community", | |||||
"homepage": "https://symfony.com/contributors" | |||||
} | |||||
], | |||||
"description": "Symfony HttpClient component", | |||||
"homepage": "https://symfony.com", | |||||
"time": "2020-03-27T16:56:45+00:00" | |||||
}, | |||||
{ | |||||
"name": "symfony/http-client-contracts", | |||||
"version": "v2.0.1", | |||||
"source": { | |||||
"type": "git", | |||||
"url": "https://github.com/symfony/http-client-contracts.git", | |||||
"reference": "378868b61b85c5cac6822d4f84e26999c9f2e881" | |||||
}, | |||||
"dist": { | |||||
"type": "zip", | |||||
"url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/378868b61b85c5cac6822d4f84e26999c9f2e881", | |||||
"reference": "378868b61b85c5cac6822d4f84e26999c9f2e881", | |||||
"shasum": "" | |||||
}, | |||||
"require": { | |||||
"php": "^7.2.5" | |||||
}, | |||||
"suggest": { | |||||
"symfony/http-client-implementation": "" | |||||
}, | |||||
"type": "library", | |||||
"extra": { | |||||
"branch-alias": { | |||||
"dev-master": "2.0-dev" | |||||
} | |||||
}, | |||||
"autoload": { | |||||
"psr-4": { | |||||
"Symfony\\Contracts\\HttpClient\\": "" | |||||
} | |||||
}, | |||||
"notification-url": "https://packagist.org/downloads/", | |||||
"license": [ | |||||
"MIT" | |||||
], | |||||
"authors": [ | |||||
{ | |||||
"name": "Nicolas Grekas", | |||||
"email": "p@tchwork.com" | |||||
}, | |||||
{ | |||||
"name": "Symfony Community", | |||||
"homepage": "https://symfony.com/contributors" | |||||
} | |||||
], | |||||
"description": "Generic abstractions related to HTTP clients", | |||||
"homepage": "https://symfony.com", | |||||
"keywords": [ | |||||
"abstractions", | |||||
"contracts", | |||||
"decoupling", | |||||
"interfaces", | |||||
"interoperability", | |||||
"standards" | |||||
], | |||||
"time": "2019-11-26T23:25:11+00:00" | |||||
}, | |||||
{ | { | ||||
"name": "symfony/http-foundation", | "name": "symfony/http-foundation", | ||||
"version": "v5.0.7", | "version": "v5.0.7", | ||||
@@ -23,5 +23,10 @@ services: | |||||
resource: '../src/Controller' | resource: '../src/Controller' | ||||
tags: ['controller.service_arguments'] | tags: ['controller.service_arguments'] | ||||
# add more service definitions when explicit configuration is needed | |||||
# please note that last definitions always *replace* previous ones | |||||
App\Exchanges\Kuna\KunaSigner: | |||||
bind: | |||||
$publicKey: 'test' | |||||
$privateKey: 'test' | |||||
Http\Client\HttpClient: | |||||
class: Nyholm\HttpClient\Client |
@@ -0,0 +1,31 @@ | |||||
<?php | |||||
declare(strict_types=1); | |||||
namespace App\Arbitration\Inner; | |||||
use App\Exchanges\MarketInterface; | |||||
class ArbitrationChain | |||||
{ | |||||
/** | |||||
* @var MarketInterface[] | |||||
*/ | |||||
private array $markets; | |||||
public function __construct(MarketInterface ...$markets) | |||||
{ | |||||
$this->markets = $markets; | |||||
} | |||||
/** | |||||
* @return MarketInterface[] | |||||
*/ | |||||
public function getMarkets(): array | |||||
{ | |||||
return $this->markets; | |||||
} | |||||
} |
@@ -0,0 +1,158 @@ | |||||
<?php | |||||
declare(strict_types=1); | |||||
namespace App\Arbitration\Inner\Stuff; | |||||
use App\Arbitration\Inner\ArbitrationChain; | |||||
use App\Exchanges\ExchangeInterface; | |||||
use App\Exchanges\MarketInterface; | |||||
use App\Utils\Money\CurrencyPair; | |||||
class ArbitrationMarketChainsFinder | |||||
{ | |||||
/** | |||||
* @param ExchangeInterface $exchange | |||||
* @param int $maxChainDepth | |||||
* @return ArbitrationChain[] | |||||
*/ | |||||
public function findChainsInExchange(ExchangeInterface $exchange, int $maxChainDepth = 3): array | |||||
{ | |||||
/** | |||||
* @var MarketInterface[] $markets | |||||
*/ | |||||
$markets = $exchange->getMarkets(); | |||||
[$rows, $map] = $this->buildMap($markets); | |||||
$cycles = []; | |||||
foreach (array_keys($rows) as $row) { | |||||
$rowCycles = []; | |||||
$this->findCycle($map, [$row], $rowCycles, $maxChainDepth); | |||||
$cycles[] = $this->clearDuplicates($rowCycles); | |||||
} | |||||
$cycles = array_merge(...$cycles); | |||||
$cycles = $this->clearDuplicates($cycles); | |||||
$result = []; | |||||
foreach ($cycles as $cycle) { | |||||
$result[] = $this->cycleToChain( | |||||
$cycle, | |||||
$rows, | |||||
$markets | |||||
); | |||||
} | |||||
return $result; | |||||
} | |||||
protected function cycleToChain(array $cycle, array $rows, array $markets): ArbitrationChain | |||||
{ | |||||
$cycleMarkets = []; | |||||
for($i = 0; $i < \count($cycle); $i++) { | |||||
$base = $rows[$cycle[$i]]; | |||||
if ($i + 1 >= \count($cycle)) { | |||||
$quote = $rows[$cycle[0]]; | |||||
} else { | |||||
$quote = $rows[$cycle[$i + 1]]; | |||||
} | |||||
foreach ($markets as $market) { | |||||
/** | |||||
* @var CurrencyPair $pair | |||||
*/ | |||||
$pair = $market->getPair(); | |||||
if ( | |||||
( | |||||
$pair->getBase()->isEqual($base) | |||||
&& $pair->getQuote()->isEqual($quote) | |||||
) | |||||
|| ( | |||||
$pair->getBase()->isEqual($quote) | |||||
&& $pair->getQuote()->isEqual($base) | |||||
) | |||||
) { | |||||
$cycleMarkets[] = $market; | |||||
} | |||||
} | |||||
} | |||||
return new ArbitrationChain(...$cycleMarkets); | |||||
} | |||||
/** | |||||
* @param MarketInterface[] $markets | |||||
* @return array | |||||
*/ | |||||
protected function buildMap(array $markets): array | |||||
{ | |||||
$map = []; | |||||
$rows = []; | |||||
foreach ($markets as $market) { | |||||
$pair = $market->getPair(); | |||||
$base = $pair->getBase(); | |||||
if (!in_array((string)$base, $rows)) { | |||||
$rows[] = $base; | |||||
} | |||||
$baseId = array_search((string)$base, $rows); | |||||
$quote = $pair->getQuote(); | |||||
if (!in_array((string)$quote, $rows)) { | |||||
$rows[] = $quote; | |||||
} | |||||
$quoteId = array_search((string)$quote, $rows); | |||||
$map[$quoteId][$baseId] = 1; | |||||
$map[$baseId][$quoteId] = 1; | |||||
} | |||||
return [$rows, $map]; | |||||
} | |||||
protected function clearDuplicates(array $cycles): array | |||||
{ | |||||
$orderedCycles = []; | |||||
foreach ($cycles as $i => $cycle) { | |||||
$orderedCycle = $cycle; | |||||
sort($orderedCycle); | |||||
$orderedCycle = implode(',', $orderedCycle); | |||||
if (in_array($orderedCycle, $orderedCycles)) { | |||||
unset($cycles[$i]); | |||||
continue; | |||||
} | |||||
$orderedCycles[] = $orderedCycle; | |||||
} | |||||
return $cycles; | |||||
} | |||||
protected function findCycle(array $map, array $path, array &$result, int $maxDepth) | |||||
{ | |||||
if (\count($path) > $maxDepth) { | |||||
return; | |||||
} | |||||
$key = $path[\count($path) - 1]; | |||||
$row = $map[$key]; | |||||
unset($row[$key]); | |||||
foreach ($row as $next => $v) { | |||||
if (array_search($next, $path)) { | |||||
continue; | |||||
} | |||||
$nextPath = $path; | |||||
$nextPath[] = $next; | |||||
if (\count($path) > 2 && $path[0] === $next) { | |||||
$result[] = $path; | |||||
continue; | |||||
} | |||||
$this->findCycle($map, $nextPath, $result, $maxDepth); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,50 @@ | |||||
<?php | |||||
declare(strict_types=1); | |||||
namespace App\Command; | |||||
use App\Arbitration\Inner\Stuff\ArbitrationMarketChainsFinder; | |||||
use App\Exchanges\Kuna\KunaExchange; | |||||
use App\Exchanges\MarketInterface; | |||||
use Symfony\Component\Console\Command\Command; | |||||
use Symfony\Component\Console\Input\InputInterface; | |||||
use Symfony\Component\Console\Output\OutputInterface; | |||||
class TestCommand extends Command | |||||
{ | |||||
protected static $defaultName = 'test:test'; | |||||
/** | |||||
* @var KunaExchange | |||||
*/ | |||||
private KunaExchange $kunaExchange; | |||||
public function __construct( | |||||
KunaExchange $kunaExchange | |||||
) | |||||
{ | |||||
parent::__construct(self::$defaultName); | |||||
$this->kunaExchange = $kunaExchange; | |||||
} | |||||
public function execute(InputInterface $input, OutputInterface $output) | |||||
{ | |||||
$finder = new ArbitrationMarketChainsFinder(); | |||||
$chains = $finder->findChainsInExchange($this->kunaExchange, 5); | |||||
foreach ($chains as $chain) { | |||||
echo implode(' -> ', array_map( | |||||
function (MarketInterface $market) { | |||||
return $market->getPair()->getBase() . '/' . $market->getPair()->getQuote(); | |||||
}, | |||||
$chain->getMarkets() | |||||
)) . PHP_EOL; | |||||
} | |||||
return 0; | |||||
} | |||||
} |
@@ -13,13 +13,8 @@ class KunaApiExecutor | |||||
{ | { | ||||
public const API_BASE_URL = 'https://api.kuna.io'; | public const API_BASE_URL = 'https://api.kuna.io'; | ||||
/** | |||||
* @var ClientInterface | |||||
*/ | |||||
private ClientInterface $client; | private ClientInterface $client; | ||||
/** | |||||
* @var KunaSigner | |||||
*/ | |||||
private KunaSigner $signer; | private KunaSigner $signer; | ||||
public function __construct( | public function __construct( | ||||
@@ -42,6 +37,20 @@ class KunaApiExecutor | |||||
) | ) | ||||
); | ); | ||||
return json_decode($response->getBody(), true); | |||||
return json_decode((string)$response->getBody(), true); | |||||
} | |||||
public function getBook(string $symbol): array | |||||
{ | |||||
$response = $this | |||||
->client | |||||
->sendRequest( | |||||
new Request( | |||||
'GET', | |||||
self::API_BASE_URL . '/v3/book/' . $symbol | |||||
) | |||||
); | |||||
return json_decode((string)$response->getBody(), true); | |||||
} | } | ||||
} | } |
@@ -9,6 +9,9 @@ namespace App\Exchanges\Kuna; | |||||
use App\Exchanges\MarketInterface; | use App\Exchanges\MarketInterface; | ||||
use App\Utils\Money\Currency; | use App\Utils\Money\Currency; | ||||
use App\Utils\Money\CurrencyPair; | use App\Utils\Money\CurrencyPair; | ||||
use App\Utils\Money\Money; | |||||
use App\Utils\Order\Book\OrderBook; | |||||
use App\Utils\Order\Order; | |||||
class KunaMarket implements MarketInterface | class KunaMarket implements MarketInterface | ||||
{ | { | ||||
@@ -48,4 +51,29 @@ class KunaMarket implements MarketInterface | |||||
{ | { | ||||
return $this->currencyPair; | return $this->currencyPair; | ||||
} | } | ||||
public function getOrderBook(): OrderBook | |||||
{ | |||||
$asks = []; | |||||
$bids = []; | |||||
foreach ($this->apiExecutor->getBook($this->id) as $orderData) { | |||||
$order = new Order( | |||||
new Money((string)$orderData[0], $this->currencyPair->getQuote()), | |||||
new Money((string)abs($orderData[1]), $this->currencyPair->getBase()), | |||||
); | |||||
$ordersCount = $orderData[2]; | |||||
while ($ordersCount) { | |||||
if ($orderData[1] > 0) { | |||||
$bids[] = clone $order; | |||||
} else { | |||||
$asks[] = clone $order; | |||||
} | |||||
$ordersCount--; | |||||
} | |||||
} | |||||
return new OrderBook($bids, $asks); | |||||
} | |||||
} | } |
@@ -19,4 +19,22 @@ class Currency | |||||
$this->code = $code; | $this->code = $code; | ||||
$this->precision = $precision; | $this->precision = $precision; | ||||
} | } | ||||
public function isEqual(Currency $currency): bool | |||||
{ | |||||
return $this->code === $currency->getCode(); | |||||
} | |||||
/** | |||||
* @return string | |||||
*/ | |||||
public function getCode(): string | |||||
{ | |||||
return $this->code; | |||||
} | |||||
public function __toString() | |||||
{ | |||||
return $this->code; | |||||
} | |||||
} | } |
@@ -6,24 +6,18 @@ declare(strict_types=1); | |||||
namespace App\Utils\Order\Book; | namespace App\Utils\Order\Book; | ||||
use App\Utils\Money\Money; | |||||
use App\Utils\Order\Order; | |||||
class OrderBook | class OrderBook | ||||
{ | { | ||||
/** | |||||
* @var array | |||||
*/ | |||||
private array $bids; | private array $bids; | ||||
/** | |||||
* @var array | |||||
*/ | |||||
private array $asks; | private array $asks; | ||||
/** | /** | ||||
* OrderBook constructor. | * OrderBook constructor. | ||||
* @param Money[] $bids | |||||
* @param Money[] $asks | |||||
* @param Order[] $bids | |||||
* @param Order[] $asks | |||||
*/ | */ | ||||
public function __construct( | public function __construct( | ||||
array $bids, | array $bids, | ||||
@@ -33,4 +27,20 @@ class OrderBook | |||||
$this->bids = $bids; | $this->bids = $bids; | ||||
$this->asks = $asks; | $this->asks = $asks; | ||||
} | } | ||||
/** | |||||
* @return Order[] | |||||
*/ | |||||
public function getAsks() | |||||
{ | |||||
return $this->asks; | |||||
} | |||||
/** | |||||
* @return Order[] | |||||
*/ | |||||
public function getBids() | |||||
{ | |||||
return $this->bids; | |||||
} | |||||
} | } |
@@ -0,0 +1,39 @@ | |||||
<?php | |||||
declare(strict_types=1); | |||||
namespace App\Utils\Order; | |||||
use App\Utils\Money\Money; | |||||
class Order | |||||
{ | |||||
private Money $from; | |||||
private Money $to; | |||||
public function __construct( | |||||
Money $from, | |||||
Money $to | |||||
) | |||||
{ | |||||
$this->from = $from; | |||||
$this->to = $to; | |||||
} | |||||
/** | |||||
* @return Money | |||||
*/ | |||||
public function getFrom(): Money | |||||
{ | |||||
return $this->from; | |||||
} | |||||
/** | |||||
* @return Money | |||||
*/ | |||||
public function getTo(): Money | |||||
{ | |||||
return $this->to; | |||||
} | |||||
} |
@@ -116,6 +116,12 @@ | |||||
"src/Kernel.php" | "src/Kernel.php" | ||||
] | ] | ||||
}, | }, | ||||
"symfony/http-client": { | |||||
"version": "v5.0.7" | |||||
}, | |||||
"symfony/http-client-contracts": { | |||||
"version": "v2.0.1" | |||||
}, | |||||
"symfony/http-foundation": { | "symfony/http-foundation": { | ||||
"version": "v5.0.7" | "version": "v5.0.7" | ||||
}, | }, | ||||