386 lines
11 KiB
Python
386 lines
11 KiB
Python
"""Add Trade model for execution history with CGT tracking
|
|
|
|
Revision ID: 005
|
|
Revises: 004
|
|
Create Date: 2025-12-26 15:00:00.000000
|
|
|
|
This migration adds the trades table for tracking buy/sell trade executions
|
|
with full capital gains tax (CGT) support for Australian tax compliance.
|
|
Each trade belongs to a portfolio and includes acquisition details, cost basis,
|
|
holding period calculations, and CGT discount eligibility.
|
|
|
|
Table: trades
|
|
Columns:
|
|
Core Trade:
|
|
- id: Primary key, auto-increment
|
|
- portfolio_id: Foreign key to portfolios.id (cascade delete)
|
|
- symbol: Stock/asset symbol (STRING 20, uppercase)
|
|
- side: Trade side (ENUM: BUY, SELL)
|
|
- quantity: Number of units traded (NUMERIC 19,4)
|
|
- price: Price per unit (NUMERIC 19,4)
|
|
- total_value: Total trade value (NUMERIC 19,4)
|
|
- order_type: Order type (ENUM: MARKET, LIMIT, STOP, STOP_LIMIT)
|
|
- status: Trade status (ENUM: PENDING, FILLED, PARTIAL, CANCELLED, REJECTED)
|
|
- executed_at: Timestamp when executed (DATETIME, nullable)
|
|
|
|
Signal:
|
|
- signal_source: Source of trading signal (STRING 100, nullable)
|
|
- signal_confidence: Confidence score 0-100 (NUMERIC 5,2, nullable)
|
|
|
|
CGT (Australian Tax):
|
|
- acquisition_date: Date asset acquired (DATE)
|
|
- cost_basis_per_unit: Purchase price per unit (NUMERIC 19,4, nullable)
|
|
- cost_basis_total: Total purchase cost (NUMERIC 19,4, nullable)
|
|
- holding_period_days: Days held (INTEGER, nullable)
|
|
- cgt_discount_eligible: Eligible for 50% CGT discount (BOOLEAN, default: false)
|
|
- cgt_gross_gain: Gross capital gain (NUMERIC 19,4, nullable)
|
|
- cgt_gross_loss: Gross capital loss (NUMERIC 19,4, nullable)
|
|
- cgt_net_gain: Net capital gain after discount (NUMERIC 19,4, nullable)
|
|
|
|
Currency:
|
|
- currency: 3-letter currency code (STRING 3, default: AUD)
|
|
- fx_rate_to_aud: FX rate to AUD (NUMERIC 19,8, default: 1.0)
|
|
- total_value_aud: Total value in AUD (NUMERIC 19,4, nullable)
|
|
|
|
Timestamps:
|
|
- created_at: Timestamp when created (auto)
|
|
- updated_at: Timestamp when last updated (auto)
|
|
|
|
Constraints:
|
|
- CHECK quantity > 0
|
|
- CHECK price > 0
|
|
- CHECK total_value > 0
|
|
- CHECK signal_confidence >= 0 AND signal_confidence <= 100
|
|
- CHECK holding_period_days >= 0 OR holding_period_days IS NULL
|
|
- CHECK fx_rate_to_aud > 0
|
|
|
|
Indexes:
|
|
- ix_trades_portfolio_id: Index on portfolio_id
|
|
- ix_trades_symbol: Index on symbol
|
|
- ix_trades_side: Index on side
|
|
- ix_trades_status: Index on status
|
|
- ix_trades_acquisition_date: Index on acquisition_date
|
|
- ix_trade_portfolio_symbol: Composite index on (portfolio_id, symbol)
|
|
- ix_trade_portfolio_side: Composite index on (portfolio_id, side)
|
|
- ix_trade_status_executed: Composite index on (status, executed_at)
|
|
|
|
Relationships:
|
|
- trades.portfolio_id -> portfolios.id (CASCADE DELETE)
|
|
"""
|
|
from typing import Sequence, Union
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
|
|
|
|
# revision identifiers, used by Alembic.
|
|
revision: str = '005'
|
|
down_revision: Union[str, None] = '004'
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
"""Create trades table with all constraints and indexes.
|
|
|
|
Creates the trades table for tracking trade executions with CGT support
|
|
and proper foreign keys, constraints, and indexes.
|
|
"""
|
|
# Create trades table
|
|
op.create_table(
|
|
'trades',
|
|
# Primary key
|
|
sa.Column(
|
|
'id',
|
|
sa.Integer(),
|
|
nullable=False,
|
|
primary_key=True,
|
|
autoincrement=True
|
|
),
|
|
|
|
# Foreign key to portfolios (cascade delete)
|
|
sa.Column(
|
|
'portfolio_id',
|
|
sa.Integer(),
|
|
sa.ForeignKey('portfolios.id', ondelete='CASCADE'),
|
|
nullable=False,
|
|
comment='Portfolio this trade belongs to'
|
|
),
|
|
|
|
# Core trade fields
|
|
sa.Column(
|
|
'symbol',
|
|
sa.String(length=20),
|
|
nullable=False,
|
|
comment='Stock/asset symbol (uppercase)'
|
|
),
|
|
|
|
sa.Column(
|
|
'side',
|
|
sa.String(length=10),
|
|
nullable=False,
|
|
comment='Trade side: BUY or SELL'
|
|
),
|
|
|
|
sa.Column(
|
|
'quantity',
|
|
sa.Numeric(precision=19, scale=4),
|
|
nullable=False,
|
|
comment='Number of units traded'
|
|
),
|
|
|
|
sa.Column(
|
|
'price',
|
|
sa.Numeric(precision=19, scale=4),
|
|
nullable=False,
|
|
comment='Price per unit'
|
|
),
|
|
|
|
sa.Column(
|
|
'total_value',
|
|
sa.Numeric(precision=19, scale=4),
|
|
nullable=False,
|
|
comment='Total trade value (quantity * price)'
|
|
),
|
|
|
|
sa.Column(
|
|
'order_type',
|
|
sa.String(length=20),
|
|
nullable=False,
|
|
comment='Order type: MARKET, LIMIT, STOP, STOP_LIMIT'
|
|
),
|
|
|
|
sa.Column(
|
|
'status',
|
|
sa.String(length=20),
|
|
nullable=False,
|
|
server_default='PENDING',
|
|
comment='Trade status: PENDING, FILLED, PARTIAL, CANCELLED, REJECTED'
|
|
),
|
|
|
|
sa.Column(
|
|
'executed_at',
|
|
sa.DateTime(timezone=True),
|
|
nullable=True,
|
|
comment='Timestamp when trade was executed (nullable for pending)'
|
|
),
|
|
|
|
# Signal fields
|
|
sa.Column(
|
|
'signal_source',
|
|
sa.String(length=100),
|
|
nullable=True,
|
|
comment='Source of trading signal (e.g., RSI_DIVERGENCE)'
|
|
),
|
|
|
|
sa.Column(
|
|
'signal_confidence',
|
|
sa.Numeric(precision=5, scale=2),
|
|
nullable=True,
|
|
comment='Signal confidence score 0-100'
|
|
),
|
|
|
|
# CGT (Capital Gains Tax) fields
|
|
sa.Column(
|
|
'acquisition_date',
|
|
sa.Date(),
|
|
nullable=False,
|
|
comment='Date asset was acquired (for CGT)'
|
|
),
|
|
|
|
sa.Column(
|
|
'cost_basis_per_unit',
|
|
sa.Numeric(precision=19, scale=4),
|
|
nullable=True,
|
|
comment='Purchase price per unit for CGT calculation'
|
|
),
|
|
|
|
sa.Column(
|
|
'cost_basis_total',
|
|
sa.Numeric(precision=19, scale=4),
|
|
nullable=True,
|
|
comment='Total purchase cost for CGT calculation'
|
|
),
|
|
|
|
sa.Column(
|
|
'holding_period_days',
|
|
sa.Integer(),
|
|
nullable=True,
|
|
comment='Days held (for 50% CGT discount eligibility)'
|
|
),
|
|
|
|
sa.Column(
|
|
'cgt_discount_eligible',
|
|
sa.Boolean(),
|
|
nullable=False,
|
|
server_default='false',
|
|
comment='Eligible for 50% CGT discount (held >365 days)'
|
|
),
|
|
|
|
sa.Column(
|
|
'cgt_gross_gain',
|
|
sa.Numeric(precision=19, scale=4),
|
|
nullable=True,
|
|
comment='Gross capital gain before discount'
|
|
),
|
|
|
|
sa.Column(
|
|
'cgt_gross_loss',
|
|
sa.Numeric(precision=19, scale=4),
|
|
nullable=True,
|
|
comment='Gross capital loss'
|
|
),
|
|
|
|
sa.Column(
|
|
'cgt_net_gain',
|
|
sa.Numeric(precision=19, scale=4),
|
|
nullable=True,
|
|
comment='Net capital gain after 50% discount'
|
|
),
|
|
|
|
# Currency fields
|
|
sa.Column(
|
|
'currency',
|
|
sa.String(length=3),
|
|
nullable=False,
|
|
server_default='AUD',
|
|
comment='Currency code (ISO 4217, e.g., AUD, USD)'
|
|
),
|
|
|
|
sa.Column(
|
|
'fx_rate_to_aud',
|
|
sa.Numeric(precision=19, scale=8),
|
|
nullable=False,
|
|
server_default='1.0',
|
|
comment='Foreign exchange rate to AUD'
|
|
),
|
|
|
|
sa.Column(
|
|
'total_value_aud',
|
|
sa.Numeric(precision=19, scale=4),
|
|
nullable=True,
|
|
comment='Total value in AUD (for multi-currency portfolios)'
|
|
),
|
|
|
|
# Timestamps (from TimestampMixin)
|
|
sa.Column(
|
|
'created_at',
|
|
sa.DateTime(timezone=True),
|
|
nullable=False,
|
|
server_default=sa.text('CURRENT_TIMESTAMP'),
|
|
comment='Timestamp when trade was created'
|
|
),
|
|
|
|
sa.Column(
|
|
'updated_at',
|
|
sa.DateTime(timezone=True),
|
|
nullable=False,
|
|
server_default=sa.text('CURRENT_TIMESTAMP'),
|
|
comment='Timestamp when trade was last updated'
|
|
),
|
|
|
|
# Check constraints: positive values
|
|
sa.CheckConstraint(
|
|
'quantity > 0',
|
|
name='ck_trade_quantity_positive'
|
|
),
|
|
|
|
sa.CheckConstraint(
|
|
'price > 0',
|
|
name='ck_trade_price_positive'
|
|
),
|
|
|
|
sa.CheckConstraint(
|
|
'total_value > 0',
|
|
name='ck_trade_total_value_positive'
|
|
),
|
|
|
|
# Check constraint: signal confidence range
|
|
sa.CheckConstraint(
|
|
'signal_confidence >= 0 AND signal_confidence <= 100',
|
|
name='ck_trade_signal_confidence_range'
|
|
),
|
|
|
|
# Check constraint: holding period non-negative
|
|
sa.CheckConstraint(
|
|
'holding_period_days >= 0 OR holding_period_days IS NULL',
|
|
name='ck_trade_holding_period_positive'
|
|
),
|
|
|
|
# Check constraint: FX rate positive
|
|
sa.CheckConstraint(
|
|
'fx_rate_to_aud > 0',
|
|
name='ck_trade_fx_rate_positive'
|
|
),
|
|
)
|
|
|
|
# Create indexes for efficient queries
|
|
# Single column indexes
|
|
op.create_index(
|
|
'ix_trades_portfolio_id',
|
|
'trades',
|
|
['portfolio_id']
|
|
)
|
|
|
|
op.create_index(
|
|
'ix_trades_symbol',
|
|
'trades',
|
|
['symbol']
|
|
)
|
|
|
|
op.create_index(
|
|
'ix_trades_side',
|
|
'trades',
|
|
['side']
|
|
)
|
|
|
|
op.create_index(
|
|
'ix_trades_status',
|
|
'trades',
|
|
['status']
|
|
)
|
|
|
|
op.create_index(
|
|
'ix_trades_acquisition_date',
|
|
'trades',
|
|
['acquisition_date']
|
|
)
|
|
|
|
# Composite indexes for common query patterns
|
|
op.create_index(
|
|
'ix_trade_portfolio_symbol',
|
|
'trades',
|
|
['portfolio_id', 'symbol']
|
|
)
|
|
|
|
op.create_index(
|
|
'ix_trade_portfolio_side',
|
|
'trades',
|
|
['portfolio_id', 'side']
|
|
)
|
|
|
|
op.create_index(
|
|
'ix_trade_status_executed',
|
|
'trades',
|
|
['status', 'executed_at']
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
"""Drop trades table and all associated indexes and constraints.
|
|
|
|
WARNING: This will permanently delete all trade execution data!
|
|
"""
|
|
# Drop all indexes
|
|
op.drop_index('ix_trade_status_executed', 'trades')
|
|
op.drop_index('ix_trade_portfolio_side', 'trades')
|
|
op.drop_index('ix_trade_portfolio_symbol', 'trades')
|
|
op.drop_index('ix_trades_acquisition_date', 'trades')
|
|
op.drop_index('ix_trades_status', 'trades')
|
|
op.drop_index('ix_trades_side', 'trades')
|
|
op.drop_index('ix_trades_symbol', 'trades')
|
|
op.drop_index('ix_trades_portfolio_id', 'trades')
|
|
|
|
# Drop the table (constraints are dropped automatically)
|
|
op.drop_table('trades')
|