Compare commits

..

18 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
26 changed files with 510 additions and 482 deletions
+4 -21
View File
@@ -40,13 +40,10 @@ LINKEDIN_CLIENT_SECRET=
FACEBOOK_CLIENT_ID=
FACEBOOK_CLIENT_SECRET=
APP_NAME=Investbrain
APP_TIMEZONE=UTC
APP_ENV=production
APP_DEBUG=true
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
SELF_HOSTED=true
FILESYSTEM_DISK=local
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
CACHE_STORE=redis
DB_CONNECTION=mysql
DB_HOST=investbrain-mysql
@@ -55,19 +52,7 @@ DB_DATABASE=investbrain
DB_USERNAME=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_PATH=/tmp/database_server.sock
REDIS_PASSWORD=null
REDIS_PORT=6379
@@ -85,5 +70,3 @@ AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
+2 -1
View File
@@ -4,7 +4,8 @@
| Version | Supported |
| ------- | ------------------ |
| 1.0.x | :white_check_mark: |
| 1.1.x | :white_check_mark: |
| 1.0.x | :x: |
| < 1.0.0 | :x: |
## Reporting a Vulnerability
+4 -2
View File
@@ -9,9 +9,11 @@ use Illuminate\Support\Carbon;
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;
}
+21 -14
View File
@@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class Dividend extends Model
@@ -109,22 +110,28 @@ class Dividend extends Model
public static function syncHoldings(string $symbol): void
{
// group by holdings
$dividends = self::select(['holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount'])
->selectRaw('
(COALESCE(CASE WHEN transactions.transaction_type = "BUY"
AND date(transactions.date) <= date(dividends.date)
THEN transactions.quantity ELSE 0 END, 0)
- COALESCE(CASE WHEN transactions.transaction_type = "SELL"
AND date(transactions.date) <= date(dividends.date)
THEN transactions.quantity ELSE 0 END, 0))
* dividends.dividend_amount
AS total_received
')
->join('transactions', 'transactions.symbol', '=', 'dividends.symbol')
$subQuery = self::select([
'holdings.portfolio_id',
'dividends.date',
'dividends.symbol',
'dividends.dividend_amount',
])->selectRaw("
(COALESCE(SUM(CASE WHEN transactions.transaction_type = 'BUY'
AND date(transactions.date) <= date(dividends.date)
THEN transactions.quantity ELSE 0 END), 0)
- COALESCE(SUM(CASE WHEN transactions.transaction_type = 'SELL'
AND date(transactions.date) <= date(dividends.date)
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')
->where('dividends.symbol', $symbol)
->groupBy('holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount', 'total_received')
->havingRaw('total_received > 0')
->groupBy('holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount');
$dividends = DB::table(DB::raw("({$subQuery->toSql()}) as sub"))
->mergeBindings($subQuery->getQuery())
->where('total_received', '>', 0)
->get();
// 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.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')
->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)
@@ -192,10 +210,10 @@ class Holding extends Model
$query = Transaction::where([
'portfolio_id' => $this->portfolio_id,
'symbol' => $this->symbol,
])->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 = "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 = '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 = '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")
->first();
$total_quantity = round($query->qty_purchases - $query->qty_sales, 3);
@@ -203,9 +221,8 @@ class Holding extends Model
$average_cost_basis = (
$query->qty_purchases > 0
&& $total_quantity > 0
)
? $query->total_cost_basis / $query->qty_purchases
: 0;
) ? $query->total_cost_basis / $query->qty_purchases
: 0;
// update holding
$this->fill([
@@ -247,12 +264,44 @@ class Holding extends Model
$end_date = now();
}
// MySQL default interval
$date_interval = 'DATE_ADD(date, INTERVAL 1 DAY)';
$castNumberType = 'decimal';
// Use SQLite interval grammar
if (config('database.default') === 'sqlite') {
$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
$max_recursion_var_name = 'cte_max_recursion_depth';
@@ -269,39 +318,29 @@ class Holding extends Model
DB::statement("SET $max_recursion_var_name=1000000;");
}
return 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")
)
// 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([
'date_series.date',
DB::raw("
ROUND(
COALESCE(SUM(CASE WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity ELSE 0 END), 0) -
COALESCE(SUM(CASE WHEN transactions.transaction_type = 'SELL' THEN transactions.quantity ELSE 0 END), 0), 3) AS `owned`
"),
{$quantityQuery} AS owned
"),
DB::raw("
COALESCE(CASE
WHEN (
ROUND(
COALESCE(SUM(CASE WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity ELSE 0 END), 0) -
COALESCE(SUM(CASE WHEN transactions.transaction_type = 'SELL' THEN transactions.quantity ELSE 0 END), 0), 3)
) = 0 THEN 0
ELSE SUM(CASE
WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity * transactions.cost_basis
ELSE 0
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`"),
CASE
WHEN ({$quantityQuery}) = 0 THEN 0
ELSE SUM(CASE
WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity * transactions.cost_basis
ELSE 0
END)
END AS cost_basis
"),
DB::raw("COALESCE(SUM(CASE WHEN transaction_type = 'SELL' THEN ((sale_price - cost_basis) * quantity) ELSE 0 END), 0) AS realized_gains"),
])
->leftJoin('transactions', function ($join) {
$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)) {
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(
$total_performance,
['date', 'portfolio_id'],
+3 -3
View File
@@ -101,7 +101,7 @@ class Split extends Model
->where([
'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)
->join('holdings', 'splits.symbol', 'holdings.symbol')
->orderBy('splits.date', 'ASC')
@@ -115,8 +115,8 @@ class Split extends Model
'portfolio_id' => $split->portfolio_id,
])
->whereDate('transactions.date', '<', $split->date->format('Y-m-d'))
->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')
->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")
->value('qty_owned');
if ($qty_owned > 0) {
+3 -3
View File
@@ -19,7 +19,7 @@ class Spotlight
}
$portfolios = $request->user()->portfolios()
->where('title', 'LIKE', '%'.$request->input('search').'%')
->whereFullText('title', $request->input('search'))
->limit(5)
->get();
$portfolios->each(function ($portfolio) use ($results) {
@@ -35,8 +35,8 @@ class Spotlight
$holdings = $request->user()->holdings()
->where('holdings.quantity', '>', 0)
->where(function ($query) use ($request) {
return $query->where('holdings.symbol', 'LIKE', '%'.$request->input('search').'%')
->orWhere('market_data.name', 'LIKE', '%'.$request->input('search').'%');
return $query->whereFullText('holdings.symbol', $request->input('search'))
->orWhereFullText('market_data.name', $request->input('search'));
})
->limit(5)
->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',
'key' => env('APP_KEY') ?: when(file_exists('storage/app/.key'), fn () => trim(file_get_contents('storage/app/.key'))),
'key' => env('APP_KEY')
?: when(file_exists(storage_path('app/.key')), fn () => trim(file_get_contents(storage_path('app/.key')))),
'previous_keys' => [
...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],
],
'sentry' => [
'driver' => 'sentry',
'level' => env('LOG_LEVEL', 'error'),
],
'stderr' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
@@ -17,7 +17,7 @@ return new class extends Migration
{
Schema::create('portfolios', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('title');
$table->string('title')->when(config('database.default') != 'sqlite', fn ($ctx) => $ctx->fulltext());
$table->text('notes')->nullable();
$table->boolean('wishlist')->default(false);
$table->timestamps();
@@ -18,8 +18,8 @@ class CreateMarketDataTable extends Migration
public function up()
{
Schema::create('market_data', function (Blueprint $table) {
$table->string('symbol', 15)->primary();
$table->string('name')->nullable();
$table->string('symbol', 25)->primary();
$table->string('name')->nullable()->when(config('database.default') != 'sqlite', fn ($ctx) => $ctx->fulltext());
$table->float('market_value', 12, 4)->nullable();
$table->float('fifty_two_week_low', 12, 4)->nullable();
$table->float('fifty_two_week_high', 12, 4)->nullable();
@@ -2,7 +2,6 @@
declare(strict_types=1);
use App\Models\MarketData;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
@@ -19,7 +18,7 @@ class CreateDividendsTable extends Migration
Schema::create('dividends', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->date('date');
$table->foreignIdFor(MarketData::class, 'symbol');
$table->string('symbol', 25);
$table->float('dividend_amount', 12, 4);
$table->timestamps();
});
@@ -2,7 +2,6 @@
declare(strict_types=1);
use App\Models\MarketData;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
@@ -19,7 +18,7 @@ class CreateSplitsTable extends Migration
Schema::create('splits', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->date('date');
$table->foreignIdFor(MarketData::class, 'symbol');
$table->string('symbol', 25);
$table->float('split_amount', 12, 4);
$table->timestamps();
});
@@ -2,7 +2,6 @@
declare(strict_types=1);
use App\Models\MarketData;
use App\Models\Portfolio;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
@@ -19,7 +18,7 @@ class CreateTransactionsTable extends Migration
{
Schema::create('transactions', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->foreignIdFor(MarketData::class, 'symbol');
$table->string('symbol', 25);
$table->foreignIdFor(Portfolio::class, 'portfolio_id')->constrained()->onDelete('cascade');
$table->string('transaction_type', 15);
$table->float('quantity', 12, 4);
@@ -2,7 +2,6 @@
declare(strict_types=1);
use App\Models\MarketData;
use App\Models\Portfolio;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
@@ -20,12 +19,13 @@ class CreateHoldingsTable extends Migration
Schema::create('holdings', function (Blueprint $table) {
$table->uuid('id')->primary();
$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('average_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('dividends_earned', 12, 4)->default(0);
$table->boolean('reinvest_dividends')->default(false);
$table->timestamp('splits_synced_at')->nullable();
$table->timestamps();
});
+33 -23
View File
@@ -12,6 +12,8 @@ class MarketDataSeeder extends Seeder
{
use WithoutModelEvents;
public array $rows = [];
/**
* Run the database seeds.
*/
@@ -26,7 +28,6 @@ class MarketDataSeeder extends Seeder
if (($handle = fopen($csvFilePath, 'r')) !== false) {
$header = null;
$rows = [];
$rowCount = 0;
while (($row = fgetcsv($handle, 0, ',')) !== false) {
@@ -38,46 +39,55 @@ class MarketDataSeeder extends Seeder
} else {
try {
$data = array_combine($header, $row);
$data = array_combine($header, $row);
$rows[] = [
'symbol' => $data['symbol'],
'name' => $data['name'],
'meta_data' => json_encode([
'country' => $data['country'],
'first_trade_year' => $data['first_trade_year'],
'sector' => $data['sector'],
'industry' => $data['industry'],
]),
];
$this->rows[] = [
'symbol' => $data['symbol'],
'name' => $data['name'],
'meta_data' => json_encode([
'country' => $data['country'],
'first_trade_year' => $data['first_trade_year'],
'sector' => $data['sector'],
'industry' => $data['industry'],
]),
];
$rowCount++;
$rowCount++;
if ($rowCount % $chunkSize == 0) {
DB::table('market_data')->insertOrIgnore($rows);
$rows = [];
}
} catch (\Throwable $e) {
if ($rowCount % $chunkSize == 0) {
throw new \Exception('Error: '.$e->getMessage());
$this->bulkInsert($this->rows);
}
}
}
// final clean up
if (! empty($rows)) {
DB::table('market_data')->insertOrIgnore($rows);
if (! empty($this->rows)) {
$this->bulkInsert($this->rows);
}
// Close the CSV file
fclose($handle);
echo "Imported $rowCount market data items successfully!\n";
echo "\n > Imported $rowCount market data items successfully!";
} else {
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
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
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
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
@@ -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
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
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
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
@@ -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
BRFS,BRF S.A.,Brazil,,Consumer Staples,Meat/Poultry/Fish
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
BRKHU,BurTech Acquisition Corp. Unit,United States,2021,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)
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
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)
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
@@ -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
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-A,Heico Corporation,United States,,,
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
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
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-A,Haverty Furniture Companies Inc.,United States,,,
HWBK,Hawthorn Bancshares Inc. 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
@@ -11488,7 +11478,6 @@ NYNYR,Empire Resorts Inc.,United States,,,
LLEX,"Lilis Energy, Inc.",United States,,Independent Oil & Gas,
EZT,"Entergy Texas, Inc.",United States,,,
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,,,
EMG,"Emergent Capital, Inc.",United States,,,
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,
EMP-A.TO,Empire Company Limited,Canada,,Gold,
ECCA,Eagle Point Credit Company Inc.,United States,,,
ECC,Eagle Point Credit Company Inc.,United States,,,
NESV,"National Energy Services, Inc.",United States,,,
YECO,Yulong Eco-Materials Limited,United States,,,
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,
GIG,"GigPeak, Inc.",United States,,,
SGEN,"Seattle Genetics, Inc.",United States,,Biotechnology,
SAND,Sandstorm Gold Ltd.,United States,,Gold,
PGEM,"Ply Gem Holdings, Inc",United States,,General Building Materials,
ININ,"Interactive Intelligence 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,,,
VELA.L,Vela Technologies PLC,United Kingdom,,,
VEGPF,Vectura Group plc,United States,,,
United States.TO,Americas Silver Corporation,Canada,,Industrial Metals & Minerals,
URHG,"United Resource Holdings Group, Inc.",United States,,,
UQA.VI,UNIQA Insurance Group AG,Austria,,,
UIBGF,UIB Group Limited,United States,,,
@@ -20863,7 +20849,6 @@ USMT,US Metro Bank,United States,,,
USK.F,"Skullcandy, Inc.",France,,,
USF.AX,"US Select Private Opportunities Fund, LP",Australia,,,
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,,,
US1.F,"Ultratech, Inc.",France,,,
URXZD,TENTH AVENUE PETROLEUM CORPORAT,United States,,,
@@ -22474,7 +22459,6 @@ O3X2.F,AAN Ventures Inc,France,,,
D94417.CR,DP04947-0011,Venezuela,,,
ENV.CR,ENVASES VENEZOLANOS S.A.,Venezuela,,,
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,,,
RAP1V.HE,Rapala VMC Corporation,Finland,,,
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,,,
WUMSF,Wumart Stores Inc.,United States,,,
WSSH,West Shore Bank Corp.,United States,,,
WSO-B,"Watsco, Inc.",United States,,,
WRCKF,WESC AB,United States,,,
WMSI,"Williams Industries, Incorporated",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,
0G29.L,Semperit Aktiengesellschaft Holding,United Kingdom,,,
0IPT.L,Akastor ASA,United Kingdom,,,
United StatesK.IS,United Statesk Seramik Sanayi A.S.,Turkey,,,
0NCV.L,Lenzing Aktiengesellschaft,United Kingdom,,,
0OK7.L,Akka Technologies,United Kingdom,,,
1AKA.BE,"AKER SOLUTIONS ASA NK1,08",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,,,
T1W.HM,Aktiengesellschaft Tokugawa,Germany,,,
0OIU.L,Arctic Paper S.A.,United Kingdom,,,
Can't render this file because it is too large.
+1 -3
View File
@@ -10,9 +10,7 @@ services:
ports:
- 8000:80
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"
ASSET_URL: "http://localhost:8000"
DB_CONNECTION: mysql
DB_HOST: investbrain-mysql
DB_PORT: 3306
@@ -25,7 +23,7 @@ services:
REDIS_HOST: investbrain-redis
volumes:
- 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:
- mysql
- redis
+4 -2
View File
@@ -14,11 +14,13 @@ mkdir -p storage/framework/cache \
storage/app \
storage/logs
echo -e "\n > Storage directory scaffolding is OK... "
# Ensure storage directory is permissioned for www-data
chmod -R 775 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 exists / generate if required
KEY_FILE="storage/app/.key"
@@ -36,7 +38,7 @@ if [ -z "$APP_KEY" ] && [ ! -s "$KEY_FILE" ]; then
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 -e "\n > Oops! The required APP_KEY configuration is missing! Generated app key and saved in $KEY_FILE"
echo "$APP_KEY" > "$KEY_FILE"
@@ -3,16 +3,16 @@
use App\Models\Holding;
use Livewire\Volt\Component;
new class extends Component {
new class extends Component
{
// props
public Holding $holding;
protected $listeners = [
'transaction-updated' => '$refresh',
'transaction-saved' => '$refresh'
'transaction-saved' => '$refresh',
];
// methods
}; ?>
@@ -1,33 +1,38 @@
<?php
use App\Models\Transaction;
use App\Models\Portfolio;
use App\Rules\SymbolValidationRule;
use App\Models\Transaction;
use App\Rules\QuantityValidationRule;
use Illuminate\Support\Collection;
use Livewire\Attributes\{Computed};
use App\Rules\SymbolValidationRule;
use App\Traits\WithTrimStrings;
use Livewire\Volt\Component;
use Mary\Traits\Toast;
use Illuminate\Validation\Rule;
use App\Traits\WithTrimStrings;
new class extends Component {
new class extends Component
{
use Toast;
use WithTrimStrings;
// props
public ?Portfolio $portfolio;
public ?Transaction $transaction;
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 ?string $portfolio_id;
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
public function rules()
@@ -36,19 +41,19 @@ new class extends Component {
'symbol' => ['required', 'string', new SymbolValidationRule],
'transaction_type' => 'required|string|in:BUY,SELL',
'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' => [
'required',
'numeric',
'min:0',
new QuantityValidationRule($this->portfolio, $this->symbol, $this->transaction_type, $this->date)
'required',
'numeric',
'min:0',
new QuantityValidationRule($this->portfolio, $this->symbol, $this->transaction_type, $this->date),
],
'cost_basis' => 'exclude_if:transaction_type,SELL|min:0|numeric',
'sale_price' => 'exclude_if:transaction_type,BUY|min:0|numeric',
];
}
public function mount()
public function mount()
{
if (isset($this->transaction)) {
@@ -59,7 +64,7 @@ new class extends Component {
$this->quantity = $this->transaction->quantity;
$this->cost_basis = $this->transaction->cost_basis;
$this->sale_price = $this->transaction->sale_price;
} else {
$this->transaction_type = 'BUY';
$this->portfolio_id = isset($this->portfolio) ? $this->portfolio->id : '';
@@ -70,9 +75,8 @@ new class extends Component {
public function update()
{
$this->authorize('fullAccess', $this->portfolio);
$this->transaction->update($this->validate());
// $this->transaction->owner_id = auth()->user()->id;
$this->transaction->save();
$this->success(__('Transaction updated'));
@@ -83,7 +87,7 @@ new class extends Component {
public function save()
{
if (!isset($this->portfolio)) {
if (! isset($this->portfolio)) {
$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]));
}
public function updatedSymbol($value)
{
$this->symbol = strtoupper($value);
}
}; ?>
<div class="" x-data="{ transaction_type: @entangle('transaction_type') }">
@@ -2,29 +2,31 @@
use App\Models\DailyChange;
use App\Models\Portfolio;
use Livewire\Attributes\{Title, Rule};
use Livewire\Volt\Component;
new class extends Component {
new class extends Component
{
// props
public ?Portfolio $portfolio;
public String $name = 'portfolio';
public String $scope = 'YTD';
public Array $scopeOptions = [
public string $name = 'portfolio';
public string $scope = 'YTD';
public array $scopeOptions = [
['id' => '1M', 'name' => '1 month', 'method' => 'subMonths', 'args' => [1]],
['id' => '3M', 'name' => '3 months', 'method' => 'subMonths', 'args' => [3]],
['id' => 'YTD', 'name' => 'Year to date', 'method' => 'startOfYear', 'args' => []],
['id' => '1Y', 'name' => '1 year', 'method' => 'subYears', 'args' => [1]],
['id' => '3Y', 'name' => '3 years', 'method' => 'subYears', 'args' => [3]],
['id' => 'ALL', 'name' => 'All time', 'method' => null]
['id' => 'ALL', 'name' => 'All time', 'method' => null],
];
// data
public Array $chartSeries;
public array $chartSeries;
// methods
public function mount()
public function mount()
{
$this->chartSeries = $this->generatePerformanceData();
}
@@ -33,52 +35,53 @@ new class extends Component {
{
$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)) {
// portfolio
$dailyChangeQuery->portfolio($this->portfolio->id);
} else {
$dailyChangeQuery->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
*/
')
->withoutWishlists()
->groupBy('date')
->orderBy('date');
// dashboard
$dailyChangeQuery->withoutWishlists();
}
if ($filterMethod['method']) {
$dailyChangeQuery->whereDate('date', '>=', now()->{$filterMethod['method']}(...$filterMethod['args']));
}
// dd($dailyChangeQuery->toSql());
$dailyChange = $dailyChangeQuery->get();
$dailyChange = $dailyChangeQuery
->orderBy('date')
->groupBy('date')
->get();
return [
'series' => [
[
'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'),
'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'),
'data' => $dailyChange->map(fn($data) => [$data->date, $data->total_gain])->toArray()
'data' => $dailyChange->map(fn ($data) => [$data->date, $data->total_gain])->toArray(),
],
// [
// 'name' => __('Dividends Earned'),
// 'data' => $dailyChange->map(fn($data) => [$data->date, $data->total_dividends_earned])->toArray()
@@ -87,7 +90,7 @@ new class extends Component {
// 'name' => __('Realized Gains'),
// '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'];
}
}; ?>
<x-card class="bg-slate-100 dark:bg-base-200 rounded-lg mb-6">