Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c4b7d399ea | |||
| ffe53e91c0 | |||
| aeb1b12afe | |||
| fe81ec7ee7 | |||
| f0ecc0fd3d | |||
| 03b75fb683 | |||
| dc93621547 | |||
| 7ab6f79e56 | |||
| 9e48f21c8d | |||
| 10e6de8df4 | |||
| 00fbdec6f1 | |||
| 730903c383 | |||
| 5fc9455908 | |||
| 28e0ad68fc | |||
| ca48d702a7 | |||
| 812b9ed075 | |||
| 93a0595652 | |||
| 8a357e8cab | |||
| 22e12977f8 | |||
| 732cf02317 | |||
| 6dea75651b | |||
| 6cff252813 | |||
| 0d06ca6a04 | |||
| a3f875270b |
+4
-21
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
+102
-50
@@ -7,6 +7,7 @@ namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class Holding extends Model
|
||||
@@ -62,8 +63,8 @@ class Holding extends Model
|
||||
return $this->hasMany(Dividend::class, 'symbol', 'symbol')
|
||||
->select(['dividends.symbol', 'dividends.date', 'dividends.dividend_amount'])
|
||||
->selectRaw("SUM(
|
||||
CASE WHEN transaction_type = 'BUY'
|
||||
AND transactions.symbol = dividends.symbol
|
||||
CASE WHEN transaction_type = 'BUY'
|
||||
AND transactions.symbol = dividends.symbol
|
||||
AND transactions.portfolio_id = '$this->portfolio_id'
|
||||
AND date(dividends.date) >= date(transactions.date)
|
||||
THEN transactions.quantity
|
||||
@@ -71,22 +72,22 @@ class Holding extends Model
|
||||
) AS purchased")
|
||||
->selectRaw("SUM(
|
||||
CASE WHEN transaction_type = 'SELL'
|
||||
AND transactions.symbol = dividends.symbol
|
||||
AND transactions.portfolio_id = '$this->portfolio_id'
|
||||
AND transactions.symbol = dividends.symbol
|
||||
AND transactions.portfolio_id = '$this->portfolio_id'
|
||||
AND date(dividends.date) >= date(transactions.date)
|
||||
THEN transactions.quantity
|
||||
ELSE 0 END
|
||||
) AS sold")
|
||||
->selectRaw("SUM(
|
||||
(CASE WHEN transaction_type = 'BUY'
|
||||
AND transactions.symbol = dividends.symbol
|
||||
(CASE WHEN transaction_type = 'BUY'
|
||||
AND transactions.symbol = dividends.symbol
|
||||
AND transactions.portfolio_id = '$this->portfolio_id'
|
||||
AND date(transactions.date) <= date(dividends.date)
|
||||
AND date(transactions.date) <= date(dividends.date)
|
||||
THEN transactions.quantity ELSE 0 END
|
||||
- CASE WHEN transaction_type = 'SELL'
|
||||
AND transactions.symbol = dividends.symbol
|
||||
- CASE WHEN transaction_type = 'SELL'
|
||||
AND transactions.symbol = dividends.symbol
|
||||
AND transactions.portfolio_id = '$this->portfolio_id'
|
||||
AND date(transactions.date) <= date(dividends.date)
|
||||
AND date(transactions.date) <= date(dividends.date)
|
||||
THEN transactions.quantity ELSE 0 END)
|
||||
* dividends.dividend_amount
|
||||
) AS total_received")
|
||||
@@ -99,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");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -147,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)
|
||||
@@ -191,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);
|
||||
@@ -202,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([
|
||||
@@ -246,49 +264,83 @@ 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 {
|
||||
|
||||
DB::statement('SET cte_max_recursion_depth=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
|
||||
// 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
|
||||
) 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([
|
||||
'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')
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -100,7 +100,8 @@ return [
|
||||
|
||||
'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' => [
|
||||
...array_filter(
|
||||
|
||||
@@ -77,6 +77,6 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'profile_photo_disk' => 'public',
|
||||
'profile_photo_disk' => env('JETSTREAM_PROFILE_PHOTO_DISK', 'public'),
|
||||
|
||||
];
|
||||
|
||||
+1
-1
@@ -116,7 +116,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'inject_assets' => true,
|
||||
'inject_assets' => false,
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
+5
-5
@@ -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
|
||||
@@ -36,10 +34,12 @@ services:
|
||||
container_name: investbrain-redis
|
||||
restart: unless-stopped
|
||||
tty: true
|
||||
networks:
|
||||
- investbrain-network
|
||||
command:
|
||||
- --loglevel warning
|
||||
volumes:
|
||||
- investbrain-redis:/data
|
||||
networks:
|
||||
- investbrain-network
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: investbrain-mysql
|
||||
|
||||
+46
-18
@@ -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 APP_NAME=Investbrain
|
||||
ENV VITE_APP_NAME=Investbrain
|
||||
|
||||
# Set the working directory
|
||||
COPY . /var/app
|
||||
WORKDIR /var/app
|
||||
|
||||
# Install required packages
|
||||
RUN apt-get update && apt-get upgrade -y \
|
||||
&& apt-get install -y \
|
||||
nginx \
|
||||
libfreetype-dev \
|
||||
libjpeg62-turbo-dev \
|
||||
libpng-dev \
|
||||
@@ -20,22 +19,18 @@ RUN apt-get update && apt-get upgrade -y \
|
||||
libicu-dev \
|
||||
libpq-dev \
|
||||
binutils libc6-dev \
|
||||
supervisor \
|
||||
unzip curl git \
|
||||
nodejs npm \
|
||||
# Clean up APT
|
||||
&& apt-get -y autoremove \
|
||||
&& apt-get clean \
|
||||
&& 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
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
# Remove default nginx config
|
||||
RUN rm /etc/nginx/sites-enabled/default \
|
||||
&& rm -rf /var/www/html \
|
||||
&& ln -s /var/app /var/www/app
|
||||
# Install PHP extensions
|
||||
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
|
||||
&& docker-php-ext-install -j$(nproc) gd zip
|
||||
|
||||
# Copy application files
|
||||
COPY . .
|
||||
|
||||
# Install Composer and Node.js Install PHP dependencies and build front end assets
|
||||
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
|
||||
@@ -43,13 +38,46 @@ RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local
|
||||
&& npm install && npm run build \
|
||||
&& rm -rf node_modules
|
||||
|
||||
# Stage 2: Production stage
|
||||
FROM php:8.3-fpm-alpine
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /var/app
|
||||
|
||||
# Copy necessary files from the builder stage
|
||||
COPY --from=builder /var/app /var/app
|
||||
COPY --from=builder /usr/local/etc/php/conf.d /usr/local/etc/php/conf.d
|
||||
COPY --from=builder /usr/local/bin/composer /usr/local/bin/composer
|
||||
|
||||
# Install required Alpine packages
|
||||
RUN apk add --no-cache \
|
||||
nginx \
|
||||
supervisor \
|
||||
libpng-dev \
|
||||
libzip-dev \
|
||||
icu-dev \
|
||||
postgresql-dev \
|
||||
freetype-dev \
|
||||
libjpeg-turbo-dev \
|
||||
bash \
|
||||
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
|
||||
&& docker-php-ext-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 ./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
|
||||
|
||||
# Set permissions and link storage
|
||||
RUN php artisan storage:link \
|
||||
&& chown -R www-data:www-data . \
|
||||
&& chown -R www-data:www-data . \
|
||||
&& chmod +x ./docker/entrypoint.sh
|
||||
|
||||
# Serve on port 80
|
||||
@@ -59,4 +87,4 @@ EXPOSE 80
|
||||
HEALTHCHECK --interval=30s --timeout=10s --retries=3 CMD curl -f http://localhost/up || exit 1
|
||||
|
||||
# Run everything else
|
||||
ENTRYPOINT ["/bin/bash", "./docker/entrypoint.sh"]
|
||||
ENTRYPOINT ["/bin/sh", "./docker/entrypoint.sh"]
|
||||
|
||||
+17
-6
@@ -8,17 +8,23 @@ echo "CiAgKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKi
|
||||
echo -e "\n====================== Validating environment... ====================== "
|
||||
|
||||
# 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
|
||||
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 is generated
|
||||
if [[ -z "$APP_KEY" ]]; then
|
||||
echo -e "\n > Oops! The required APP_KEY configuration is missing in your environment! "
|
||||
# Ensure app key exists / generate if required
|
||||
KEY_FILE="storage/app/.key"
|
||||
if [ -z "$APP_KEY" ] && [ ! -s "$KEY_FILE" ]; then
|
||||
|
||||
draw_box() {
|
||||
local text="$1"
|
||||
@@ -30,7 +36,12 @@ if [[ -z "$APP_KEY" ]]; then
|
||||
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
|
||||
else
|
||||
echo -e "\n > APP_KEY is OK... "
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user