Compare commits

...

15 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
23 changed files with 242 additions and 202 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
+78 -39
View File
@@ -100,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");
} }
/** /**
@@ -148,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)
@@ -192,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);
@@ -203,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([
@@ -247,12 +264,44 @@ 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 { }
// Default CTE time series query (for MySQL and SQLite)
$timeSeriesQuery = DB::table(DB::raw("(
WITH RECURSIVE date_series AS (
SELECT '{$start_date->format('Y-m-d')}' AS date
UNION ALL
SELECT $date_interval
FROM 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 // MySQL default
$max_recursion_var_name = 'cte_max_recursion_depth'; $max_recursion_var_name = 'cte_max_recursion_depth';
@@ -269,39 +318,29 @@ class Holding extends Model
DB::statement("SET $max_recursion_var_name=1000000;"); DB::statement("SET $max_recursion_var_name=1000000;");
} }
return DB::table(DB::raw("( // Extracted query for counting QTY owned
WITH RECURSIVE date_series AS ( $quantityQuery = "ROUND(CAST(COALESCE(
SELECT '{$start_date->format('Y-m-d')}' AS date SUM(CASE WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity ELSE 0 END)
UNION ALL - SUM(CASE WHEN transactions.transaction_type = 'SELL' THEN transactions.quantity ELSE 0 END),
SELECT $date_interval 0
FROM date_series ) AS {$castNumberType}), 3)";
WHERE date < '{$end_date->format('Y-m-d')}'
) return $timeSeriesQuery
SELECT date_series.date
FROM date_series
) as date_series")
)
->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();
+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.
-1
View File
@@ -11,7 +11,6 @@ services:
- 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_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
@@ -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">