| 203 | | -- Core query inside report_stock_volatility_and_activity() |
| 204 | | WITH daily_returns AS ( |
| 205 | | SELECT sh.stock_id AS dr_stock_id, |
| 206 | | sh.price AS dr_price, |
| 207 | | (sh.price - LAG(sh.price) OVER ( |
| 208 | | PARTITION BY sh.stock_id ORDER BY sh.timestamp)) |
| 209 | | / NULLIF(LAG(sh.price) OVER ( |
| 210 | | PARTITION BY sh.stock_id ORDER BY sh.timestamp), 0) |
| 211 | | AS dr_daily_return |
| 212 | | FROM stock_history sh |
| 213 | | WHERE sh.timestamp >= p_from_date |
| 214 | | AND sh.timestamp <= p_to_date |
| 215 | | ), |
| 216 | | price_stats AS ( |
| 217 | | SELECT dr.dr_stock_id, |
| 218 | | COUNT(*) AS ps_total_points, |
| 219 | | MIN(dr.dr_price) AS ps_min_price, |
| 220 | | MAX(dr.dr_price) AS ps_max_price, |
| 221 | | AVG(dr.dr_price) AS ps_avg_price, |
| 222 | | COALESCE(STDDEV(dr.dr_daily_return) * 100, 0) AS ps_volatility_pct |
| 223 | | FROM daily_returns dr |
| 224 | | GROUP BY dr.dr_stock_id |
| 225 | | ), |
| 226 | | txn_stats AS ( |
| 227 | | SELECT t.stock_id AS ts_stock_id, |
| 228 | | SUM(t.price * t.quantity) AS ts_total_volume, |
| 229 | | COUNT(*) AS ts_total_count, |
| 230 | | COUNT(*) FILTER (WHERE t.type = 'BUY') AS ts_buy_count, |
| 231 | | COUNT(*) FILTER (WHERE t.type = 'SELL') AS ts_sell_count |
| 232 | | FROM transactions t |
| 233 | | WHERE t.timestamp >= p_from_date |
| 234 | | AND t.timestamp <= p_to_date |
| 235 | | GROUP BY t.stock_id |
| | 203 | CREATE OR REPLACE FUNCTION report_stock_volatility_and_activity( |
| | 204 | p_from_date TIMESTAMP DEFAULT NULL, |
| | 205 | p_to_date TIMESTAMP DEFAULT NULL |
| 237 | | SELECT s.id, s.symbol, s.name, s.current_price, ... |
| 238 | | FROM stock s |
| 239 | | LEFT JOIN price_stats ps ON s.id = ps.dr_stock_id |
| 240 | | LEFT JOIN txn_stats ts ON s.id = ts.ts_stock_id |
| 241 | | ORDER BY historical_volatility_pct DESC, activity_score DESC; |
| | 207 | RETURNS TABLE ( |
| | 208 | stock_id INTEGER, |
| | 209 | symbol TEXT, |
| | 210 | stock_name TEXT, |
| | 211 | current_price NUMERIC, |
| | 212 | period_min_price NUMERIC, |
| | 213 | period_max_price NUMERIC, |
| | 214 | price_range_pct NUMERIC, |
| | 215 | avg_daily_price NUMERIC, |
| | 216 | historical_volatility_pct NUMERIC, |
| | 217 | total_history_points BIGINT, |
| | 218 | total_txn_volume NUMERIC, |
| | 219 | total_txn_count BIGINT, |
| | 220 | buy_txn_count BIGINT, |
| | 221 | sell_txn_count BIGINT, |
| | 222 | buy_sell_ratio NUMERIC, |
| | 223 | active_watchlist_users BIGINT, |
| | 224 | pending_trade_requests BIGINT, |
| | 225 | trend_direction TEXT, |
| | 226 | trend_slope NUMERIC, |
| | 227 | activity_score NUMERIC, |
| | 228 | volatility_rank BIGINT, |
| | 229 | activity_rank BIGINT |
| | 230 | ) AS $$ |
| | 231 | BEGIN |
| | 232 | RETURN QUERY |
| | 233 | WITH |
| | 234 | -- Volatility: stddev of daily returns computed with LAG window function |
| | 235 | -- computing daily return per row using LAG window function |
| | 236 | daily_returns AS ( |
| | 237 | SELECT |
| | 238 | sh.stock_id AS dr_stock_id, |
| | 239 | sh.price AS dr_price, |
| | 240 | (sh.price - LAG(sh.price) OVER ( |
| | 241 | PARTITION BY sh.stock_id ORDER BY sh.timestamp |
| | 242 | )) / NULLIF(LAG(sh.price) OVER ( |
| | 243 | PARTITION BY sh.stock_id ORDER BY sh.timestamp |
| | 244 | ), 0) AS dr_daily_return |
| | 245 | FROM stock_history sh |
| | 246 | WHERE (p_from_date IS NULL OR sh.timestamp >= p_from_date) |
| | 247 | AND (p_to_date IS NULL OR sh.timestamp <= p_to_date) |
| | 248 | ), |
| | 249 | -- aggregate per stock |
| | 250 | price_stats AS ( |
| | 251 | SELECT |
| | 252 | dr.dr_stock_id AS ps_stock_id, |
| | 253 | COUNT(*) AS ps_total_points, |
| | 254 | MIN(dr.dr_price) AS ps_min_price, |
| | 255 | MAX(dr.dr_price) AS ps_max_price, |
| | 256 | AVG(dr.dr_price) AS ps_avg_price, |
| | 257 | COALESCE(STDDEV(dr.dr_daily_return) * 100, 0) AS ps_volatility_pct |
| | 258 | FROM daily_returns dr |
| | 259 | GROUP BY dr.dr_stock_id |
| | 260 | ), |
| | 261 | -- trend least-squares linear regression slope on (row_number, price) |
| | 262 | |
| | 263 | price_ordered AS ( |
| | 264 | SELECT |
| | 265 | sh.stock_id AS po_stock_id, |
| | 266 | ROW_NUMBER() OVER (PARTITION BY sh.stock_id ORDER BY sh.timestamp) AS po_rn, |
| | 267 | sh.price AS po_price |
| | 268 | FROM stock_history sh |
| | 269 | WHERE (p_from_date IS NULL OR sh.timestamp >= p_from_date) |
| | 270 | AND (p_to_date IS NULL OR sh.timestamp <= p_to_date) |
| | 271 | ), |
| | 272 | trend_calc AS ( |
| | 273 | SELECT |
| | 274 | po.po_stock_id AS tc_stock_id, |
| | 275 | ROUND( |
| | 276 | ((COUNT(*) * SUM(po.po_rn * po.po_price) - SUM(po.po_rn) * SUM(po.po_price)) |
| | 277 | / NULLIF(COUNT(*) * SUM(po.po_rn * po.po_rn) - SUM(po.po_rn) * SUM(po.po_rn), 0))::NUMERIC |
| | 278 | , 6) AS tc_slope |
| | 279 | FROM price_ordered po |
| | 280 | GROUP BY po.po_stock_id |
| | 281 | ), |
| | 282 | -- transctikomn stats per stock in period |
| | 283 | txn_stats AS ( |
| | 284 | SELECT |
| | 285 | t.stock_id AS ts_stock_id, |
| | 286 | SUM(t.price * t.quantity) AS ts_total_volume, |
| | 287 | COUNT(*) AS ts_total_count, |
| | 288 | COUNT(*) FILTER (WHERE t.type = 'BUY') AS ts_buy_count, |
| | 289 | COUNT(*) FILTER (WHERE t.type = 'SELL') AS ts_sell_count |
| | 290 | FROM transactions t |
| | 291 | WHERE (p_from_date IS NULL OR t.timestamp >= p_from_date) |
| | 292 | AND (p_to_date IS NULL OR t.timestamp <= p_to_date) |
| | 293 | GROUP BY t.stock_id |
| | 294 | ), |
| | 295 | -- watchlist interest stock |
| | 296 | watchlist_stats AS ( |
| | 297 | SELECT |
| | 298 | w.stock_id AS ws_stock_id, |
| | 299 | COUNT(DISTINCT w.user_id) AS ws_active_watchers |
| | 300 | FROM watchlist w |
| | 301 | GROUP BY w.stock_id |
| | 302 | ), |
| | 303 | -- pending trade requests per stock |
| | 304 | pending_stats AS ( |
| | 305 | SELECT |
| | 306 | s.id AS pst_stock_id, |
| | 307 | COUNT(tr.id) AS pst_pending_count |
| | 308 | FROM trade_request tr |
| | 309 | JOIN stock s ON tr.stock_symbol = s.symbol |
| | 310 | WHERE tr.status = 'PENDING' |
| | 311 | GROUP BY s.id |
| | 312 | ) |
| | 313 | |
| | 314 | SELECT |
| | 315 | s.id::INTEGER AS stock_id, |
| | 316 | s.symbol::TEXT, |
| | 317 | s.name::TEXT AS stock_name, |
| | 318 | s.current_price::NUMERIC, |
| | 319 | COALESCE(ps.ps_min_price, s.current_price)::NUMERIC AS period_min_price, |
| | 320 | COALESCE(ps.ps_max_price, s.current_price)::NUMERIC AS period_max_price, |
| | 321 | ROUND( |
| | 322 | ((COALESCE(ps.ps_max_price, s.current_price) |
| | 323 | - COALESCE(ps.ps_min_price, s.current_price)) |
| | 324 | / NULLIF(COALESCE(ps.ps_min_price, s.current_price), 0) * 100)::NUMERIC |
| | 325 | , 2)::NUMERIC AS price_range_pct, |
| | 326 | COALESCE(ps.ps_avg_price, s.current_price)::NUMERIC AS avg_daily_price, |
| | 327 | COALESCE(ps.ps_volatility_pct, 0)::NUMERIC AS historical_volatility_pct, |
| | 328 | COALESCE(ps.ps_total_points, 0)::BIGINT AS total_history_points, |
| | 329 | COALESCE(ts.ts_total_volume, 0)::NUMERIC AS total_txn_volume, |
| | 330 | COALESCE(ts.ts_total_count, 0)::BIGINT AS total_txn_count, |
| | 331 | COALESCE(ts.ts_buy_count, 0)::BIGINT AS buy_txn_count, |
| | 332 | COALESCE(ts.ts_sell_count, 0)::BIGINT AS sell_txn_count, |
| | 333 | ROUND( |
| | 334 | (COALESCE(ts.ts_buy_count, 0)::NUMERIC |
| | 335 | / NULLIF(COALESCE(ts.ts_sell_count, 0), 0))::NUMERIC |
| | 336 | , 2)::NUMERIC AS buy_sell_ratio, |
| | 337 | COALESCE(ws.ws_active_watchers, 0)::BIGINT AS active_watchlist_users, |
| | 338 | COALESCE(pst.pst_pending_count, 0)::BIGINT AS pending_trade_requests, |
| | 339 | CASE |
| | 340 | WHEN tc.tc_slope > 0.01 THEN 'UPTREND' |
| | 341 | WHEN tc.tc_slope < -0.01 THEN 'DOWNTREND' |
| | 342 | ELSE 'SIDEWAYS' |
| | 343 | END::TEXT AS trend_direction, |
| | 344 | COALESCE(tc.tc_slope, 0)::NUMERIC AS trend_slope, |
| | 345 | ROUND( |
| | 346 | (COALESCE(ws.ws_active_watchers, 0) * 2.0 |
| | 347 | + COALESCE(ts.ts_total_count, 0) * 1.0 |
| | 348 | + COALESCE(pst.pst_pending_count, 0) * 3.0)::NUMERIC |
| | 349 | , 2)::NUMERIC AS activity_score, |
| | 350 | RANK() OVER ( |
| | 351 | ORDER BY COALESCE(ps.ps_volatility_pct, 0) DESC |
| | 352 | )::BIGINT AS volatility_rank, |
| | 353 | RANK() OVER ( |
| | 354 | ORDER BY ( |
| | 355 | COALESCE(ws.ws_active_watchers, 0) * 2.0 |
| | 356 | + COALESCE(ts.ts_total_count, 0) * 1.0 |
| | 357 | + COALESCE(pst.pst_pending_count, 0) * 3.0 |
| | 358 | ) DESC |
| | 359 | )::BIGINT AS activity_rank |
| | 360 | FROM stock s |
| | 361 | LEFT JOIN price_stats ps ON s.id = ps.ps_stock_id |
| | 362 | LEFT JOIN trend_calc tc ON s.id = tc.tc_stock_id |
| | 363 | LEFT JOIN txn_stats ts ON s.id = ts.ts_stock_id |
| | 364 | LEFT JOIN watchlist_stats ws ON s.id = ws.ws_stock_id |
| | 365 | LEFT JOIN pending_stats pst ON s.id = pst.pst_stock_id |
| | 366 | ORDER BY historical_volatility_pct DESC, activity_score DESC; |
| | 367 | END; |
| | 368 | $$ LANGUAGE plpgsql; |