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=self._render_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=self._render_pairs_html(pairs), content_type="text/html") def _render_quality_html(self, quality: List[Dict[str, Any]]) -> str: rows = "".join( f"" f"{q.get('instrument','')}" f"{q.get('record_count','')}" f"{q.get('latest_tstamp','')}" f"{q.get('status','')}" f"{q.get('reason','')}" f"" for q in sorted(quality, key=lambda x: str(x.get("instrument", ""))) ) return f""" Data Quality

Data Quality

{rows}
InstrumentRecordsLatestStatusReason
""" def _render_pairs_html(self, pairs: List[Dict[str, Any]]) -> str: if not pairs: body = "

No pairs available. Check data quality and try again.

" else: body_rows = [] for p in pairs: body_rows.append( "" f"{p.get('instrument_a','')}" f"{p.get('instrument_b','')}" f"{p.get('rank_eg','')}" f"{p.get('rank_adf','')}" f"{p.get('rank_j','')}" f"{p.get('pvalue_eg','')}" f"{p.get('pvalue_adf','')}" f"{p.get('pvalue_j','')}" "" ) body = "\n".join(body_rows) return f""" Pair Selection

Pair Selection

{body}
Instrument A Instrument B Rank-EG Rank-ADF Rank-J EG p-value ADF p-value Johansen pseudo p
""" if __name__ == "__main__": App() CvttAppConfig() PairSelector() App.instance().run()