Compare commits

...

31 Commits

Author SHA1 Message Date
hackerESQ c4b7d399ea Update SECURITY.md 2025-03-17 18:19:12 -05:00
hackerESQ ffe53e91c0 Merge pull request #75 from investbrainapp/simplify-asset-url
feat: simplify self host install by removing asset_url env
2025-03-17 18:18:32 -05:00
hackerESQ aeb1b12afe feat: simplify self host install by removing asset_url env 2025-03-17 18:18:12 -05:00
hackerESQ fe81ec7ee7 fix: adds reinvest column back to holdings table 2025-03-13 20:45:00 -05:00
hackerESQ f0ecc0fd3d fix: create profile photo disk for jetstream 2025-03-12 12:02:34 -05:00
hackerESQ 03b75fb683 adds sentry log driver 2025-03-11 17:55:51 -05:00
hackerESQ dc93621547 simplify example.env 2025-03-10 22:59:15 -05:00
hackerESQ 7ab6f79e56 feat: adds pgsql compatibility (#72) 2025-03-10 21:17:24 -05:00
hackerESQ 9e48f21c8d fix: better pgsql support 2025-03-07 19:30:06 -06:00
hackerESQ 10e6de8df4 chore: clean up market data seed 2025-03-07 19:15:10 -06:00
hackerESQ 00fbdec6f1 fix: improve seeder (and remove symbol dupes) 2025-03-07 18:43:55 -06:00
hackerESQ 730903c383 fix: compatible with pgsql 2025-03-07 17:45:54 -06:00
hackerESQ 5fc9455908 fix: longer exception 2025-03-07 17:27:08 -06:00
hackerESQ 28e0ad68fc fix: truncate exception so meaningful data shows first 2025-03-07 17:20:15 -06:00
hackerESQ ca48d702a7 chore: simplify .env file 2025-03-07 17:07:47 -06:00
hackerESQ 812b9ed075 chore: critical security update for livewire/volt 2025-03-06 19:51:06 -06:00
hackerESQ 93a0595652 Merge pull request #70 from investbrainapp/use-correct-storage-path
refactor: use correct storage path for app key check
2025-03-06 18:36:23 -06:00
hackerESQ 8a357e8cab refactor: use correct storage path for app key check 2025-03-06 18:35:57 -06:00
hackerESQ 22e12977f8 Merge pull request #69 from investbrainapp/save-app-key-to-file
refactor: storage scaffolding and save generated app key to file
2025-03-06 16:57:00 -06:00
hackerESQ 732cf02317 refactor: storage directory scaffoling and save generated app key to file 2025-03-06 16:55:54 -06:00
hackerESQ 6dea75651b fix: version response will be an object 2025-02-25 20:16:13 -06:00
Oscar Padilla 6cff252813 fix: support mariadb in sync:daily-change (#64)
* fix: support mariadb in sync:daily-change

* use version() instead of system variables

---------

Co-authored-by: hackerESQ <corey@coreyvarma.com>
2025-02-25 20:09:03 -06:00
Karjack182 0d06ca6a04 Updated Dockerfile (#65)
Optimized Dockerfile

* Updated Dockerfile to utilize a multi-stage build. This will make the built image lighter, by about 300mb.
Also safer, as 8.3-fpm has 55 known vulnerabilities, while 8.3-fpm-alpine has 0.

* Removed runtime dependencies from the build stage.

* remove unneeded php extensions from stage 1 build step
2025-02-25 19:04:02 -06:00
hackerESQ a3f875270b fix: quiet redis logs 2025-02-01 10:52:15 -06:00
hackerESQ 00a1312ee3 fix: move storage:link to dockerfile 2025-01-30 19:16:33 -06:00
hackerESQ 1195faca0f docs: add more helpful comments 2025-01-30 19:00:55 -06:00
hackerESQ a39f255e52 fix: ensure permissions are set and storage dir is scaffolded 2025-01-30 18:48:34 -06:00
hackerESQ cac2460153 fix: predis always the default 2025-01-30 18:13:16 -06:00
hackerESQ 894da4ef9b fix: make redis default 2025-01-30 18:13:01 -06:00
hackerESQ a705b794fd docs: add note about "broken styling" 2025-01-29 23:13:28 -06:00
hackerESQ 37da6885ee fix: use laravel up health endpoint 2025-01-29 23:06:42 -06:00
33 changed files with 663 additions and 561 deletions
+1 -1
View File
@@ -13,4 +13,4 @@ storage/framework/cache/*
storage/framework/sessions/* storage/framework/sessions/*
storage/framework/testing/* storage/framework/testing/*
storage/framework/views/* storage/framework/views/*
storage/framework/logs/* storage/logs/*
+4 -22
View File
@@ -40,13 +40,10 @@ LINKEDIN_CLIENT_SECRET=
FACEBOOK_CLIENT_ID= FACEBOOK_CLIENT_ID=
FACEBOOK_CLIENT_SECRET= FACEBOOK_CLIENT_SECRET=
APP_NAME=Investbrain FILESYSTEM_DISK=local
APP_TIMEZONE=UTC SESSION_DRIVER=redis
APP_ENV=production QUEUE_CONNECTION=redis
APP_DEBUG=true CACHE_STORE=redis
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
SELF_HOSTED=true
DB_CONNECTION=mysql DB_CONNECTION=mysql
DB_HOST=investbrain-mysql DB_HOST=investbrain-mysql
@@ -55,20 +52,7 @@ DB_DATABASE=investbrain
DB_USERNAME=investbrain DB_USERNAME=investbrain
DB_PASSWORD=investbrain DB_PASSWORD=investbrain
SESSION_DRIVER=redis
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
FILESYSTEM_DISK=local
QUEUE_CONNECTION=redis
CACHE_STORE=redis
REDIS_CLIENT=predis
REDIS_HOST=investbrain-redis REDIS_HOST=investbrain-redis
REDIS_PATH=/tmp/database_server.sock
REDIS_PASSWORD=null REDIS_PASSWORD=null
REDIS_PORT=6379 REDIS_PORT=6379
@@ -86,5 +70,3 @@ AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1 AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET= AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
+8
View File
@@ -214,6 +214,14 @@ docker exec -it investbrain-app tail -f storage/logs/laravel.log
<details> <details>
**<summary>Application styling is broken and images are too big</summary>**
If you're serving Investbrain from a DNS name (e.g. example.com), it's likely that you haven't updated the `ASSET_URL` environment yet. The URL provided there will be used to generate absolute URLs for images, JS, and CSS assets on the front end of the application.
</details>
<details>
**<summary>Market data not refreshing on fresh install</summary>** **<summary>Market data not refreshing on fresh install</summary>**
If you're unable to refresh market data out of the box (i.e. your market data provider is set to Yahoo), there is a chance Yahoo is being blocked by a firewall or adblocker. Pihole is known to block `fc.yahoo.com` which is the domain used to query Yahoo. If you're unable to refresh market data out of the box (i.e. your market data provider is set to Yahoo), there is a chance Yahoo is being blocked by a firewall or adblocker. Pihole is known to block `fc.yahoo.com` which is the domain used to query Yahoo.
+2 -1
View File
@@ -4,7 +4,8 @@
| Version | Supported | | Version | Supported |
| ------- | ------------------ | | ------- | ------------------ |
| 1.0.x | :white_check_mark: | | 1.1.x | :white_check_mark: |
| 1.0.x | :x: |
| < 1.0.0 | :x: | | < 1.0.0 | :x: |
## Reporting a Vulnerability ## Reporting a Vulnerability
+3 -1
View File
@@ -9,9 +9,11 @@ use Illuminate\Support\Carbon;
class Quote extends MarketDataType class Quote extends MarketDataType
{ {
public function setName(string $name): self public function setName($name): self
{ {
if (! empty($name)) {
$this->items['name'] = (string) $name; $this->items['name'] = (string) $name;
}
return $this; return $this;
} }
+17 -10
View File
@@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class Dividend extends Model class Dividend extends Model
@@ -109,22 +110,28 @@ class Dividend extends Model
public static function syncHoldings(string $symbol): void public static function syncHoldings(string $symbol): void
{ {
// group by holdings // group by holdings
$dividends = self::select(['holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount']) $subQuery = self::select([
->selectRaw(' 'holdings.portfolio_id',
(COALESCE(CASE WHEN transactions.transaction_type = "BUY" 'dividends.date',
'dividends.symbol',
'dividends.dividend_amount',
])->selectRaw("
(COALESCE(SUM(CASE WHEN transactions.transaction_type = 'BUY'
AND date(transactions.date) <= date(dividends.date) AND date(transactions.date) <= date(dividends.date)
THEN transactions.quantity ELSE 0 END, 0) THEN transactions.quantity ELSE 0 END), 0)
- COALESCE(CASE WHEN transactions.transaction_type = "SELL" - COALESCE(SUM(CASE WHEN transactions.transaction_type = 'SELL'
AND date(transactions.date) <= date(dividends.date) AND date(transactions.date) <= date(dividends.date)
THEN transactions.quantity ELSE 0 END, 0)) THEN transactions.quantity ELSE 0 END), 0))
* dividends.dividend_amount * dividends.dividend_amount
AS total_received AS total_received
') ")->join('transactions', 'transactions.symbol', '=', 'dividends.symbol')
->join('transactions', 'transactions.symbol', '=', 'dividends.symbol')
->join('holdings', 'transactions.portfolio_id', '=', 'holdings.portfolio_id') ->join('holdings', 'transactions.portfolio_id', '=', 'holdings.portfolio_id')
->where('dividends.symbol', $symbol) ->where('dividends.symbol', $symbol)
->groupBy('holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount', 'total_received') ->groupBy('holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount');
->havingRaw('total_received > 0')
$dividends = DB::table(DB::raw("({$subQuery->toSql()}) as sub"))
->mergeBindings($subQuery->getQuery())
->where('total_received', '>', 0)
->get(); ->get();
// iterate through holdings and update // iterate through holdings and update
+77 -25
View File
@@ -7,6 +7,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
class Holding extends Model class Holding extends Model
@@ -99,7 +100,25 @@ class Holding extends Model
->whereRaw("transactions.portfolio_id = '$this->portfolio_id'") ->whereRaw("transactions.portfolio_id = '$this->portfolio_id'")
->whereRaw("transactions.symbol = '$this->symbol'"); ->whereRaw("transactions.symbol = '$this->symbol'");
}) })
->having('total_received', '>', 0); ->havingRaw("SUM(
(CASE
WHEN transaction_type = 'BUY'
AND transactions.symbol = dividends.symbol
AND transactions.portfolio_id = '$this->portfolio_id'
AND transactions.date <= dividends.date
THEN transactions.quantity
ELSE 0
END)
-
(CASE
WHEN transaction_type = 'SELL'
AND transactions.symbol = dividends.symbol
AND transactions.portfolio_id = '$this->portfolio_id'
AND transactions.date <= dividends.date
THEN transactions.quantity
ELSE 0
END)
) * dividends.dividend_amount > 0");
} }
/** /**
@@ -147,7 +166,7 @@ class Holding extends Model
{ {
return $query->selectRaw('COALESCE(market_data.market_value * holdings.quantity, 0) AS total_market_value') return $query->selectRaw('COALESCE(market_data.market_value * holdings.quantity, 0) AS total_market_value')
->selectRaw('COALESCE((market_data.market_value - holdings.average_cost_basis) * holdings.quantity, 0) AS market_gain_dollars') ->selectRaw('COALESCE((market_data.market_value - holdings.average_cost_basis) * holdings.quantity, 0) AS market_gain_dollars')
->selectRaw('COALESCE(((market_data.market_value - holdings.average_cost_basis) / holdings.average_cost_basis) * 100, 0) AS market_gain_percent'); ->selectRaw('COALESCE(((market_data.market_value - holdings.average_cost_basis) / NULLIF(holdings.average_cost_basis, 0)) * 100, 0) AS market_gain_percent');
} }
public function scopePortfolio($query, $portfolio) public function scopePortfolio($query, $portfolio)
@@ -191,10 +210,10 @@ class Holding extends Model
$query = Transaction::where([ $query = Transaction::where([
'portfolio_id' => $this->portfolio_id, 'portfolio_id' => $this->portfolio_id,
'symbol' => $this->symbol, 'symbol' => $this->symbol,
])->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN quantity ELSE 0 END) AS `qty_purchases`') ])->selectRaw("SUM(CASE WHEN transaction_type = 'BUY' THEN quantity ELSE 0 END) AS qty_purchases")
->selectRaw('SUM(CASE WHEN transaction_type = "SELL" THEN quantity ELSE 0 END) AS `qty_sales`') ->selectRaw("SUM(CASE WHEN transaction_type = 'SELL' THEN quantity ELSE 0 END) AS qty_sales")
->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN (quantity * cost_basis) ELSE 0 END) AS `total_cost_basis`') ->selectRaw("SUM(CASE WHEN transaction_type = 'BUY' THEN (quantity * cost_basis) ELSE 0 END) AS total_cost_basis")
->selectRaw('SUM(CASE WHEN transaction_type = "SELL" THEN (quantity * sale_price) ELSE 0 END) AS `total_sale_price`') ->selectRaw("SUM(CASE WHEN transaction_type = 'SELL' THEN (quantity * sale_price) ELSE 0 END) AS total_sale_price")
->first(); ->first();
$total_quantity = round($query->qty_purchases - $query->qty_sales, 3); $total_quantity = round($query->qty_purchases - $query->qty_sales, 3);
@@ -202,8 +221,7 @@ class Holding extends Model
$average_cost_basis = ( $average_cost_basis = (
$query->qty_purchases > 0 $query->qty_purchases > 0
&& $total_quantity > 0 && $total_quantity > 0
) ) ? $query->total_cost_basis / $query->qty_purchases
? $query->total_cost_basis / $query->qty_purchases
: 0; : 0;
// update holding // update holding
@@ -246,17 +264,18 @@ class Holding extends Model
$end_date = now(); $end_date = now();
} }
// MySQL default interval
$date_interval = 'DATE_ADD(date, INTERVAL 1 DAY)'; $date_interval = 'DATE_ADD(date, INTERVAL 1 DAY)';
$castNumberType = 'decimal';
// Use SQLite interval grammar
if (config('database.default') === 'sqlite') { if (config('database.default') === 'sqlite') {
$date_interval = "date(date, '+1 day')"; $date_interval = "date(date, '+1 day')";
} else {
DB::statement('SET cte_max_recursion_depth=1000000;');
} }
return DB::table(DB::raw("( // Default CTE time series query (for MySQL and SQLite)
$timeSeriesQuery = DB::table(DB::raw("(
WITH RECURSIVE date_series AS ( WITH RECURSIVE date_series AS (
SELECT '{$start_date->format('Y-m-d')}' AS date SELECT '{$start_date->format('Y-m-d')}' AS date
UNION ALL UNION ALL
@@ -266,29 +285,62 @@ class Holding extends Model
) )
SELECT date_series.date SELECT date_series.date
FROM date_series FROM date_series
) as date_series") ) as date_series"));
)
// PGSql time series query
if (config('database.default') === 'pgsql') {
$timeSeriesQuery = DB::table(DB::raw("
generate_series(
date '{$start_date->format('Y-m-d')}',
date '{$end_date->format('Y-m-d')}',
interval '1 day'
) as date_series"));
$castNumberType = 'numeric';
}
// Set MySQL-like query CTE max iterations
if (config('database.default') === 'mysql') {
// MySQL default
$max_recursion_var_name = 'cte_max_recursion_depth';
// Determine if running MySQL or MariaDB
$versionString = Arr::get(
DB::select('SELECT VERSION() as version;'),
'0', new \stdClass
)->version;
if (stripos($versionString, 'MariaDB') !== false) {
$max_recursion_var_name = 'max_recursive_iterations'; // Must be MariaDB
}
DB::statement("SET $max_recursion_var_name=1000000;");
}
// Extracted query for counting QTY owned
$quantityQuery = "ROUND(CAST(COALESCE(
SUM(CASE WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity ELSE 0 END)
- SUM(CASE WHEN transactions.transaction_type = 'SELL' THEN transactions.quantity ELSE 0 END),
0
) AS {$castNumberType}), 3)";
return $timeSeriesQuery
->select([ ->select([
'date_series.date', 'date_series.date',
DB::raw(" DB::raw("
ROUND( {$quantityQuery} AS owned
COALESCE(SUM(CASE WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity ELSE 0 END), 0) -
COALESCE(SUM(CASE WHEN transactions.transaction_type = 'SELL' THEN transactions.quantity ELSE 0 END), 0), 3) AS `owned`
"), "),
DB::raw(" DB::raw("
COALESCE(CASE CASE
WHEN ( WHEN ({$quantityQuery}) = 0 THEN 0
ROUND(
COALESCE(SUM(CASE WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity ELSE 0 END), 0) -
COALESCE(SUM(CASE WHEN transactions.transaction_type = 'SELL' THEN transactions.quantity ELSE 0 END), 0), 3)
) = 0 THEN 0
ELSE SUM(CASE ELSE SUM(CASE
WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity * transactions.cost_basis WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity * transactions.cost_basis
ELSE 0 ELSE 0
END) END)
END, 0) AS cost_basis END AS cost_basis
"), "),
DB::raw("COALESCE(SUM(CASE WHEN transaction_type = 'SELL' THEN ((sale_price - cost_basis) * quantity) ELSE 0 END), 0) AS `realized_gains`"), DB::raw("COALESCE(SUM(CASE WHEN transaction_type = 'SELL' THEN ((sale_price - cost_basis) * quantity) ELSE 0 END), 0) AS realized_gains"),
]) ])
->leftJoin('transactions', function ($join) { ->leftJoin('transactions', function ($join) {
$join->on(DB::raw('DATE(transactions.date)'), '<=', 'date_series.date') $join->on(DB::raw('DATE(transactions.date)'), '<=', 'date_series.date')
+5
View File
@@ -211,6 +211,11 @@ class Portfolio extends Model
if (! empty($total_performance)) { if (! empty($total_performance)) {
DB::transaction(function () use ($total_performance) { DB::transaction(function () use ($total_performance) {
// delete old history
$firstDate = array_keys($total_performance)[0];
$this->daily_change()->where('date', '<', $firstDate)->delete();
// upsert new history
$this->daily_change()->upsert( $this->daily_change()->upsert(
$total_performance, $total_performance,
['date', 'portfolio_id'], ['date', 'portfolio_id'],
+3 -3
View File
@@ -101,7 +101,7 @@ class Split extends Model
->where([ ->where([
'splits.symbol' => $symbol, 'splits.symbol' => $symbol,
]) ])
->whereDate('splits.date', '>', DB::raw('IFNULL(holdings.splits_synced_at, "0000-00-00")')) ->whereDate('splits.date', '>', DB::raw("COALESCE(holdings.splits_synced_at, '1901-01-01')"))
->where('holdings.quantity', '>', 0) ->where('holdings.quantity', '>', 0)
->join('holdings', 'splits.symbol', 'holdings.symbol') ->join('holdings', 'splits.symbol', 'holdings.symbol')
->orderBy('splits.date', 'ASC') ->orderBy('splits.date', 'ASC')
@@ -115,8 +115,8 @@ class Split extends Model
'portfolio_id' => $split->portfolio_id, 'portfolio_id' => $split->portfolio_id,
]) ])
->whereDate('transactions.date', '<', $split->date->format('Y-m-d')) ->whereDate('transactions.date', '<', $split->date->format('Y-m-d'))
->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN quantity ELSE 0 END) - ->selectRaw("SUM(CASE WHEN transaction_type = 'BUY' THEN quantity ELSE 0 END) -
SUM(CASE WHEN transaction_type = "SELL" THEN quantity ELSE 0 END) AS qty_owned') SUM(CASE WHEN transaction_type = 'SELL' THEN quantity ELSE 0 END) AS qty_owned")
->value('qty_owned'); ->value('qty_owned');
if ($qty_owned > 0) { if ($qty_owned > 0) {
+3 -3
View File
@@ -19,7 +19,7 @@ class Spotlight
} }
$portfolios = $request->user()->portfolios() $portfolios = $request->user()->portfolios()
->where('title', 'LIKE', '%'.$request->input('search').'%') ->whereFullText('title', $request->input('search'))
->limit(5) ->limit(5)
->get(); ->get();
$portfolios->each(function ($portfolio) use ($results) { $portfolios->each(function ($portfolio) use ($results) {
@@ -35,8 +35,8 @@ class Spotlight
$holdings = $request->user()->holdings() $holdings = $request->user()->holdings()
->where('holdings.quantity', '>', 0) ->where('holdings.quantity', '>', 0)
->where(function ($query) use ($request) { ->where(function ($query) use ($request) {
return $query->where('holdings.symbol', 'LIKE', '%'.$request->input('search').'%') return $query->whereFullText('holdings.symbol', $request->input('search'))
->orWhere('market_data.name', 'LIKE', '%'.$request->input('search').'%'); ->orWhereFullText('market_data.name', $request->input('search'));
}) })
->limit(5) ->limit(5)
->get(); ->get();
Generated
+261 -275
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -100,7 +100,8 @@ return [
'cipher' => 'AES-256-CBC', 'cipher' => 'AES-256-CBC',
'key' => env('APP_KEY'), 'key' => env('APP_KEY')
?: when(file_exists(storage_path('app/.key')), fn () => trim(file_get_contents(storage_path('app/.key')))),
'previous_keys' => [ 'previous_keys' => [
...array_filter( ...array_filter(
+1 -1
View File
@@ -17,7 +17,7 @@ return [
| |
*/ */
'default' => env('CACHE_STORE', 'database'), 'default' => env('CACHE_STORE', 'redis'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
+1 -1
View File
@@ -77,6 +77,6 @@ return [
| |
*/ */
'profile_photo_disk' => 'public', 'profile_photo_disk' => env('JETSTREAM_PROFILE_PHOTO_DISK', 'public'),
]; ];
+1 -1
View File
@@ -116,7 +116,7 @@ return [
| |
*/ */
'inject_assets' => true, 'inject_assets' => false,
/* /*
|--------------------------------------------------------------------------- |---------------------------------------------------------------------------
+5
View File
@@ -96,6 +96,11 @@ return [
'processors' => [PsrLogMessageProcessor::class], 'processors' => [PsrLogMessageProcessor::class],
], ],
'sentry' => [
'driver' => 'sentry',
'level' => env('LOG_LEVEL', 'error'),
],
'stderr' => [ 'stderr' => [
'driver' => 'monolog', 'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'), 'level' => env('LOG_LEVEL', 'debug'),
+1 -1
View File
@@ -15,7 +15,7 @@ return [
| |
*/ */
'default' => env('QUEUE_CONNECTION', 'database'), 'default' => env('QUEUE_CONNECTION', 'redis'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
+1 -1
View File
@@ -20,7 +20,7 @@ return [
| |
*/ */
'driver' => env('SESSION_DRIVER', 'database'), 'driver' => env('SESSION_DRIVER', 'redis'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@@ -17,7 +17,7 @@ return new class extends Migration
{ {
Schema::create('portfolios', function (Blueprint $table) { Schema::create('portfolios', function (Blueprint $table) {
$table->uuid('id')->primary(); $table->uuid('id')->primary();
$table->string('title'); $table->string('title')->when(config('database.default') != 'sqlite', fn ($ctx) => $ctx->fulltext());
$table->text('notes')->nullable(); $table->text('notes')->nullable();
$table->boolean('wishlist')->default(false); $table->boolean('wishlist')->default(false);
$table->timestamps(); $table->timestamps();
@@ -18,8 +18,8 @@ class CreateMarketDataTable extends Migration
public function up() public function up()
{ {
Schema::create('market_data', function (Blueprint $table) { Schema::create('market_data', function (Blueprint $table) {
$table->string('symbol', 15)->primary(); $table->string('symbol', 25)->primary();
$table->string('name')->nullable(); $table->string('name')->nullable()->when(config('database.default') != 'sqlite', fn ($ctx) => $ctx->fulltext());
$table->float('market_value', 12, 4)->nullable(); $table->float('market_value', 12, 4)->nullable();
$table->float('fifty_two_week_low', 12, 4)->nullable(); $table->float('fifty_two_week_low', 12, 4)->nullable();
$table->float('fifty_two_week_high', 12, 4)->nullable(); $table->float('fifty_two_week_high', 12, 4)->nullable();
@@ -2,7 +2,6 @@
declare(strict_types=1); declare(strict_types=1);
use App\Models\MarketData;
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
@@ -19,7 +18,7 @@ class CreateDividendsTable extends Migration
Schema::create('dividends', function (Blueprint $table) { Schema::create('dividends', function (Blueprint $table) {
$table->uuid('id')->primary(); $table->uuid('id')->primary();
$table->date('date'); $table->date('date');
$table->foreignIdFor(MarketData::class, 'symbol'); $table->string('symbol', 25);
$table->float('dividend_amount', 12, 4); $table->float('dividend_amount', 12, 4);
$table->timestamps(); $table->timestamps();
}); });
@@ -2,7 +2,6 @@
declare(strict_types=1); declare(strict_types=1);
use App\Models\MarketData;
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
@@ -19,7 +18,7 @@ class CreateSplitsTable extends Migration
Schema::create('splits', function (Blueprint $table) { Schema::create('splits', function (Blueprint $table) {
$table->uuid('id')->primary(); $table->uuid('id')->primary();
$table->date('date'); $table->date('date');
$table->foreignIdFor(MarketData::class, 'symbol'); $table->string('symbol', 25);
$table->float('split_amount', 12, 4); $table->float('split_amount', 12, 4);
$table->timestamps(); $table->timestamps();
}); });
@@ -2,7 +2,6 @@
declare(strict_types=1); declare(strict_types=1);
use App\Models\MarketData;
use App\Models\Portfolio; use App\Models\Portfolio;
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
@@ -19,7 +18,7 @@ class CreateTransactionsTable extends Migration
{ {
Schema::create('transactions', function (Blueprint $table) { Schema::create('transactions', function (Blueprint $table) {
$table->uuid('id')->primary(); $table->uuid('id')->primary();
$table->foreignIdFor(MarketData::class, 'symbol'); $table->string('symbol', 25);
$table->foreignIdFor(Portfolio::class, 'portfolio_id')->constrained()->onDelete('cascade'); $table->foreignIdFor(Portfolio::class, 'portfolio_id')->constrained()->onDelete('cascade');
$table->string('transaction_type', 15); $table->string('transaction_type', 15);
$table->float('quantity', 12, 4); $table->float('quantity', 12, 4);
@@ -2,7 +2,6 @@
declare(strict_types=1); declare(strict_types=1);
use App\Models\MarketData;
use App\Models\Portfolio; use App\Models\Portfolio;
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
@@ -20,12 +19,13 @@ class CreateHoldingsTable extends Migration
Schema::create('holdings', function (Blueprint $table) { Schema::create('holdings', function (Blueprint $table) {
$table->uuid('id')->primary(); $table->uuid('id')->primary();
$table->foreignIdFor(Portfolio::class, 'portfolio_id')->constrained()->onDelete('cascade'); $table->foreignIdFor(Portfolio::class, 'portfolio_id')->constrained()->onDelete('cascade');
$table->foreignIdFor(MarketData::class, 'symbol'); $table->string('symbol', 25)->when(config('database.default') != 'sqlite', fn ($ctx) => $ctx->fulltext());
$table->float('quantity', 12, 4); $table->float('quantity', 12, 4);
$table->float('average_cost_basis', 12, 4)->default(0); $table->float('average_cost_basis', 12, 4)->default(0);
$table->float('total_cost_basis', 12, 4)->default(0); $table->float('total_cost_basis', 12, 4)->default(0);
$table->float('realized_gain_dollars', 12, 4)->default(0); $table->float('realized_gain_dollars', 12, 4)->default(0);
$table->float('dividends_earned', 12, 4)->default(0); $table->float('dividends_earned', 12, 4)->default(0);
$table->boolean('reinvest_dividends')->default(false);
$table->timestamp('splits_synced_at')->nullable(); $table->timestamp('splits_synced_at')->nullable();
$table->timestamps(); $table->timestamps();
}); });
+21 -11
View File
@@ -12,6 +12,8 @@ class MarketDataSeeder extends Seeder
{ {
use WithoutModelEvents; use WithoutModelEvents;
public array $rows = [];
/** /**
* Run the database seeds. * Run the database seeds.
*/ */
@@ -26,7 +28,6 @@ class MarketDataSeeder extends Seeder
if (($handle = fopen($csvFilePath, 'r')) !== false) { if (($handle = fopen($csvFilePath, 'r')) !== false) {
$header = null; $header = null;
$rows = [];
$rowCount = 0; $rowCount = 0;
while (($row = fgetcsv($handle, 0, ',')) !== false) { while (($row = fgetcsv($handle, 0, ',')) !== false) {
@@ -38,10 +39,9 @@ class MarketDataSeeder extends Seeder
} else { } else {
try {
$data = array_combine($header, $row); $data = array_combine($header, $row);
$rows[] = [ $this->rows[] = [
'symbol' => $data['symbol'], 'symbol' => $data['symbol'],
'name' => $data['name'], 'name' => $data['name'],
'meta_data' => json_encode([ 'meta_data' => json_encode([
@@ -55,29 +55,39 @@ class MarketDataSeeder extends Seeder
$rowCount++; $rowCount++;
if ($rowCount % $chunkSize == 0) { if ($rowCount % $chunkSize == 0) {
DB::table('market_data')->insertOrIgnore($rows);
$rows = [];
}
} catch (\Throwable $e) {
throw new \Exception('Error: '.$e->getMessage()); $this->bulkInsert($this->rows);
} }
} }
} }
// final clean up // final clean up
if (! empty($rows)) { if (! empty($this->rows)) {
DB::table('market_data')->insertOrIgnore($rows);
$this->bulkInsert($this->rows);
} }
// Close the CSV file // Close the CSV file
fclose($handle); fclose($handle);
echo "Imported $rowCount market data items successfully!\n"; echo "\n > Imported $rowCount market data items successfully!";
} else { } else {
echo "Failed to open the CSV.\n"; echo "Failed to open the CSV.\n";
} }
} }
public function bulkInsert(array $rows)
{
try {
DB::table('market_data')->insertOrIgnore($rows);
$this->rows = [];
} catch (\Throwable $e) {
throw new \Exception('Error: '.$e->getMessage());
}
}
} }
-19
View File
@@ -241,8 +241,6 @@ AKA,a.k.a. Brands Holding Corp. Common Stock,,2021,Consumer Discretionary,Catalo
AKAM,Akamai Technologies Inc. Common Stock,United States,1999,Consumer Discretionary,Business Services AKAM,Akamai Technologies Inc. Common Stock,United States,1999,Consumer Discretionary,Business Services
AKAN,Akanda Corp. Common Shares,United Kingdom,2022,Health Care, Medicinal Chemicals and Botanical Products AKAN,Akanda Corp. Common Shares,United Kingdom,2022,Health Care, Medicinal Chemicals and Botanical Products
AKBA,Akebia Therapeutics Inc. Common Stock,United States,2014,Health Care,Biotechnology: Pharmaceutical Preparations AKBA,Akebia Therapeutics Inc. Common Stock,United States,2014,Health Care,Biotechnology: Pharmaceutical Preparations
AKO-A,Embotelladora Andina S.A.,Chile,,,
AKO-B,Embotelladora Andina S.A.,Chile,,,
AKR,Acadia Realty Trust Common Stock,United States,,Real Estate,Real Estate Investment Trusts AKR,Acadia Realty Trust Common Stock,United States,,Real Estate,Real Estate Investment Trusts
AKRO,Akero Therapeutics Inc. Common Stock,United States,2019,Health Care,Biotechnology: Pharmaceutical Preparations AKRO,Akero Therapeutics Inc. Common Stock,United States,2019,Health Care,Biotechnology: Pharmaceutical Preparations
AKTS,Akoustis Technologies Inc. Common Stock,United States,,Utilities,Telecommunications Equipment AKTS,Akoustis Technologies Inc. Common Stock,United States,,Utilities,Telecommunications Equipment
@@ -789,8 +787,6 @@ BERY,Berry Global Group Inc. Common Stock,United States,2012,Industrials,Plastic
BEST,BEST Inc. American Depositary Shares each representing twenty (20) Class A Ordinary Shares,Cayman Islands,2023,Industrials,Trucking Freight/Courier Services BEST,BEST Inc. American Depositary Shares each representing twenty (20) Class A Ordinary Shares,Cayman Islands,2023,Industrials,Trucking Freight/Courier Services
BETR,Better Home & Finance Holding Company Class A Common Stock,United Kingdom,2021,Finance,Finance: Consumer Services BETR,Better Home & Finance Holding Company Class A Common Stock,United Kingdom,2021,Finance,Finance: Consumer Services
BETRW,Better Home & Finance Holding Company Warrant,United Kingdom,2021,Finance,Finance: Consumer Services BETRW,Better Home & Finance Holding Company Warrant,United Kingdom,2021,Finance,Finance: Consumer Services
BF-A,Brown Forman Corporation,United States,,,
BF-B,Brown Forman Corporation,United States,,,
BFAC,Battery Future Acquisition Corp. Class A Ordinary Shares,,2022,Finance,Blank Checks BFAC,Battery Future Acquisition Corp. Class A Ordinary Shares,,2022,Finance,Blank Checks
BFAM,Bright Horizons Family Solutions Inc. Common Stock,United States,2013,Consumer Discretionary,Other Consumer Services BFAM,Bright Horizons Family Solutions Inc. Common Stock,United States,2013,Consumer Discretionary,Other Consumer Services
BFC,Bank First Corporation Common Stock,United States,,Finance,Major Banks BFC,Bank First Corporation Common Stock,United States,,Finance,Major Banks
@@ -1004,8 +1000,6 @@ BREA,Brera Holdings PLC Class B Ordinary Shares,Ireland,2023,Consumer Discretion
BRFH,Barfresh Food Group Inc. Common Stock,United States,,Consumer Staples,Packaged Foods BRFH,Barfresh Food Group Inc. Common Stock,United States,,Consumer Staples,Packaged Foods
BRFS,BRF S.A.,Brazil,,Consumer Staples,Meat/Poultry/Fish BRFS,BRF S.A.,Brazil,,Consumer Staples,Meat/Poultry/Fish
BRID,Bridgford Foods Corporation Common Stock,United States,,Consumer Staples,Specialty Foods BRID,Bridgford Foods Corporation Common Stock,United States,,Consumer Staples,Specialty Foods
BRK-A,Berkshire Hathaway Inc.,United States,,,
BRK-B,Berkshire Hathaway Inc.,United States,,,
BRKH,BurTech Acquisition Corp. Class A Common Stock,United States,2022,Finance,Blank Checks BRKH,BurTech Acquisition Corp. Class A Common Stock,United States,2022,Finance,Blank Checks
BRKHU,BurTech Acquisition Corp. Unit,United States,2021,Finance,Blank Checks BRKHU,BurTech Acquisition Corp. Unit,United States,2021,Finance,Blank Checks
BRKHW,BurTech Acquisition Corp. Warrants,United States,2022,Finance,Blank Checks BRKHW,BurTech Acquisition Corp. Warrants,United States,2022,Finance,Blank Checks
@@ -1542,8 +1536,6 @@ CRBP,Corbus Pharmaceuticals Holdings Inc. Common Stock,United States,,Health Car
CRBU,Caribou Biosciences Inc. Common Stock,United States,2021,Health Care,Biotechnology: Biological Products (No Diagnostic Substances) CRBU,Caribou Biosciences Inc. Common Stock,United States,2021,Health Care,Biotechnology: Biological Products (No Diagnostic Substances)
CRC,California Resources Corporation Common Stock,United States,2020,Energy,Oil & Gas Production CRC,California Resources Corporation Common Stock,United States,2020,Energy,Oil & Gas Production
CRCT,Cricut Inc. Class A Common Stock,United States,2021,Technology,Industrial Machinery/Components CRCT,Cricut Inc. Class A Common Stock,United States,2021,Technology,Industrial Machinery/Components
CRD-A,Crawford & Company,United States,,,
CRD-B,Crawford & Company,United States,,,
CRDF,Cardiff Oncology Inc. Common Stock,United States,,Health Care,Biotechnology: Biological Products (No Diagnostic Substances) CRDF,Cardiff Oncology Inc. Common Stock,United States,,Health Care,Biotechnology: Biological Products (No Diagnostic Substances)
CRDL,Cardiol Therapeutics Inc. Class A Common Shares,Canada,,Health Care,Biotechnology: Biological Products (No Diagnostic Substances) CRDL,Cardiol Therapeutics Inc. Class A Common Shares,Canada,,Health Care,Biotechnology: Biological Products (No Diagnostic Substances)
CRDO,Credo Technology Group Holding Ltd Ordinary Shares,United States,2022,Technology,Semiconductors CRDO,Credo Technology Group Holding Ltd Ordinary Shares,United States,2022,Technology,Semiconductors
@@ -2850,7 +2842,6 @@ HE,Hawaiian Electric Industries Inc. Common Stock,United States,,Utilities,Elect
HEAR,Turtle Beach Corporation Common Stock,United States,,Telecommunications,Telecommunications Equipment HEAR,Turtle Beach Corporation Common Stock,United States,,Telecommunications,Telecommunications Equipment
HEES,H&E Equipment Services Inc. Common Stock,United States,2006,Industrials,Misc Corporate Leasing Services HEES,H&E Equipment Services Inc. Common Stock,United States,2006,Industrials,Misc Corporate Leasing Services
HEI,Heico Corporation Common Stock,United States,2000,Industrials,Aerospace HEI,Heico Corporation Common Stock,United States,2000,Industrials,Aerospace
HEI-A,Heico Corporation,United States,,,
HELE,Helen of Troy Limited Common Stock,Bermuda,,Consumer Discretionary,Home Furnishings HELE,Helen of Troy Limited Common Stock,Bermuda,,Consumer Discretionary,Home Furnishings
HEPA,Hepion Pharmaceuticals Inc. Common Stock,United States,,Health Care,Biotechnology: Pharmaceutical Preparations HEPA,Hepion Pharmaceuticals Inc. Common Stock,United States,,Health Care,Biotechnology: Pharmaceutical Preparations
HEPS,D-Market Electronic Services & Trading American Depositary Shares,Turkey,2021,Consumer Discretionary,Catalog/Specialty Distribution HEPS,D-Market Electronic Services & Trading American Depositary Shares,Turkey,2021,Consumer Discretionary,Catalog/Specialty Distribution
@@ -3011,7 +3002,6 @@ HUnited States,Houston American Energy Corporation Common Stock,United States,,E
HUT,Hut 8 Corp. Common Stock,United States,,Finance,Finance: Consumer Services HUT,Hut 8 Corp. Common Stock,United States,,Finance,Finance: Consumer Services
HUYA,HUYA Inc. American depositary shares each representing one Class A ordinary share,United States,2018,Technology,Computer Software: Programming Data Processing HUYA,HUYA Inc. American depositary shares each representing one Class A ordinary share,United States,2018,Technology,Computer Software: Programming Data Processing
HVT,Haverty Furniture Companies Inc. Common Stock,United States,,Consumer Discretionary,Other Specialty Stores HVT,Haverty Furniture Companies Inc. Common Stock,United States,,Consumer Discretionary,Other Specialty Stores
HVT-A,Haverty Furniture Companies Inc.,United States,,,
HWBK,Hawthorn Bancshares Inc. Common Stock,United States,,Finance,Major Banks HWBK,Hawthorn Bancshares Inc. Common Stock,United States,,Finance,Major Banks
HWC,Hancock Whitney Corporation Common Stock,United States,,Finance,Major Banks HWC,Hancock Whitney Corporation Common Stock,United States,,Finance,Major Banks
HWCPZ,Hancock Whitney Corporation 6.25% Subordinated Notes due 2060,United States,,Finance,Major Banks HWCPZ,Hancock Whitney Corporation 6.25% Subordinated Notes due 2060,United States,,Finance,Major Banks
@@ -11488,7 +11478,6 @@ NYNYR,Empire Resorts Inc.,United States,,,
LLEX,"Lilis Energy, Inc.",United States,,Independent Oil & Gas, LLEX,"Lilis Energy, Inc.",United States,,Independent Oil & Gas,
EZT,"Entergy Texas, Inc.",United States,,, EZT,"Entergy Texas, Inc.",United States,,,
EXPN.L,Experian plc,United Kingdom,,Business Services, EXPN.L,Experian plc,United Kingdom,,Business Services,
ETX,Eaton Vance Municipal Income 2028 Term Trust,United States,,,
EMI,Eaton Vance Michigan Municipal Income Trust,United States,,, EMI,Eaton Vance Michigan Municipal Income Trust,United States,,,
EMG,"Emergent Capital, Inc.",United States,,, EMG,"Emergent Capital, Inc.",United States,,,
EMCF,Emclaire Financial Corp.,United States,,Regional - Northeast Banks, EMCF,Emclaire Financial Corp.,United States,,Regional - Northeast Banks,
@@ -11531,7 +11520,6 @@ ENZY,Enzymotec Ltd.,United States,,Biotechnology,
ENBP,ENB Financial Corp,United States,,Regional - Northeast Banks, ENBP,ENB Financial Corp,United States,,Regional - Northeast Banks,
EMP-A.TO,Empire Company Limited,Canada,,Gold, EMP-A.TO,Empire Company Limited,Canada,,Gold,
ECCA,Eagle Point Credit Company Inc.,United States,,, ECCA,Eagle Point Credit Company Inc.,United States,,,
ECC,Eagle Point Credit Company Inc.,United States,,,
NESV,"National Energy Services, Inc.",United States,,, NESV,"National Energy Services, Inc.",United States,,,
YECO,Yulong Eco-Materials Limited,United States,,, YECO,Yulong Eco-Materials Limited,United States,,,
TIK,Tel-Instrument Electronics Corp.,United States,,Aerospace/Defense - Major Diversified, TIK,Tel-Instrument Electronics Corp.,United States,,Aerospace/Defense - Major Diversified,
@@ -13136,7 +13124,6 @@ HRG,"HRG Group, Inc.",United States,,Conglomerates,
GSS,Golden Star Resources Ltd.,United States,,Gold, GSS,Golden Star Resources Ltd.,United States,,Gold,
GIG,"GigPeak, Inc.",United States,,, GIG,"GigPeak, Inc.",United States,,,
SGEN,"Seattle Genetics, Inc.",United States,,Biotechnology, SGEN,"Seattle Genetics, Inc.",United States,,Biotechnology,
SAND,Sandstorm Gold Ltd.,United States,,Gold,
PGEM,"Ply Gem Holdings, Inc",United States,,General Building Materials, PGEM,"Ply Gem Holdings, Inc",United States,,General Building Materials,
ININ,"Interactive Intelligence Group, Inc.",United States,,, ININ,"Interactive Intelligence Group, Inc.",United States,,,
SNR,New Senior Investment Group Inc.,United States,,, SNR,New Senior Investment Group Inc.,United States,,,
@@ -13682,7 +13669,6 @@ VIG.VI,Vienna Insurance Group AG,Austria,,,
TACXF,TAC GOLD,United States,,, TACXF,TAC GOLD,United States,,,
VELA.L,Vela Technologies PLC,United Kingdom,,, VELA.L,Vela Technologies PLC,United Kingdom,,,
VEGPF,Vectura Group plc,United States,,, VEGPF,Vectura Group plc,United States,,,
United States.TO,Americas Silver Corporation,Canada,,Industrial Metals & Minerals,
URHG,"United Resource Holdings Group, Inc.",United States,,, URHG,"United Resource Holdings Group, Inc.",United States,,,
UQA.VI,UNIQA Insurance Group AG,Austria,,, UQA.VI,UNIQA Insurance Group AG,Austria,,,
UIBGF,UIB Group Limited,United States,,, UIBGF,UIB Group Limited,United States,,,
@@ -20863,7 +20849,6 @@ USMT,US Metro Bank,United States,,,
USK.F,"Skullcandy, Inc.",France,,, USK.F,"Skullcandy, Inc.",France,,,
USF.AX,"US Select Private Opportunities Fund, LP",Australia,,, USF.AX,"US Select Private Opportunities Fund, LP",Australia,,,
USBL,"United States Basketball League, Inc.",United States,,Conglomerates, USBL,"United States Basketball League, Inc.",United States,,Conglomerates,
United States.AX,Uraniumsa Limited,Australia,,Industrial Metals & Minerals,
US2.BE,"USG CORP. DL-,10",Germany,,, US2.BE,"USG CORP. DL-,10",Germany,,,
US1.F,"Ultratech, Inc.",France,,, US1.F,"Ultratech, Inc.",France,,,
URXZD,TENTH AVENUE PETROLEUM CORPORAT,United States,,, URXZD,TENTH AVENUE PETROLEUM CORPORAT,United States,,,
@@ -22474,7 +22459,6 @@ O3X2.F,AAN Ventures Inc,France,,,
D94417.CR,DP04947-0011,Venezuela,,, D94417.CR,DP04947-0011,Venezuela,,,
ENV.CR,ENVASES VENEZOLANOS S.A.,Venezuela,,, ENV.CR,ENVASES VENEZOLANOS S.A.,Venezuela,,,
BVF.DU,VALEANT PHARMACEUT. INTL,Germany,,, BVF.DU,VALEANT PHARMACEUT. INTL,Germany,,,
CUnited StatesN.IS,Cuhadaroglu Metal Sanayi ve Pazarlama A.S.,Turkey,,,
PUM9.L,Puma VCT 9 plc,United Kingdom,,, PUM9.L,Puma VCT 9 plc,United Kingdom,,,
RAP1V.HE,Rapala VMC Corporation,Finland,,, RAP1V.HE,Rapala VMC Corporation,Finland,,,
VIS.BE,"VISCOFAN SA INH. EO 0,70",Germany,,, VIS.BE,"VISCOFAN SA INH. EO 0,70",Germany,,,
@@ -22540,7 +22524,6 @@ WWSG,Worldwide Strategies Incorporated,United States,,Diversified Communication
WWTH,"With, Inc.",United States,,, WWTH,"With, Inc.",United States,,,
WUMSF,Wumart Stores Inc.,United States,,, WUMSF,Wumart Stores Inc.,United States,,,
WSSH,West Shore Bank Corp.,United States,,, WSSH,West Shore Bank Corp.,United States,,,
WSO-B,"Watsco, Inc.",United States,,,
WRCKF,WESC AB,United States,,, WRCKF,WESC AB,United States,,,
WMSI,"Williams Industries, Incorporated",United States,,, WMSI,"Williams Industries, Incorporated",United States,,,
WLKR,Walker Innovation Inc.,United States,,, WLKR,Walker Innovation Inc.,United States,,,
@@ -29715,12 +29698,10 @@ AK1.SG,AMETEK INC. Registered Shares D,Germany,,,
SRT.DE,Sartorius Aktiengesellschaft,Germany,,Scientific & Technical Instruments, SRT.DE,Sartorius Aktiengesellschaft,Germany,,Scientific & Technical Instruments,
0G29.L,Semperit Aktiengesellschaft Holding,United Kingdom,,, 0G29.L,Semperit Aktiengesellschaft Holding,United Kingdom,,,
0IPT.L,Akastor ASA,United Kingdom,,, 0IPT.L,Akastor ASA,United Kingdom,,,
United StatesK.IS,United Statesk Seramik Sanayi A.S.,Turkey,,,
0NCV.L,Lenzing Aktiengesellschaft,United Kingdom,,, 0NCV.L,Lenzing Aktiengesellschaft,United Kingdom,,,
0OK7.L,Akka Technologies,United Kingdom,,, 0OK7.L,Akka Technologies,United Kingdom,,,
1AKA.BE,"AKER SOLUTIONS ASA NK1,08",Germany,,, 1AKA.BE,"AKER SOLUTIONS ASA NK1,08",Germany,,,
AK2.BE,"AK STEEL HLDG",Germany,,, AK2.BE,"AK STEEL HLDG",Germany,,,
United StatesS.IS,United Statess Yatirimlar Holding A.S.,Turkey,,,
0QBM.L,Alior Bank S.A.,United Kingdom,,, 0QBM.L,Alior Bank S.A.,United Kingdom,,,
T1W.HM,Aktiengesellschaft Tokugawa,Germany,,, T1W.HM,Aktiengesellschaft Tokugawa,Germany,,,
0OIU.L,Arctic Paper S.A.,United Kingdom,,, 0OIU.L,Arctic Paper S.A.,United Kingdom,,,
Can't render this file because it is too large.
+12 -10
View File
@@ -8,23 +8,22 @@ services:
restart: unless-stopped restart: unless-stopped
tty: true tty: true
ports: ports:
- "${APP_PORT:-8000}:80" - 8000:80
environment: environment: # You can either use these properties OR an .env file. Do not use both!
APP_KEY: "" # Generate a key using `echo base64:$(openssl rand -base64 32)`
APP_URL: "http://localhost:8000" APP_URL: "http://localhost:8000"
ASSET_URL: "http://localhost:8000"
DB_CONNECTION: mysql DB_CONNECTION: mysql
DB_HOST: investbrain-mysql DB_HOST: investbrain-mysql
DB_PORT: 3306 DB_PORT: 3306
DB_DATABASE: ${DB_DATABASE:-investbrain} DB_DATABASE: investbrain
DB_USERNAME: ${DB_USERNAME:-investbrain} DB_USERNAME: investbrain
DB_PASSWORD: ${DB_PASSWORD:-investbrain} DB_PASSWORD: investbrain
SESSION_DRIVER: redis SESSION_DRIVER: redis
QUEUE_CONNECTION: redis QUEUE_CONNECTION: redis
CACHE_STORE: redis CACHE_STORE: redis
REDIS_HOST: investbrain-redis REDIS_HOST: investbrain-redis
volumes: volumes:
- ./storage:/var/app/storage - investbrain-storage:/var/app/storage # You can use a volume...
# - /path/to/storage:/var/app/storage:delegated # ...or you can use a path on host
depends_on: depends_on:
- mysql - mysql
- redis - redis
@@ -35,10 +34,12 @@ services:
container_name: investbrain-redis container_name: investbrain-redis
restart: unless-stopped restart: unless-stopped
tty: true tty: true
networks: command:
- investbrain-network - --loglevel warning
volumes: volumes:
- investbrain-redis:/data - investbrain-redis:/data
networks:
- investbrain-network
mysql: mysql:
image: mysql:8.0 image: mysql:8.0
container_name: investbrain-mysql container_name: investbrain-mysql
@@ -55,5 +56,6 @@ services:
networks: networks:
- investbrain-network - investbrain-network
volumes: volumes:
investbrain-storage:
investbrain-redis: investbrain-redis:
investbrain-mysql: investbrain-mysql:
+49 -24
View File
@@ -1,19 +1,16 @@
FROM php:8.3-fpm # Stage 1: Build stage
FROM php:8.3-fpm AS builder
ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive
ENV APP_NAME=Investbrain ENV APP_NAME=Investbrain
ENV VITE_APP_NAME=Investbrain ENV VITE_APP_NAME=Investbrain
ENV APP_DEBUG=true
ENV SELF_HOSTED=true
# Set the working directory # Set the working directory
COPY . /var/app
WORKDIR /var/app WORKDIR /var/app
# Install required packages # Install required packages
RUN apt-get update && apt-get upgrade -y \ RUN apt-get update && apt-get upgrade -y \
&& apt-get install -y \ && apt-get install -y \
nginx \
libfreetype-dev \ libfreetype-dev \
libjpeg62-turbo-dev \ libjpeg62-turbo-dev \
libpng-dev \ libpng-dev \
@@ -22,44 +19,72 @@ RUN apt-get update && apt-get upgrade -y \
libicu-dev \ libicu-dev \
libpq-dev \ libpq-dev \
binutils libc6-dev \ binutils libc6-dev \
supervisor \
unzip curl git \ unzip curl git \
nodejs npm \ nodejs npm \
# Clean up APT
&& apt-get -y autoremove \ && apt-get -y autoremove \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Install PHP extensions # Install PHP extensions
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) gd zip
# Copy application files
COPY . .
# Install Composer and Node.js Install PHP dependencies and build front end assets
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
&& composer install --no-scripts --optimize-autoloader \
&& npm install && npm run build \
&& rm -rf node_modules
# Stage 2: Production stage
FROM php:8.3-fpm-alpine
# Set the working directory
WORKDIR /var/app
# Copy necessary files from the builder stage
COPY --from=builder /var/app /var/app
COPY --from=builder /usr/local/etc/php/conf.d /usr/local/etc/php/conf.d
COPY --from=builder /usr/local/bin/composer /usr/local/bin/composer
# Install required Alpine packages
RUN apk add --no-cache \
nginx \
supervisor \
libpng-dev \
libzip-dev \
icu-dev \
postgresql-dev \
freetype-dev \
libjpeg-turbo-dev \
bash \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \ && docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) \ && docker-php-ext-install -j$(nproc) \
gd pgsql zip pdo_mysql mysqli intl gd pgsql zip pdo_mysql mysqli intl
# Remove default nginx config # Remove default nginx config
RUN rm /etc/nginx/sites-enabled/default \ RUN rm -rf /var/www/html \
&& rm -rf /var/www/html \
&& ln -s /var/app /var/www/app && ln -s /var/app /var/www/app
# Set permissions and ensure www-data has a shell available # Create required directories for supervisord
RUN chown -R www-data:www-data . \ RUN mkdir -p /var/log/supervisor /var/run/supervisor
&& chmod -R 775 ./storage \
&& chmod +x ./docker/entrypoint.sh \
&& usermod -s /bin/bash www-data
# Install Composer and Node.js Install PHP dependencies and build front end assets
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
&& composer install --no-scripts --optimize-autoloader \
&& npm install && npm run build
# Copy over configs # Copy over configs
COPY ./docker/nginx.conf /etc/nginx/conf.d/default.conf COPY ./docker/nginx.conf /etc/nginx/http.d/default.conf
COPY ./docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf COPY ./docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Set permissions and link storage
RUN php artisan storage:link \
&& chown -R www-data:www-data . \
&& chmod +x ./docker/entrypoint.sh
# Serve on port 80 # Serve on port 80
EXPOSE 80 EXPOSE 80
# Set up healthcheck # Set up healthcheck
HEALTHCHECK --interval=30s --timeout=10s --retries=3 CMD curl -f http://localhost || exit 1 HEALTHCHECK --interval=30s --timeout=10s --retries=3 CMD curl -f http://localhost/up || exit 1
# Run everything else # Run everything else
ENTRYPOINT ["/bin/bash", "./docker/entrypoint.sh"] ENTRYPOINT ["/bin/sh", "./docker/entrypoint.sh"]
+45 -23
View File
@@ -2,25 +2,29 @@
cd /var/app cd /var/app
# Starting Investbrain
echo "CiAgKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioKICAqICBJSUkgICBOICAgTiAgViAgIFYgIEVFRUVFICBTU1NTICBUVFRUVCAgQkJCQkIgICBSUlJSICAgIEFBQUFBICBJSUkgICBOICAgTiAgKgogICogICBJICAgIE5OICBOICBWICAgViAgRSAgICAgIFMgICAgICAgVCAgICBCICAgIEIgIFIgICBSICAgQSAgIEEgICBJICAgIE5OICBOICAqCiAgKiAgIEkgICAgTiBOIE4gIFYgICBWICBFRUVFICAgU1NTUyAgICBUICAgIEJCQkJCICAgUlJSUiAgICBBQUFBQSAgIEkgICAgTiBOIE4gICoKICAqICAgSSAgICBOICBOTiAgViAgIFYgIEUgICAgICAgICAgUyAgIFQgICAgQiAgICBCICBSICBSICAgIEEgICBBICAgSSAgICBOICBOTiAgKgogICogIElJSSAgIE4gICBOICAgVlZWICAgRUVFRUUgIFNTU1MgICAgVCAgICBCQkJCQiAgIFIgICBSICAgQSAgIEEgIElJSSAgIE4gICBOICAqCiAgKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioKICA=" | base64 -d
echo -e "\n====================== Validating environment... ====================== " echo -e "\n====================== Validating environment... ====================== "
for dir in storage/framework/cache storage/framework/sessions storage/framework/views; do
if [ ! -d "$dir" ]; then # Ensure app storage directory is scaffolded
echo -e "\n > $dir is missing. Creating scaffold for storage directory... " mkdir -p storage/framework/cache \
mkdir -p storage/framework/{cache,sessions,views} storage/framework/sessions \
storage/framework/views \
storage/app \
storage/logs
echo -e "\n > Storage directory scaffolding is OK... "
# Ensure storage directory is permissioned for www-data
chmod -R 775 storage chmod -R 775 storage
chown -R www-data:www-data storage chown -R www-data:www-data storage
fi
done
if [ ! -L "public/storage" ]; then echo -e "\n > Permissions are OK... "
echo -e "\n > Creating symbolic link for app public storage... "
php artisan storage:link # Ensure app key exists / generate if required
fi KEY_FILE="storage/app/.key"
if [ -z "$APP_KEY" ] && [ ! -s "$KEY_FILE" ]; then
if [[ -z "$APP_KEY" ]]; then
echo -e "\n > Oops! The required APP_KEY configuration is missing in your environment! "
echo -e "\n > You should set this APP_KEY in your .env file! "
draw_box() { draw_box() {
local text="$1" local text="$1"
@@ -32,25 +36,43 @@ if [[ -z "$APP_KEY" ]]; then
echo "$border" echo "$border"
} }
export APP_KEY=$(php artisan key:generate --show) export APP_KEY="$(php artisan key:generate --show)"
echo -e "\n > Oops! The required APP_KEY configuration is missing! Generated app key and saved in $KEY_FILE"
echo "$APP_KEY" > "$KEY_FILE"
draw_box $APP_KEY draw_box $APP_KEY
else
echo -e "\n > APP_KEY is OK... "
fi fi
echo -e "\n====================== Running migrations... ====================== " echo -e "\n====================== Running migrations... ====================== "
run_migrations() {
php artisan migrate --force # Wait 60 seconds for database to be ready
} RETRIES=12
RETRIES=12 # wait 60 seconds for database to be ready
DELAY=5 DELAY=5
run_migrations() {
sleep $DELAY
# php artisan migrate --force
output=$(php artisan migrate --force 2>/dev/null)
if [[ $? -eq 0 ]]; then
echo "$output"
return 0
else
return 1
fi
}
until run_migrations; do until run_migrations; do
RETRIES=$((RETRIES-1)) RETRIES=$((RETRIES-1))
if [ $RETRIES -le 0 ]; then if [[ $RETRIES -le 0 ]]; then
echo -e "\n > Database is not ready after $RETRIES attempts. Exiting... " echo -e "\n > Database is not ready after one minute. Exiting... \n"
exit 1 exit 1
fi fi
echo -e "\n > Waiting for database to be ready... retrying in $DELAY seconds. " echo -e "\n > Waiting for database to be ready... retrying in $DELAY seconds. \n"
sleep $DELAY
done done
echo -e "\n====================== Spinning up Supervisor daemon... ====================== \n" echo -e "\n====================== Spinning up Supervisor daemon... ====================== \n"
exec supervisord -c /etc/supervisor/conf.d/supervisord.conf exec supervisord -c /etc/supervisor/conf.d/supervisord.conf
+10 -5
View File
@@ -2,33 +2,38 @@
nodaemon=true nodaemon=true
user=root user=root
pidfile=/var/run/supervisord.pid pidfile=/var/run/supervisord.pid
logfile=/var/log/supervisor/supervisord.log
[program:nginx] [program:nginx]
command=nginx -g 'daemon off;' command=nginx -g 'daemon off;'
autostart=true autostart=true
autorestart=true autorestart=true
redirect_stderr=true redirect_stderr=true
redirect_stdout=true
[program:php] [program:php]
command=php-fpm -F command=php-fpm -F
autostart=true autostart=true
autorestart=true autorestart=true
redirect_stderr=true redirect_stderr=true
redirect_stdout=true
[program:scheduler] [program:scheduler]
command=php artisan schedule:work command=php artisan schedule:work
user=www-data
autorestart=true autorestart=true
redirect_stderr=true redirect_stderr=true
redirect_stdout=true stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
[program:queue-worker] [program:queue-worker]
command=php artisan queue:work --sleep=3 --tries=1 --memory=256 --timeout=3600
process_name=%(program_name)s_%(process_num)02d process_name=%(program_name)s_%(process_num)02d
command=php artisan queue:work --sleep=3 --tries=1 --memory=256 --timeout=3600
user=www-data
autorestart=true autorestart=true
redirect_stderr=true redirect_stderr=true
redirect_stdout=true stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
numprocs=2 numprocs=2
stopasgroup=true
killasgroup=true
[supervisorctl] [supervisorctl]
@@ -3,14 +3,14 @@
use App\Models\Holding; use App\Models\Holding;
use Livewire\Volt\Component; use Livewire\Volt\Component;
new class extends Component { new class extends Component
{
// props // props
public Holding $holding; public Holding $holding;
protected $listeners = [ protected $listeners = [
'transaction-updated' => '$refresh', 'transaction-updated' => '$refresh',
'transaction-saved' => '$refresh' 'transaction-saved' => '$refresh',
]; ];
// methods // methods
@@ -1,33 +1,38 @@
<?php <?php
use App\Models\Transaction;
use App\Models\Portfolio; use App\Models\Portfolio;
use App\Rules\SymbolValidationRule; use App\Models\Transaction;
use App\Rules\QuantityValidationRule; use App\Rules\QuantityValidationRule;
use Illuminate\Support\Collection; use App\Rules\SymbolValidationRule;
use Livewire\Attributes\{Computed}; use App\Traits\WithTrimStrings;
use Livewire\Volt\Component; use Livewire\Volt\Component;
use Mary\Traits\Toast; use Mary\Traits\Toast;
use Illuminate\Validation\Rule;
use App\Traits\WithTrimStrings;
new class extends Component { new class extends Component
{
use Toast; use Toast;
use WithTrimStrings; use WithTrimStrings;
// props // props
public ?Portfolio $portfolio; public ?Portfolio $portfolio;
public ?Transaction $transaction; public ?Transaction $transaction;
public ?String $portfolio_id; public ?string $portfolio_id;
public String $symbol;
public String $transaction_type;
public String $date;
public Float $quantity;
public ?Float $cost_basis;
public ?Float $sale_price;
public Bool $confirmingTransactionDeletion = false; public string $symbol;
public string $transaction_type;
public string $date;
public float $quantity;
public ?float $cost_basis;
public ?float $sale_price;
public bool $confirmingTransactionDeletion = false;
// methods // methods
public function rules() public function rules()
@@ -41,7 +46,7 @@ new class extends Component {
'required', 'required',
'numeric', 'numeric',
'min:0', 'min:0',
new QuantityValidationRule($this->portfolio, $this->symbol, $this->transaction_type, $this->date) new QuantityValidationRule($this->portfolio, $this->symbol, $this->transaction_type, $this->date),
], ],
'cost_basis' => 'exclude_if:transaction_type,SELL|min:0|numeric', 'cost_basis' => 'exclude_if:transaction_type,SELL|min:0|numeric',
'sale_price' => 'exclude_if:transaction_type,BUY|min:0|numeric', 'sale_price' => 'exclude_if:transaction_type,BUY|min:0|numeric',
@@ -72,7 +77,6 @@ new class extends Component {
$this->authorize('fullAccess', $this->portfolio); $this->authorize('fullAccess', $this->portfolio);
$this->transaction->update($this->validate()); $this->transaction->update($this->validate());
// $this->transaction->owner_id = auth()->user()->id;
$this->transaction->save(); $this->transaction->save();
$this->success(__('Transaction updated')); $this->success(__('Transaction updated'));
@@ -107,6 +111,11 @@ new class extends Component {
$this->success(__('Transaction deleted'), redirectTo: route('holding.show', ['portfolio' => $this->portfolio->id, 'symbol' => $this->symbol])); $this->success(__('Transaction deleted'), redirectTo: route('holding.show', ['portfolio' => $this->portfolio->id, 'symbol' => $this->symbol]));
} }
public function updatedSymbol($value)
{
$this->symbol = strtoupper($value);
}
}; ?> }; ?>
<div class="" x-data="{ transaction_type: @entangle('transaction_type') }"> <div class="" x-data="{ transaction_type: @entangle('transaction_type') }">
@@ -2,26 +2,28 @@
use App\Models\DailyChange; use App\Models\DailyChange;
use App\Models\Portfolio; use App\Models\Portfolio;
use Livewire\Attributes\{Title, Rule};
use Livewire\Volt\Component; use Livewire\Volt\Component;
new class extends Component { new class extends Component
{
// props // props
public ?Portfolio $portfolio; public ?Portfolio $portfolio;
public String $name = 'portfolio';
public String $scope = 'YTD'; public string $name = 'portfolio';
public Array $scopeOptions = [
public string $scope = 'YTD';
public array $scopeOptions = [
['id' => '1M', 'name' => '1 month', 'method' => 'subMonths', 'args' => [1]], ['id' => '1M', 'name' => '1 month', 'method' => 'subMonths', 'args' => [1]],
['id' => '3M', 'name' => '3 months', 'method' => 'subMonths', 'args' => [3]], ['id' => '3M', 'name' => '3 months', 'method' => 'subMonths', 'args' => [3]],
['id' => 'YTD', 'name' => 'Year to date', 'method' => 'startOfYear', 'args' => []], ['id' => 'YTD', 'name' => 'Year to date', 'method' => 'startOfYear', 'args' => []],
['id' => '1Y', 'name' => '1 year', 'method' => 'subYears', 'args' => [1]], ['id' => '1Y', 'name' => '1 year', 'method' => 'subYears', 'args' => [1]],
['id' => '3Y', 'name' => '3 years', 'method' => 'subYears', 'args' => [3]], ['id' => '3Y', 'name' => '3 years', 'method' => 'subYears', 'args' => [3]],
['id' => 'ALL', 'name' => 'All time', 'method' => null] ['id' => 'ALL', 'name' => 'All time', 'method' => null],
]; ];
// data // data
public Array $chartSeries; public array $chartSeries;
// methods // methods
public function mount() public function mount()
@@ -33,15 +35,7 @@ new class extends Component {
{ {
$filterMethod = collect($this->scopeOptions)->where('id', $this->scope)->first(); $filterMethod = collect($this->scopeOptions)->where('id', $this->scope)->first();
$dailyChangeQuery = DailyChange::myDailyChanges(); $dailyChangeQuery = DailyChange::myDailyChanges()->selectRaw('
if (isset($this->portfolio)) {
$dailyChangeQuery->portfolio($this->portfolio->id);
} else {
$dailyChangeQuery->selectRaw('
date, date,
SUM(total_market_value) as total_market_value, SUM(total_market_value) as total_market_value,
SUM(total_cost_basis) as total_cost_basis, SUM(total_cost_basis) as total_cost_basis,
@@ -50,19 +44,28 @@ new class extends Component {
SUM(realized_gains) as realized_gains, SUM(realized_gains) as realized_gains,
SUM(total_dividends_earned) as total_dividends_earned SUM(total_dividends_earned) as total_dividends_earned
*/ */
') ');
->withoutWishlists()
->groupBy('date')
->orderBy('date');
if (isset($this->portfolio)) {
// portfolio
$dailyChangeQuery->portfolio($this->portfolio->id);
} else {
// dashboard
$dailyChangeQuery->withoutWishlists();
} }
if ($filterMethod['method']) { if ($filterMethod['method']) {
$dailyChangeQuery->whereDate('date', '>=', now()->{$filterMethod['method']}(...$filterMethod['args'])); $dailyChangeQuery->whereDate('date', '>=', now()->{$filterMethod['method']}(...$filterMethod['args']));
} }
// dd($dailyChangeQuery->toSql());
$dailyChange = $dailyChangeQuery->get(); $dailyChange = $dailyChangeQuery
->orderBy('date')
->groupBy('date')
->get();
return [ return [
'series' => [ 'series' => [
@@ -76,7 +79,7 @@ new class extends Component {
], ],
[ [
'name' => __('Market Gain'), 'name' => __('Market Gain'),
'data' => $dailyChange->map(fn($data) => [$data->date, $data->total_gain])->toArray() 'data' => $dailyChange->map(fn ($data) => [$data->date, $data->total_gain])->toArray(),
], ],
// [ // [
@@ -87,7 +90,7 @@ new class extends Component {
// 'name' => __('Realized Gains'), // 'name' => __('Realized Gains'),
// 'data' => $dailyChange->map(fn($data) => [$data->date, $data->realized_gains])->toArray() // 'data' => $dailyChange->map(fn($data) => [$data->date, $data->realized_gains])->toArray()
// ], // ],
] ],
]; ];
} }
@@ -102,7 +105,6 @@ new class extends Component {
{ {
return collect($this->scopeOptions)->where('id', $scope)->first()['name']; return collect($this->scopeOptions)->where('id', $scope)->first()['name'];
} }
}; ?> }; ?>
<x-card class="bg-slate-100 dark:bg-base-200 rounded-lg mb-6"> <x-card class="bg-slate-100 dark:bg-base-200 rounded-lg mb-6">