File size: 6,944 Bytes
6fe7c36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- Table: stock_transactions
-- Stores all buy/sell transactions for the portfolio
CREATE TABLE IF NOT EXISTS stock_transactions (
    id SERIAL PRIMARY KEY,
    transaction_id UUID DEFAULT uuid_generate_v4() UNIQUE NOT NULL,
    symbol VARCHAR(10) NOT NULL,
    transaction_type VARCHAR(10) NOT NULL CHECK (transaction_type IN ('BUY', 'SELL')),
    quantity DECIMAL(15, 4) NOT NULL CHECK (quantity > 0),
    price DECIMAL(15, 4) NOT NULL CHECK (price >= 0),
    transaction_date DATE NOT NULL,
    notes TEXT,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Table: portfolio_positions
-- Stores current aggregated positions (calculated from transactions)
CREATE TABLE IF NOT EXISTS portfolio_positions (
    symbol VARCHAR(10) PRIMARY KEY,
    total_quantity DECIMAL(15, 4) NOT NULL CHECK (total_quantity >= 0),
    avg_cost_basis DECIMAL(15, 4) NOT NULL CHECK (avg_cost_basis >= 0),
    first_purchase_date DATE,
    last_transaction_date DATE,
    total_invested DECIMAL(15, 2) NOT NULL DEFAULT 0,
    realized_gains DECIMAL(15, 2) DEFAULT 0,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Table: portfolio_snapshots
-- Stores historical portfolio value snapshots for performance tracking
CREATE TABLE IF NOT EXISTS portfolio_snapshots (
    id SERIAL PRIMARY KEY,
    snapshot_date DATE NOT NULL,
    total_value DECIMAL(15, 2) NOT NULL,
    total_cost_basis DECIMAL(15, 2) NOT NULL,
    total_gain_loss DECIMAL(15, 2) NOT NULL,
    total_gain_loss_pct DECIMAL(10, 4) NOT NULL,
    num_positions INTEGER NOT NULL,
    snapshot_data JSONB,  -- Store detailed position data
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Table: stock_metadata
-- Cache stock information to reduce API calls
CREATE TABLE IF NOT EXISTS stock_metadata (
    symbol VARCHAR(10) PRIMARY KEY,
    company_name VARCHAR(255),
    sector VARCHAR(100),
    industry VARCHAR(100),
    market_cap BIGINT,
    last_updated TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Indexes for performance optimization
CREATE INDEX IF NOT EXISTS idx_transactions_symbol ON stock_transactions(symbol);
CREATE INDEX IF NOT EXISTS idx_transactions_date ON stock_transactions(transaction_date DESC);
CREATE INDEX IF NOT EXISTS idx_transactions_type ON stock_transactions(transaction_type);
CREATE INDEX IF NOT EXISTS idx_snapshots_date ON portfolio_snapshots(snapshot_date DESC);
CREATE INDEX IF NOT EXISTS idx_stock_metadata_sector ON stock_metadata(sector);

-- Function to update the updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = CURRENT_TIMESTAMP;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Triggers to automatically update updated_at
CREATE TRIGGER update_stock_transactions_updated_at
    BEFORE UPDATE ON stock_transactions
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_at_column();

CREATE TRIGGER update_portfolio_positions_updated_at
    BEFORE UPDATE ON portfolio_positions
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_at_column();

CREATE TRIGGER update_stock_metadata_updated_at
    BEFORE UPDATE ON stock_metadata
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_at_column();

-- Function to recalculate portfolio positions
-- This ensures positions are always in sync with transactions
CREATE OR REPLACE FUNCTION recalculate_position(p_symbol VARCHAR)
RETURNS VOID AS $$
DECLARE
    v_total_quantity DECIMAL(15, 4);
    v_avg_cost_basis DECIMAL(15, 4);
    v_total_invested DECIMAL(15, 2);
    v_realized_gains DECIMAL(15, 2);
    v_first_purchase DATE;
    v_last_transaction DATE;
BEGIN
    -- Calculate position from transactions
    WITH position_calc AS (
        SELECT
            SUM(CASE WHEN transaction_type = 'BUY' THEN quantity ELSE -quantity END) as net_quantity,
            SUM(CASE WHEN transaction_type = 'BUY' THEN quantity * price ELSE 0 END) as total_cost,
            SUM(CASE WHEN transaction_type = 'BUY' THEN quantity ELSE 0 END) as total_bought,
            MIN(CASE WHEN transaction_type = 'BUY' THEN transaction_date END) as first_buy,
            MAX(transaction_date) as last_trans,
            SUM(CASE WHEN transaction_type = 'SELL' THEN quantity * price ELSE 0 END) -
            SUM(CASE WHEN transaction_type = 'SELL' THEN quantity *
                (SELECT AVG(price) FROM stock_transactions WHERE symbol = p_symbol AND transaction_type = 'BUY')
                ELSE 0 END) as realized_gain
        FROM stock_transactions
        WHERE symbol = p_symbol
    )
    SELECT
        net_quantity,
        CASE WHEN total_bought > 0 THEN total_cost / total_bought ELSE 0 END,
        total_cost,
        COALESCE(realized_gain, 0),
        first_buy,
        last_trans
    INTO v_total_quantity, v_avg_cost_basis, v_total_invested, v_realized_gains, v_first_purchase, v_last_transaction
    FROM position_calc;

    -- Update or delete position
    IF v_total_quantity > 0 THEN
        INSERT INTO portfolio_positions (
            symbol, total_quantity, avg_cost_basis, total_invested,
            realized_gains, first_purchase_date, last_transaction_date
        )
        VALUES (
            p_symbol, v_total_quantity, v_avg_cost_basis, v_total_invested,
            v_realized_gains, v_first_purchase, v_last_transaction
        )
        ON CONFLICT (symbol) DO UPDATE SET
            total_quantity = EXCLUDED.total_quantity,
            avg_cost_basis = EXCLUDED.avg_cost_basis,
            total_invested = EXCLUDED.total_invested,
            realized_gains = EXCLUDED.realized_gains,
            first_purchase_date = EXCLUDED.first_purchase_date,
            last_transaction_date = EXCLUDED.last_transaction_date;
    ELSE
        -- If position is fully closed, delete it
        DELETE FROM portfolio_positions WHERE symbol = p_symbol;
    END IF;
END;
$$ LANGUAGE plpgsql;

-- Trigger to automatically recalculate positions when transactions change
CREATE OR REPLACE FUNCTION trigger_recalculate_position()
RETURNS TRIGGER AS $$
BEGIN
    IF TG_OP = 'DELETE' THEN
        PERFORM recalculate_position(OLD.symbol);
        RETURN OLD;
    ELSE
        PERFORM recalculate_position(NEW.symbol);
        RETURN NEW;
    END IF;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER auto_recalculate_position
    AFTER INSERT OR UPDATE OR DELETE ON stock_transactions
    FOR EACH ROW
    EXECUTE FUNCTION trigger_recalculate_position();

-- View: current_portfolio_summary
-- Provides a quick overview of the portfolio
CREATE OR REPLACE VIEW current_portfolio_summary AS
SELECT
    COUNT(*) as total_positions,
    SUM(total_quantity) as total_shares,
    SUM(total_invested) as total_invested,
    SUM(realized_gains) as total_realized_gains
FROM portfolio_positions;