feat: adds pgsql compatibility (#72)
This commit is contained in:
@@ -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
|
||||
|
||||
+78
-39
@@ -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')
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user