adds finnhub market data provider

This commit is contained in:
hackerESQ
2024-09-12 21:05:01 -05:00
parent 166fdee521
commit 2075d8273c
11 changed files with 213 additions and 26 deletions
+1
View File
@@ -61,6 +61,7 @@ MAIL_FROM_NAME="${APP_NAME}"
MARKET_DATA_PROVIDER=yahoo MARKET_DATA_PROVIDER=yahoo
MARKET_DATA_REFRESH=30 MARKET_DATA_REFRESH=30
ALPHAVANTAGE_API_KEY= ALPHAVANTAGE_API_KEY=
FINNHUB_API_KEY=
AWS_ACCESS_KEY_ID= AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY= AWS_SECRET_ACCESS_KEY=
+1 -1
View File
@@ -8,7 +8,7 @@ Investbrain helps you manage and track the performance of your investments.
## Under the hood ## 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 ## Installation
@@ -15,7 +15,7 @@ class AlphaVantageMarketData implements MarketDataInterface
return $this->quote($symbol)->isNotEmpty(); return $this->quote($symbol)->isNotEmpty();
} }
public function quote($symbol): Collection public function quote(String $symbol): Collection
{ {
$quote = Alphavantage::core()->quoteEndpoint($symbol); $quote = Alphavantage::core()->quoteEndpoint($symbol);
$quote = Arr::get($quote, 'Global Quote', []); $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); $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); $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'); $history = Alphavantage::timeSeries()->daily($symbol, 'full');
+4 -4
View File
@@ -13,7 +13,7 @@ class FakeMarketData implements MarketDataInterface
return true; return true;
} }
public function quote($symbol): Collection public function quote(String $symbol): Collection
{ {
return collect([ 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([ 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([ 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); $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(); return $this->quote($symbol)->isNotEmpty();
} }
public function quote($symbol): Collection public function quote(String $symbol): Collection
{ {
$quote = $this->client->getQuote($symbol); $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)) 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)) 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)) return collect($this->client->getHistoricalQuoteData($symbol, ApiClient::INTERVAL_1_DAY, $startDate, $endDate))
+7 -6
View File
@@ -11,15 +11,16 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function register(): void public function register(): void
{ {
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.");
}
$market_data = config(
"investbrain." .
config('investbrain.default', 'yahoo')
);
$this->app->bind( $this->app->bind(
\App\Interfaces\MarketData\MarketDataInterface::class, \App\Interfaces\MarketData\MarketDataInterface::class,
$market_data config("investbrain.interfaces.$interface")
); );
} }
+8 -1
View File
@@ -6,6 +6,7 @@
"license": "CC-BY-NC 4.0", "license": "CC-BY-NC 4.0",
"require": { "require": {
"php": "^8.2", "php": "^8.2",
"finnhub/client": "dev-master",
"laravel/framework": "^11.9", "laravel/framework": "^11.9",
"laravel/jetstream": "^5.1", "laravel/jetstream": "^5.1",
"laravel/sanctum": "^4.0", "laravel/sanctum": "^4.0",
@@ -27,6 +28,12 @@
"nunomaduro/collision": "^8.0", "nunomaduro/collision": "^8.0",
"phpunit/phpunit": "^11.0.1" "phpunit/phpunit": "^11.0.1"
}, },
"repositories": [
{
"type": "vcs",
"url": "https://github.com/hackerESQ/finnhub-php"
}
],
"autoload": { "autoload": {
"files": [ "files": [
"app/Support/Helpers.php" "app/Support/Helpers.php"
@@ -73,6 +80,6 @@
"php-http/discovery": true "php-http/discovery": true
} }
}, },
"minimum-stability": "stable", "minimum-stability": "dev",
"prefer-stable": true "prefer-stable": true
} }
Generated
+67 -3
View File
@@ -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": "40816463478b2d00c6a7c18e71aa7e7c", "content-hash": "94cd2d270d276ef1634b6b44dc12145e",
"packages": [ "packages": [
{ {
"name": "bacon/bacon-qr-code", "name": "bacon/bacon-qr-code",
@@ -902,6 +902,68 @@
}, },
"time": "2023-11-17T15:01:25+00:00" "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", "name": "fruitcake/php-cors",
"version": "v1.3.0", "version": "v1.3.0",
@@ -9805,8 +9867,10 @@
} }
], ],
"aliases": [], "aliases": [],
"minimum-stability": "stable", "minimum-stability": "dev",
"stability-flags": [], "stability-flags": {
"finnhub/client": 20
},
"prefer-stable": true, "prefer-stable": true,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
+5
View File
@@ -0,0 +1,5 @@
<?php
return [
'key' => env('FINNHUB_API_KEY'),
];
+6 -3
View File
@@ -6,9 +6,12 @@ return [
'default' => env('MARKET_DATA_PROVIDER', 'yahoo'), 'default' => env('MARKET_DATA_PROVIDER', 'yahoo'),
'yahoo' => App\Interfaces\MarketData\YahooMarketData::class, 'interfaces' => [
'alphavantage' => App\Interfaces\MarketData\AlphaVantageMarketData::class, 'yahoo' => App\Interfaces\MarketData\YahooMarketData::class,
'fake' => App\Interfaces\MarketData\FakeMarketData::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) 'self_hosted' => env('SELF_HOSTED', true)
]; ];