Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 921ac28ba7 | |||
| 2b099b3156 | |||
| ee7668df6f | |||
| a882b5aadb | |||
| bad82fb41b | |||
| 5aca9008cb | |||
| 712a4c6c57 | |||
| 78f0d21b73 | |||
| 19cac58692 | |||
| 7d77b6fbc8 | |||
| e4e08091af | |||
| 292d43b154 | |||
| eae4422ad8 | |||
| 53d463b8b5 | |||
| 827644bb32 | |||
| 21e8672a12 |
@@ -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
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
This file was added by Shift #157267 in order to open a
|
||||
Pull Request since no other commits were made.
|
||||
|
||||
You should remove this file.
|
||||
@@ -28,7 +28,7 @@ Investbrain is a smart open-source investment tracker that helps you manage, tra
|
||||
|
||||
## Under the hood
|
||||
|
||||
Investbrain is a Laravel PHP web application that leverages Livewire and Tailwind for its frontend. Most databases should work, including MySQL and SQLite. Out of the box, we feature many market data providers. But we also offer an extensible market data provider interface for intrepid developers to create their own! We also offer integrations with OpenAI and Ollama for our ["chat with your holdings"](#chat-with-your-holdings) capability. Finally, of course we have robust support for i18n, a11y, and dark mode.
|
||||
Investbrain is a Laravel PHP web application that has an extensible market data provider interface. Out of the box, we feature many market data providers. But intrepid developers can [create their own providers](#custom-providers)! We also offer integrations with OpenAI and Ollama for our ["chat with your holdings"](#chat-with-your-holdings) capability. Finally, of course we have robust support for i18n, a11y, and dark mode.
|
||||
|
||||
## Self hosting
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -92,7 +92,7 @@ Your selected providers should be listed in your environment variables. Each sho
|
||||
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.
|
||||
In the above example, Yahoo Finance will be attempted first. If Yahoo Finance fails to retrieve market data, the application will automatically try Alpha Vantage.
|
||||
|
||||
### Custom providers
|
||||
|
||||
@@ -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` |
|
||||
@@ -178,7 +181,7 @@ Easy as that!
|
||||
|
||||
## Command line utilities
|
||||
|
||||
Investbrain comes bundled with several helpful command line utilities to make managing your portfolios and holdings more efficient. Keep in mind these commands are extremely powerful and can make irreversable changes to your holdings.
|
||||
Investbrain comes bundled with several helpful command line utilities to make managing your portfolios and holdings more efficient. Keep in mind these commands are extremely powerful and can make irreversable changes to your holdings. Just to be safe, we recommend backing up your portfolios before using these commands.
|
||||
|
||||
To run these commands, you can use `docker exec` like this:
|
||||
|
||||
@@ -186,7 +189,12 @@ To run these commands, you can use `docker exec` like this:
|
||||
docker exec -it investbrain-app php artisan <replace with command you want to run>
|
||||
```
|
||||
|
||||
Just to be safe, we recommend backing up your portfolios before using these commands:
|
||||
If you need more details on what the command does, you can take a look at the options available using the `help` option:
|
||||
|
||||
```bash
|
||||
<command you want to run> --help
|
||||
```
|
||||
|
||||
|
||||
| Command | Description |
|
||||
| ------------- | ------------- |
|
||||
@@ -195,8 +203,9 @@ Just to be safe, we recommend backing up your portfolios before using these comm
|
||||
| refresh:split-data | Refreshes splits data with your configured market data provider. Will also create new transactions to account for any splits. |
|
||||
| refresh:currency-data | Grabs the latest daily currency exchange rate data and persists to the database. |
|
||||
| capture:daily-change | Captures a snapshot of each portfolio's daily performance. |
|
||||
| sync:daily-change | Re-calculates daily snapshots of your portfolio's daily performance. Useful to fill in gaps in your portfolio charts. (Note: this is an extremely resource intensive query.) |
|
||||
| sync:holdings | Re-calculates performance of holdings with related transactions (i.e. dividends, realized gains, etc). |
|
||||
| sync:daily-change | Syncs daily snapshots of your portfolio's daily performance. Useful to fill in gaps in your portfolio charts. (Note: this is an extremely resource intensive query.) |
|
||||
| sync:holdings | Syncs performance of holdings with related transactions (i.e. dividends, realized gains, etc). |
|
||||
| fix:cost-basis-for-sales | Utility to automatically re-calculates cost basis for sale transactions. |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Portfolio;
|
||||
use App\Models\Transaction;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class FixCostBasisForSales extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'fix:cost-basis-for-sales
|
||||
{--portfolio= : The ID of the portfolio to fix.}
|
||||
{--user= : The user ID of transactions to fix.}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Fixes broken costs basis for sale transactions';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
|
||||
if (empty($this->option('user')) && empty($this->option('portfolio'))) {
|
||||
|
||||
$this->error('Must provide at least a user or portfolio.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$transactions = Transaction::where(['transaction_type' => 'SELL']);
|
||||
|
||||
if ($this->option('user')) {
|
||||
|
||||
$portfolios = Portfolio::fullAccess($this->option('user'))->get('id')
|
||||
->pluck('id')
|
||||
->toArray();
|
||||
|
||||
$transactions->whereIn('portfolio_id', $portfolios);
|
||||
|
||||
} else {
|
||||
|
||||
$transactions->where(['portfolio_id' => $this->option('portfolio')]);
|
||||
}
|
||||
|
||||
$transactions = $transactions->get();
|
||||
|
||||
$this->line("Fixing cost basis for {$transactions->count()} sale transactions...");
|
||||
|
||||
$transactions->chunk(10)->each(function ($chunk) {
|
||||
|
||||
dispatch(function () use ($chunk) {
|
||||
|
||||
$chunk->each(function ($transaction) {
|
||||
|
||||
$cost_basis = Transaction::where([
|
||||
'portfolio_id' => $transaction->portfolio_id,
|
||||
'symbol' => $transaction->symbol,
|
||||
'transaction_type' => 'BUY',
|
||||
])->whereDate('date', '<=', $transaction->date)
|
||||
->selectRaw('SUM(transactions.cost_basis * transactions.quantity) as total_cost_basis')
|
||||
->selectRaw('SUM(transactions.quantity) as total_quantity')
|
||||
->first();
|
||||
|
||||
$average_cost_basis = empty($cost_basis->total_quantity)
|
||||
? 0
|
||||
: $cost_basis->total_cost_basis / $cost_basis->total_quantity;
|
||||
|
||||
$transaction->cost_basis = $average_cost_basis ?? 0;
|
||||
|
||||
$transaction->save();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$this->line('Done!');
|
||||
}
|
||||
}
|
||||
@@ -21,9 +21,9 @@ class HoldingController extends Controller
|
||||
$query->where('transactions.symbol', $symbol);
|
||||
},
|
||||
])
|
||||
->symbol($symbol)
|
||||
->portfolio($portfolio->id)
|
||||
->firstOrFail();
|
||||
->symbol($symbol)
|
||||
->portfolio($portfolio->id)
|
||||
->firstOrFail();
|
||||
|
||||
$formattedTransactions = $holding->getFormattedTransactions();
|
||||
|
||||
|
||||
@@ -8,11 +8,13 @@ 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 Carbon\CarbonInterval;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class AlpacaMarketData implements MarketDataInterface
|
||||
{
|
||||
@@ -23,6 +25,11 @@ class AlpacaMarketData implements MarketDataInterface
|
||||
public string $apiBaseUrl = 'https://api.alpaca.markets/';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createNewClient();
|
||||
}
|
||||
|
||||
private function createNewClient()
|
||||
{
|
||||
$this->client = Http::withOptions([
|
||||
'headers' => [
|
||||
@@ -45,15 +52,15 @@ class AlpacaMarketData implements MarketDataInterface
|
||||
|
||||
$quote = $response->json('trade');
|
||||
|
||||
if (is_null(Arr::get($quote, 'p'))) {
|
||||
throw new \Exception('Could not find ticker on Alpaca');
|
||||
}
|
||||
throw_if(empty(Arr::get($quote, 'p')), NotFoundHttpException::class, "Symbol `{$symbol}` was not found");
|
||||
|
||||
$fundamental = cache()->remember(
|
||||
'ap-symbol-'.$symbol,
|
||||
1440,
|
||||
function () use ($symbol) {
|
||||
|
||||
$this->createNewClient();
|
||||
|
||||
$basic = $this->client->baseUrl($this->apiBaseUrl)->get("v2/assets/{$symbol}")->json();
|
||||
$fifty_two_week = $this->client->baseUrl($this->dataBaseUrl)->withQueryParameters([
|
||||
'timeframe' => '12M',
|
||||
@@ -125,24 +132,49 @@ class AlpacaMarketData implements MarketDataInterface
|
||||
|
||||
public function history(string $symbol, $startDate, $endDate): Collection
|
||||
{
|
||||
$response = $this->client->baseUrl($this->dataBaseUrl)->withQueryParameters([
|
||||
'timeframe' => '1D',
|
||||
'start' => Carbon::parse($startDate)->format('Y-m-d'),
|
||||
'end' => Carbon::parse($endDate)->subHours(36)->format('Y-m-d'), // todo: can't query recent SIP data
|
||||
])->get("v2/stocks/{$symbol}/bars");
|
||||
$startDate = Carbon::parse($startDate);
|
||||
$endDate = Carbon::parse($endDate)->subHours(36); // alpaca has sip data limits
|
||||
|
||||
$history = $response->json('bars');
|
||||
$allHistory = collect();
|
||||
|
||||
return collect($history)
|
||||
->map(function ($history) use ($symbol) {
|
||||
$chunks = 1000;
|
||||
|
||||
$date = Carbon::parse($history['t'])->format('Y-m-d');
|
||||
$period = CarbonInterval::days($chunks)->toPeriod($startDate, $endDate);
|
||||
foreach ($period as $startDate) {
|
||||
|
||||
return [$date => new Ohlc([
|
||||
'symbol' => $symbol,
|
||||
'date' => $date,
|
||||
'close' => Arr::get($history, 'c'),
|
||||
])];
|
||||
});
|
||||
$chunkEnd = $startDate->copy()->addDays($chunks - 1);
|
||||
|
||||
if ($chunkEnd->gt($endDate)) {
|
||||
$chunkEnd = $endDate;
|
||||
}
|
||||
|
||||
$this->createNewClient();
|
||||
|
||||
$response = $this->client->baseUrl($this->dataBaseUrl)->withQueryParameters([
|
||||
'timeframe' => '1D',
|
||||
'start' => $startDate->format('Y-m-d'),
|
||||
'end' => $chunkEnd->format('Y-m-d'),
|
||||
])->get("v2/stocks/{$symbol}/bars");
|
||||
|
||||
$history = $response->json('bars');
|
||||
|
||||
throw_if(empty($history), NotFoundHttpException::class, "Symbol `{$symbol}` was not found");
|
||||
|
||||
$chunkedHistory = collect($history)
|
||||
->mapWithKeys(function ($history) use ($symbol) {
|
||||
|
||||
$date = Carbon::parse($history['t'])->format('Y-m-d');
|
||||
|
||||
return [$date => new Ohlc([
|
||||
'symbol' => $symbol,
|
||||
'date' => $date,
|
||||
'close' => Arr::get($history, 'c'),
|
||||
])];
|
||||
});
|
||||
|
||||
$allHistory = $allHistory->merge($chunkedHistory);
|
||||
}
|
||||
|
||||
return $allHistory;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
@@ -145,7 +145,7 @@ class AlphaVantageMarketData implements MarketDataInterface
|
||||
return [$date => new Ohlc([
|
||||
'symbol' => $symbol,
|
||||
'date' => $date,
|
||||
'close' => Arr::get($history, '4. close'),
|
||||
'close' => (float) Arr::get($history, '4. close'),
|
||||
])];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
<?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;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
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();
|
||||
|
||||
throw_if(empty(Arr::get($quote, 'price')), NotFoundHttpException::class, "Symbol `{$symbol}` was not found");
|
||||
|
||||
$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');
|
||||
|
||||
$history = $response->json('values');
|
||||
|
||||
throw_if(empty($history), NotFoundHttpException::class, "Symbol `{$symbol}` was not found");
|
||||
|
||||
return collect($history)
|
||||
->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
|
||||
|
||||
@@ -5,9 +5,8 @@ declare(strict_types=1);
|
||||
namespace App\Rules;
|
||||
|
||||
use App\Models\Portfolio;
|
||||
use App\Models\Transaction;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class QuantityValidationRule implements ValidationRule
|
||||
{
|
||||
@@ -21,7 +20,7 @@ class QuantityValidationRule implements ValidationRule
|
||||
protected ?string $symbol,
|
||||
protected ?string $transactionType,
|
||||
protected string|Carbon|null $date
|
||||
) { }
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validate the attribute.
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Currency;
|
||||
|
||||
if (! function_exists('currency')) {
|
||||
|
||||
// /**
|
||||
// * Returns an instance of the currency model
|
||||
// * */
|
||||
// function currency(): Currency
|
||||
// {
|
||||
// return new Currency;
|
||||
// }
|
||||
}
|
||||
+1
-3
@@ -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": {
|
||||
@@ -54,9 +55,6 @@
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"files": [
|
||||
"app/Support/Helpers.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
|
||||
Generated
+933
-533
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
],
|
||||
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ return [
|
||||
*/
|
||||
'components' => [
|
||||
'spotlight' => [
|
||||
'class' => 'App\Support\Spotlight',
|
||||
'class' => App\Support\Spotlight::class,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
'secret' => env('TWELVEDATA_API_SECRET'),
|
||||
];
|
||||
@@ -83,7 +83,7 @@ new class extends Component
|
||||
|
||||
<x-menu-item title="{{ __('Log Out') }}" icon="o-power" onclick="event.preventDefault(); document.getElementById('logout').submit();" />
|
||||
<form id="logout" action="{{ route('logout') }}" method="POST" style="display: none;">
|
||||
{{ csrf_field() }}
|
||||
@csrf
|
||||
</form>
|
||||
|
||||
</x-dropdown>
|
||||
|
||||
@@ -52,10 +52,10 @@ new class extends Component
|
||||
{{ Number::currency($holding->dividends_earned ?? 0, $holding->market_data->currency) }}
|
||||
</p>
|
||||
|
||||
<p class="pt-2 text-sm" title="{{ \Carbon\Carbon::parse($holding->market_data->updated_at)->toIso8601String() }}">
|
||||
<p class="pt-2 text-sm" title="{{ \Illuminate\Support\Carbon::parse($holding->market_data->updated_at)->toIso8601String() }}">
|
||||
{{ __('Last Refreshed') }}:
|
||||
{{ !is_null($holding->market_data->updated_at)
|
||||
? \Carbon\Carbon::parse($holding->market_data->updated_at)->diffForHumans()
|
||||
? \Illuminate\Support\Carbon::parse($holding->market_data->updated_at)->diffForHumans()
|
||||
: '' }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -98,6 +98,6 @@ new class extends Component
|
||||
{{ Number::currency($row->dividends_earned ?? 0, $row->market_data->currency) }}
|
||||
@endscope
|
||||
@scope('cell_market_data_updated_at', $row)
|
||||
{{ \Carbon\Carbon::parse($row->market_data_updated_at)->diffForHumans() }}
|
||||
{{ \Illuminate\Support\Carbon::parse($row->market_data_updated_at)->diffForHumans() }}
|
||||
@endscope
|
||||
</x-table>
|
||||
|
||||
Reference in New Issue
Block a user