tigusigalpa / coinquant-php
Official PHP/Laravel SDK for the CoinQuant Public API
Requires
- php: >=8.1
- guzzlehttp/guzzle: ^7.8
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- phpunit/phpunit: ^10.0|^11.0
README
PHP & Laravel SDK for the CoinQuant Public API
Installation · Quick start · Laravel · Streaming · Backtesting · API reference · FAQ
Related Projects and Documentation
- coinquant-php Wiki - Complete guides, API notes, and practical usage examples.
- coinquant-go - The companion CoinQuant SDK for Golang projects.
About
CoinQuant takes a trading idea described in plain English and turns it into a backtestable strategy. This SDK wraps the public API so you don't have to deal with raw cURL, SSE parsing, or polling loops by hand.
Works as a standalone PHP library. In Laravel it registers automatically — ServiceProvider and Facade included.
Features
- PHP 8.1+ and Laravel 10 / 11 / 12 / 13 — strictly typed, no legacy baggage.
- Standalone or Laravel —
new CoinQuantClient($token)anywhere, or theCoinQuantfacade with auto-discovery. - All 37 public endpoints — health, chats, strategies, versions, backtests, reports, templates, credits, community.
- Streaming via generators —
prompt(),streamPrompt(),streamChatMessage()classify events intochat,strategy,report,error, orunknown. - One-call backtests —
createBacktestAndWait()submits, polls, and returns results with CSV exports. - Schema materialization —
finalizeChat()turns an AI-drafted schema into a backtestable strategy version. - Actionable exceptions —
CoinQuantExceptioncarries HTTP status,request_id, machinecode, and message. - Guzzle under the hood — pass your own configured client for retries, proxies, or tracing.
Installation
composer require tigusigalpa/coinquant-php
Requires PHP 8.1+. Guzzle is pulled in automatically.
Quick start
Create a client and check your balance:
use CoinQuant\CoinQuantClient; $client = new CoinQuantClient(getenv('COINQUANT_TOKEN')); $credits = $client->getCredits(); echo "Available credits: {$credits['available_credits_total']}";
Every method returns the decoded data payload as a plain PHP array — standard $response['field'] access.
Authentication
CoinQuant uses a JWT bearer token. To get one:
- Open the CoinQuant web app.
- Go to Settings → Service Accounts → New key.
- Copy the one-time
access_token(you won't see it again).
Tokens last 30 days and work on every endpoint except the human-profile surfaces (/v1/me, /v1/billing/*). Keep
the token in an environment variable — COINQUANT_TOKEN — and don't commit it.
$client = new CoinQuantClient(getenv('COINQUANT_TOKEN'));
Custom Guzzle client
If you need retries, a proxy, or request logging, pass a pre-configured Guzzle client:
use GuzzleHttp\Client as GuzzleClient; $http = new GuzzleClient([ 'timeout' => 60, 'headers' => ['Authorization' => 'Bearer ' . getenv('COINQUANT_TOKEN')], ]); $client = new CoinQuantClient(getenv('COINQUANT_TOKEN'), $http);
Laravel integration
The package auto-registers on Laravel 10+ — no manual wiring needed. Set your token:
# .env COINQUANT_TOKEN=your_token_here
Publish the config file if you need to change the base URL:
php artisan vendor:publish --tag=coinquant-config
Then use the facade anywhere — controllers, jobs, commands:
use CoinQuant\Laravel\Facades\CoinQuant; $credits = CoinQuant::getCredits(); $result = CoinQuant::prompt('Research BTC volatility this week.');
The client is also bound as a singleton, so you can type-hint it for dependency injection:
use CoinQuant\CoinQuantClient; public function __construct(private CoinQuantClient $coinquant) {}
On Laravel versions without package auto-discovery, register
CoinQuant\Laravel\CoinQuantServiceProvider::classand theCoinQuantalias manually inconfig/app.php.
Streaming
The AI engine responds over Server-Sent Events. The SDK reads the stream and returns a classified StreamResult:
$result = $client->prompt('Generate a BTCUSDT 1h EMA crossover strategy.'); echo $result->text; // the assembled text reply echo $result->type; // 'chat' | 'strategy' | 'report' | 'error' | 'unknown'
To react to each event as it arrives (e.g. stream tokens to a browser), iterate the generator directly:
use CoinQuant\Streaming\StreamEvent; foreach ($client->streamPrompt('Explain the RSI indicator.') as $event) { if ($event->type === StreamEvent::TYPE_CHAT) { echo $event->text; // flush to the client in real time } }
Response classification
CoinQuant decides the response shape, not the caller — a "simple" prompt can come back as a full report. The SDK classifies based on the actual events, in this order:
| Priority | Condition | type |
|---|---|---|
| 1 | Any error event, or HTTP status ≥ 400 |
error |
| 2 | A result event carrying strategy_id, strategy_version_id, or schema |
strategy |
| 3 | Any report_block or report event |
report |
| 4 | Only chunk text events |
chat |
| 5 | None of the above | unknown |
Schema-only strategies
When the AI returns a strategy schema but no strategy_version_id, it's a blueprint that isn't backtestable yet.
Materialize it with one call:
if ($result->type === 'strategy' && $result->strategyVersionId === null && $result->chatId !== null) { $strategy = $client->finalizeChat($result->chatId, 'EMA Crossover', 'My first strategy'); echo "Ready to backtest: {$strategy['latest_version']['id']}"; }
Backtesting
Backtests run asynchronously. createBacktestAndWait() submits the run, polls until it reaches a terminal status
(completed, failed, cancelled, error, timeout), and on success returns the results plus both CSV exports:
$outcome = $client->createBacktestAndWait('STRATEGY_VERSION_ID', 900 /* timeout s */, 5 /* poll s */); echo "Status: {$outcome['detail']['status']}"; if ($outcome['results'] !== null) { $metrics = $outcome['results']['metrics']; echo "Total Return: {$metrics['Total Return']}%"; echo "Sharpe: {$metrics['Sharpe Ratio']}"; // $outcome['summary_csv'] and $outcome['trades_csv'] hold the raw CSV exports. }
If you'd rather drive the loop yourself, use createBacktest(), getBacktest(), getBacktestResults(),
getBacktestSummaryCsv(), and getBacktestTradesCsv() directly.
Handling errors
Any non-2xx response throws a CoinQuantException with the details you need to debug:
use CoinQuant\Exceptions\CoinQuantException; try { $credits = $client->getCredits(); } catch (CoinQuantException $e) { logger()->error('CoinQuant call failed', [ 'status' => $e->getHttpStatus(), 'code' => $e->getErrorCode(), 'request_id' => $e->getRequestId(), 'message' => $e->getMessage(), ]); if ($e->getHttpStatus() === 402) { // Out of credits — top up and retry. } }
Quote the request_id when contacting CoinQuant support — they can trace the exact call.
Complete workflow
From idea to metrics:
// 1. Describe the idea and let the AI draft a strategy. $res = $client->prompt('Long BTCUSDT when price crosses above the 200 EMA on 1h, exit on cross below.'); // 2. Materialize it if it came back schema-only. $versionId = $res->strategyVersionId ?? $client->finalizeChat($res->chatId, 'EMA 200 Crossover', '')['latest_version']['id']; // 3. Backtest and wait for the verdict. $outcome = $client->createBacktestAndWait($versionId, 900, 5); print_r($outcome['results']['metrics']);
Full API reference
| Endpoint | Method |
|---|---|
GET /health |
health() |
GET /v1/chats |
listChats($options = []) |
POST /v1/chats |
createChat($data) |
GET /v1/chats/{id} |
getChat($id) |
PATCH /v1/chats/{id} |
updateChat($id, $data) |
DELETE /v1/chats/{id} |
deleteChat($id) |
GET /v1/chats/{id}/messages |
listMessages($id, $options = []) |
POST /v1/chats/{id}/messages |
appendMessage($id, $data) |
POST /v1/chats/{id}/messages:stream |
streamChatMessage($id, $content, $options = []) |
POST /v1/prompts/stream |
streamPrompt($message, $chatId = null, $options = []) |
GET /v1/strategies |
listStrategies($options = []) |
POST /v1/strategies |
createStrategy($data) / finalizeChat(...) |
GET /v1/strategies/{id} |
getStrategy($id) |
PATCH /v1/strategies/{id} |
updateStrategy($id, $data) |
GET /v1/strategies/{id}/versions |
listStrategyVersions($id, $options = []) |
GET /v1/strategies/{id}/versions/{vid} |
getStrategyVersion($id, $vid) |
PATCH /v1/strategies/{id}/versions/{vid} |
updateStrategyVersion($id, $vid, $data) |
GET /v1/backtests |
listBacktests($options = []) |
POST /v1/backtests |
createBacktest($strategyVersionId) |
GET /v1/backtests/{id} |
getBacktest($id) |
GET /v1/backtests/{id}/results |
getBacktestResults($id) |
GET /v1/backtests/{id}/exports/summary.csv |
getBacktestSummaryCsv($id) |
GET /v1/backtests/{id}/exports/trades.csv |
getBacktestTradesCsv($id) |
POST /v1/backtests/compare |
compareBacktests($ids) |
POST /v1/backtests/{id}/duplicate |
duplicateBacktest($id) |
GET /v1/reports |
listReports($options = []) |
GET /v1/reports/{id} |
getReport($id) |
GET /v1/templates |
listTemplates($options = []) |
GET /v1/templates/{id} |
getTemplate($id) |
GET /v1/credits |
getCredits() |
GET /v1/credits/usage |
getCreditUsage($options = []) |
GET /v1/credits/transactions |
listCreditTransactions($options = []) |
GET /v1/credits/estimates/tick |
estimateTickCredits($days, $minutes) |
GET /v1/community/leaderboards |
listLeaderboards($options = []) |
GET /v1/community/backtests |
listCommunityBacktests($options = []) |
GET /v1/community/backtests/{id} |
getCommunityBacktest($id, $include = '') |
GET /v1/community/me |
getCommunityMe($include = '') |
GET /v1/community/me/activities |
listCommunityActivities($options = []) |
Examples
Each file in examples/ is a self-contained script:
| File | What it shows |
|---|---|
auth_check.php |
Verify connectivity and read your credit balance |
send_prompt.php |
Stream a prompt and materialize a schema-only strategy |
backtest_poll.php |
Kick off a backtest and wait for the results |
community_leaderboard.php |
Browse the public leaderboard (no token required) |
export COINQUANT_TOKEN=your_token_here
php examples/auth_check.php
FAQ
Do I need a token for everything?
No. The health check and community endpoints (leaderboards, backtests) work anonymously. Everything else needs a
service-account token.
Why did my "simple" prompt come back as a report?
That's expected — CoinQuant decides the response shape. Always branch on $result->type rather than on what you asked
for.
How long do tokens last?
30 days. When one expires you'll get a 401; create a new key in Settings → Service Accounts.
A backtest keeps returning 409 on results.
The run isn't finished yet. Use createBacktestAndWait() (it polls for you), or keep calling getBacktest() until the
status is terminal.
Does it work outside Laravel?
Yes. CoinQuantClient is a plain PHP class with no framework dependencies — the Laravel provider and facade are
optional.
Can I customize timeouts or add retries? Yes — pass your own configured Guzzle client to the constructor.
Contributing
Issues and pull requests are welcome. Target PHP 8.1+, keep the code strictly typed, run php -l on changed files, and
describe your change clearly.
License
MIT License.
Author
Igor Sazonov
- GitHub: @tigusigalpa
- Email: sovletig@gmail.com