diff --git a/.env.example b/.env.example index 4956528..47777dd 100644 --- a/.env.example +++ b/.env.example @@ -26,6 +26,7 @@ ALPHAVANTAGE_API_KEY= FINNHUB_API_KEY= ALPACA_API_KEY= ALPACA_API_SECRET= +TWELVEDATA_API_SECRET= # Cadence to refresh market data (in minutes) MARKET_DATA_REFRESH=30 diff --git a/README.md b/README.md index 99b25e1..852e994 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Always keep in mind the limitations of LLMs. When in doubt, consult a licensed i ## Market data providers -Investbrain includes an extensible market data provider interface that allows you to retrieve stock market data from multiple providers, such as [Yahoo Finance](https://finance.yahoo.com/), [Finnhub](https://finnhub.io/pricing-stock-api-market-data), [Alpaca](https://alpaca.markets/), and [Alpha Vantage](https://www.alphavantage.co/support/). The interface includes a built-in fallback mechanism to ensure reliable data access, even if a provider fails. +Investbrain includes an extensible market data provider interface that allows you to retrieve stock market data from multiple providers, such as [Yahoo Finance](https://finance.yahoo.com/), [Twelve Data](https://twelvedata.com), [Finnhub](https://finnhub.io/pricing-stock-api-market-data), [Alpaca](https://alpaca.markets/), and [Alpha Vantage](https://www.alphavantage.co/support/). The interface includes a built-in fallback mechanism to ensure reliable data access, even if a provider fails. ### Configuration @@ -138,9 +138,12 @@ There are several optional configurations available when installing using the re | APP_URL | The URL where your Investbrain installation will be accessible | http://localhost | | APP_PORT | The HTTP port exposed by the NGINX container | 8000 | | APP_KEY | Must be set during install - encryption key for various security-related functions | `null` | -| MARKET_DATA_PROVIDER | The market data provider to use (either `yahoo`, `alphavantage`, `alpaca`, or `finnhub`) | yahoo | +| MARKET_DATA_PROVIDER | The market data provider to use (either `yahoo`, `twelvedata`, `alphavantage`, `alpaca`, or `finnhub`) | yahoo | | ALPHAVANTAGE_API_KEY | If using the Alpha Vantage provider | `null` | | FINNHUB_API_KEY | If using the Finnhub provider | `null` | +| ALPACA_API_KEY | If using the Alpaca provider | `null` | +| ALPACA_API_SECRET | If using the Alpaca provider | `null` | +| TWELVEDATA_API_SECRET | If using the Twelve Data provider | `null` | | MARKET_DATA_REFRESH | Cadence to refresh market data in minutes | 30 | | APP_TIMEZONE | Timezone for the application, including daily change captures | UTC | | AI_CHAT_ENABLED | Whether to enable AI chat features | `false` | diff --git a/app/Interfaces/MarketData/TwelveDataMarketData.php b/app/Interfaces/MarketData/TwelveDataMarketData.php new file mode 100644 index 0000000..df7207c --- /dev/null +++ b/app/Interfaces/MarketData/TwelveDataMarketData.php @@ -0,0 +1,169 @@ +createNewClient(); + } + + private function createNewClient() + { + $this->client = Http::withOptions([ + 'headers' => [ + 'content-type' => 'application/json', + 'accept' => 'application/json', + ], + ])->withQueryParameters([ + 'apikey' => config('twelvedata.secret'), + ]); + } + + public function exists(string $symbol): bool + { + + return (bool) $this->quote($symbol); + } + + public function quote(string $symbol): Quote + { + + $response = $this->client + ->baseUrl($this->apiBaseUrl) + ->withQueryParameters(['symbol' => $symbol]) + ->get('price'); + + $quote = $response->json(); + + if (! isset($quote['price'])) { + throw new \Exception('Could not find ticker on Twelve Data'); + } + + $current_market_value = Arr::get($quote, 'price'); + + $fundamental = cache()->remember( + 'twelve-data-symbol-'.$symbol, + 1440, + function () use ($symbol) { + + $this->createNewClient(); + + $response = $this->client + ->baseUrl($this->apiBaseUrl) + ->withQueryParameters(['symbol' => $symbol]) + ->get('quote'); + + return $response->json(); + } + ); + + return new Quote([ + 'name' => Arr::get($fundamental, 'name'), + 'symbol' => $symbol, + 'currency' => Arr::get($fundamental, 'currency'), + 'market_value' => (float) $current_market_value, + 'fifty_two_week_high' => (float) Arr::get($fundamental, 'fifty_two_week.high'), + 'fifty_two_week_low' => (float) Arr::get($fundamental, 'fifty_two_week.low'), + 'meta_data' => [ + 'exchange' => Arr::get($fundamental, 'exchange'), + 'source' => 'twelvedata', + ], + ]); + } + + public function dividends(string $symbol, $startDate, $endDate): Collection + { + + $response = $this->client + ->baseUrl($this->apiBaseUrl) + ->withQueryParameters([ + 'symbol' => $symbol, + 'start_date' => Carbon::parse($startDate)->toDateString(), + 'end_date' => Carbon::parse($endDate)->toDateString(), + ]) + ->get('dividends'); + + $dividends = $response->json('dividends'); + + return collect($dividends) + ->map(function ($dividend) use ($symbol) { + + return new Dividend([ + 'symbol' => $symbol, + 'date' => Arr::get($dividend, 'ex_date'), + 'dividend_amount' => Arr::get($dividend, 'amount'), + ]); + }); + } + + public function splits(string $symbol, $startDate, $endDate): Collection + { + + $response = $this->client + ->baseUrl($this->apiBaseUrl) + ->withQueryParameters([ + 'symbol' => $symbol, + 'start_date' => Carbon::parse($startDate)->toDateString(), + 'end_date' => Carbon::parse($endDate)->toDateString(), + ]) + ->get('splits'); + + $splits = $response->json('splits'); + + return collect($splits) + ->map(function ($split) use ($symbol) { + + return new Split([ + 'symbol' => $symbol, + 'date' => Arr::get($split, 'date'), + 'split_amount' => Arr::get($split, 'from_factor') / Arr::get($split, 'to_factor'), + ]); + }); + } + + public function history(string $symbol, $startDate, $endDate): Collection + { + + $response = $this->client + ->baseUrl($this->apiBaseUrl) + ->withQueryParameters([ + 'symbol' => $symbol, + 'interval' => '1day', + 'start_date' => Carbon::parse($startDate)->toDateString(), + 'end_date' => Carbon::parse($endDate)->toDateString(), + ]) + ->get('time_series'); + + $values = $response->json('values'); + + return collect($values) + ->mapWithKeys(function ($history) use ($symbol) { + + $date = Carbon::parse(Arr::get($history, 'datetime'))->toDateString(); + + return [$date => new Ohlc([ + 'symbol' => $symbol, + 'date' => $date, + 'close' => (float) Arr::get($history, 'close'), + ])]; + }); + } +} diff --git a/config/investbrain.php b/config/investbrain.php index e896cdf..36a796d 100644 --- a/config/investbrain.php +++ b/config/investbrain.php @@ -13,6 +13,7 @@ return [ 'alphavantage' => App\Interfaces\MarketData\AlphaVantageMarketData::class, 'alpaca' => App\Interfaces\MarketData\AlpacaMarketData::class, 'finnhub' => App\Interfaces\MarketData\FinnhubMarketData::class, + 'twelvedata' => App\Interfaces\MarketData\TwelveDataMarketData::class, 'fake' => App\Interfaces\MarketData\FakeMarketData::class, ], diff --git a/config/twelvedata.php b/config/twelvedata.php new file mode 100644 index 0000000..ac8e92a --- /dev/null +++ b/config/twelvedata.php @@ -0,0 +1,7 @@ + env('TWELVEDATA_API_SECRET'), +];