Compare commits

...

24 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
27 changed files with 597 additions and 517 deletions
+4 -21
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,19 +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_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
@@ -85,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}"
+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
+4 -2
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
{ {
$this->items['name'] = (string) $name; if (! empty($name)) {
$this->items['name'] = (string) $name;
}
return $this; return $this;
} }
+21 -14
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',
AND date(transactions.date) <= date(dividends.date) 'dividends.symbol',
THEN transactions.quantity ELSE 0 END, 0) 'dividends.dividend_amount',
- COALESCE(CASE WHEN transactions.transaction_type = "SELL" ])->selectRaw("
AND date(transactions.date) <= date(dividends.date) (COALESCE(SUM(CASE WHEN transactions.transaction_type = 'BUY'
THEN transactions.quantity ELSE 0 END, 0)) AND date(transactions.date) <= date(dividends.date)
* dividends.dividend_amount THEN transactions.quantity ELSE 0 END), 0)
AS total_received - COALESCE(SUM(CASE WHEN transactions.transaction_type = 'SELL'
') AND date(transactions.date) <= date(dividends.date)
->join('transactions', 'transactions.symbol', '=', 'dividends.symbol') THEN transactions.quantity ELSE 0 END), 0))
* dividends.dividend_amount
AS total_received
")->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
+92 -40
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,9 +221,8 @@ 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
$this->fill([ $this->fill([
@@ -246,49 +264,83 @@ 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)
WITH RECURSIVE date_series AS ( $timeSeriesQuery = DB::table(DB::raw("(
SELECT '{$start_date->format('Y-m-d')}' AS date WITH RECURSIVE date_series AS (
UNION ALL SELECT '{$start_date->format('Y-m-d')}' AS date
SELECT $date_interval UNION ALL
FROM date_series SELECT $date_interval
WHERE date < '{$end_date->format('Y-m-d')}'
)
SELECT date_series.date
FROM date_series FROM date_series
) as date_series") WHERE date < '{$end_date->format('Y-m-d')}'
) )
SELECT date_series.date
FROM 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( ELSE SUM(CASE
COALESCE(SUM(CASE WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity ELSE 0 END), 0) - WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity * transactions.cost_basis
COALESCE(SUM(CASE WHEN transactions.transaction_type = 'SELL' THEN transactions.quantity ELSE 0 END), 0), 3) ELSE 0
) = 0 THEN 0 END)
ELSE SUM(CASE END AS cost_basis
WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity * transactions.cost_basis "),
ELSE 0 DB::raw("COALESCE(SUM(CASE WHEN transaction_type = 'SELL' THEN ((sale_price - cost_basis) * quantity) ELSE 0 END), 0) AS realized_gains"),
END)
END, 0) 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`"),
]) ])
->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
@@ -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'),
@@ -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();
}); });
+33 -23
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,46 +39,55 @@ 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([
'country' => $data['country'], 'country' => $data['country'],
'first_trade_year' => $data['first_trade_year'], 'first_trade_year' => $data['first_trade_year'],
'sector' => $data['sector'], 'sector' => $data['sector'],
'industry' => $data['industry'], 'industry' => $data['industry'],
]), ]),
]; ];
$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.
+5 -5
View File
@@ -10,9 +10,7 @@ services:
ports: ports:
- 8000:80 - 8000:80
environment: # You can either use these properties OR an .env file. Do not use both! 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
@@ -25,7 +23,7 @@ services:
REDIS_HOST: investbrain-redis REDIS_HOST: investbrain-redis
volumes: volumes:
- investbrain-storage:/var/app/storage # You can use a volume... - investbrain-storage:/var/app/storage # You can use a volume...
# - /path/to/storage:/var/app/storage # ...or you can use a path on host # - /path/to/storage:/var/app/storage:delegated # ...or you can use a path on host
depends_on: depends_on:
- mysql - mysql
- redis - redis
@@ -36,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
+45 -17
View File
@@ -1,17 +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
# 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 \
@@ -20,22 +19,18 @@ 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
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) \
gd pgsql zip pdo_mysql mysqli intl
# Remove default nginx config # Install PHP extensions
RUN rm /etc/nginx/sites-enabled/default \ RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& rm -rf /var/www/html \ && docker-php-ext-install -j$(nproc) gd zip
&& ln -s /var/app /var/www/app
# Copy application files
COPY . .
# Install Composer and Node.js Install PHP dependencies and build front end assets # 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 \ RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
@@ -43,8 +38,41 @@ RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local
&& npm install && npm run build \ && npm install && npm run build \
&& rm -rf node_modules && 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-install -j$(nproc) \
gd pgsql zip pdo_mysql mysqli intl
# Remove default nginx config
RUN rm -rf /var/www/html \
&& ln -s /var/app /var/www/app
# Create required directories for supervisord
RUN mkdir -p /var/log/supervisor /var/run/supervisor
# 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 # Set permissions and link storage
@@ -59,4 +87,4 @@ EXPOSE 80
HEALTHCHECK --interval=30s --timeout=10s --retries=3 CMD curl -f http://localhost/up || 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"]
+17 -6
View File
@@ -8,17 +8,23 @@ echo "CiAgKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKi
echo -e "\n====================== Validating environment... ====================== " echo -e "\n====================== Validating environment... ====================== "
# Ensure app storage directory is scaffolded # Ensure app storage directory is scaffolded
mkdir -p storage/{{framework/cache,framework/sessions,framework/views},app,logs} mkdir -p storage/framework/cache \
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 # 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
echo -e "\n > Storage directory scaffolding is OK... " echo -e "\n > Permissions are OK... "
# Ensure app key is generated # Ensure app key exists / generate if required
if [[ -z "$APP_KEY" ]]; then KEY_FILE="storage/app/.key"
echo -e "\n > Oops! The required APP_KEY configuration is missing in your environment! " if [ -z "$APP_KEY" ] && [ ! -s "$KEY_FILE" ]; then
draw_box() { draw_box() {
local text="$1" local text="$1"
@@ -30,7 +36,12 @@ 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 else
echo -e "\n > APP_KEY is OK... " echo -e "\n > APP_KEY is OK... "
@@ -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()
@@ -36,12 +41,12 @@ new class extends Component {
'symbol' => ['required', 'string', new SymbolValidationRule], 'symbol' => ['required', 'string', new SymbolValidationRule],
'transaction_type' => 'required|string|in:BUY,SELL', 'transaction_type' => 'required|string|in:BUY,SELL',
'portfolio_id' => 'required|exists:portfolios,id', 'portfolio_id' => 'required|exists:portfolios,id',
'date' => ['required', 'date_format:Y-m-d', 'before_or_equal:' . now()->format('Y-m-d')], 'date' => ['required', 'date_format:Y-m-d', 'before_or_equal:'.now()->format('Y-m-d')],
'quantity' => [ 'quantity' => [
'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'));
@@ -83,7 +87,7 @@ new class extends Component {
public function save() public function save()
{ {
if (!isset($this->portfolio)) { if (! isset($this->portfolio)) {
$this->portfolio = Portfolio::find($this->portfolio_id); $this->portfolio = Portfolio::find($this->portfolio_id);
} }
@@ -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,50 +35,51 @@ 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('
date,
SUM(total_market_value) as total_market_value,
SUM(total_cost_basis) as total_cost_basis,
SUM(total_gain) as total_gain
/* ,
SUM(realized_gains) as realized_gains,
SUM(total_dividends_earned) as total_dividends_earned
*/
');
if (isset($this->portfolio)) { if (isset($this->portfolio)) {
// portfolio
$dailyChangeQuery->portfolio($this->portfolio->id); $dailyChangeQuery->portfolio($this->portfolio->id);
} else { } else {
$dailyChangeQuery->selectRaw(' // dashboard
date, $dailyChangeQuery->withoutWishlists();
SUM(total_market_value) as total_market_value,
SUM(total_cost_basis) as total_cost_basis,
SUM(total_gain) as total_gain
/* ,
SUM(realized_gains) as realized_gains,
SUM(total_dividends_earned) as total_dividends_earned
*/
')
->withoutWishlists()
->groupBy('date')
->orderBy('date');
} }
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' => [
[ [
'name' => __('Market Value'), 'name' => __('Market Value'),
'data' => $dailyChange->map(fn($data) => [$data->date, $data->total_market_value])->toArray(), 'data' => $dailyChange->map(fn ($data) => [$data->date, $data->total_market_value])->toArray(),
], ],
[ [
'name' => __('Cost Basis'), 'name' => __('Cost Basis'),
'data' => $dailyChange->map(fn($data) => [$data->date, $data->total_cost_basis])->toArray(), 'data' => $dailyChange->map(fn ($data) => [$data->date, $data->total_cost_basis])->toArray(),
], ],
[ [
'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">