|
1 | 1 | """ |
2 | 2 | View Stellarcarbon's asset stats. Optionally, filter by recipient. |
| 3 | +Compute year-over-year metrics based on historical sinking transactions. |
3 | 4 |
|
4 | 5 | Author: Alex Olieman <https://keybase.io/alioli> |
5 | 6 | """ |
6 | 7 |
|
7 | 8 | from decimal import Decimal |
8 | 9 |
|
| 10 | +import numpy as np |
| 11 | +import pandas as pd |
9 | 12 | from sqlalchemy import func, select, union_all |
10 | 13 |
|
11 | 14 | from sc_audit.db_schema.association import SinkStatus |
12 | 15 | from sc_audit.db_schema.mint import MintedBlock |
13 | 16 | from sc_audit.db_schema.sink import SinkingTx |
14 | 17 | from sc_audit.session_manager import Session |
| 18 | +from sc_audit.views.sink_status import view_sinking_txs |
15 | 19 |
|
16 | 20 |
|
17 | 21 | def get_carbon_stats(recipient: str | None = None) -> dict[str, Decimal]: |
@@ -49,3 +53,66 @@ def get_carbon_stats(recipient: str | None = None) -> dict[str, Decimal]: |
49 | 53 | stats["carbon_pending"] = c_sunk - c_retired |
50 | 54 |
|
51 | 55 | return stats |
| 56 | + |
| 57 | + |
| 58 | +def view_yoy_analytics() -> pd.DataFrame: |
| 59 | + df = view_sinking_txs(order='asc') |
| 60 | + if df.empty: |
| 61 | + return pd.DataFrame() |
| 62 | + |
| 63 | + # Compute price for USDC transactions |
| 64 | + df['price'] = np.where( |
| 65 | + df['dest_asset_code'] == 'USDC', |
| 66 | + df['dest_asset_amount'].astype(float) / df['carbon_amount'].astype(float), |
| 67 | + np.nan |
| 68 | + ) |
| 69 | + |
| 70 | + # Interpolate prices linearly, then extrapolate using forward fill for trailing CARBON transactions |
| 71 | + df['price'] = df['price'].interpolate(method='linear').ffill() |
| 72 | + |
| 73 | + # Convert CARBON denominated amounts to USD using the interpolated price |
| 74 | + mask = df['dest_asset_code'] == 'CARBON' |
| 75 | + df.loc[mask, 'dest_asset_amount'] = df.loc[mask, 'price'] * df.loc[mask, 'carbon_amount'].astype(float) |
| 76 | + |
| 77 | + # Ensure all dest_asset_amount are float for consistent summing |
| 78 | + df['dest_asset_amount'] = df['dest_asset_amount'].astype(float) |
| 79 | + |
| 80 | + # Add year and month columns for binning |
| 81 | + df['year'] = df['created_at'].dt.year |
| 82 | + df['month'] = df['created_at'].dt.month |
| 83 | + |
| 84 | + # Calculate monthly active users (distinct recipients per month) |
| 85 | + mau_df = df.groupby(['year', 'month'])['recipient'].nunique().reset_index() |
| 86 | + mau_yearly = ( |
| 87 | + mau_df.groupby('year')['recipient'] |
| 88 | + .mean() |
| 89 | + .reset_index() |
| 90 | + .rename(columns={'recipient': 'mau'}) |
| 91 | + ) |
| 92 | + mau_yearly['mau'] = mau_yearly['mau'].astype(float).round(2) |
| 93 | + |
| 94 | + # Calculate yearly aggregates |
| 95 | + yearly = df.groupby('year').agg( |
| 96 | + num_tx=('hash', 'count'), |
| 97 | + volume_usd=('dest_asset_amount', 'sum'), |
| 98 | + carbon_sunk=('carbon_amount', 'sum') |
| 99 | + ).reset_index() |
| 100 | + yearly['volume_usd'] = yearly['volume_usd'].astype(float).round(2) |
| 101 | + |
| 102 | + # Merge MAU with yearly data |
| 103 | + result = yearly.merge(mau_yearly, on='year', how='left') |
| 104 | + |
| 105 | + # Sort by year |
| 106 | + result = result.sort_values('year') |
| 107 | + |
| 108 | + # Calculate year-over-year percentage changes |
| 109 | + for col in ['mau', 'num_tx', 'volume_usd', 'carbon_sunk']: |
| 110 | + result[f'yoy_{col}'] = (result[col].astype(float).pct_change() * 100).round(2) |
| 111 | + |
| 112 | + # Reorder columns: year, then each core column followed by its yoy column |
| 113 | + result = result[[ |
| 114 | + 'year', 'mau', 'yoy_mau', 'num_tx', 'yoy_num_tx', |
| 115 | + 'volume_usd', 'yoy_volume_usd', 'carbon_sunk', 'yoy_carbon_sunk', |
| 116 | + ]] |
| 117 | + |
| 118 | + return result |
0 commit comments