"""合成数据(MVP 零依赖可跑)。 生成随机游走 OHLCV,灌入 DB。仅用于让回测链路在没有真实数据源时也能跑通演示。 阶段1 接 Tushare/AKShare 后,这里仅保留为"离线测试夹具"。 """ from __future__ import annotations from datetime import datetime, timedelta, timezone import numpy as np from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from ..domain import Bar from ..models import Candle def _trading_days(n: int) -> list[datetime]: """粗略生成 n 个工作日(跳过周末;节假日由阶段1 的交易日历服务处理)。""" start = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(days=int(n * 1.6)) days: list[datetime] = [] d = start while len(days) < n: if d.weekday() < 5: days.append(d.replace(hour=15, minute=0, second=0, microsecond=0)) d += timedelta(days=1) return days def generate_ohlcv(n: int = 500, seed: int = 42) -> list[Bar]: """随机游走 + A 股风格的价格区间(5~30 元)。""" rng = np.random.default_rng(seed) rets = rng.normal(loc=0.0003, scale=0.018, size=n) price = 10.0 * np.cumprod(1 + rets) days = _trading_days(n) bars: list[Bar] = [] for i in range(n): close = float(price[i]) op = close * (1 + rng.normal(0, 0.005)) hi = max(op, close) * (1 + abs(rng.normal(0, 0.006))) lo = min(op, close) * (1 - abs(rng.normal(0, 0.006))) vol = float(rng.integers(1_000_000, 10_000_000)) bars.append( Bar( ts=days[i], open=round(op, 2), high=round(hi, 2), low=round(lo, 2), close=round(close, 2), volume=vol, ) ) return bars async def seed_if_empty(session: AsyncSession, symbol: str = "DEMO", n: int = 500) -> None: """若库中无该 symbol 数据,则灌入合成数据。""" existing = await session.execute( select(Candle.id).where(Candle.symbol == symbol).limit(1) ) if existing.scalars().first() is not None: return bars = generate_ohlcv(n=n) for b in bars: session.add( Candle( symbol=symbol, timeframe="1d", ts=b.ts, open=b.open, high=b.high, low=b.low, close=b.close, volume=b.volume, ) ) await session.commit()