516 lines
21 KiB
Python
516 lines
21 KiB
Python
"""模拟盘 API 路由(spec §10)。
|
||
|
||
create 建 paper_account(持久化配置);GET 查询净值/成交/状态。
|
||
回放执行(engine.run)由 orchestrator 异步触发或容器内同步跑,端到端冒烟在容器
|
||
(本机无 NAS parquet + vnpy 完整依赖),本模块只做 account 管理 + 查询。
|
||
"""
|
||
import json
|
||
import sqlite3
|
||
import time
|
||
|
||
from fastapi import APIRouter, Depends, Header, HTTPException
|
||
from pydantic import BaseModel
|
||
|
||
from .auth import verify_token as verify_token_impl
|
||
from . import instance_store
|
||
from .validation import validate_backtest_range
|
||
|
||
router = APIRouter()
|
||
_db_path = {"path": None}
|
||
|
||
|
||
def set_db_path(p):
|
||
_db_path["path"] = p
|
||
if p:
|
||
from sanguo_trader.persistence import init_db
|
||
init_db(p) # app 启动建表(幂等),保证 GET 查询不报 no such table
|
||
|
||
|
||
async def verify_token(authorization: str | None = Header(None)):
|
||
if authorization is None or not authorization.startswith("Bearer "):
|
||
raise HTTPException(401, "Missing/invalid authorization")
|
||
return verify_token_impl(authorization.split(" ", 1)[1])
|
||
|
||
|
||
class StrategyCfg(BaseModel):
|
||
name: str
|
||
params: dict = {}
|
||
match_session: str = "next_open"
|
||
symbol: str
|
||
listing_days: int = 0
|
||
max_allocation: float | None = None # 软限额(spec §195),None=用 initial_capital
|
||
|
||
|
||
class PaperCreateRequest(BaseModel):
|
||
name: str = "paper"
|
||
mode: str = "replay"
|
||
interval: str = "d"
|
||
symbols: list[str]
|
||
strategies: list[StrategyCfg]
|
||
initial_capital: float = 1_000_000
|
||
rate: float = 0.0003
|
||
slippage: float = 0.0
|
||
pricetick: float = 0.01
|
||
stamp_duty_rate: float = 0.0005
|
||
transfer_fee_rate: float = 0.00001
|
||
min_commission: float = 5.0
|
||
start: str
|
||
end: str
|
||
# 组合策略实走(E1):strategy_type=portfolio 时 mode 必须 live,
|
||
# strategies[0].name=组合策略名,pool/max_pool/benchmark 进 params
|
||
strategy_type: str = "cta"
|
||
# 撮合引擎(影子柜台 P1):eod_replay=日终回放(NAS 20:30) / shadow=影子柜台(VPS 盘中实时)
|
||
engine: str = "eod_replay"
|
||
pool: str = "all"
|
||
max_pool: int = 30
|
||
benchmark: str = "000300.XSHG"
|
||
# §12.6 实例做实:账户绑档案;空=发起即建档(自动创建实例再发起)
|
||
instance_id: int | None = None
|
||
|
||
|
||
def _resolve_file_by_class(class_name: str) -> str:
|
||
"""类名 → 策略文件名(发起即建档时反查 code_file;查不到返回空)。"""
|
||
try:
|
||
from .strategy_registry import list_strategy_files
|
||
|
||
for f in list_strategy_files()["files"]:
|
||
if f.get("class_name") == class_name:
|
||
return f.get("name", "")
|
||
except Exception:
|
||
pass
|
||
return ""
|
||
|
||
|
||
def _ensure_instance_for_paper(req: PaperCreateRequest) -> int:
|
||
"""§12.6 D1/D5:发起时绑档案。带合法 instance_id 用之;否则发起即建档。"""
|
||
from . import instance_store
|
||
|
||
if req.instance_id and instance_store.get_instance_params_snapshot(req.instance_id):
|
||
return req.instance_id
|
||
strat = req.strategies[0] if req.strategies else None
|
||
cls = strat.name if strat else ""
|
||
sym = req.pool if req.strategy_type == "portfolio" else ",".join(req.symbols)
|
||
return instance_store.create_instance({
|
||
"code_file": _resolve_file_by_class(cls),
|
||
"name": f"{cls}·{req.mode}·{time.strftime('%m%d')}",
|
||
"type": req.strategy_type,
|
||
"params": dict(strat.params) if strat else {},
|
||
"symbol_or_pool": sym,
|
||
"interval": req.interval,
|
||
"match_session": strat.match_session if strat else "next_open",
|
||
})
|
||
|
||
|
||
@router.post("/paper/create", dependencies=[Depends(verify_token)])
|
||
def create_paper(req: PaperCreateRequest):
|
||
from sanguo_trader.persistence import init_db, save_account
|
||
import threading
|
||
|
||
db = _db_path["path"] or ":memory:"
|
||
init_db(db)
|
||
# 实例绑定先于日期归一(model_dump 要带上 instance_id 落库)
|
||
req.instance_id = _ensure_instance_for_paper(req)
|
||
# §12.6 D1 发起时快照:绑已有档案 → 账户参数用档案当时的参数(复印件),
|
||
# 后续改档案不影响本账户(漂移可见,手动同步)
|
||
snap = instance_store.get_instance_params_snapshot(req.instance_id) if req.instance_id else None
|
||
if snap and req.strategies:
|
||
req.strategies[0].params = dict(snap.get("params") or {})
|
||
# §12.6 补:发起时代码版本快照(运行可回溯当时跑的哪版代码)
|
||
from .code_versions import snapshot_code
|
||
|
||
code_snap = snapshot_code((snap or {}).get("code_file") or "")
|
||
code_hash = code_snap["code_hash"] if code_snap else None
|
||
# 实走/影子是开放账户:起止日期无意义,开始=创建当天(组合日终重放依赖 start_date,
|
||
# 空值会崩),结束留空;仅回放保留用户填的历史区间
|
||
if req.mode in ("live", "shadow"):
|
||
from datetime import date
|
||
|
||
req.start = date.today().isoformat()
|
||
req.end = ""
|
||
elif req.mode == "replay":
|
||
# 回放保留用户填的历史区间 → 校验(未来日期/超数据范围等 400,同回测口径)
|
||
validate_backtest_range(req.start, req.end)
|
||
if req.strategy_type == "portfolio":
|
||
if req.mode not in ("live", "shadow"):
|
||
raise HTTPException(400, "组合策略模拟盘仅支持实走(live)/影子(shadow)模式;历史回放请用「组合回测」")
|
||
payload = req.model_dump()
|
||
payload["code_hash"] = code_hash
|
||
payload["engine"] = "shadow" if req.mode == "shadow" else "eod_replay"
|
||
payload["symbols"] = [req.pool]
|
||
payload["strategies"] = [{
|
||
"name": (req.strategies[0].name if req.strategies else "all_weather"),
|
||
"params": {"max_pool": req.max_pool, "benchmark": req.benchmark},
|
||
}]
|
||
aid = save_account(db, payload)
|
||
from sanguo_trader.persistence import update_account_status
|
||
update_account_status(db, aid, "running")
|
||
return {"account_id": aid, "status": "running"}
|
||
|
||
cta_payload = req.model_dump()
|
||
cta_payload["code_hash"] = code_hash
|
||
cta_payload["engine"] = "shadow" if req.mode == "shadow" else "eod_replay"
|
||
aid = save_account(db, cta_payload)
|
||
status = "created"
|
||
if req.mode == "replay": # 回放后台线程跑,create 立即返回(避免阻塞 worker 502)
|
||
def _bg():
|
||
from sanguo_trader.persistence import update_account_status
|
||
from .instance_store import update_instance_run
|
||
try:
|
||
_run_replay(db, aid, req)
|
||
update_account_status(db, aid, "done")
|
||
# §12.6 回放完成回写档案(收益=末次净值/初始-1)
|
||
if req.instance_id:
|
||
from sanguo_trader.persistence import load_last_balance
|
||
last = load_last_balance(db, aid)
|
||
ret = None
|
||
if last and req.initial_capital:
|
||
ret = (last.get("total_equity", 0) - req.initial_capital) / req.initial_capital
|
||
update_instance_run(req.instance_id, "replay", "done", ret, {"account_id": aid})
|
||
except Exception as e: # noqa: BLE001
|
||
update_account_status(db, aid, "failed", str(e))
|
||
if req.instance_id:
|
||
update_instance_run(req.instance_id, "replay", "failed")
|
||
threading.Thread(target=_bg, daemon=True).start()
|
||
status = "running"
|
||
elif req.mode in ("live", "shadow"): # 实走:每日 20:30 step;影子:等 VPS 影子柜台进程接管
|
||
from sanguo_trader.persistence import update_account_status
|
||
update_account_status(db, aid, "running")
|
||
status = "running"
|
||
return {"account_id": aid, "status": status}
|
||
|
||
|
||
@router.post("/paper/sync/{instance_id}", dependencies=[Depends(verify_token)])
|
||
def sync_instance_params(instance_id: int):
|
||
"""§12.6 D2/D3 参数同步:档案当前参数 → 该档案全部运行中模拟账户。
|
||
|
||
实走+影子一起换(影子与实盘/对照账户参数必须锁死,否则双轨对账失效)。
|
||
实盘不在此列(D2:实盘不提供在线改参,停了重发)。
|
||
生效时机(2026-08-15 实证):实走(CTA/组合)每日结算从 DB 重读 strategies
|
||
→ 次日 20:30 生效;影子柜台进程常驻内存 → 需重启影子进程生效。
|
||
"""
|
||
from . import instance_store
|
||
|
||
snap = instance_store.get_instance_params_snapshot(instance_id)
|
||
if snap is None:
|
||
raise HTTPException(404, "实例不存在")
|
||
db = _db_path["path"] or ":memory:"
|
||
synced = 0
|
||
with sqlite3.connect(db) as conn:
|
||
rows = conn.execute(
|
||
"SELECT id, strategies FROM paper_accounts "
|
||
"WHERE instance_id=? AND status='running'",
|
||
(instance_id,),
|
||
).fetchall()
|
||
for aid, raw in rows:
|
||
try:
|
||
strats = json.loads(raw) if isinstance(raw, str) else (raw or [])
|
||
except (ValueError, TypeError):
|
||
continue
|
||
if not strats:
|
||
continue
|
||
strats[0]["params"] = dict(snap["params"])
|
||
conn.execute(
|
||
"UPDATE paper_accounts SET strategies=?, updated_at=? WHERE id=?",
|
||
(json.dumps(strats, ensure_ascii=False), time.strftime("%Y-%m-%d %H:%M:%S"), aid),
|
||
)
|
||
synced += 1
|
||
conn.commit()
|
||
return {"synced": synced}
|
||
|
||
|
||
@router.get("/paper", dependencies=[Depends(verify_token)])
|
||
def list_papers():
|
||
"""模拟盘列表(启用聚宽级模拟交易列表页)。每行带最新净值 + 收益率。"""
|
||
from sanguo_trader.persistence import load_last_balance
|
||
|
||
db = _db_path["path"]
|
||
with sqlite3.connect(db) as conn:
|
||
conn.row_factory = sqlite3.Row
|
||
rows = conn.execute(
|
||
"SELECT * FROM paper_accounts ORDER BY id DESC"
|
||
).fetchall()
|
||
# #71 持仓数(volume>0 的持仓行,对齐实盘列表)
|
||
pos_counts = {
|
||
r[0]: r[1] for r in conn.execute(
|
||
"SELECT account_id, COUNT(*) FROM paper_positions "
|
||
"WHERE volume > 0 GROUP BY account_id"
|
||
).fetchall()
|
||
}
|
||
out = []
|
||
for r in rows:
|
||
item = dict(r)
|
||
item["position_count"] = pos_counts.get(item["id"], 0)
|
||
cap = item.get("initial_capital") or 0
|
||
last = load_last_balance(db, item["id"])
|
||
if last and cap:
|
||
item["latest_equity"] = last.get("total_equity")
|
||
item["latest_date"] = last.get("date")
|
||
item["total_return"] = (last.get("total_equity", 0) - cap) / cap
|
||
else:
|
||
item["latest_equity"] = None
|
||
item["latest_date"] = None
|
||
item["total_return"] = None
|
||
out.append(item)
|
||
return {"accounts": out}
|
||
|
||
|
||
# ===== 双轨对账(影子柜台 vs 实盘模拟,设计 §8.2 / 影子 P3 前半)=====
|
||
# 注意:须注册在 /paper/{aid} 之前,否则 "reconcile" 被当作 aid → 422
|
||
|
||
@router.get("/paper/reconcile", dependencies=[Depends(verify_token)])
|
||
def list_reconcile_pairs(date: str | None = None):
|
||
"""双轨配对列表 + 各配对对账报告(按需现算并落库)。
|
||
|
||
自动配对:运行中影子账户(mode=shadow)按策略名匹配 live_accounts。
|
||
date 缺省 = 今天。
|
||
"""
|
||
from sanguo_trader.shadow.reconcile_report import (
|
||
build_reconcile_report, find_dual_track_pairs, load_reconcile_report,
|
||
save_reconcile_report,
|
||
)
|
||
|
||
db = _db_path["path"]
|
||
out = []
|
||
for pair in find_dual_track_pairs(db):
|
||
saved = load_reconcile_report(
|
||
db, pair["live_account_id"], pair["shadow_account_id"], date or "")
|
||
report = saved or build_reconcile_report(
|
||
db, pair["live_account_id"], pair["shadow_account_id"], date)
|
||
save_reconcile_report(db, report)
|
||
# §12.6 补:双轨代码版本一致性(对账 FAIL 先查这行——两边代码不同价差必然大)
|
||
with sqlite3.connect(db) as conn:
|
||
hashes = dict(conn.execute(
|
||
"SELECT id, code_hash FROM paper_accounts WHERE id IN (?,?)",
|
||
(pair["live_account_id"], pair["shadow_account_id"]),
|
||
).fetchall())
|
||
h1 = hashes.get(pair["live_account_id"])
|
||
h2 = hashes.get(pair["shadow_account_id"])
|
||
code_match = None if not (h1 and h2) else h1 == h2
|
||
out.append({**pair, "code_match": code_match, "report": report})
|
||
return {"pairs": out}
|
||
|
||
|
||
@router.get("/paper/reconcile/{live_id}/{shadow_id}", dependencies=[Depends(verify_token)])
|
||
def get_reconcile(live_id: int, shadow_id: int, date: str | None = None,
|
||
refresh: bool = False):
|
||
"""单配对对账报告。refresh=true 强制重算(缺省读已存,无则现算)。"""
|
||
from sanguo_trader.shadow.reconcile_report import (
|
||
build_reconcile_report, load_reconcile_report, save_reconcile_report,
|
||
)
|
||
|
||
db = _db_path["path"]
|
||
if not refresh:
|
||
saved = load_reconcile_report(db, live_id, shadow_id, date or "")
|
||
if saved is not None:
|
||
return saved
|
||
report = build_reconcile_report(db, live_id, shadow_id, date)
|
||
save_reconcile_report(db, report)
|
||
return report
|
||
|
||
|
||
@router.get("/paper/{aid}", dependencies=[Depends(verify_token)])
|
||
def get_paper(aid: int):
|
||
db = _db_path["path"]
|
||
with sqlite3.connect(db) as conn:
|
||
conn.row_factory = sqlite3.Row
|
||
row = conn.execute(
|
||
"SELECT * FROM paper_accounts WHERE id=?", (aid,)
|
||
).fetchone()
|
||
if not row:
|
||
raise HTTPException(404, "account not found")
|
||
return dict(row)
|
||
|
||
|
||
@router.get("/paper/{aid}/equity", dependencies=[Depends(verify_token)])
|
||
def get_equity(aid: int):
|
||
from sanguo_trader.persistence import list_daily_balance
|
||
|
||
return list_daily_balance(_db_path["path"], aid)
|
||
|
||
|
||
@router.get("/paper/{aid}/trades", dependencies=[Depends(verify_token)])
|
||
def get_trades(aid: int):
|
||
from sanguo_trader.persistence import list_trades
|
||
|
||
return list_trades(_db_path["path"], aid)
|
||
|
||
|
||
@router.get("/paper/{aid}/strategies", dependencies=[Depends(verify_token)])
|
||
def get_strategies(aid: int):
|
||
"""分策略归因:成交/拒单/费用聚合(spec §7)。"""
|
||
from sanguo_trader.persistence import list_strategy_summary
|
||
|
||
return list_strategy_summary(_db_path["path"], aid)
|
||
|
||
|
||
@router.get("/paper/{aid}/positions", dependencies=[Depends(verify_token)])
|
||
def get_positions(aid: int):
|
||
"""当前持仓快照(实走监控,Phase 3c)。{symbol:{volume,frozen,avg_price}} → list。"""
|
||
from sanguo_trader.persistence import load_positions
|
||
|
||
pos = load_positions(_db_path["path"], aid, "account")
|
||
return [
|
||
{"symbol": sym, "volume": p["volume"], "frozen": p.get("frozen", 0),
|
||
"avg_price": p["avg_price"]}
|
||
for sym, p in pos.items()
|
||
]
|
||
|
||
|
||
@router.get("/paper/{aid}/pending", dependencies=[Depends(verify_token)])
|
||
def get_pending(aid: int):
|
||
"""跨日 pending 订单(实走监控,Phase 3c)。"""
|
||
from sanguo_trader.persistence import load_pending_orders
|
||
|
||
return load_pending_orders(_db_path["path"], aid)
|
||
|
||
|
||
# ===== 生命周期管理(spec §10 补全:停止/恢复/删除/编辑)=====
|
||
|
||
def _get_account_row(db, aid: int) -> dict:
|
||
with sqlite3.connect(db) as conn:
|
||
conn.row_factory = sqlite3.Row
|
||
row = conn.execute("SELECT * FROM paper_accounts WHERE id=?", (aid,)).fetchone()
|
||
if not row:
|
||
raise HTTPException(404, "account not found")
|
||
return dict(row)
|
||
|
||
|
||
@router.post("/paper/{aid}/stop", dependencies=[Depends(verify_token)])
|
||
def stop_paper(aid: int):
|
||
"""停止实走/影子:置 status=stopped,日终 step 与影子柜台都只选 running → 自动跳过。"""
|
||
from sanguo_trader.persistence import update_account_status
|
||
|
||
db = _db_path["path"]
|
||
acc = _get_account_row(db, aid)
|
||
if acc.get("mode") not in ("live", "shadow"):
|
||
raise HTTPException(400, "仅实走/影子账户支持停止;回放账户为一次性任务")
|
||
update_account_status(db, aid, "stopped")
|
||
return {"account_id": aid, "status": "stopped"}
|
||
|
||
|
||
@router.post("/paper/{aid}/resume", dependencies=[Depends(verify_token)])
|
||
def resume_paper(aid: int):
|
||
"""恢复实走/影子:次日 20:30 起继续 step / 影子柜台继续接管。"""
|
||
from sanguo_trader.persistence import update_account_status
|
||
|
||
db = _db_path["path"]
|
||
acc = _get_account_row(db, aid)
|
||
if acc.get("mode") not in ("live", "shadow"):
|
||
raise HTTPException(400, "仅实走/影子账户支持恢复")
|
||
update_account_status(db, aid, "running")
|
||
return {"account_id": aid, "status": "running"}
|
||
|
||
|
||
@router.delete("/paper/{aid}", dependencies=[Depends(verify_token)])
|
||
def delete_paper(aid: int):
|
||
"""删除模拟盘账户及其全部数据(净值/成交/持仓/挂单,不可恢复)。"""
|
||
db = _db_path["path"]
|
||
_get_account_row(db, aid)
|
||
tables = ("paper_accounts", "paper_daily_balance", "paper_trades",
|
||
"paper_positions", "paper_pending_orders", "paper_shadow_orders")
|
||
with sqlite3.connect(db) as conn:
|
||
for t in tables:
|
||
if t == "paper_accounts":
|
||
conn.execute("DELETE FROM paper_accounts WHERE id=?", (aid,))
|
||
else:
|
||
conn.execute(f"DELETE FROM {t} WHERE account_id=?", (aid,))
|
||
conn.commit()
|
||
return {"account_id": aid, "deleted": True}
|
||
|
||
|
||
class PaperUpdateRequest(BaseModel):
|
||
"""可编辑字段(其余字段沿用原值;策略参数/标的改动自下次 step 生效)。"""
|
||
name: str | None = None
|
||
symbols: list[str] | None = None
|
||
strategies: list[StrategyCfg] | None = None
|
||
initial_capital: float | None = None
|
||
|
||
|
||
@router.put("/paper/{aid}", dependencies=[Depends(verify_token)])
|
||
def update_paper(aid: int, req: PaperUpdateRequest):
|
||
db = _db_path["path"]
|
||
_get_account_row(db, aid)
|
||
sets, args = [], []
|
||
if req.name is not None:
|
||
sets.append("name=?"); args.append(req.name)
|
||
if req.symbols is not None:
|
||
sets.append("symbols=?"); args.append(json.dumps(req.symbols))
|
||
if req.strategies is not None:
|
||
sets.append("strategies=?")
|
||
args.append(json.dumps([s.model_dump() for s in req.strategies]))
|
||
if req.initial_capital is not None:
|
||
sets.append("initial_capital=?"); args.append(req.initial_capital)
|
||
if not sets:
|
||
return {"account_id": aid, "updated": False}
|
||
with sqlite3.connect(db) as conn:
|
||
conn.execute(f"UPDATE paper_accounts SET {', '.join(sets)} WHERE id=?", (*args, aid))
|
||
conn.commit()
|
||
return {"account_id": aid, "updated": True}
|
||
|
||
|
||
class _DataSourceWrapper:
|
||
"""包装 iter_bars/fetch_day 给 PaperEngine/live_orchestrator。"""
|
||
|
||
def __init__(self, cfg):
|
||
self.cfg = cfg
|
||
|
||
def iter_bars(self, symbols, start, end, interval, adjust="qfq", cfg=None):
|
||
from sanguo_trader.data_source import iter_bars
|
||
|
||
return iter_bars(symbols, start, end, interval, adjust, cfg or self.cfg)
|
||
|
||
def fetch_day(self, symbol, date, interval, adjust="qfq", cfg=None):
|
||
from sanguo_trader.data_source import fetch_day
|
||
|
||
return fetch_day(symbol, date, interval, adjust, cfg or self.cfg)
|
||
|
||
|
||
def _run_replay(db, aid, req: PaperCreateRequest):
|
||
"""构造引擎 + 跑回放(容器内有 vnpy_ctastrategy + NAS parquet,本机仅空转)。"""
|
||
from sanguo_trader.account import Account
|
||
from sanguo_trader.cta_adapter import PaperCtaEngine
|
||
from sanguo_trader.engine import PaperEngine
|
||
from sanguo_trader.models import AccountConfig
|
||
from sanguo_trader.strategy_runner import StrategyRunner
|
||
from sanguo_data.config import find_config_path, load_config
|
||
from sanguo_data.datareader import guess_exchange
|
||
from .strategy_registry import get_strategy_class
|
||
|
||
data_cfg = load_config(find_config_path())
|
||
acc_cfg = AccountConfig(
|
||
initial_capital=req.initial_capital, rate=req.rate, slippage=req.slippage,
|
||
pricetick=req.pricetick, stamp_duty_rate=req.stamp_duty_rate,
|
||
transfer_fee_rate=req.transfer_fee_rate, min_commission=req.min_commission,
|
||
)
|
||
account = Account(req.initial_capital)
|
||
runners: list = []
|
||
for s in req.strategies:
|
||
cls = get_strategy_class(s.name)
|
||
if cls is None:
|
||
continue # 策略不可用(本机无 vnpy_ctastrategy)→ 跳过
|
||
from sanguo_trader.limit import lot_size_for
|
||
cta = PaperCtaEngine(s.name, match_session=s.match_session,
|
||
listing_days=s.listing_days,
|
||
size=lot_size_for(s.symbol)) # 主板 100 / 科创 200 股一手
|
||
vt_symbol = f"{s.symbol}.{guess_exchange(s.symbol).value}"
|
||
strat = cls(cta, s.name, vt_symbol, s.params) # CtaTemplate(cta_engine, name, vt_symbol, setting)
|
||
strat.trading = True # 允许 send_order(等价 on_start)
|
||
try:
|
||
from vnpy.trader.utility import ArrayManager
|
||
if not hasattr(strat, "am"):
|
||
strat.am = ArrayManager(20) # 默认 100 根才 inited,短区间不够;用 20 兼容
|
||
except Exception:
|
||
pass
|
||
cta.set_strategy(strat)
|
||
runners.append(StrategyRunner(
|
||
s.name, strategy=strat, paper_cta_engine=cta, symbol=s.symbol,
|
||
max_allocation=(s.max_allocation if s.max_allocation is not None
|
||
else req.initial_capital)))
|
||
from sanguo_data.dividend_source import build_dividend_calendar
|
||
div_calendar = build_dividend_calendar(req.symbols, req.start, req.end)
|
||
pe = PaperEngine(account, runners, _DataSourceWrapper(data_cfg), acc_cfg,
|
||
db, aid, req.symbols, req.start, req.end, req.interval,
|
||
risk_free_rate=getattr(data_cfg, "risk_free_rate", 0.0),
|
||
dividends_by_date=div_calendar)
|
||
pe.run()
|