adds finnhub market data provider
This commit is contained in:
@@ -61,6 +61,7 @@ MAIL_FROM_NAME="${APP_NAME}"
|
||||
MARKET_DATA_PROVIDER=yahoo
|
||||
MARKET_DATA_REFRESH=30
|
||||
ALPHAVANTAGE_API_KEY=
|
||||
FINNHUB_API_KEY=
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
|
||||
@@ -8,7 +8,7 @@ Investbrain helps you manage and track the performance of your investments.
|
||||
|
||||
## Under the hood
|
||||
|
||||
Investbrain is a Laravel PHP web application that leverages Livewire, Mary UI, and Tailwind for its frontend. Most databases should work, including MySQL and SQLite. Out of the box, we feature two market data providers: Yahoo Finance and Alpha Vantage. But we also offer an extensible market data provider interface for intrepid developers to create their own! Finally, of course we have robust support for i18n, a11y, and dark mode.
|
||||
Investbrain is a Laravel PHP web application that leverages Livewire, Mary UI, and Tailwind for its frontend. Most databases should work, including MySQL and SQLite. Out of the box, we feature two market data providers: [Yahoo Finance](https://finance.yahoo.com/), [Finnhub](https://finnhub.io/pricing-stock-api-market-data), and [Alpha Vantage](https://www.alphavantage.co/support/). But we also offer an extensible market data provider interface for intrepid developers to create their own! Finally, of course we have robust support for i18n, a11y, and dark mode.
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class AlphaVantageMarketData implements MarketDataInterface
|
||||
return $this->quote($symbol)->isNotEmpty();
|
||||
}
|
||||
|
||||
public function quote($symbol): Collection
|
||||
public function quote(String $symbol): Collection
|
||||
{
|
||||
$quote = Alphavantage::core()->quoteEndpoint($symbol);
|
||||
$quote = Arr::get($quote, 'Global Quote', []);
|
||||
@@ -49,7 +49,7 @@ class AlphaVantageMarketData implements MarketDataInterface
|
||||
]);
|
||||
}
|
||||
|
||||
public function dividends($symbol, $startDate, $endDate): Collection
|
||||
public function dividends(String $symbol, $startDate, $endDate): Collection
|
||||
{
|
||||
$dividends = Alphavantage::fundamentals()->dividends($symbol);
|
||||
|
||||
@@ -67,7 +67,7 @@ class AlphaVantageMarketData implements MarketDataInterface
|
||||
});
|
||||
}
|
||||
|
||||
public function splits($symbol, $startDate, $endDate): Collection
|
||||
public function splits(String $symbol, $startDate, $endDate): Collection
|
||||
{
|
||||
|
||||
$splits = Alphavantage::fundamentals()->splits($symbol);
|
||||
@@ -86,7 +86,7 @@ class AlphaVantageMarketData implements MarketDataInterface
|
||||
});
|
||||
}
|
||||
|
||||
public function history($symbol, $startDate, $endDate): Collection
|
||||
public function history(String $symbol, $startDate, $endDate): Collection
|
||||
{
|
||||
|
||||
$history = Alphavantage::timeSeries()->daily($symbol, 'full');
|
||||
|
||||
@@ -13,7 +13,7 @@ class FakeMarketData implements MarketDataInterface
|
||||
return true;
|
||||
}
|
||||
|
||||
public function quote($symbol): Collection
|
||||
public function quote(String $symbol): Collection
|
||||
{
|
||||
|
||||
return collect([
|
||||
@@ -31,7 +31,7 @@ class FakeMarketData implements MarketDataInterface
|
||||
]);
|
||||
}
|
||||
|
||||
public function dividends($symbol, $startDate, $endDate): Collection
|
||||
public function dividends(String $symbol, $startDate, $endDate): Collection
|
||||
{
|
||||
|
||||
return collect([
|
||||
@@ -53,7 +53,7 @@ class FakeMarketData implements MarketDataInterface
|
||||
]);
|
||||
}
|
||||
|
||||
public function splits($symbol, $startDate, $endDate): Collection
|
||||
public function splits(String $symbol, $startDate, $endDate): Collection
|
||||
{
|
||||
|
||||
return collect([
|
||||
@@ -65,7 +65,7 @@ class FakeMarketData implements MarketDataInterface
|
||||
]);
|
||||
}
|
||||
|
||||
public function history($symbol, $startDate, $endDate): Collection
|
||||
public function history(String $symbol, $startDate, $endDate): Collection
|
||||
{
|
||||
$numDays = Carbon::parse($startDate)->diffInDays($endDate, true);
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Interfaces\MarketData;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class FinnhubMarketData implements MarketDataInterface
|
||||
{
|
||||
public \Finnhub\Api\DefaultApi $client;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
$this->client = new \Finnhub\Api\DefaultApi(
|
||||
new \GuzzleHttp\Client(),
|
||||
\Finnhub\Configuration::getDefaultConfiguration()->setApiKey('token', config('finnhub.key'))
|
||||
);
|
||||
}
|
||||
public function exists(String $symbol): Bool
|
||||
{
|
||||
|
||||
return $this->quote($symbol)->isNotEmpty();
|
||||
}
|
||||
|
||||
public function quote($symbol): Collection
|
||||
{
|
||||
|
||||
|
||||
$quote = $this->client->quote($symbol);
|
||||
|
||||
$fundamental = cache()->tags(['quote', 'finnhub', $symbol])->remember(
|
||||
'symbol-'.$symbol,
|
||||
1440,
|
||||
function () use ($symbol) {
|
||||
return $this->client->companyBasicFinancials($symbol, "all");
|
||||
}
|
||||
);
|
||||
|
||||
if (empty($fundamental)) return collect();
|
||||
|
||||
return collect([
|
||||
'name' => Arr::get($fundamental, 'metric.name'),
|
||||
'symbol' => $symbol,
|
||||
'market_value' => Arr::get($quote, 'c'),
|
||||
'fifty_two_week_high' => Arr::get($fundamental, 'metric.52WeekHigh'),
|
||||
'fifty_two_week_low' => Arr::get($fundamental, 'metric.52WeekLow'),
|
||||
'forward_pe' => Arr::get($fundamental, 'metric.forwardPE'), // confirm
|
||||
'trailing_pe' => Arr::get($fundamental, 'metric.trailingPE'), // confirm
|
||||
'market_cap' => Arr::get($fundamental, 'metric.marketCapitalization'), // confirm
|
||||
'book_value' => Arr::get($fundamental, 'metric.bookValuePerShare'), // confirm
|
||||
'last_dividend_date' => Arr::get($fundamental, 'metric.lastDivDate'), // confirm
|
||||
'dividend_yield' => Arr::get($fundamental, 'metric.dividendYield'), // confirm
|
||||
]);
|
||||
}
|
||||
|
||||
public function dividends($symbol, $startDate, $endDate): Collection
|
||||
{
|
||||
$dividends = $this->client->stockDividends($symbol, $startDate->format('Y-m-d'), $endDate->format('Y-m-d'));
|
||||
|
||||
return collect($dividends)->map(function($dividend) use ($symbol) {
|
||||
|
||||
return [
|
||||
'symbol' => $symbol,
|
||||
'date' => Carbon::parse(Arr::get($dividend, 'date'))
|
||||
->format('Y-m-d H:i:s'),
|
||||
'dividend_amount' => Arr::get($dividend, 'amount'),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
public function splits($symbol, $startDate, $endDate): Collection
|
||||
{
|
||||
|
||||
$splits = $this->client->stockSplits($symbol, $startDate->format('Y-m-d'), $endDate->format('Y-m-d'));
|
||||
|
||||
return collect($splits)->map(function($split) use ($symbol) {
|
||||
|
||||
return [
|
||||
'symbol' => $symbol,
|
||||
'date' => Carbon::parse(Arr::get($split, 'date'))
|
||||
->format('Y-m-d H:i:s'),
|
||||
'split_amount' => Arr::get($split, 'toFactor') / Arr::get($split, 'fromFactor'),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
public function history($symbol, $startDate, $endDate): Collection
|
||||
{
|
||||
|
||||
$history = $this->client->stockCandles($symbol, "D", $startDate->timestamp, $endDate->timestamp);
|
||||
|
||||
$timestamps = Arr::get($history, 't', []);
|
||||
$closes = Arr::get($history, 'c', []);
|
||||
|
||||
return collect($timestamps)->mapWithKeys(function ($timestamp, $index) use ($symbol, $closes) {
|
||||
$date = Carbon::createFromTimestamp($timestamp)->format('Y-m-d');
|
||||
return [ $date => [
|
||||
'symbol' => $symbol,
|
||||
'date' => $date,
|
||||
'close' => (float) $closes[$index],
|
||||
]];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ class YahooMarketData implements MarketDataInterface
|
||||
return $this->quote($symbol)->isNotEmpty();
|
||||
}
|
||||
|
||||
public function quote($symbol): Collection
|
||||
public function quote(String $symbol): Collection
|
||||
{
|
||||
|
||||
$quote = $this->client->getQuote($symbol);
|
||||
@@ -44,7 +44,7 @@ class YahooMarketData implements MarketDataInterface
|
||||
]);
|
||||
}
|
||||
|
||||
public function dividends($symbol, $startDate, $endDate): Collection
|
||||
public function dividends(String $symbol, $startDate, $endDate): Collection
|
||||
{
|
||||
|
||||
return collect($this->client->getHistoricalDividendData($symbol, $startDate, $endDate))
|
||||
@@ -58,7 +58,7 @@ class YahooMarketData implements MarketDataInterface
|
||||
});
|
||||
}
|
||||
|
||||
public function splits($symbol, $startDate, $endDate): Collection
|
||||
public function splits(String $symbol, $startDate, $endDate): Collection
|
||||
{
|
||||
|
||||
return collect($this->client->getHistoricalSplitData($symbol, $startDate, $endDate))
|
||||
@@ -73,7 +73,7 @@ class YahooMarketData implements MarketDataInterface
|
||||
});
|
||||
}
|
||||
|
||||
public function history($symbol, $startDate, $endDate): Collection
|
||||
public function history(String $symbol, $startDate, $endDate): Collection
|
||||
{
|
||||
|
||||
return collect($this->client->getHistoricalQuoteData($symbol, ApiClient::INTERVAL_1_DAY, $startDate, $endDate))
|
||||
|
||||
@@ -11,15 +11,16 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
|
||||
$market_data = config(
|
||||
"investbrain." .
|
||||
config('investbrain.default', 'yahoo')
|
||||
);
|
||||
if (!in_array(
|
||||
$interface = config('investbrain.default', 'yahoo'),
|
||||
array_keys(config('investbrain.interfaces', []))
|
||||
)) {
|
||||
throw new \Exception("Error: '$interface' is not a valid market data interface.");
|
||||
}
|
||||
|
||||
$this->app->bind(
|
||||
\App\Interfaces\MarketData\MarketDataInterface::class,
|
||||
$market_data
|
||||
config("investbrain.interfaces.$interface")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+8
-1
@@ -6,6 +6,7 @@
|
||||
"license": "CC-BY-NC 4.0",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"finnhub/client": "dev-master",
|
||||
"laravel/framework": "^11.9",
|
||||
"laravel/jetstream": "^5.1",
|
||||
"laravel/sanctum": "^4.0",
|
||||
@@ -27,6 +28,12 @@
|
||||
"nunomaduro/collision": "^8.0",
|
||||
"phpunit/phpunit": "^11.0.1"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/hackerESQ/finnhub-php"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"files": [
|
||||
"app/Support/Helpers.php"
|
||||
@@ -73,6 +80,6 @@
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
|
||||
Generated
+67
-3
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "40816463478b2d00c6a7c18e71aa7e7c",
|
||||
"content-hash": "94cd2d270d276ef1634b6b44dc12145e",
|
||||
"packages": [
|
||||
{
|
||||
"name": "bacon/bacon-qr-code",
|
||||
@@ -902,6 +902,68 @@
|
||||
},
|
||||
"time": "2023-11-17T15:01:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "finnhub/client",
|
||||
"version": "dev-master",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/hackerESQ/finnhub-php.git",
|
||||
"reference": "1f1b35a0c0a6a68f9a791e3ac5cdb6f44ff69d80"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/hackerESQ/finnhub-php/zipball/1f1b35a0c0a6a68f9a791e3ac5cdb6f44ff69d80",
|
||||
"reference": "1f1b35a0c0a6a68f9a791e3ac5cdb6f44ff69d80",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-curl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"guzzlehttp/guzzle": "^7.2",
|
||||
"php": "^7.3 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^2.12",
|
||||
"phpunit/phpunit": "^8.0 || ^9.0"
|
||||
},
|
||||
"default-branch": true,
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Finnhub\\": "lib/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Finnhub\\Test\\": "test/"
|
||||
}
|
||||
},
|
||||
"license": [
|
||||
"unlicense"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "OpenAPI-Generator contributors",
|
||||
"homepage": "https://openapi-generator.tech"
|
||||
}
|
||||
],
|
||||
"description": "Official Finnhub stock API PHP library. https://finnhub.io/",
|
||||
"homepage": "https://openapi-generator.tech",
|
||||
"keywords": [
|
||||
"api",
|
||||
"openapi",
|
||||
"openapi-generator",
|
||||
"openapitools",
|
||||
"php",
|
||||
"rest",
|
||||
"sdk"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/hackerESQ/finnhub-php/tree/master"
|
||||
},
|
||||
"time": "2024-09-13T01:29:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "fruitcake/php-cors",
|
||||
"version": "v1.3.0",
|
||||
@@ -9805,8 +9867,10 @@
|
||||
}
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"minimum-stability": "dev",
|
||||
"stability-flags": {
|
||||
"finnhub/client": 20
|
||||
},
|
||||
"prefer-stable": true,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'key' => env('FINNHUB_API_KEY'),
|
||||
];
|
||||
@@ -6,9 +6,12 @@ return [
|
||||
|
||||
'default' => env('MARKET_DATA_PROVIDER', 'yahoo'),
|
||||
|
||||
'interfaces' => [
|
||||
'yahoo' => App\Interfaces\MarketData\YahooMarketData::class,
|
||||
'alphavantage' => App\Interfaces\MarketData\AlphaVantageMarketData::class,
|
||||
'finnhub' => App\Interfaces\MarketData\FinnhubMarketData::class,
|
||||
'fake' => App\Interfaces\MarketData\FakeMarketData::class,
|
||||
],
|
||||
|
||||
'self_hosted' => env('SELF_HOSTED', true)
|
||||
];
|
||||
Reference in New Issue
Block a user