from __future__ import annotations import asyncio from typing import Any, Dict, List from aiohttp import web from cvttpy_tools.app import App from cvttpy_tools.base import NamedObject from cvttpy_tools.config import CvttAppConfig from cvttpy_tools.logger import Log from cvttpy_tools.web.rest_service import RestService from cvttpy_trading.trading.exchange_config import ExchangeAccounts from cvttpy_trading.trading.instrument import ExchangeInstrument from pairs_trading.lib.pair_selector_engine import PairSelectionEngine class PairSelector(NamedObject): instruments_: List[ExchangeInstrument] engine_: PairSelectionEngine rest_service_: RestService def __init__(self) -> None: App.instance().add_cmdline_arg("--oneshot", action="store_true", default=False) App.instance().add_call(App.Stage.Config, self._on_config()) App.instance().add_call(App.Stage.Run, self.run()) async def _on_config(self) -> None: cfg = CvttAppConfig.instance() self.instruments_ = self._load_instruments(cfg) price_field = cfg.get_value("model/stat_model_price", "close") self.engine_ = PairSelectionEngine( config=cfg, instruments=self.instruments_, price_field=price_field, ) self.rest_service_ = RestService(config_key="/api/REST") self.rest_service_.add_handler("GET", "/data_quality", self._on_data_quality) self.rest_service_.add_handler("GET", "/pair_selection", self._on_pair_selection) def _load_instruments(self, cfg: CvttAppConfig) -> List[ExchangeInstrument]: instruments_cfg = cfg.get_value("instruments", []) instruments: List[ExchangeInstrument] = [] assert len(instruments_cfg) >= 2, "at least two instruments required" for item in instruments_cfg: if isinstance(item, str): parts = item.split(":", 1) if len(parts) != 2: raise ValueError(f"invalid instrument format: {item}") exch_acct, instrument_id = parts elif isinstance(item, dict): exch_acct = item.get("exch_acct", "") instrument_id = item.get("instrument_id", "") if not exch_acct or not instrument_id: raise ValueError(f"invalid instrument config: {item}") else: raise ValueError(f"unsupported instrument entry: {item}") exch_inst = ExchangeAccounts.instance().get_exchange_instrument( exch_acct=exch_acct, instrument_id=instrument_id ) assert exch_inst is not None, f"no ExchangeInstrument for {exch_acct}:{instrument_id}" exch_inst.user_data_["exch_acct"] = exch_acct instruments.append(exch_inst) return instruments async def run(self) -> None: oneshot = App.instance().get_argument("oneshot", False) while True: await self.engine_.run_once() if oneshot: break sleep_for = self.engine_.sleep_seconds_until_next_cycle() await asyncio.sleep(sleep_for) async def _on_data_quality(self, request: web.Request) -> web.Response: fmt = request.query.get("format", "html").lower() quality = self.engine_.quality_dicts() if fmt == "json": return web.json_response(quality) return web.Response(text=HtmlRenderer.render_data_quality_html(quality), content_type="text/html") async def _on_pair_selection(self, request: web.Request) -> web.Response: fmt = request.query.get("format", "html").lower() pairs = self.engine_.pair_dicts() if fmt == "json": return web.json_response(pairs) return web.Response(text=HtmlRenderer.render_pair_selection_html(pairs), content_type="text/html") class HtmlRenderer: @staticmethod def render_data_quality_html(quality: List[Dict[str, Any]]) -> str: rows = "".join( f"
| Instrument | Records | Latest | Status | Reason |
|---|
No pairs available. Check data quality and try again.
" else: body_rows = [] for p in pairs: body_rows.append( "| Instrument A | Instrument B | Rank-EG | Rank-ADF | Rank-J | EG p-value | ADF p-value | Johansen pseudo p |
|---|