- Backend (FastAPI, SQLAlchemy 2, Alembic): single-admin cookie auth, news/crawler/market APIs with SSE and WebSocket streaming - Crawler (Scrapy + Scrapy-Redis): wscn_news, wscn_live, cls_depth, cls_telegraph spiders, idempotent upserts with revisions, raw response archiving, per-source subprocess supervision, restart and backoff - Market worker: full-history Binance spot/USD-M kline backfill, realtime WS stream, hourly and on-reconnect catch-up - Frontend (Vue 3, Element Plus, UnoCSS, KLineChart): news feed, crawler console with live logs, Binance-style chart page - Deploy: docker compose, nginx site, migrate and daily backup scripts
87 lines
3.9 KiB
Python
87 lines
3.9 KiB
Python
"""market tables
|
|
|
|
Revision ID: fb4db40f2e86
|
|
Revises: ac70d5a07cf0
|
|
Create Date: 2026-10-13 04:54:37.566401
|
|
|
|
"""
|
|
from typing import Sequence, Union
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
|
|
|
|
# revision identifiers, used by Alembic.
|
|
revision: str = 'fb4db40f2e86'
|
|
down_revision: Union[str, Sequence[str], None] = 'ac70d5a07cf0'
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
"""Upgrade schema."""
|
|
# ### commands auto generated by Alembic - please adjust! ###
|
|
op.create_table('kline_sync_state',
|
|
sa.Column('market', sa.String(length=8), nullable=False),
|
|
sa.Column('symbol', sa.String(length=32), nullable=False),
|
|
sa.Column('interval', sa.String(length=4), nullable=False),
|
|
sa.Column('earliest_open_time', sa.BigInteger(), nullable=True),
|
|
sa.Column('latest_open_time', sa.BigInteger(), nullable=True),
|
|
sa.Column('backfill_done', sa.Boolean(), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.PrimaryKeyConstraint('market', 'symbol', 'interval')
|
|
)
|
|
op.create_table('klines',
|
|
sa.Column('market', sa.String(length=8), nullable=False),
|
|
sa.Column('symbol', sa.String(length=32), nullable=False),
|
|
sa.Column('interval', sa.String(length=4), nullable=False),
|
|
sa.Column('open_time', sa.BigInteger(), nullable=False),
|
|
sa.Column('open', sa.Numeric(), nullable=False),
|
|
sa.Column('high', sa.Numeric(), nullable=False),
|
|
sa.Column('low', sa.Numeric(), nullable=False),
|
|
sa.Column('close', sa.Numeric(), nullable=False),
|
|
sa.Column('volume', sa.Numeric(), nullable=False),
|
|
sa.Column('close_time', sa.BigInteger(), nullable=False),
|
|
sa.Column('quote_volume', sa.Numeric(), nullable=False),
|
|
sa.Column('trades', sa.BigInteger(), nullable=False),
|
|
sa.Column('taker_buy_base', sa.Numeric(), nullable=False),
|
|
sa.Column('taker_buy_quote', sa.Numeric(), nullable=False),
|
|
sa.PrimaryKeyConstraint('market', 'symbol', 'interval', 'open_time'),
|
|
postgresql_partition_by='LIST (interval)'
|
|
)
|
|
op.create_table('market_symbols',
|
|
sa.Column('id', sa.Integer(), nullable=False),
|
|
sa.Column('market', sa.String(length=8), nullable=False),
|
|
sa.Column('symbol', sa.String(length=32), nullable=False),
|
|
sa.Column('base', sa.String(length=16), nullable=False),
|
|
sa.Column('quote', sa.String(length=16), nullable=False),
|
|
sa.Column('status', sa.String(length=16), nullable=False),
|
|
sa.Column('is_default', sa.Boolean(), nullable=False),
|
|
sa.Column('added_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('last_error', sa.Text(), nullable=True),
|
|
sa.PrimaryKeyConstraint('id'),
|
|
sa.UniqueConstraint('market', 'symbol')
|
|
)
|
|
# ### end Alembic commands ###
|
|
# 每个周期一个分区;周期列表固定写在迁移里,不依赖运行时代码
|
|
for interval in ["1M", "1w", "3d", "1d", "12h", "8h", "6h", "4h", "2h", "1h", "30m", "15m", "5m", "3m", "1m"]:
|
|
op.execute(f"CREATE TABLE klines_{interval.lower() if interval != '1M' else '1mo'} PARTITION OF klines FOR VALUES IN ('{interval}')")
|
|
symbols = sa.table(
|
|
"market_symbols",
|
|
sa.column("market", sa.String), sa.column("symbol", sa.String), sa.column("base", sa.String),
|
|
sa.column("quote", sa.String), sa.column("status", sa.String), sa.column("is_default", sa.Boolean),
|
|
)
|
|
op.bulk_insert(symbols, [
|
|
{"market": "spot", "symbol": f"{base}USDT", "base": base, "quote": "USDT", "status": "pending", "is_default": True}
|
|
for base in ("BTC", "ETH", "BNB")
|
|
])
|
|
|
|
|
|
def downgrade() -> None:
|
|
"""Downgrade schema."""
|
|
# ### commands auto generated by Alembic - please adjust! ###
|
|
op.drop_table('market_symbols')
|
|
op.drop_table('klines')
|
|
op.drop_table('kline_sync_state')
|
|
# ### end Alembic commands ###
|