adds fallback capability to market data providers
This commit is contained in:
@@ -32,7 +32,7 @@ docker compose up
|
||||
|
||||
In the previous step, all of the default configurations are set automatically. This includes creating a .env file and setting the required Laravel `APP_KEY`.
|
||||
|
||||
If everything worked as expected, you should now be able to access Investbrain in the browser at:
|
||||
If everything worked as expected, you should now be able to access Investbrain in the browser at. You should create an account by visiting:
|
||||
|
||||
```bash
|
||||
http://localhost:8000/register
|
||||
@@ -40,21 +40,63 @@ http://localhost:8000/register
|
||||
|
||||
Congrats! You've just installed Investbrain!
|
||||
|
||||
## Configuration (optional)
|
||||
## Market data providers
|
||||
|
||||
There are several configurations available when installing using the recommended [Docker method](#Installation). These options are configurable using an environment file. Changes can be made in your [.env](https://github.com/investbrainapp/investbrain/blob/main/.env.example) file before installation.
|
||||
Investbrain includes an extensible market data provider interface that allows you to retrieve stock market data from multiple providers, such as Yahoo Finance, Alpha Vantage, or Finnhub. The interface includes a built-in fallback mechanism to ensure reliable data access, even if a provider fails.
|
||||
|
||||
### Configuration
|
||||
|
||||
You can specify the provider you want to use in your .env file:
|
||||
|
||||
```bash
|
||||
MARKET_DATA_PROVIDER=yahoo
|
||||
```
|
||||
|
||||
You can also use Investbrain's built-in fallback mechanism to ensure reliable data access, even if a provider fails. If any provider fails, Investbrain will automatically attempt to retrieve data from the next available provider, continuing through your configured providers until one returns successfully.
|
||||
|
||||
Your selected providers should be listed in your .env file. Each should be separated by a comma:
|
||||
|
||||
```bash
|
||||
MARKET_DATA_PROVIDER=yahoo,alphavantage
|
||||
```
|
||||
|
||||
In the above example, Yahoo Finance will be attempted first and the Alpha Vantage provider will be used as the fallback. If Yahoo Finance fails to retrieve market data, the application will automatically try Alpha Vantage.
|
||||
|
||||
### Custom providers
|
||||
|
||||
If you wish to create your own market data provider, you can create your own implementation of the [MarketDataInterface](https://github.com/investbrainapp/investbrain/blob/main/app/Interfaces/MarketData/MarketDataInterface.php). You can refer to any existing market data implementation as an examples.
|
||||
|
||||
Once you've created your market data implementation, be sure add your custom provider to the Investbrain configuration file, under the interfaces section:
|
||||
|
||||
```php
|
||||
|
||||
'interfaces' => [
|
||||
// * * *
|
||||
'custom_provider' => \App\Services\CustomProviderMarketData::class,
|
||||
// * * *
|
||||
],
|
||||
```
|
||||
|
||||
And add your custom provider to your .env file:
|
||||
|
||||
```bash
|
||||
MARKET_DATA_PROVIDER=yahoo,alphavantage,custom_provider
|
||||
```
|
||||
|
||||
Feel free to submit a PR with any custom providers you create.
|
||||
|
||||
## Configuration
|
||||
|
||||
There are several optional configurations available when installing using the recommended [Docker method](#Installation). These options are configurable using an environment file. Changes can be made in your [.env](https://github.com/investbrainapp/investbrain/blob/main/.env.example) file before installation.
|
||||
|
||||
| Option | Description | Default |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| APP_URL | The URL where your Investbrain installation will be accessible | http://localhost |
|
||||
| APP_PORT | The HTTP port exposed by the NGINX container | 8000 |
|
||||
| DB_HOST | The location of your database host where Investbrain is installed | investbrain-mysql |
|
||||
| DB_DATABASE | The name of the database where Investbrain is installed | investbrain |
|
||||
| DB_USERNAME | Your database username | investbrain |
|
||||
| DB_PASSWORD | Your database password | investbrain |
|
||||
| MARKET_DATA_PROVIDER | The market data provider to use (either `yahoo` or `alphavantage`) | yahoo |
|
||||
| MARKET_DATA_PROVIDER | The market data provider to use (either `yahoo`, `alphavantage`, or `finnhub`) | yahoo |
|
||||
| MARKET_DATA_REFRESH | Cadence to refresh market data in minutes | 30 |
|
||||
| ALPHAVANTAGE_API_KEY | If using the Alpha Vantage provider | `null` |
|
||||
| FINNHUB_API_KEY | If using the Finnhub provider | `null` |
|
||||
|
||||
> Note: These options affect the [docker-compose.yml](https://github.com/investbrainapp/investbrain/blob/main/docker-compose.yml) file, so if you decide to make any changes to these default configurations, you'll have to restart the Docker containers before your changes take effect.
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Interfaces\MarketData;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class FallbackInterface
|
||||
{
|
||||
|
||||
protected string $latest_error;
|
||||
|
||||
public function __call($method, $arguments)
|
||||
{
|
||||
|
||||
$providers = explode(',', config('investbrain.provider', 'yahoo'));
|
||||
|
||||
foreach ($providers as $provider) {
|
||||
|
||||
$provider = trim($provider);
|
||||
|
||||
try {
|
||||
|
||||
if (!in_array($provider, array_keys(config('investbrain.interfaces', [])))) {
|
||||
|
||||
throw new \Exception("Provider [{$provider}] is not a valid market data interface.");
|
||||
}
|
||||
|
||||
$provider_class_name = config("investbrain.interfaces.$provider");
|
||||
|
||||
return app()->make($provider_class_name)->$method(...$arguments);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
$this->latest_error = $e->getMessage();
|
||||
|
||||
Log::warning("Failed calling method {$method} ({$provider}): {$this->latest_error}");
|
||||
}
|
||||
}
|
||||
|
||||
throw new \Exception("Could not get market data: {$this->latest_error}");
|
||||
}
|
||||
}
|
||||
@@ -11,16 +11,9 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
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.");
|
||||
}
|
||||
|
||||
$this->app->bind(
|
||||
\App\Interfaces\MarketData\MarketDataInterface::class,
|
||||
config("investbrain.interfaces.$interface")
|
||||
\App\Interfaces\MarketData\FallbackInterface::class
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ return [
|
||||
|
||||
'refresh' => env('MARKET_DATA_REFRESH', 30), // minutes
|
||||
|
||||
'default' => env('MARKET_DATA_PROVIDER', 'yahoo'),
|
||||
'provider' => env('MARKET_DATA_PROVIDER', 'yahoo'),
|
||||
|
||||
'interfaces' => [
|
||||
'yahoo' => App\Interfaces\MarketData\YahooMarketData::class,
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use App\Interfaces\MarketData\YahooMarketData;
|
||||
use App\Interfaces\MarketData\FallbackInterface;
|
||||
use App\Interfaces\MarketData\AlphaVantageMarketData;
|
||||
|
||||
class FallbackInterfaceTest extends TestCase
|
||||
{
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Log::spy();
|
||||
}
|
||||
|
||||
public function testFallbackToNextProviderOnFailure()
|
||||
{
|
||||
config()->set('investbrain.provider', 'yahoo,alphavantage');
|
||||
config()->set('investbrain.interfaces', [
|
||||
'yahoo' => YahooMarketData::class,
|
||||
'alphavantage' => AlphaVantageMarketData::class,
|
||||
]);
|
||||
|
||||
$yahooMock = Mockery::mock(YahooMarketData::class);
|
||||
$yahooMock->shouldReceive('quote')
|
||||
->andThrow(new \Exception("Yahoo failed"));
|
||||
|
||||
$alphaMock = Mockery::mock(AlphaVantageMarketData::class);
|
||||
$alphaMock->shouldReceive('quote')
|
||||
->andReturn(collect(['Alpha data']));
|
||||
|
||||
$this->app->instance(YahooMarketData::class, $yahooMock);
|
||||
$this->app->instance(AlphaVantageMarketData::class, $alphaMock);
|
||||
|
||||
$fallbackInterface = new FallbackInterface();
|
||||
|
||||
$result = $fallbackInterface->quote('ACME');
|
||||
|
||||
$this->assertEquals(collect(['Alpha data']), $result);
|
||||
|
||||
Log::shouldHaveReceived('warning')->with('Failed calling method quote (yahoo): Yahoo failed');
|
||||
}
|
||||
|
||||
public function testAllProvidersFail()
|
||||
{
|
||||
config()->set('investbrain.provider', 'yahoo,alpha');
|
||||
config()->set('investbrain.interfaces', [
|
||||
'yahoo' => YahooMarketData::class,
|
||||
'alphavantage' => AlphaVantageMarketData::class,
|
||||
]);
|
||||
|
||||
$yahooMock = Mockery::mock(YahooMarketData::class);
|
||||
$yahooMock->shouldReceive('quote')
|
||||
->andThrow(new \Exception("Yahoo failed"));
|
||||
|
||||
$alphaMock = Mockery::mock(AlphaVantageMarketData::class);
|
||||
$alphaMock->shouldReceive('quote')
|
||||
->andThrow(new \Exception("Alpha failed"));
|
||||
|
||||
$this->app->instance(YahooMarketData::class, $yahooMock);
|
||||
$this->app->instance(AlphaVantageMarketData::class, $alphaMock);
|
||||
|
||||
$fallbackInterface = new FallbackInterface();
|
||||
|
||||
$this->expectException(\Exception::class);
|
||||
$this->expectExceptionMessage('Could not get market data: Provider [alpha] is not a valid market data interface.');
|
||||
|
||||
$fallbackInterface->quote('AAPL');
|
||||
|
||||
Log::shouldHaveReceived('warning')->with('Failed calling method quote (yahoo): Yahoo failed');
|
||||
Log::shouldHaveReceived('warning')->with('Failed calling method quote (alpha): Alpha failed');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user