Compare commits

..

4 Commits

Author SHA1 Message Date
hackerESQ eae4422ad8 fix: alphavantage multiply by string 2025-08-26 19:29:10 -05:00
hackerESQ 53d463b8b5 chore: upgrade deps 2025-08-26 19:27:26 -05:00
hackerESQ 827644bb32 fix: yahoo driver 2025-08-26 19:13:00 -05:00
hackerESQ 21e8672a12 feat: add twelve data market data provider 2025-08-26 18:26:12 -05:00
9 changed files with 1122 additions and 537 deletions
+1
View File
@@ -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
+5 -2
View File
@@ -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` |
@@ -69,7 +69,7 @@ class AlphaVantageMarketData implements MarketDataInterface
? Arr::get($fundamental, 'DividendDate')
: null,
'dividend_yield' => Arr::get($fundamental, 'DividendYield') != 'None'
? Arr::get($fundamental, 'DividendYield') * 100
? ((float) Arr::get($fundamental, 'DividendYield')) * 100
: null,
'meta_data' => [
'industry' => Arr::get($fundamental, 'Industry'),
@@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
namespace App\Interfaces\MarketData;
use App\Interfaces\MarketData\Types\Dividend;
use App\Interfaces\MarketData\Types\Ohlc;
use App\Interfaces\MarketData\Types\Quote;
use App\Interfaces\MarketData\Types\Split;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
class TwelveDataMarketData implements MarketDataInterface
{
public PendingRequest $client;
public string $apiBaseUrl = 'https://api.twelvedata.com/';
public function __construct()
{
$this->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'),
])];
});
}
}
@@ -21,7 +21,10 @@ class YahooMarketData implements MarketDataInterface
{
// create yahoo finance client factory
$this->client = YahooFinance::createApiClient();
$this->client = YahooFinance::createApiClient(
clientOptions: ['headers' => ['User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36']],
cache: app('cache.psr6')
);
}
public function exists(string $symbol): bool
+1
View File
@@ -26,6 +26,7 @@
"robsontenorio/mary": "^1.35",
"scheb/yahoo-finance-api": "^5.0",
"staudenmeir/eloquent-has-many-deep": "^1.20",
"symfony/cache": "^7.3",
"tschucki/alphavantage-laravel": "^0.0"
},
"require-dev": {
Generated
+933 -533
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -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,
],
+7
View File
@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
return [
'secret' => env('TWELVEDATA_API_SECRET'),
];