This commit is contained in:
Oleg Sheynin 2026-01-11 13:33:58 +00:00
parent 6dd0f97d74
commit b196863a34
26 changed files with 5365 additions and 5566 deletions

173
.vscode/launch.json vendored
View File

@ -36,181 +36,60 @@
], ],
}, },
{ {
"name": "-------- OLS --------",
},
{
"name": "CRYPTO OLS (rolling)",
"type": "debugpy",
"request": "launch",
"python": "/home/oleg/.pyenv/python3.12-venv/bin/python",
"program": "${workspaceFolder}/research/backtest.py",
"args": [
"--config=${workspaceFolder}/configuration/ols.cfg",
"--instruments=ADA-USDT:CRYPTO:BNBSPOT,SOL-USDT:CRYPTO:BNBSPOT",
"--date_pattern=20250605",
"--result_db=${workspaceFolder}/research/results/crypto/%T.ols.ADA-SOL.20250605.crypto_results.db",
],
"env": {
"PYTHONPATH": "${workspaceFolder}/lib"
},
"console": "integratedTerminal"
},
{
"name": "CRYPTO OLS (optimized)",
"type": "debugpy",
"request": "launch",
"python": "/home/oleg/.pyenv/python3.12-venv/bin/python",
"program": "${workspaceFolder}/research/backtest.py",
"args": [
"--config=${workspaceFolder}/configuration/ols-opt.cfg",
"--instruments=ADA-USDT:CRYPTO:BNBSPOT,SOL-USDT:CRYPTO:BNBSPOT",
"--date_pattern=20250605",
"--result_db=${workspaceFolder}/research/results/crypto/%T.ols-opt.ADA-SOL.20250605.crypto_results.db",
],
"env": {
"PYTHONPATH": "${workspaceFolder}/lib"
},
"console": "integratedTerminal"
},
// {
// "name": "CRYPTO OLS (expanding)",
// "type": "debugpy",
// "request": "launch",
// "python": "/home/oleg/.pyenv/python3.12-venv/bin/python",
// "program": "${workspaceFolder}/research/backtest.py",
// "args": [
// "--config=${workspaceFolder}/configuration/ols-exp.cfg",
// "--instruments=ADA-USDT:CRYPTO:BNBSPOT,SOL-USDT:CRYPTO:BNBSPOT",
// "--date_pattern=20250605",
// "--result_db=${workspaceFolder}/research/results/crypto/%T.ols-exp.ADA-SOL.20250605.crypto_results.db",
// ],
// "env": {
// "PYTHONPATH": "${workspaceFolder}/lib"
// },
// "console": "integratedTerminal"
// },
{
"name": "EQUITY OLS (rolling)",
"type": "debugpy",
"request": "launch",
"python": "/home/oleg/.pyenv/python3.12-venv/bin/python",
"program": "${workspaceFolder}/research/backtest.py",
"args": [
"--config=${workspaceFolder}/configuration/ols.cfg",
"--instruments=COIN:EQUITY:ALPACA,MSTR:EQUITY:ALPACA",
"--date_pattern=20250605",
"--result_db=${workspaceFolder}/research/results/equity/%T.ols.COIN-MSTR.20250605.equity_results.db",
],
"env": {
"PYTHONPATH": "${workspaceFolder}/lib"
},
"console": "integratedTerminal"
},
{
"name": "EQUITY-CRYPTO OLS (rolling)",
"type": "debugpy",
"request": "launch",
"python": "/home/oleg/.pyenv/python3.12-venv/bin/python",
"program": "${workspaceFolder}/research/backtest.py",
"args": [
"--config=${workspaceFolder}/configuration/ols.cfg",
"--instruments=COIN:EQUITY:ALPACA,BTC-USDT:CRYPTO:BNBSPOT",
"--date_pattern=20250605",
"--result_db=${workspaceFolder}/research/results/intermarket/%T.ols.COIN-BTC.20250605.equity_results.db",
],
"env": {
"PYTHONPATH": "${workspaceFolder}/lib"
},
"console": "integratedTerminal"
},
{
"name": "-------- VECM --------", "name": "-------- VECM --------",
}, },
{ {
"name": "CRYPTO VECM (rolling)", "name": "CRYPTO VECM BACKTEST (optimized)",
"type": "debugpy",
"request": "launch",
"python": "/home/oleg/.pyenv/python3.12-venv/bin/python",
"program": "${workspaceFolder}/research/backtest.py",
"args": [
"--config=${workspaceFolder}/configuration/vecm.cfg",
"--instruments=ADA-USDT:CRYPTO:BNBSPOT,SOL-USDT:CRYPTO:BNBSPOT",
"--date_pattern=20250605",
"--result_db=${workspaceFolder}/research/results/crypto/%T.vecm.ADA-SOL.20250605.crypto_results.db",
],
"env": {
"PYTHONPATH": "${workspaceFolder}/lib"
},
"console": "integratedTerminal"
},
{
"name": "CRYPTO VECM (optimized)",
"type": "debugpy", "type": "debugpy",
"request": "launch", "request": "launch",
"python": "/home/oleg/.pyenv/python3.12-venv/bin/python", "python": "/home/oleg/.pyenv/python3.12-venv/bin/python",
"program": "${workspaceFolder}/research/backtest.py", "program": "${workspaceFolder}/research/backtest.py",
"args": [ "args": [
"--config=${workspaceFolder}/configuration/vecm-opt.cfg", "--config=${workspaceFolder}/configuration/vecm-opt.cfg",
"--instruments=ADA-USDT:CRYPTO:BNBSPOT,SOL-USDT:CRYPTO:BNBSPOT", "--instruments=CRYPTO:BNBSPOT:PAIR-ADA-USDT,CRYPTO:BNBSPOT:PAIR-SOL-USDT",
"--date_pattern=20250605", "--date_pattern=20250910",
"--result_db=${workspaceFolder}/research/results/crypto/%T.vecm-opt.ADA-SOL.20250605.crypto_results.db", "--result_db=${workspaceFolder}/research/results/crypto/%T.vecm-opt.ADA-SOL.20250605.crypto_results.db",
], ],
"env": { "env": {
"PYTHONPATH": "${workspaceFolder}/lib" "PYTHONPATH": "${workspaceFolder}/..",
"CONFIG_SERVICE": "cloud16.cvtt.vpn:6789"
}, },
"console": "integratedTerminal" "console": "integratedTerminal"
}, },
// { // {
// "name": "CRYPTO VECM (expanding)", // "name": "EQUITY VECM (rolling)",
// "type": "debugpy", // "type": "debugpy",
// "request": "launch", // "request": "launch",
// "python": "/home/oleg/.pyenv/python3.12-venv/bin/python", // "python": "/home/oleg/.pyenv/python3.12-venv/bin/python",
// "program": "${workspaceFolder}/research/backtest.py", // "program": "${workspaceFolder}/research/backtest.py",
// "args": [ // "args": [
// "--config=${workspaceFolder}/configuration/vecm-exp.cfg", // "--config=${workspaceFolder}/configuration/vecm.cfg",
// "--instruments=ADA-USDT:CRYPTO:BNBSPOT,SOL-USDT:CRYPTO:BNBSPOT", // "--instruments=COIN:EQUITY:ALPACA,MSTR:EQUITY:ALPACA",
// "--date_pattern=20250605", // "--date_pattern=20250605",
// "--result_db=${workspaceFolder}/research/results/crypto/%T.vecm-exp.ADA-SOL.20250605.crypto_results.db", // "--result_db=${workspaceFolder}/research/results/equity/%T.vecm.COIN-MSTR.20250605.equity_results.db",
// ],
// "env": {
// "PYTHONPATH": "${workspaceFolder}/lib"
// },
// "console": "integratedTerminal"
// },
// {
// "name": "EQUITY-CRYPTO VECM (rolling)",
// "type": "debugpy",
// "request": "launch",
// "python": "/home/oleg/.pyenv/python3.12-venv/bin/python",
// "program": "${workspaceFolder}/research/backtest.py",
// "args": [
// "--config=${workspaceFolder}/configuration/vecm.cfg",
// "--instruments=COIN:EQUITY:ALPACA,BTC-USDT:CRYPTO:BNBSPOT",
// "--date_pattern=20250605",
// "--result_db=${workspaceFolder}/research/results/intermarket/%T.vecm.COIN-BTC.20250601.equity_results.db",
// ], // ],
// "env": { // "env": {
// "PYTHONPATH": "${workspaceFolder}/lib" // "PYTHONPATH": "${workspaceFolder}/lib"
// }, // },
// "console": "integratedTerminal" // "console": "integratedTerminal"
// }, // },
{
"name": "EQUITY VECM (rolling)",
"type": "debugpy",
"request": "launch",
"python": "/home/oleg/.pyenv/python3.12-venv/bin/python",
"program": "${workspaceFolder}/research/backtest.py",
"args": [
"--config=${workspaceFolder}/configuration/vecm.cfg",
"--instruments=COIN:EQUITY:ALPACA,MSTR:EQUITY:ALPACA",
"--date_pattern=20250605",
"--result_db=${workspaceFolder}/research/results/equity/%T.vecm.COIN-MSTR.20250605.equity_results.db",
],
"env": {
"PYTHONPATH": "${workspaceFolder}/lib"
},
"console": "integratedTerminal"
},
{
"name": "EQUITY-CRYPTO VECM (rolling)",
"type": "debugpy",
"request": "launch",
"python": "/home/oleg/.pyenv/python3.12-venv/bin/python",
"program": "${workspaceFolder}/research/backtest.py",
"args": [
"--config=${workspaceFolder}/configuration/vecm.cfg",
"--instruments=COIN:EQUITY:ALPACA,BTC-USDT:CRYPTO:BNBSPOT",
"--date_pattern=20250605",
"--result_db=${workspaceFolder}/research/results/intermarket/%T.vecm.COIN-BTC.20250601.equity_results.db",
],
"env": {
"PYTHONPATH": "${workspaceFolder}/lib"
},
"console": "integratedTerminal"
},
{ {
"name": "-------- B a t c h e s --------", "name": "-------- B a t c h e s --------",
}, },

View File

@ -6,7 +6,7 @@
], ],
"python.testing.cwd": "${workspaceFolder}", "python.testing.cwd": "${workspaceFolder}",
"python.testing.autoTestDiscoverOnSaveEnabled": true, "python.testing.autoTestDiscoverOnSaveEnabled": true,
"python.defaultInterpreterPath": "/usr/bin/python3", "python.defaultInterpreterPath": "/home/oleg/.pyenv/python3.12-venv/bin/python3",
"python.testing.pytestPath": "python3", "python.testing.pytestPath": "python3",
"python.analysis.extraPaths": [ "python.analysis.extraPaths": [
"${workspaceFolder}", "${workspaceFolder}",

View File

@ -90,8 +90,7 @@ class PairsTrader(NamedObject):
strategy_config = self.config_.get_subconfig("strategy_config", Config({})) strategy_config = self.config_.get_subconfig("strategy_config", Config({}))
self.live_strategy_ = PtLiveStrategy( self.live_strategy_ = PtLiveStrategy(
config=strategy_config, config=strategy_config,
instruments=self.instruments_, pairs_trader=self,
pairs_trader=self
) )
Log.info(f"{self.fname()} Strategy created: {self.live_strategy_}") Log.info(f"{self.fname()} Strategy created: {self.live_strategy_}")
@ -133,8 +132,11 @@ class PairsTrader(NamedObject):
await self.live_strategy_.on_mkt_data_hist_snapshot(hist_aggr=history) await self.live_strategy_.on_mkt_data_hist_snapshot(hist_aggr=history)
async def _on_api_request(self, request: web.Request) -> web.Response: async def _on_api_request(self, request: web.Request) -> web.Response:
# TODO choose pair
# TODO confirm chosen pair (after selection is implemented)
return web.Response() # TODO API request handler implementation return web.Response() # TODO API request handler implementation
async def run(self) -> None: async def run(self) -> None:
Log.info(f"{self.fname()} ...") Log.info(f"{self.fname()} ...")
pass pass

View File

@ -23,8 +23,8 @@
"dis-equilibrium_open_trshld": 2.0, "dis-equilibrium_open_trshld": 2.0,
"dis-equilibrium_close_trshld": 0.5, "dis-equilibrium_close_trshld": 0.5,
"training_size": 120, "training_size": 120,
"model_class": "pt_strategy.models.OLSModel", "model_class": "pairs_trading.lib.pt_strategy.models.OLSModel",
"model_data_policy_class": "pt_strategy.model_data_policy.ExpandingWindowDataPolicy", "model_data_policy_class": "pairs_trading.lib.pt_strategy.model_data_policy.ExpandingWindowDataPolicy",
# ====== Stop Conditions ====== # ====== Stop Conditions ======
"stop_close_conditions": { "stop_close_conditions": {

View File

@ -22,11 +22,11 @@
}, },
"dis-equilibrium_open_trshld": 1.75, "dis-equilibrium_open_trshld": 1.75,
"dis-equilibrium_close_trshld": 0.9, "dis-equilibrium_close_trshld": 0.9,
"model_class": "pt_strategy.models.OLSModel", "model_class": "pairs_trading.lib.pt_strategy.models.OLSModel",
# "model_data_policy_class": "pt_strategy.model_data_policy.EGOptimizedWndDataPolicy", # "model_data_policy_class": "pairs_trading.lib.pt_strategy.model_data_policy.EGOptimizedWndDataPolicy",
# "model_data_policy_class": "pt_strategy.model_data_policy.ADFOptimizedWndDataPolicy", # "model_data_policy_class": "pairs_trading.lib.pt_strategy.model_data_policy.ADFOptimizedWndDataPolicy",
"model_data_policy_class": "pt_strategy.model_data_policy.JohansenOptdWndDataPolicy", "model_data_policy_class": "pairs_trading.lib.pt_strategy.model_data_policy.JohansenOptdWndDataPolicy",
"min_training_size": 60, "min_training_size": 60,
"max_training_size": 150, "max_training_size": 150,

View File

@ -22,11 +22,11 @@
}, },
"dis-equilibrium_open_trshld": 1.75, "dis-equilibrium_open_trshld": 1.75,
"dis-equilibrium_close_trshld": 0.9, "dis-equilibrium_close_trshld": 0.9,
"model_class": "pt_strategy.models.OLSModel", "model_class": "pairs_trading.lib.pt_strategy.models.OLSModel",
"training_size": 120, "training_size": 120,
"model_data_policy_class": "pt_strategy.model_data_policy.RollingWindowDataPolicy", "model_data_policy_class": "pairs_trading.lib.pt_strategy.model_data_policy.RollingWindowDataPolicy",
# "model_data_policy_class": "pt_strategy.model_data_policy.OptimizedWindowDataPolicy", # "model_data_policy_class": "pairs_trading.lib.pt_strategy.model_data_policy.OptimizedWindowDataPolicy",
# "min_training_size": 60, # "min_training_size": 60,
# "max_training_size": 150, # "max_training_size": 150,

View File

@ -23,11 +23,11 @@
}, },
"dis-equilibrium_open_trshld": 1.75, "dis-equilibrium_open_trshld": 1.75,
"dis-equilibrium_close_trshld": 1.0, "dis-equilibrium_close_trshld": 1.0,
"model_class": "pt_strategy.models.VECMModel", "model_class": "pairs_trading.lib.pt_strategy.models.VECMModel",
"training_size": 120, "training_size": 120,
"model_data_policy_class": "pt_strategy.model_data_policy.RollingWindowDataPolicy", "model_data_policy_class": "pairs_trading.lib.pt_strategy.model_data_policy.RollingWindowDataPolicy",
# "model_data_policy_class": "pt_strategy.model_data_policy.OptimizedWindowDataPolicy", # "model_data_policy_class": "pairs_trading.lib.pt_strategy.model_data_policy.OptimizedWindowDataPolicy",
# "min_training_size": 60, # "min_training_size": 60,
# "max_training_size": 150, # "max_training_size": 150,

View File

@ -1,9 +1,16 @@
{ {
"refdata": {
"assets": @inc=http://@env{CONFIG_SERVICE}/refdata/assets
, "instruments": @inc=http://@env{CONFIG_SERVICE}/refdata/instruments
, "exchange_instruments": @inc=http://@env{CONFIG_SERVICE}/refdata/exchange_instruments
, "dynamic_instrument_exchanges": ["ALPACA"]
, "exchanges": @inc=http://@env{CONFIG_SERVICE}/refdata/exchanges
},
"market_data_loading": { "market_data_loading": {
"CRYPTO": { "CRYPTO": {
"data_directory": "./data/crypto", "data_directory": "./data/crypto",
"db_table_name": "md_1min_bars", "db_table_name": "md_1min_bars",
"instrument_id_pfx": "PAIR-", "instrument_id_pfx": "PAIR-",
}, },
"EQUITY": { "EQUITY": {
"data_directory": "./data/equity", "data_directory": "./data/equity",
@ -24,11 +31,11 @@
"dis-equilibrium_open_trshld": 1.75, "dis-equilibrium_open_trshld": 1.75,
"dis-equilibrium_close_trshld": 1.0, "dis-equilibrium_close_trshld": 1.0,
"model_class": "pt_strategy.models.VECMModel", "model_class": "pairs_trading.lib.pt_strategy.models.VECMModel",
# "training_size": 120, # "training_size": 120,
# "model_data_policy_class": "pt_strategy.model_data_policy.RollingWindowDataPolicy", # "model_data_policy_class": "pairs_trading.lib.pt_strategy.model_data_policy.RollingWindowDataPolicy",
"model_data_policy_class": "pt_strategy.model_data_policy.ADFOptimizedWndDataPolicy", "model_data_policy_class": "pairs_trading.lib.pt_strategy.model_data_policy.ADFOptimizedWndDataPolicy",
"min_training_size": 60, "min_training_size": 60,
"max_training_size": 150, "max_training_size": 150,

View File

@ -10,19 +10,23 @@ import pandas as pd
from cvttpy_tools.base import NamedObject from cvttpy_tools.base import NamedObject
from cvttpy_tools.app import App from cvttpy_tools.app import App
from cvttpy_tools.config import Config from cvttpy_tools.config import Config
from cvttpy_tools.settings.cvtt_types import IntervalSecT from cvttpy_tools.settings.cvtt_types import BookIdT, IntervalSecT
from cvttpy_tools.timeutils import SecPerHour from cvttpy_tools.timeutils import SecPerHour, current_nanoseconds
from cvttpy_tools.logger import Log from cvttpy_tools.logger import Log
# --- # ---
from cvttpy_trading.trading.instrument import ExchangeInstrument from cvttpy_trading.trading.instrument import ExchangeInstrument
from cvttpy_trading.trading.mkt_data.md_summary import MdTradesAggregate from cvttpy_trading.trading.mkt_data.md_summary import MdTradesAggregate
from cvttpy_trading.trading.trading_instructions import TradingInstructions from cvttpy_trading.trading.trading_instructions import TradingInstructions
from cvttpy_trading.trading.accounting.cvtt_book import CvttBook
from cvttpy_trading.trading.trading_instructions import TargetPositionSignal
# --- # ---
from pairs_trading.lib.pt_strategy.model_data_policy import ModelDataPolicy from pairs_trading.lib.pt_strategy.model_data_policy import ModelDataPolicy
from pairs_trading.lib.pt_strategy.pt_model import Prediction from pairs_trading.lib.pt_strategy.pt_model import Prediction
from pairs_trading.lib.pt_strategy.trading_pair import PairState, TradingPair from pairs_trading.lib.pt_strategy.trading_pair import LiveTradingPair
from pairs_trading.apps.pairs_trader import PairsTrader from pairs_trading.apps.pairs_trader import PairsTrader
from pairs_trading.lib.pt_strategy.pt_market_data import LiveMarketData
""" """
@ -51,7 +55,7 @@ class PtLiveStrategy(NamedObject):
open_threshold_: float open_threshold_: float
close_threshold_: float close_threshold_: float
trading_pair_: TradingPair trading_pair_: LiveTradingPair
model_data_policy_: ModelDataPolicy model_data_policy_: ModelDataPolicy
pairs_trader_: PairsTrader pairs_trader_: PairsTrader
@ -60,28 +64,29 @@ class PtLiveStrategy(NamedObject):
# for presentation: history of prediction values and trading signals # for presentation: history of prediction values and trading signals
predictions_df_: pd.DataFrame predictions_df_: pd.DataFrame
trading_signals_df_: pd.DataFrame trading_signals_df_: pd.DataFrame
# book_: CvttBook
def __init__( def __init__(
self, self,
config: Config, config: Config,
instruments: List[ExchangeInstrument],
pairs_trader: PairsTrader, pairs_trader: PairsTrader,
): ):
self.trading_pair_ = TradingPair( self.pairs_trader_ = pairs_trader
config=cast(Dict[str, Any], config.data()), self.trading_pair_ = LiveTradingPair(
instruments=[{"instrument_id": ei.instrument_id()} for ei in instruments], config=config,
instruments=self.pairs_trader_.instruments_,
) )
self.predictions_df_ = pd.DataFrame() self.predictions_df_ = pd.DataFrame()
self.trading_signals_df_ = pd.DataFrame() self.trading_signals_df_ = pd.DataFrame()
self.pairs_trader_ = pairs_trader # self.book_ = book
import copy import copy
# modified config must be passed to PtMarketData # modified config must be passed to PtMarketData
self.config_ = Config(json_src=copy.deepcopy(config.data())) self.config_ = Config(json_src=copy.deepcopy(config.data()))
self.instruments_ = instruments self.instruments_ = self.pairs_trader_.instruments_
App.instance().add_call( App.instance().add_call(
stage=App.Stage.Config, func=self._on_config(), can_run_now=True stage=App.Stage.Config, func=self._on_config(), can_run_now=True
@ -95,9 +100,6 @@ class PtLiveStrategy(NamedObject):
await self.pairs_trader_.subscribe_md() await self.pairs_trader_.subscribe_md()
self.model_data_policy_ = ModelDataPolicy.create(
self.config_, is_real_time=True, pair=self.trading_pair_
)
self.open_threshold_ = self.config_.get_value( self.open_threshold_ = self.config_.get_value(
"dis-equilibrium_open_trshld", 0.0 "dis-equilibrium_open_trshld", 0.0
) )
@ -121,13 +123,22 @@ class PtLiveStrategy(NamedObject):
if not self._is_md_actual(hist_aggr=hist_aggr): if not self._is_md_actual(hist_aggr=hist_aggr):
return return
market_data_df: Optional[pd.DataFrame] = self._create_md_pdf(hist_aggr=hist_aggr) market_data_df: pd.DataFrame = self._create_md_df(hist_aggr=hist_aggr)
if market_data_df is None: if len(market_data_df) == 0:
Log.warning(f"{self.fname()} Unable to create market data df") Log.warning(f"{self.fname()} Unable to create market data df")
return return
self.trading_pair_.market_data_ = market_data_df self.trading_pair_.market_data_ = market_data_df
self.model_data_policy_.advance() self.model_data_policy_ = ModelDataPolicy.create(
self.config_,
is_real_time=True,
pair=self.trading_pair_,
mkt_data=market_data_df,
)
assert (
self.model_data_policy_ is not None
), f"{self.fname()}: Unable to create ModelDataPolicy"
prediction = self.trading_pair_.run( prediction = self.trading_pair_.run(
market_data_df, self.model_data_policy_.advance() market_data_df, self.model_data_policy_.advance()
) )
@ -135,7 +146,7 @@ class PtLiveStrategy(NamedObject):
[self.predictions_df_, prediction.to_df()], ignore_index=True [self.predictions_df_, prediction.to_df()], ignore_index=True
) )
trading_instructions: Optional[TradingInstructions] = ( trading_instructions: List[TradingInstructions] = (
self._create_trading_instructions( self._create_trading_instructions(
prediction=prediction, last_row=market_data_df.iloc[-1] prediction=prediction, last_row=market_data_df.iloc[-1]
) )
@ -144,10 +155,74 @@ class PtLiveStrategy(NamedObject):
await self._send_trading_instructions(trading_instructions) await self._send_trading_instructions(trading_instructions)
def _is_md_actual(self, hist_aggr: List[MdTradesAggregate]) -> bool: def _is_md_actual(self, hist_aggr: List[MdTradesAggregate]) -> bool:
return False # URGENT _is_md_actual return False # URGENT _is_md_actual
def _create_md_pdf(self, hist_aggr: List[MdTradesAggregate]) -> Optional[pd.DataFrame]: def _create_md_df(self, hist_aggr: List[MdTradesAggregate]) -> pd.DataFrame:
return None # URGENT _create_md_pdf """
tstamp time_ns symbol open high low close volume num_trades vwap
0 2025-09-10 11:30:00 1757503800000000000 ADA-USDT 0.8750 0.8750 0.8743 0.8743 50710.500 0 0.874489
1 2025-09-10 11:30:00 1757503800000000000 SOL-USDT 219.9700 219.9800 219.6600 219.7000 2648.582 0 219.787847
2 2025-09-10 11:31:00 1757503860000000000 SOL-USDT 219.7000 219.7300 219.6200 219.6200 1134.886 0 219.663460
3 2025-09-10 11:31:00 1757503860000000000 ADA-USDT 0.8743 0.8745 0.8741 0.8741 10696.400 0 0.874234
4 2025-09-10 11:32:00 1757503920000000000 ADA-USDT 0.8742 0.8742 0.8739 0.8740 18546.900 0 0.874037
"""
rows: List[Dict[str, Any]] = []
for aggr in hist_aggr:
exch_inst = aggr.exch_inst_
rows.append(
{
# convert nanoseconds → tz-aware pandas timestamp
"tstamp": pd.to_datetime(aggr.time_ns_, unit="ns", utc=True),
"time_ns": aggr.time_ns_,
"symbol": exch_inst.instrument_id().split("-", 1)[1],
"exchange_id": exch_inst.exchange_id_,
"instrument_id": exch_inst.instrument_id(),
"open": exch_inst.get_price(aggr.open_),
"high": exch_inst.get_price(aggr.high_),
"low": exch_inst.get_price(aggr.low_),
"close": exch_inst.get_price(aggr.close_),
"volume": exch_inst.get_quantity(aggr.volume_),
"num_trades": aggr.num_trades_,
"vwap": exch_inst.get_price(aggr.vwap_),
}
)
source_md_df = pd.DataFrame(
rows,
columns=[
"tstamp",
"time_ns",
"symbol",
"exchange_id",
"instrument_id",
"open",
"high",
"low",
"close",
"volume",
"num_trades",
"vwap",
],
)
# automatic sorting
source_md_df.sort_values(
by=["time_ns", "symbol"],
ascending=True,
inplace=True,
kind="mergesort", # stable sort
)
source_md_df.reset_index(drop=True, inplace=True)
pt_mkt_data = LiveMarketData(config=self.config_, instruments=self.instruments_)
pt_mkt_data.origin_mkt_data_df_ = source_md_df
pt_mkt_data.set_market_data()
return pt_mkt_data.market_data_df_
def interval_sec(self) -> IntervalSecT: def interval_sec(self) -> IntervalSecT:
return self.interval_sec_ return self.interval_sec_
@ -156,271 +231,110 @@ class PtLiveStrategy(NamedObject):
return self.history_depth_sec_ return self.history_depth_sec_
async def _send_trading_instructions( async def _send_trading_instructions(
self, trading_instructions: TradingInstructions self, trading_instructions: List[TradingInstructions]
) -> None: ) -> None:
await self.pairs_trader_.ti_sender_.send_trading_instructions(trading_instructions) for ti in trading_instructions:
pass # URGENT _send_trading_instructions Log.info(f"{self.fname()} Sending trading instructions {ti}")
await self.pairs_trader_.ti_sender_.send_trading_instructions(ti)
def _create_trading_instructions( def _create_trading_instructions(
self, prediction: Prediction, last_row: pd.Series self, prediction: Prediction, last_row: pd.Series
) -> Optional[TradingInstructions]: ) -> List[TradingInstructions]:
trd_instructions: List[TradingInstructions] = []
pair = self.trading_pair_ pair = self.trading_pair_
res: Optional[TradingInstructions]
scaled_disequilibrium = prediction.scaled_disequilibrium_ scaled_disequilibrium = prediction.scaled_disequilibrium_
abs_scaled_disequilibrium = abs(scaled_disequilibrium) abs_scaled_disequilibrium = abs(scaled_disequilibrium)
if pair.is_closed(): if abs_scaled_disequilibrium >= self.open_threshold_:
if abs_scaled_disequilibrium >= self.open_threshold_: trd_instructions = self._create_open_trade_instructions(
trd_instructions = self._create_open_trade_instructions( pair, row=last_row, prediction=prediction
pair, row=last_row, prediction=prediction )
)
elif pair.is_open(): elif abs_scaled_disequilibrium <= self.close_threshold_ or pair.to_stop_close_conditions(predicted_row=last_row):
if abs_scaled_disequilibrium <= self.close_threshold_: trd_instructions = self._create_close_trade_instructions(
trd_instructions = self._create_close_trade_instructions( pair, row=last_row # , prediction=prediction
pair, row=last_row # , prediction=prediction )
)
elif pair.to_stop_close_conditions(predicted_row=last_row):
trd_instructions = self._create_close_trade_instructions(
pair, row=last_row
)
return trd_instructions return trd_instructions
def _strength(self, scaled_disequilibrium) -> float:
# URGENT PtLiveStrategy._strength()
return 1.0
def _create_open_trade_instructions( def _create_open_trade_instructions(
self, pair: TradingPair, row: pd.Series, prediction: Prediction self, pair: LiveTradingPair, row: pd.Series, prediction: Prediction
) -> Optional[TradingInstructions]: ) -> List[TradingInstructions]:
ti: Optional[TradingInstructions] = None
scaled_disequilibrium = prediction.scaled_disequilibrium_
# URGENT _create_open_trade_instructions
# if scaled_disequilibrium > 0:
# side_a = "SELL"
# trd_inst_a = TradingInstruction(
# type_=TradingInstructionType.TARGET_POSITION,
# exch_instr_=pair.get_instrument_a(),
# specifics_={"side": "SELL", "strength": -1},
# )
# side_b = "BUY"
# else:
# side_a = "BUY"
# side_b = "SELL"
# colname_a, colname_b = pair.exec_prices_colnames()
# px_a = row[f"{colname_a}"]
# px_b = row[f"{colname_b}"]
# tstamp = row["tstamp"]
# diseqlbrm = prediction.disequilibrium_
# scaled_disequilibrium = prediction.scaled_disequilibrium_
# df = self._trades_df()
# # save closing sides
# pair.user_data_["open_side_a"] = side_a # used in oustanding positions
# pair.user_data_["open_side_b"] = side_b
# pair.user_data_["open_px_a"] = px_a
# pair.user_data_["open_px_b"] = px_b
# pair.user_data_["open_tstamp"] = tstamp
# pair.user_data_["close_side_a"] = side_b # used for closing trades
# pair.user_data_["close_side_b"] = side_a
# # create opening trades
# df.loc[len(df)] = {
# "time": tstamp,
# "symbol": pair.symbol_a_,
# "side": side_a,
# "action": "OPEN",
# "price": px_a,
# "disequilibrium": diseqlbrm,
# "signed_scaled_disequilibrium": scaled_disequilibrium,
# "scaled_disequilibrium": abs(scaled_disequilibrium),
# # "pair": pair,
# }
# df.loc[len(df)] = {
# "time": tstamp,
# "symbol": pair.symbol_b_,
# "side": side_b,
# "action": "OPEN",
# "price": px_b,
# "disequilibrium": diseqlbrm,
# "scaled_disequilibrium": abs(scaled_disequilibrium),
# "signed_scaled_disequilibrium": scaled_disequilibrium,
# # "pair": pair,
# }
# ti: List[TradingInstruction] = self._create_trading_instructions(
# prediction=prediction, last_row=row
# )
return ti
def _create_close_trade_instructions(
self, pair: TradingPair, row: pd.Series # , prediction: Prediction
) -> Optional[TradingInstructions]:
ti: Optional[TradingInstructions] = None
# URGENT _create_close_trade_instructions
return ti
def _handle_outstanding_positions(self) -> Optional[pd.DataFrame]:
trades = None
pair = self.trading_pair_
# Outstanding positions
if pair.user_data_["state"] == PairState.OPEN:
print(f"{pair}: *** Position is NOT CLOSED. ***")
# outstanding positions
if self.config_.key_exists("close_outstanding_positions"):
close_position_row = pd.Series(pair.market_data_.iloc[-2])
# close_position_row["disequilibrium"] = 0.0
# close_position_row["scaled_disequilibrium"] = 0.0
# close_position_row["signed_scaled_disequilibrium"] = 0.0
trades = self._create_close_trades(
pair=pair, row=close_position_row, prediction=None
)
if trades is not None:
trades["status"] = PairState.CLOSE_POSITION.name
print(f"CLOSE_POSITION TRADES:\n{trades}")
pair.user_data_["state"] = PairState.CLOSE_POSITION
pair.on_close_trades(trades)
else:
pair.add_outstanding_position(
symbol=pair.symbol_a_,
open_side=pair.user_data_["open_side_a"],
open_px=pair.user_data_["open_px_a"],
open_tstamp=pair.user_data_["open_tstamp"],
last_mkt_data_row=pair.market_data_.iloc[-1],
)
pair.add_outstanding_position(
symbol=pair.symbol_b_,
open_side=pair.user_data_["open_side_b"],
open_px=pair.user_data_["open_px_b"],
open_tstamp=pair.user_data_["open_tstamp"],
last_mkt_data_row=pair.market_data_.iloc[-1],
)
return trades
def _trades_df(self) -> pd.DataFrame:
types = {
"time": "datetime64[ns]",
"action": "string",
"symbol": "string",
"side": "string",
"price": "float64",
"disequilibrium": "float64",
"scaled_disequilibrium": "float64",
"signed_scaled_disequilibrium": "float64",
# "pair": "object",
}
columns = list(types.keys())
return pd.DataFrame(columns=columns).astype(types)
def _create_open_trades(
self, pair: TradingPair, row: pd.Series, prediction: Prediction
) -> Optional[pd.DataFrame]:
colname_a, colname_b = pair.exec_prices_colnames()
tstamp = row["tstamp"]
diseqlbrm = prediction.disequilibrium_ diseqlbrm = prediction.disequilibrium_
scaled_disequilibrium = prediction.scaled_disequilibrium_ scaled_disequilibrium = prediction.scaled_disequilibrium_
px_a = row[f"{colname_a}"]
px_b = row[f"{colname_b}"]
# creating the trades
df = self._trades_df()
print(f"OPEN_TRADES: {row["tstamp"]} {scaled_disequilibrium=}")
if diseqlbrm > 0: if diseqlbrm > 0:
side_a = "SELL" side_a = -1
side_b = "BUY" side_b = 1
else: else:
side_a = "BUY" side_a = 1
side_b = "SELL" side_b = -1
# save closing sides ti_a: Optional[TradingInstructions] = TradingInstructions(
pair.user_data_["open_side_a"] = side_a # used in oustanding positions book=self.pairs_trader_.book_id_,
pair.user_data_["open_side_b"] = side_b strategy_id=self.__class__.__name__,
pair.user_data_["open_px_a"] = px_a ti_type=TradingInstructions.Type.TARGET_POSITION,
pair.user_data_["open_px_b"] = px_b issued_ts_ns=current_nanoseconds(),
pair.user_data_["open_tstamp"] = tstamp data=TargetPositionSignal(
strength=side_a * self._strength(scaled_disequilibrium),
base_asset=pair.get_instrument_a().base_asset_id_,
quote_asset=pair.get_instrument_a().quote_asset_id_,
user_data={}
),
)
if not ti_a:
return []
ti_b: Optional[TradingInstructions] = TradingInstructions(
book=self.pairs_trader_.book_id_,
strategy_id=self.__class__.__name__,
ti_type=TradingInstructions.Type.TARGET_POSITION,
issued_ts_ns=current_nanoseconds(),
data=TargetPositionSignal(
strength=side_b * self._strength(scaled_disequilibrium),
base_asset=pair.get_instrument_b().base_asset_id_,
quote_asset=pair.get_instrument_b().quote_asset_id_,
user_data={}
),
)
if not ti_b:
return []
return [ti_a, ti_b]
pair.user_data_["close_side_a"] = side_b # used for closing trades
pair.user_data_["close_side_b"] = side_a
# create opening trades def _create_close_trade_instructions(
df.loc[len(df)] = { self, pair: LiveTradingPair, row: pd.Series
"time": tstamp, ) -> List[TradingInstructions]:
"symbol": pair.symbol_a_, ti_a: Optional[TradingInstructions] = TradingInstructions(
"side": side_a, book=self.pairs_trader_.book_id_,
"action": "OPEN", strategy_id=self.__class__.__name__,
"price": px_a, ti_type=TradingInstructions.Type.TARGET_POSITION,
"disequilibrium": diseqlbrm, issued_ts_ns=current_nanoseconds(),
"signed_scaled_disequilibrium": scaled_disequilibrium, data=TargetPositionSignal(
"scaled_disequilibrium": abs(scaled_disequilibrium), strength=0,
# "pair": pair, base_asset=pair.get_instrument_a().base_asset_id_,
} quote_asset=pair.get_instrument_a().quote_asset_id_,
df.loc[len(df)] = { user_data={}
"time": tstamp, ),
"symbol": pair.symbol_b_, )
"side": side_b, if not ti_a:
"action": "OPEN", return []
"price": px_b, ti_b: Optional[TradingInstructions] = TradingInstructions(
"disequilibrium": diseqlbrm, book=self.pairs_trader_.book_id_,
"scaled_disequilibrium": abs(scaled_disequilibrium), strategy_id=self.__class__.__name__,
"signed_scaled_disequilibrium": scaled_disequilibrium, ti_type=TradingInstructions.Type.TARGET_POSITION,
# "pair": pair, issued_ts_ns=current_nanoseconds(),
} data=TargetPositionSignal(
return df strength=0,
base_asset=pair.get_instrument_b().base_asset_id_,
def _create_close_trades( quote_asset=pair.get_instrument_b().quote_asset_id_,
self, pair: TradingPair, row: pd.Series, prediction: Optional[Prediction] = None user_data={}
) -> Optional[pd.DataFrame]: ),
colname_a, colname_b = pair.exec_prices_colnames() )
if not ti_b:
tstamp = row["tstamp"] return []
if prediction is not None: return [ti_a, ti_b]
diseqlbrm = prediction.disequilibrium_
signed_scaled_disequilibrium = prediction.scaled_disequilibrium_
scaled_disequilibrium = abs(prediction.scaled_disequilibrium_)
else:
diseqlbrm = 0.0
signed_scaled_disequilibrium = 0.0
scaled_disequilibrium = 0.0
px_a = row[f"{colname_a}"]
px_b = row[f"{colname_b}"]
# creating the trades
df = self._trades_df()
# create opening trades
df.loc[len(df)] = {
"time": tstamp,
"symbol": pair.symbol_a_,
"side": pair.user_data_["close_side_a"],
"action": "CLOSE",
"price": px_a,
"disequilibrium": diseqlbrm,
"scaled_disequilibrium": scaled_disequilibrium,
"signed_scaled_disequilibrium": signed_scaled_disequilibrium,
# "pair": pair,
}
df.loc[len(df)] = {
"time": tstamp,
"symbol": pair.symbol_b_,
"side": pair.user_data_["close_side_b"],
"action": "CLOSE",
"price": px_b,
"disequilibrium": diseqlbrm,
"scaled_disequilibrium": scaled_disequilibrium,
"signed_scaled_disequilibrium": signed_scaled_disequilibrium,
# "pair": pair,
}
del pair.user_data_["close_side_a"]
del pair.user_data_["close_side_b"]
del pair.user_data_["open_tstamp"]
del pair.user_data_["open_px_a"]
del pair.user_data_["open_px_b"]
del pair.user_data_["open_side_a"]
del pair.user_data_["open_side_b"]
return df

View File

@ -12,8 +12,8 @@ from cvttpy_tools.config import Config
@dataclass @dataclass
class DataWindowParams: class DataWindowParams:
training_size: int training_size_: int
training_start_index: int training_start_index_: int
class ModelDataPolicy(ABC): class ModelDataPolicy(ABC):
@ -24,16 +24,9 @@ class ModelDataPolicy(ABC):
def __init__(self, config: Config, *args: Any, **kwargs: Any): def __init__(self, config: Config, *args: Any, **kwargs: Any):
self.config_ = config self.config_ = config
training_size = config.get_value("training_size", 120)
training_start_index = 0
if kwargs.get("is_real_time", False):
training_size = 120
training_start_index = 0
else:
training_size = config.get_value("training_size", 120)
self.current_data_params_ = DataWindowParams( self.current_data_params_ = DataWindowParams(
training_size=config.get_value("training_size", 120), training_size_=config.get_value("training_size", 120),
training_start_index=0, training_start_index_=0,
) )
self.count_ = 0 self.count_ = 0
self.is_real_time_ = kwargs.get("is_real_time", False) self.is_real_time_ = kwargs.get("is_real_time", False)
@ -66,9 +59,9 @@ class RollingWindowDataPolicy(ModelDataPolicy):
def advance(self, mkt_data_df: Optional[pd.DataFrame] = None) -> DataWindowParams: def advance(self, mkt_data_df: Optional[pd.DataFrame] = None) -> DataWindowParams:
super().advance(mkt_data_df) super().advance(mkt_data_df)
if self.is_real_time_: if self.is_real_time_:
self.current_data_params_.training_start_index = -self.current_data_params_.training_size self.current_data_params_.training_start_index_ = -self.current_data_params_.training_size_
else: else:
self.current_data_params_.training_start_index += 1 self.current_data_params_.training_start_index_ += 1
return self.current_data_params_ return self.current_data_params_
@ -111,12 +104,12 @@ class OptimizedWndDataPolicy(ModelDataPolicy, ABC):
if self.is_real_time_: if self.is_real_time_:
self.end_index_ = len(self.mkt_data_df_) - 1 self.end_index_ = len(self.mkt_data_df_) - 1
else: else:
self.end_index_ = self.current_data_params_.training_start_index + self.max_training_size_ self.end_index_ = self.current_data_params_.training_start_index_ + self.max_training_size_
if self.end_index_ > len(self.mkt_data_df_) - 1: if self.end_index_ > len(self.mkt_data_df_) - 1:
self.end_index_ = len(self.mkt_data_df_) - 1 self.end_index_ = len(self.mkt_data_df_) - 1
self.current_data_params_.training_start_index = self.end_index_ - self.max_training_size_ self.current_data_params_.training_start_index_ = self.end_index_ - self.max_training_size_
if self.current_data_params_.training_start_index < 0: if self.current_data_params_.training_start_index_ < 0:
self.current_data_params_.training_start_index = 0 self.current_data_params_.training_start_index_ = 0
col_a, col_b = self.pair_.colnames() col_a, col_b = self.pair_.colnames()
self.prices_a_ = np.array(self.mkt_data_df_[col_a]) self.prices_a_ = np.array(self.mkt_data_df_[col_a])
@ -153,8 +146,8 @@ class EGOptimizedWndDataPolicy(OptimizedWndDataPolicy):
eg_pvalue = float(coint(series_a, series_b)[1]) eg_pvalue = float(coint(series_a, series_b)[1])
if eg_pvalue < last_pvalue: if eg_pvalue < last_pvalue:
last_pvalue = eg_pvalue last_pvalue = eg_pvalue
result.training_size = trn_size result.training_size_ = trn_size
result.training_start_index = start_index result.training_start_index_ = start_index
# print( # print(
# f"*** DEBUG *** end_index={self.end_index_}, best_trn_size={self.current_data_params_.training_size}, {last_pvalue=}" # f"*** DEBUG *** end_index={self.end_index_}, best_trn_size={self.current_data_params_.training_size}, {last_pvalue=}"
@ -197,8 +190,8 @@ class ADFOptimizedWndDataPolicy(OptimizedWndDataPolicy):
if adf_pvalue < last_pvalue: if adf_pvalue < last_pvalue:
last_pvalue = adf_pvalue last_pvalue = adf_pvalue
result.training_size = trn_size result.training_size_ = trn_size
result.training_start_index = start_index result.training_start_index_ = start_index
# print( # print(
# f"*** DEBUG *** end_index={self.end_index_}," # f"*** DEBUG *** end_index={self.end_index_},"
@ -247,8 +240,8 @@ class JohansenOptdWndDataPolicy(OptimizedWndDataPolicy):
continue continue
if best_trn_size > 0: if best_trn_size > 0:
result.training_size = best_trn_size result.training_size_ = best_trn_size
result.training_start_index = best_start_index result.training_start_index_ = best_start_index
else: else:
print("*** WARNING: No valid cointegration window found.") print("*** WARNING: No valid cointegration window found.")

View File

@ -1,157 +1,61 @@
from __future__ import annotations from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
import pandas as pd import pandas as pd
# --- # ---
from cvttpy_tools.base import NamedObject
from cvttpy_tools.config import Config
from cvttpy_tools.settings.cvtt_types import JsonDictT from cvttpy_tools.settings.cvtt_types import JsonDictT
# --- # ---
from cvttpy_trading.trading.mkt_data.md_summary import MdTradesAggregate from cvttpy_trading.trading.mkt_data.md_summary import MdTradesAggregate
from cvttpy_trading.trading.instrument import ExchangeInstrument
# --- # ---
from pairs_trading.lib.tools.data_loader import load_market_data from pairs_trading.lib.tools.data_loader import load_market_data
class PtMarketData(): class PtMarketData(NamedObject, ABC):
config_: Dict[str, Any] config_: Config
origin_mkt_data_df_: pd.DataFrame origin_mkt_data_df_: pd.DataFrame
market_data_df_: pd.DataFrame market_data_df_: pd.DataFrame
stat_model_price_: str
instruments_: List[ExchangeInstrument]
symbol_a_: str
symbol_b_: str
def __init__(self, config: Dict[str, Any]): def __init__(self, config: Config, instruments: List[ExchangeInstrument]):
self.config_ = config self.config_ = config
self.origin_mkt_data_df_ = pd.DataFrame() self.origin_mkt_data_df_ = pd.DataFrame()
self.market_data_df_ = pd.DataFrame() self.market_data_df_ = pd.DataFrame()
self.stat_model_price_ = self.config_.get_value("stat_model_price")
self.instruments_ = instruments
assert len(self.instruments_) > 0, "No instruments found in config"
self.symbol_a_ = self.instruments_[0].instrument_id().split("-", 1)[1]
self.symbol_b_ = self.instruments_[1].instrument_id().split("-", 1)[1]
class ResearchMarketData(PtMarketData): @abstractmethod
current_index_: int def md_columns(self) -> List[str]: ...
is_execution_price_: bool
def __init__(self, config: Dict[str, Any]):
super().__init__(config)
self.current_index_ = 0
self.is_execution_price_ = "execution_price" in self.config_
if self.is_execution_price_:
self.execution_price_column_ = self.config_["execution_price"]["column"]
self.execution_price_shift_ = self.config_["execution_price"]["shift"]
else:
self.execution_price_column_ = None
self.execution_price_shift_ = 0
def has_next(self) -> bool: @abstractmethod
return self.current_index_ < len(self.market_data_df_) def rename_columns(self, symbol_df: pd.DataFrame) -> pd.DataFrame: ...
def get_next(self) -> pd.Series: @abstractmethod
result = self.market_data_df_.iloc[self.current_index_] def tranform_df_target_colnames(self) -> List[str]: ...
self.current_index_ += 1
return result
def load(self) -> None:
datafiles: List[str] = self.config_.get("datafiles", [])
instruments: List[Dict[str, str]] = self.config_.get("instruments", [])
assert len(instruments) > 0, "No instruments found in config"
assert len(datafiles) > 0, "No datafiles found in config"
self.symbol_a_ = instruments[0]["symbol"]
self.symbol_b_ = instruments[1]["symbol"]
self.stat_model_price_ = self.config_["stat_model_price"]
extra_minutes: int def set_market_data(self) -> None:
extra_minutes = self.execution_price_shift_ self.market_data_df_ = pd.DataFrame(
self._transform_dataframe(self.origin_mkt_data_df_)[
for datafile in datafiles: ["tstamp"] + self.tranform_df_target_colnames()
md_df = load_market_data( ]
datafile=datafile, )
instruments=instruments,
db_table_name=self.config_["market_data_loading"][instruments[0]["instrument_type"]]["db_table_name"],
trading_hours=self.config_["trading_hours"],
extra_minutes=extra_minutes,
)
self.origin_mkt_data_df_ = pd.concat([self.origin_mkt_data_df_, md_df])
self.origin_mkt_data_df_ = self.origin_mkt_data_df_.sort_values(by="tstamp")
self.origin_mkt_data_df_ = self.origin_mkt_data_df_.dropna().reset_index(drop=True)
self._set_market_data()
def _set_market_data(self, ) -> None:
if self.is_execution_price_:
self.market_data_df_ = pd.DataFrame(
self._transform_dataframe(self.origin_mkt_data_df_)[["tstamp"] + self.colnames() + self.orig_exec_prices_colnames()]
)
else:
self.market_data_df_ = pd.DataFrame(
self._transform_dataframe(self.origin_mkt_data_df_)[["tstamp"] + self.colnames()]
)
self.market_data_df_ = self.market_data_df_.dropna().reset_index(drop=True) self.market_data_df_ = self.market_data_df_.dropna().reset_index(drop=True)
self.market_data_df_["tstamp"] = pd.to_datetime(self.market_data_df_["tstamp"]) self.market_data_df_["tstamp"] = pd.to_datetime(self.market_data_df_["tstamp"])
self.market_data_df_ = self.market_data_df_.sort_values("tstamp") self.market_data_df_ = self.market_data_df_.sort_values("tstamp")
self._set_execution_price_data()
def _transform_dataframe(self, df: pd.DataFrame) -> pd.DataFrame:
df_selected: pd.DataFrame
if self.is_execution_price_:
execution_price_column = self.config_["execution_price"]["column"]
df_selected = pd.DataFrame(
df[["tstamp", "symbol", self.stat_model_price_, execution_price_column]]
)
else:
df_selected = pd.DataFrame(
df[["tstamp", "symbol", self.stat_model_price_]]
)
result_df = pd.DataFrame(df_selected["tstamp"]).drop_duplicates().reset_index(drop=True)
# For each unique symbol, add a corresponding stat_model_price column
symbols = df_selected["symbol"].unique()
for symbol in symbols:
# Filter rows for this symbol
df_symbol = df_selected[df_selected["symbol"] == symbol].reset_index(
drop=True
)
# Create column name like "close-COIN"
new_price_column = f"{self.stat_model_price_}_{symbol}"
if self.is_execution_price_:
new_execution_price_column = f"{self.execution_price_column_}_{symbol}"
# Create temporary dataframe with timestamp and price
temp_df = pd.DataFrame(
{
"tstamp": df_symbol["tstamp"],
new_price_column: df_symbol[self.stat_model_price_],
new_execution_price_column: df_symbol[execution_price_column],
}
)
else:
temp_df = pd.DataFrame(
{
"tstamp": df_symbol["tstamp"],
new_price_column: df_symbol[self.stat_model_price_],
}
)
# Join with our result dataframe
result_df = pd.merge(result_df, temp_df, on="tstamp", how="left")
result_df = result_df.reset_index(
drop=True
) # do not dropna() since irrelevant symbol would affect dataset
return result_df.dropna()
def _set_execution_price_data(self) -> None:
if "execution_price" not in self.config_:
self.market_data_df_[f"exec_price_{self.symbol_a_}"] = self.market_data_df_[f"{self.stat_model_price_}_{self.symbol_a_}"]
self.market_data_df_[f"exec_price_{self.symbol_b_}"] = self.market_data_df_[f"{self.stat_model_price_}_{self.symbol_b_}"]
return
execution_price_column = self.config_["execution_price"]["column"]
execution_price_shift = self.config_["execution_price"]["shift"]
self.market_data_df_[f"exec_price_{self.symbol_a_}"] = self.market_data_df_[f"{execution_price_column}_{self.symbol_a_}"].shift(-execution_price_shift)
self.market_data_df_[f"exec_price_{self.symbol_b_}"] = self.market_data_df_[f"{execution_price_column}_{self.symbol_b_}"].shift(-execution_price_shift)
self.market_data_df_ = self.market_data_df_.dropna().reset_index(drop=True)
def colnames(self) -> List[str]: def colnames(self) -> List[str]:
return [ return [
@ -159,15 +63,161 @@ class ResearchMarketData(PtMarketData):
f"{self.stat_model_price_}_{self.symbol_b_}", f"{self.stat_model_price_}_{self.symbol_b_}",
] ]
def _transform_dataframe(self, df: pd.DataFrame) -> pd.DataFrame:
df_selected: pd.DataFrame = pd.DataFrame(df[self.md_columns()])
result_df = (
pd.DataFrame(df_selected["tstamp"]).drop_duplicates().reset_index(drop=True)
)
# For each unique symbol, add a corresponding stat_model_price column
symbols = df_selected["symbol"].unique()
for symbol in symbols:
# Filter rows for this symbol
df_symbol = df_selected[df_selected["symbol"] == symbol].reset_index(
drop=True
)
# Create column name like "close-COIN"
temp_df: pd.DataFrame = self.rename_columns(df_symbol)
# Join with our result dataframe
result_df = pd.merge(result_df, temp_df, on="tstamp", how="left")
result_df = result_df.reset_index(
drop=True
) # do not dropna() since irrelevant symbol would affect dataset
return result_df.dropna()
class ResearchMarketData(PtMarketData):
current_index_: int
is_execution_price_: bool
def __init__(self, config: Config, instruments: List[ExchangeInstrument]):
super().__init__(config, instruments)
self.current_index_ = 0
self.is_execution_price_ = self.config_.key_exists("execution_price")
if self.is_execution_price_:
self.execution_price_column_ = self.config_.get_value("execution_price")["column"]
self.execution_price_shift_ = self.config_.get_value("execution_price")["shift"]
else:
self.execution_price_column_ = None
self.execution_price_shift_ = 0
def has_next(self) -> bool:
return self.current_index_ < len(self.market_data_df_)
def get_next(self) -> pd.Series:
result = self.market_data_df_.iloc[self.current_index_]
self.current_index_ += 1
return result
def load(self) -> None:
datafiles: List[str] = self.config_.get_value("datafiles", [])
assert len(datafiles) > 0, "No datafiles found in config"
extra_minutes: int = self.execution_price_shift_
for datafile in datafiles:
md_df = load_market_data(
datafile=datafile,
instruments=self.instruments_,
db_table_name=self.config_.get_value("market_data_loading")[
self.instruments_[0].user_data_.get("instrument_type", "?instrument_type?")
]["db_table_name"],
trading_hours=self.config_.get_value("trading_hours"),
extra_minutes=extra_minutes,
)
self.origin_mkt_data_df_ = pd.concat([self.origin_mkt_data_df_, md_df])
self.origin_mkt_data_df_ = self.origin_mkt_data_df_.sort_values(by="tstamp")
self.origin_mkt_data_df_ = self.origin_mkt_data_df_.dropna().reset_index(
drop=True
)
self.set_market_data()
self._set_execution_price_data()
def _set_execution_price_data(self) -> None:
if not self.is_execution_price_:
return
if not self.config_.key_exists("execution_price"):
self.market_data_df_[f"exec_price_{self.symbol_a_}"] = self.market_data_df_[
f"{self.stat_model_price_}_{self.symbol_a_}"
]
self.market_data_df_[f"exec_price_{self.symbol_b_}"] = self.market_data_df_[
f"{self.stat_model_price_}_{self.symbol_b_}"
]
return
execution_price_column = self.config_.get_value("execution_price")["column"]
execution_price_shift = self.config_.get_value("execution_price")["shift"]
self.market_data_df_[f"exec_price_{self.symbol_a_}"] = self.market_data_df_[
f"{execution_price_column}_{self.symbol_a_}"
].shift(-execution_price_shift)
self.market_data_df_[f"exec_price_{self.symbol_b_}"] = self.market_data_df_[
f"{execution_price_column}_{self.symbol_b_}"
].shift(-execution_price_shift)
self.market_data_df_ = self.market_data_df_.dropna().reset_index(drop=True)
def md_columns(self) -> List[str]:
# @abstractmethod
if self.is_execution_price_:
return ["tstamp", "symbol", self.stat_model_price_, self.execution_price_column_]
else:
return ["tstamp", "symbol", self.stat_model_price_]
def rename_columns(self, selected_symbol_df: pd.DataFrame) -> pd.DataFrame:
# @abstractmethod
symbol = selected_symbol_df.iloc[0]["symbol"]
new_price_column = f"{self.stat_model_price_}_{symbol}"
if self.is_execution_price_:
new_execution_price_column = f"{self.execution_price_column_}_{symbol}"
# Create temporary dataframe with timestamp and price
temp_df = pd.DataFrame(
{
"tstamp": selected_symbol_df["tstamp"],
new_price_column: selected_symbol_df[self.stat_model_price_],
new_execution_price_column: selected_symbol_df[self.execution_price_column_],
}
)
else:
temp_df = pd.DataFrame(
{
"tstamp": selected_symbol_df["tstamp"],
new_price_column: selected_symbol_df[self.stat_model_price_],
}
)
return temp_df
def tranform_df_target_colnames(self):
# @abstractmethod
return self.colnames() + self.orig_exec_prices_colnames()
def orig_exec_prices_colnames(self) -> List[str]: def orig_exec_prices_colnames(self) -> List[str]:
return [ return [
f"{self.execution_price_column_}_{self.symbol_a_}", f"{self.execution_price_column_}_{self.symbol_a_}",
f"{self.execution_price_column_}_{self.symbol_b_}", f"{self.execution_price_column_}_{self.symbol_b_}",
] ] if self.is_execution_price_ else []
def exec_prices_colnames(self) -> List[str]: class LiveMarketData(PtMarketData):
return [
f"exec_price_{self.symbol_a_}",
f"exec_price_{self.symbol_b_}",
]
def __init__(self, config: Config, instruments: List[ExchangeInstrument]):
super().__init__(config, instruments)
def md_columns(self) -> List[str]:
# @abstractmethod
return ["tstamp", "symbol", self.stat_model_price_]
def rename_columns(self, selected_symbol_df: pd.DataFrame) -> pd.DataFrame:
# @abstractmethod
symbol = selected_symbol_df.iloc[0]["symbol"]
new_price_column = f"{self.stat_model_price_}_{symbol}"
temp_df = pd.DataFrame(
{
"tstamp": selected_symbol_df["tstamp"],
new_price_column: selected_symbol_df[self.stat_model_price_],
}
)
return temp_df
def tranform_df_target_colnames(self):
# @abstractmethod
return self.colnames()

View File

@ -3,6 +3,9 @@ from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Any, Dict, cast from typing import Any, Dict, cast
# ---
from cvttpy_tools.config import Config
# ---
from pairs_trading.lib.pt_strategy.prediction import Prediction from pairs_trading.lib.pt_strategy.prediction import Prediction
from pairs_trading.lib.pt_strategy.trading_pair import TradingPair from pairs_trading.lib.pt_strategy.trading_pair import TradingPair
@ -13,10 +16,10 @@ class PairsTradingModel(ABC):
... ...
@staticmethod @staticmethod
def create(config: Dict[str, Any]) -> PairsTradingModel: def create(config: Config) -> PairsTradingModel:
import importlib import importlib
model_class_name = config.get("model_class", None) model_class_name = config.get_value("model_class", None)
assert model_class_name is not None assert model_class_name is not None
module_name, class_name = model_class_name.rsplit(".", 1) module_name, class_name = model_class_name.rsplit(".", 1)
module = importlib.import_module(module_name) module = importlib.import_module(module_name)

View File

@ -1,56 +1,56 @@
from __future__ import annotations from __future__ import annotations
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional, Tuple
import pandas as pd import pandas as pd
# ---
from cvttpy_tools.config import Config from cvttpy_tools.config import Config
# ---
from cvttpy_trading.trading.instrument import ExchangeInstrument
# ---
from pairs_trading.lib.pt_strategy.model_data_policy import ModelDataPolicy from pairs_trading.lib.pt_strategy.model_data_policy import ModelDataPolicy
from pairs_trading.lib.pt_strategy.pt_market_data import ResearchMarketData from pairs_trading.lib.pt_strategy.pt_market_data import ResearchMarketData
from pairs_trading.lib.pt_strategy.pt_model import Prediction from pairs_trading.lib.pt_strategy.pt_model import Prediction
from pairs_trading.lib.pt_strategy.trading_pair import PairState, TradingPair from pairs_trading.lib.pt_strategy.trading_pair import PairState, TradingPair, ResearchTradingPair
class PtResearchStrategy: class PtResearchStrategy:
config_: Dict[str, Any] config_: Config
trading_pair_: TradingPair trading_pair_: ResearchTradingPair
model_data_policy_: ModelDataPolicy model_data_policy_: ModelDataPolicy
pt_mkt_data_: ResearchMarketData pt_mkt_data_: ResearchMarketData
trades_: List[pd.DataFrame] trades_: List[pd.DataFrame]
predictions_: pd.DataFrame predictions_df_: pd.DataFrame
def __init__( def __init__(
self, self,
config: Dict[str, Any], config: Config,
datafiles: List[str], instruments: List[ExchangeInstrument]
instruments: List[Dict[str, str]],
): ):
from pairs_trading.lib.pt_strategy.model_data_policy import ModelDataPolicy from pairs_trading.lib.pt_strategy.model_data_policy import ModelDataPolicy
from pairs_trading.lib.pt_strategy.trading_pair import TradingPair from pairs_trading.lib.pt_strategy.trading_pair import TradingPair
self.config_ = config self.config_ = config
self.trades_ = [] self.trades_ = []
self.trading_pair_ = TradingPair(config=config, instruments=instruments) self.trading_pair_ = ResearchTradingPair(config=config, instruments=instruments)
self.predictions_ = pd.DataFrame() self.predictions_df_ = pd.DataFrame()
import copy import copy
# modified config must be passed to PtMarketData # modified config must be passed to PtMarketData
config_copy = copy.deepcopy(config) config_copy = copy.deepcopy(config)
config_copy["instruments"] = instruments config_copy.set_value("instruments", instruments)
config_copy["datafiles"] = datafiles self.pt_mkt_data_ = ResearchMarketData(config=config_copy, instruments=instruments)
self.pt_mkt_data_ = ResearchMarketData(config=config_copy)
self.pt_mkt_data_.load() self.pt_mkt_data_.load()
self.model_data_policy_ = ModelDataPolicy.create( self.model_data_policy_ = ModelDataPolicy.create(
Config(config_copy), mkt_data=self.pt_mkt_data_.market_data_df_, pair=self.trading_pair_ config_copy, mkt_data=self.pt_mkt_data_.market_data_df_, pair=self.trading_pair_
) )
def outstanding_positions(self) -> List[Dict[str, Any]]: def outstanding_positions(self) -> List[Dict[str, Any]]:
return list(self.trading_pair_.user_data_.get("outstanding_positions", [])) return list(self.trading_pair_.user_data_.get("outstanding_positions", []))
def run(self) -> None: def run(self) -> None:
training_minutes = self.config_.get("training_minutes", 120) training_minutes = self.config_.get_value("training_minutes", 120)
market_data_series: pd.Series market_data_series: pd.Series
market_data_df = pd.DataFrame() market_data_df = pd.DataFrame()
@ -74,8 +74,8 @@ class PtResearchStrategy:
prediction = self.trading_pair_.run( prediction = self.trading_pair_.run(
market_data_df, self.model_data_policy_.advance(mkt_data_df=market_data_df) market_data_df, self.model_data_policy_.advance(mkt_data_df=market_data_df)
) )
self.predictions_ = pd.concat( self.predictions_df_ = pd.concat(
[self.predictions_, prediction.to_df()], ignore_index=True [self.predictions_df_, prediction.to_df()], ignore_index=True
) )
assert prediction is not None assert prediction is not None
@ -95,8 +95,8 @@ class PtResearchStrategy:
pair = self.trading_pair_ pair = self.trading_pair_
trades = None trades = None
open_threshold = self.config_["dis-equilibrium_open_trshld"] open_threshold = self.config_.get_value("dis-equilibrium_open_trshld")
close_threshold = self.config_["dis-equilibrium_close_trshld"] close_threshold = self.config_.get_value("dis-equilibrium_close_trshld")
scaled_disequilibrium = prediction.scaled_disequilibrium_ scaled_disequilibrium = prediction.scaled_disequilibrium_
abs_scaled_disequilibrium = abs(scaled_disequilibrium) abs_scaled_disequilibrium = abs(scaled_disequilibrium)
@ -145,7 +145,7 @@ class PtResearchStrategy:
if pair.user_data_["state"] == PairState.OPEN: if pair.user_data_["state"] == PairState.OPEN:
print(f"{pair}: *** Position is NOT CLOSED. ***") print(f"{pair}: *** Position is NOT CLOSED. ***")
# outstanding positions # outstanding positions
if self.config_["close_outstanding_positions"]: if self.config_.get_value("close_outstanding_positions", False):
close_position_row = pd.Series(pair.market_data_.iloc[-2]) close_position_row = pd.Series(pair.market_data_.iloc[-2])
# close_position_row["disequilibrium"] = 0.0 # close_position_row["disequilibrium"] = 0.0
# close_position_row["scaled_disequilibrium"] = 0.0 # close_position_row["scaled_disequilibrium"] = 0.0
@ -161,14 +161,14 @@ class PtResearchStrategy:
pair.on_close_trades(trades) pair.on_close_trades(trades)
else: else:
pair.add_outstanding_position( pair.add_outstanding_position(
symbol=pair.symbol_a_, symbol=pair.symbol_a(),
open_side=pair.user_data_["open_side_a"], open_side=pair.user_data_["open_side_a"],
open_px=pair.user_data_["open_px_a"], open_px=pair.user_data_["open_px_a"],
open_tstamp=pair.user_data_["open_tstamp"], open_tstamp=pair.user_data_["open_tstamp"],
last_mkt_data_row=pair.market_data_.iloc[-1], last_mkt_data_row=pair.market_data_.iloc[-1],
) )
pair.add_outstanding_position( pair.add_outstanding_position(
symbol=pair.symbol_b_, symbol=pair.symbol_b(),
open_side=pair.user_data_["open_side_b"], open_side=pair.user_data_["open_side_b"],
open_px=pair.user_data_["open_px_b"], open_px=pair.user_data_["open_px_b"],
open_tstamp=pair.user_data_["open_tstamp"], open_tstamp=pair.user_data_["open_tstamp"],
@ -192,7 +192,7 @@ class PtResearchStrategy:
return pd.DataFrame(columns=columns).astype(types) return pd.DataFrame(columns=columns).astype(types)
def _create_open_trades( def _create_open_trades(
self, pair: TradingPair, row: pd.Series, prediction: Prediction self, pair: ResearchTradingPair, row: pd.Series, prediction: Prediction
) -> Optional[pd.DataFrame]: ) -> Optional[pd.DataFrame]:
colname_a, colname_b = pair.exec_prices_colnames() colname_a, colname_b = pair.exec_prices_colnames()
@ -226,7 +226,7 @@ class PtResearchStrategy:
# create opening trades # create opening trades
df.loc[len(df)] = { df.loc[len(df)] = {
"time": tstamp, "time": tstamp,
"symbol": pair.symbol_a_, "symbol": pair.symbol_a(),
"side": side_a, "side": side_a,
"action": "OPEN", "action": "OPEN",
"price": px_a, "price": px_a,
@ -237,7 +237,7 @@ class PtResearchStrategy:
} }
df.loc[len(df)] = { df.loc[len(df)] = {
"time": tstamp, "time": tstamp,
"symbol": pair.symbol_b_, "symbol": pair.symbol_b(),
"side": side_b, "side": side_b,
"action": "OPEN", "action": "OPEN",
"price": px_b, "price": px_b,
@ -249,7 +249,7 @@ class PtResearchStrategy:
return df return df
def _create_close_trades( def _create_close_trades(
self, pair: TradingPair, row: pd.Series, prediction: Optional[Prediction] = None self, pair: ResearchTradingPair, row: pd.Series, prediction: Optional[Prediction] = None
) -> Optional[pd.DataFrame]: ) -> Optional[pd.DataFrame]:
colname_a, colname_b = pair.exec_prices_colnames() colname_a, colname_b = pair.exec_prices_colnames()
@ -271,7 +271,7 @@ class PtResearchStrategy:
# create opening trades # create opening trades
df.loc[len(df)] = { df.loc[len(df)] = {
"time": tstamp, "time": tstamp,
"symbol": pair.symbol_a_, "symbol": pair.symbol_a(),
"side": pair.user_data_["close_side_a"], "side": pair.user_data_["close_side_a"],
"action": "CLOSE", "action": "CLOSE",
"price": px_a, "price": px_a,
@ -282,7 +282,7 @@ class PtResearchStrategy:
} }
df.loc[len(df)] = { df.loc[len(df)] = {
"time": tstamp, "time": tstamp,
"symbol": pair.symbol_b_, "symbol": pair.symbol_b(),
"side": pair.user_data_["close_side_b"], "side": pair.user_data_["close_side_b"],
"action": "CLOSE", "action": "CLOSE",
"price": px_b, "price": px_b,

View File

@ -4,6 +4,8 @@ from datetime import date, datetime
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
import pandas as pd import pandas as pd
from cvttpy_trading.trading.instrument import ExchangeInstrument
from pairs_trading.lib.pt_strategy.trading_pair import TradingPair from pairs_trading.lib.pt_strategy.trading_pair import TradingPair
@ -120,7 +122,7 @@ def store_config_in_database(
config_file_path: str, config_file_path: str,
config: Dict, config: Dict,
datafiles: List[Tuple[str, str]], datafiles: List[Tuple[str, str]],
instruments: List[Dict[str, str]], instruments: List[ExchangeInstrument],
) -> None: ) -> None:
""" """
Store configuration information in the database for reference. Store configuration information in the database for reference.
@ -141,7 +143,7 @@ def store_config_in_database(
datafiles_str = ", ".join([f"{datafile}" for _, datafile in datafiles]) datafiles_str = ", ".join([f"{datafile}" for _, datafile in datafiles])
instruments_str = ", ".join( instruments_str = ", ".join(
[ [
f"{inst['symbol']}:{inst['instrument_type']}:{inst['exchange_id']}" inst.details_short()
for inst in instruments for inst in instruments
] ]
) )
@ -292,7 +294,7 @@ class PairResearchResult:
pair_return = symbol_a_return + symbol_b_return pair_return = symbol_a_return + symbol_b_return
# Create round-trip records for both symbols # Create round-trip records for both symbols
funding_per_position = self.config_.get("funding_per_pair", 10000) / 2 funding_per_position = self.config_.get_value("funding_per_pair", 10000) / 2
# Symbol A round-trip # Symbol A round-trip
day_roundtrips.append({ day_roundtrips.append({

View File

@ -1,17 +1,21 @@
from __future__ import annotations from __future__ import annotations
from abc import ABC, abstractmethod
from datetime import datetime from datetime import datetime
from enum import Enum from enum import Enum
from typing import Any, Dict, List from typing import Any, Dict, List
import pandas as pd import pandas as pd
# ---
from cvttpy_tools.base import NamedObject
from cvttpy_tools.config import Config
# --- # ---
from cvttpy_trading.trading.instrument import ExchangeInstrument from cvttpy_trading.trading.instrument import ExchangeInstrument
# --- # ---
from pairs_trading.lib.pt_strategy.model_data_policy import DataWindowParams from pairs_trading.lib.pt_strategy.model_data_policy import DataWindowParams
from pairs_trading.lib.pt_strategy.prediction import Prediction from pairs_trading.lib.pt_strategy.prediction import Prediction
from pairs_trading.lib.pt_strategy.models import PairsTradingModel
class PairState(Enum): class PairState(Enum):
@ -23,62 +27,90 @@ class PairState(Enum):
CLOSE_STOP_PROFIT = 6 CLOSE_STOP_PROFIT = 6
def get_symbol(instrument: Dict[str, str]) -> str: # def get_symbol(instrument: Dict[str, str]) -> str:
if "symbol" in instrument: # if "symbol" in instrument:
return instrument["symbol"] # return instrument["symbol"]
elif "instrument_id" in instrument: # elif "instrument_id" in instrument:
instrument_id = instrument["instrument_id"] # instrument_id = instrument["instrument_id"]
instrument_pfx = instrument_id[:instrument_id.find("-") + 1] # instrument_pfx = instrument_id[: instrument_id.find("-") + 1]
symbol = instrument_id[len(instrument_pfx):] # symbol = instrument_id[len(instrument_pfx) :]
instrument["symbol"] = symbol # instrument["symbol"] = symbol
instrument["instrument_id_pfx"] = instrument_pfx # instrument["instrument_id_pfx"] = instrument_pfx
return symbol # return symbol
else: # else:
raise ValueError(f"Invalid instrument: {instrument}, missing symbol or instrument_id") # raise ValueError(
# f"Invalid instrument: {instrument}, missing symbol or instrument_id"
# )
class TradingPair:
config_: Dict[str, Any] class TradingPair(NamedObject, ABC):
config_: Config
model_: Any # "PairsTradingModel"
market_data_: pd.DataFrame market_data_: pd.DataFrame
instruments_: List[Dict[str, str]]
symbol_a_: str
symbol_b_: str
stat_model_price_: str
model_: PairsTradingModel # type: ignore[assignment]
user_data_: Dict[str, Any]
exch_inst_a_: ExchangeInstrument user_data_: Dict[str, Any]
exch_inst_b_: ExchangeInstrument stat_model_price_: str
instruments_: List[ExchangeInstrument]
def __init__( def __init__(
self, self,
config: Dict[str, Any], config: Config,
instruments: List[Dict[str, str]], instruments: List[ExchangeInstrument],
): ):
from pairs_trading.lib.pt_strategy.pt_model import PairsTradingModel from pairs_trading.lib.pt_strategy.pt_model import PairsTradingModel
assert len(instruments) == 2, "Trading pair must have exactly 2 instruments"
self.config_ = config
self.instruments_ = instruments
self.symbol_a_ = get_symbol(instruments[0])
self.symbol_b_ = get_symbol(instruments[1])
self.model_ = PairsTradingModel.create(config)
self.stat_model_price_ = config["stat_model_price"]
self.user_data_ = {
"state": PairState.INITIAL,
}
self.config_ = config
self.model_ = PairsTradingModel.create(config)
self.user_data_ = {}
self.instruments_ = instruments
self.instruments_[0].user_data_["symbol"] = instruments[0].instrument_id().split("-", 1)[1]
self.instruments_[1].user_data_["symbol"] = instruments[1].instrument_id().split("-", 1)[1]
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
f"{self.__class__.__name__}:" f"{self.__class__.__name__}:"
f" symbol_a={self.symbol_a_}," f" symbol_a={self.symbol_a()},"
f" symbol_b={self.symbol_b_}," f" symbol_b={self.symbol_b()},"
f" model={self.model_.__class__.__name__}" f" model={self.model_.__class__.__name__}"
) )
def colnames(self) -> List[str]:
return [
f"{self.stat_model_price_}_{self.symbol_a()}",
f"{self.stat_model_price_}_{self.symbol_b()}",
]
def symbol_a(self) -> str:
return self.get_instrument_a().user_data_["symbol"]
def symbol_b(self) -> str:
return self.get_instrument_b().user_data_["symbol"]
def get_instrument_a(self) -> ExchangeInstrument:
return self.instruments_[0]
def get_instrument_b(self) -> ExchangeInstrument:
return self.instruments_[1]
class ResearchTradingPair(TradingPair):
def __init__(
self,
config: Config,
instruments: List[ExchangeInstrument],
):
assert len(instruments) == 2, "Trading pair must have exactly 2 instruments"
super().__init__(config=config, instruments=instruments)
self.stat_model_price_ = config.get_value("stat_model_price")
self.user_data_ = {
"state": PairState.INITIAL,
}
# URGENT set exchange instruments for the pair
def is_closed(self) -> bool: def is_closed(self) -> bool:
return self.user_data_["state"] in [ return self.user_data_["state"] in [
PairState.CLOSE, PairState.CLOSE,
@ -86,39 +118,34 @@ class TradingPair:
PairState.CLOSE_STOP_LOSS, PairState.CLOSE_STOP_LOSS,
PairState.CLOSE_STOP_PROFIT, PairState.CLOSE_STOP_PROFIT,
] ]
def is_open(self) -> bool: def is_open(self) -> bool:
return self.user_data_["state"] == PairState.OPEN return not self.is_closed()
def colnames(self) -> List[str]:
return [
f"{self.stat_model_price_}_{self.symbol_a_}",
f"{self.stat_model_price_}_{self.symbol_b_}",
]
def exec_prices_colnames(self) -> List[str]: def exec_prices_colnames(self) -> List[str]:
return [ return [
f"exec_price_{self.symbol_a_}", f"exec_price_{self.symbol_a()}",
f"exec_price_{self.symbol_b_}", f"exec_price_{self.symbol_b()}",
] ]
def to_stop_close_conditions(self, predicted_row: pd.Series) -> bool: def to_stop_close_conditions(self, predicted_row: pd.Series) -> bool:
config = self.config_ config = self.config_
if ( if (
"stop_close_conditions" not in config not config.key_exists("stop_close_conditions")
or config["stop_close_conditions"] is None or config.get_value("stop_close_conditions") is None
): ):
return False return False
if "profit" in config["stop_close_conditions"]: if "profit" in config.get_value("stop_close_conditions"):
current_return = self._current_return(predicted_row) current_return = self._current_return(predicted_row)
# #
# print(f"time={predicted_row['tstamp']} current_return={current_return}") # print(f"time={predicted_row['tstamp']} current_return={current_return}")
# #
if current_return >= config["stop_close_conditions"]["profit"]: if current_return >= config.get_value("stop_close_conditions")["profit"]:
print(f"STOP PROFIT: {current_return}") print(f"STOP PROFIT: {current_return}")
self.user_data_["stop_close_state"] = PairState.CLOSE_STOP_PROFIT self.user_data_["stop_close_state"] = PairState.CLOSE_STOP_PROFIT
return True return True
if "loss" in config["stop_close_conditions"]: if "loss" in config.get_value("stop_close_conditions"):
if current_return <= config["stop_close_conditions"]["loss"]: if current_return <= config.get_value("stop_close_conditions")["loss"]:
print(f"STOP LOSS: {current_return}") print(f"STOP LOSS: {current_return}")
self.user_data_["stop_close_state"] = PairState.CLOSE_STOP_LOSS self.user_data_["stop_close_state"] = PairState.CLOSE_STOP_LOSS
return True return True
@ -143,8 +170,8 @@ class TradingPair:
) )
return float(instrument_return) * 100.0 return float(instrument_return) * 100.0
instrument_a_return = _single_instrument_return(self.symbol_a_) instrument_a_return = _single_instrument_return(self.symbol_a())
instrument_b_return = _single_instrument_return(self.symbol_b_) instrument_b_return = _single_instrument_return(self.symbol_b())
return instrument_a_return + instrument_b_return return instrument_a_return + instrument_b_return
return 0.0 return 0.0
@ -165,46 +192,56 @@ class TradingPair:
open_tstamp: datetime, open_tstamp: datetime,
last_mkt_data_row: pd.Series, last_mkt_data_row: pd.Series,
) -> None: ) -> None:
assert symbol in [self.symbol_a_, self.symbol_b_], "Symbol must be one of the pair's symbols" assert symbol in [
self.symbol_a(),
self.symbol_b(),
], "Symbol must be one of the pair's symbols"
assert open_side in ["BUY", "SELL"], "Open side must be either BUY or SELL" assert open_side in ["BUY", "SELL"], "Open side must be either BUY or SELL"
assert open_px > 0, "Open price must be greater than 0" assert open_px > 0, "Open price must be greater than 0"
assert open_tstamp is not None, "Open timestamp must be provided" assert open_tstamp is not None, "Open timestamp must be provided"
assert last_mkt_data_row is not None, "Last market data row must be provided" assert last_mkt_data_row is not None, "Last market data row must be provided"
exec_prices_col_a, exec_prices_col_b = self.exec_prices_colnames() exec_prices_col_a, exec_prices_col_b = self.exec_prices_colnames()
if symbol == self.symbol_a_: if symbol == self.symbol_a():
last_px = last_mkt_data_row[exec_prices_col_a] last_px = last_mkt_data_row[exec_prices_col_a]
else: else:
last_px = last_mkt_data_row[exec_prices_col_b] last_px = last_mkt_data_row[exec_prices_col_b]
funding_per_position = self.config_["funding_per_pair"] / 2 funding_per_position = self.config_.get_value("funding_per_pair") / 2
shares = funding_per_position / open_px shares = funding_per_position / open_px
if open_side == "SELL": if open_side == "SELL":
shares = -shares shares = -shares
if "outstanding_positions" not in self.user_data_: if "outstanding_positions" not in self.user_data_:
self.user_data_["outstanding_positions"] = [] self.user_data_["outstanding_positions"] = []
self.user_data_["outstanding_positions"].append({ self.user_data_["outstanding_positions"].append(
"symbol": symbol, {
"open_side": open_side, "symbol": symbol,
"open_px": open_px, "open_side": open_side,
"shares": shares, "open_px": open_px,
"open_tstamp": open_tstamp, "shares": shares,
"last_px": last_px, "open_tstamp": open_tstamp,
"last_tstamp": last_mkt_data_row["tstamp"], "last_px": last_px,
"last_value": last_px * shares, "last_tstamp": last_mkt_data_row["tstamp"],
}) "last_value": last_px * shares,
}
)
def get_instrument_a(self) -> ExchangeInstrument: def run(self, market_data: pd.DataFrame, data_params: DataWindowParams) -> Prediction: # type: ignore[assignment]
return self.exch_inst_a_ self.market_data_ = market_data[
def get_instrument_b(self) -> ExchangeInstrument: data_params.training_start_index_ : data_params.training_start_index_
return self.exch_inst_b_ + data_params.training_size_
]
def run(self, market_data: pd.DataFrame, data_params: DataWindowParams) -> Prediction: # type: ignore[assignment]
self.market_data_ = market_data[data_params.training_start_index:data_params.training_start_index + data_params.training_size]
return self.model_.predict(pair=self) return self.model_.predict(pair=self)
class LiveTradingPair(TradingPair):
def __init__(self, config: Config, instruments: List[ExchangeInstrument]):
super().__init__(config, instruments)
def to_stop_close_conditions(self, predicted_row: pd.Series) -> bool:
# TODO LiveTradingPair.to_stop_close_conditions()
return False

View File

@ -1,12 +1,12 @@
import hjson import hjson
from typing import Dict from typing import Dict
from datetime import datetime from datetime import datetime
# ---
from cvttpy_tools.config import Config
def load_config(config_path: str) -> Dict: def load_config(config_path: str) -> Config:
with open(config_path, "r") as f: return Config(json_src=f"file://{config_path}")
config = hjson.load(f)
return dict(config)
def expand_filename(filename: str) -> str: def expand_filename(filename: str) -> str:

View File

@ -1,9 +1,10 @@
from __future__ import annotations from __future__ import annotations
import sqlite3 import sqlite3
from typing import Dict, List, cast from typing import Any, Dict, List, Tuple, cast
import pandas as pd import pandas as pd
from cvttpy_trading.trading.instrument import ExchangeInstrument
def load_sqlite_to_dataframe(db_path:str, query:str) -> pd.DataFrame: def load_sqlite_to_dataframe(db_path:str, query:str) -> pd.DataFrame:
df: pd.DataFrame = pd.DataFrame() df: pd.DataFrame = pd.DataFrame()
@ -45,19 +46,17 @@ def convert_time_to_UTC(value: str, timezone: str, extra_minutes: int = 0) -> st
def load_market_data( def load_market_data(
datafile: str, datafile: str,
instruments: List[Dict[str, str]], instruments: List[ExchangeInstrument],
db_table_name: str, db_table_name: str,
trading_hours: Dict = {}, trading_hours: Dict = {},
extra_minutes: int = 0, extra_minutes: int = 0,
) -> pd.DataFrame: ) -> pd.DataFrame:
insts = [
'"' + instrument["instrument_id_pfx"] + instrument["symbol"] + '"' inst_ids = ['"' + exch_inst.instrument_id() + '"' for exch_inst in instruments]
for instrument in instruments instrument_ids = list(set(inst_ids))
]
instrument_ids = list(set(insts))
exchange_ids = list( exchange_ids = list(
set(['"' + instrument["exchange_id"] + '"' for instrument in instruments]) set(['"' + instrument.exchange_id() + '"' for instrument in instruments])
) )
query = "select" query = "select"

View File

@ -1,17 +1,21 @@
import os import os
import glob import glob
from typing import Dict, List, Tuple from typing import Dict, List, Tuple
# ---
from cvttpy_tools.config import CvttAppConfig
# ---
from cvttpy_trading.trading.instrument import ExchangeInstrument
DayT = str DayT = str
DataFileNameT = str DataFileNameT = str
def resolve_datafiles( def resolve_datafiles(
config: Dict, date_pattern: str, instruments: List[Dict[str, str]] config: Dict, date_pattern: str, instruments: List[ExchangeInstrument]
) -> List[Tuple[DayT, DataFileNameT]]: ) -> List[Tuple[DayT, DataFileNameT]]:
resolved_files: List[Tuple[DayT, DataFileNameT]] = [] resolved_files: List[Tuple[DayT, DataFileNameT]] = []
for inst in instruments: for exch_inst in instruments:
pattern = date_pattern pattern = date_pattern
inst_type = inst["instrument_type"] inst_type = exch_inst.user_data_.get("instrument_type", "?instrument_type?")
data_dir = config["market_data_loading"][inst_type]["data_directory"] data_dir = config["market_data_loading"][inst_type]["data_directory"]
if "*" in pattern or "?" in pattern: if "*" in pattern or "?" in pattern:
# Handle wildcards # Handle wildcards

View File

@ -1,21 +0,0 @@
import argparse
from typing import Dict, List
def get_instruments(args: argparse.Namespace, config: Dict) -> List[Dict[str, str]]:
instruments = [
{
"symbol": inst.split(":")[0],
"instrument_type": inst.split(":")[1],
"exchange_id": inst.split(":")[2],
"instrument_id_pfx": config["market_data_loading"][inst.split(":")[1]][
"instrument_id_pfx"
],
"db_table_name": config["market_data_loading"][inst.split(":")[1]][
"db_table_name"
],
}
for inst in args.instruments.split(",")
]
return instruments

View File

@ -8,8 +8,8 @@ def visualize_prices(strategy: PtResearchStrategy, trading_date: str) -> None:
import seaborn as sns import seaborn as sns
pair = strategy.trading_pair_ pair = strategy.trading_pair_
SYMBOL_A = pair.symbol_a_ SYMBOL_A = pair.symbol_a()
SYMBOL_B = pair.symbol_b_ SYMBOL_B = pair.symbol_b()
TRD_DATE = f"{trading_date[0:4]}-{trading_date[4:6]}-{trading_date[6:8]}" TRD_DATE = f"{trading_date[0:4]}-{trading_date[4:6]}-{trading_date[6:8]}"
plt.style.use('seaborn-v0_8') plt.style.use('seaborn-v0_8')

View File

@ -1,13 +1,8 @@
from __future__ import annotations from __future__ import annotations
import os
from typing import Any, Dict
from pairs_trading.lib.pairs_trading.lib.tegy.results import (PairResearchResult, create_result_database, from pairs_trading.lib.pt_strategy.results import (PairResearchResult)
store_config_in_database) from pairs_trading.lib.pt_strategy.research_strategy import PtResearchStrategy
from pairs_trading.lib.pairs_trading.lib.t_strategy.research_strategy import PtResearchStrategy
from pairs_trading.lib.tools.filetools import resolve_datafiles
from pairs_trading.lib.tools.instruments import get_instruments
def visualize_trades(strategy: PtResearchStrategy, results: PairResearchResult, trading_date: str) -> None: def visualize_trades(strategy: PtResearchStrategy, results: PairResearchResult, trading_date: str) -> None:
@ -25,8 +20,8 @@ def visualize_trades(strategy: PtResearchStrategy, results: PairResearchResult,
origin_mkt_data_df = strategy.pt_mkt_data_.origin_mkt_data_df_ origin_mkt_data_df = strategy.pt_mkt_data_.origin_mkt_data_df_
mkt_data_df = strategy.pt_mkt_data_.market_data_df_ mkt_data_df = strategy.pt_mkt_data_.market_data_df_
TRD_DATE = f"{trading_date[0:4]}-{trading_date[4:6]}-{trading_date[6:8]}" TRD_DATE = f"{trading_date[0:4]}-{trading_date[4:6]}-{trading_date[6:8]}"
SYMBOL_A = pair.symbol_a_ SYMBOL_A = pair.symbol_a()
SYMBOL_B = pair.symbol_b_ SYMBOL_B = pair.symbol_b()
print(f"\nCreated trading pair: {pair}") print(f"\nCreated trading pair: {pair}")
@ -51,7 +46,7 @@ def visualize_trades(strategy: PtResearchStrategy, results: PairResearchResult,
timeline_df = pd.DataFrame({'tstamp': all_timestamps}) timeline_df = pd.DataFrame({'tstamp': all_timestamps})
# Merge with predicted data to get dis-equilibrium values # Merge with predicted data to get dis-equilibrium values
timeline_df = timeline_df.merge(strategy.predictions_[['tstamp', 'disequilibrium', 'scaled_disequilibrium', 'signed_scaled_disequilibrium']], timeline_df = timeline_df.merge(strategy.predictions_df_[['tstamp', 'disequilibrium', 'scaled_disequilibrium', 'signed_scaled_disequilibrium']],
on='tstamp', how='left') on='tstamp', how='left')
# Get Symbol_A and Symbol_B market data # Get Symbol_A and Symbol_B market data
@ -110,8 +105,8 @@ def visualize_trades(strategy: PtResearchStrategy, results: PairResearchResult,
type="line", type="line",
x0=timeline_df['tstamp'].min(), x0=timeline_df['tstamp'].min(),
x1=timeline_df['tstamp'].max(), x1=timeline_df['tstamp'].max(),
y0=strategy.config_['dis-equilibrium_open_trshld'], y0=strategy.config_.get_value('dis-equilibrium_open_trshld'),
y1=strategy.config_['dis-equilibrium_open_trshld'], y1=strategy.config_.get_value('dis-equilibrium_open_trshld'),
line=dict(color="purple", width=2, dash="dot"), line=dict(color="purple", width=2, dash="dot"),
opacity=0.7, opacity=0.7,
row=1, col=1 row=1, col=1
@ -121,8 +116,8 @@ def visualize_trades(strategy: PtResearchStrategy, results: PairResearchResult,
type="line", type="line",
x0=timeline_df['tstamp'].min(), x0=timeline_df['tstamp'].min(),
x1=timeline_df['tstamp'].max(), x1=timeline_df['tstamp'].max(),
y0=-strategy.config_['dis-equilibrium_open_trshld'], y0=-strategy.config_.get_value('dis-equilibrium_open_trshld'),
y1=-strategy.config_['dis-equilibrium_open_trshld'], y1=-strategy.config_.get_value('dis-equilibrium_open_trshld'),
line=dict(color="purple", width=2, dash="dot"), line=dict(color="purple", width=2, dash="dot"),
opacity=0.7, opacity=0.7,
row=1, col=1 row=1, col=1
@ -132,8 +127,8 @@ def visualize_trades(strategy: PtResearchStrategy, results: PairResearchResult,
type="line", type="line",
x0=timeline_df['tstamp'].min(), x0=timeline_df['tstamp'].min(),
x1=timeline_df['tstamp'].max(), x1=timeline_df['tstamp'].max(),
y0=strategy.config_['dis-equilibrium_close_trshld'], y0=strategy.config_.get_value('dis-equilibrium_close_trshld'),
y1=strategy.config_['dis-equilibrium_close_trshld'], y1=strategy.config_.get_value('dis-equilibrium_close_trshld'),
line=dict(color="brown", width=2, dash="dot"), line=dict(color="brown", width=2, dash="dot"),
opacity=0.7, opacity=0.7,
row=1, col=1 row=1, col=1
@ -143,8 +138,8 @@ def visualize_trades(strategy: PtResearchStrategy, results: PairResearchResult,
type="line", type="line",
x0=timeline_df['tstamp'].min(), x0=timeline_df['tstamp'].min(),
x1=timeline_df['tstamp'].max(), x1=timeline_df['tstamp'].max(),
y0=-strategy.config_['dis-equilibrium_close_trshld'], y0=-strategy.config_.get_value('dis-equilibrium_close_trshld'),
y1=-strategy.config_['dis-equilibrium_close_trshld'], y1=-strategy.config_.get_value('dis-equilibrium_close_trshld'),
line=dict(color="brown", width=2, dash="dot"), line=dict(color="brown", width=2, dash="dot"),
opacity=0.7, opacity=0.7,
row=1, col=1 row=1, col=1

View File

@ -1,8 +1,18 @@
from __future__ import annotations from __future__ import annotations
import os import os
from typing import Any, Dict from typing import Any, Dict, List, Tuple
# ---
from cvttpy_tools.app import App
from cvttpy_tools.base import NamedObject
from cvttpy_tools.config import CvttAppConfig
# ---
from cvttpy_trading.trading.instrument import ExchangeInstrument
from cvttpy_trading.settings.instruments import Instruments
# ---
from pairs_trading.lib.pt_strategy.results import ( from pairs_trading.lib.pt_strategy.results import (
PairResearchResult, PairResearchResult,
create_result_database, create_result_database,
@ -10,97 +20,120 @@ from pairs_trading.lib.pt_strategy.results import (
) )
from pairs_trading.lib.pt_strategy.research_strategy import PtResearchStrategy from pairs_trading.lib.pt_strategy.research_strategy import PtResearchStrategy
from pairs_trading.lib.tools.filetools import resolve_datafiles from pairs_trading.lib.tools.filetools import resolve_datafiles
from pairs_trading.lib.tools.instruments import get_instruments
InstrumentTypeT = str
def main() -> None: class Runner(NamedObject):
import argparse def __init__(self):
App()
CvttAppConfig()
from pairs_trading.lib.tools.config import expand_filename, load_config # App.instance().add_cmdline_arg(
# "--config", type=str, required=True, help="Path to the configuration file."
# )
App.instance().add_cmdline_arg(
"--date_pattern",
type=str,
required=True,
help="Date YYYYMMDD, allows * and ? wildcards",
)
App.instance().add_cmdline_arg(
"--instruments",
type=str,
required=True,
help="Comma-separated list of instrument symbols (e.g., COIN:EQUITY,GBTC:CRYPTO)",
)
App.instance().add_cmdline_arg(
"--result_db",
type=str,
required=True,
help="Path to SQLite database for storing results. Use 'NONE' to disable database output.",
)
parser = argparse.ArgumentParser(description="Run pairs trading backtest.") App.instance().add_call(stage=App.Stage.Config, func=self._on_config())
parser.add_argument( App.instance().add_call(stage=App.Stage.Run, func=self.run())
"--config", type=str, required=True, help="Path to the configuration file."
)
parser.add_argument(
"--date_pattern",
type=str,
required=True,
help="Date YYYYMMDD, allows * and ? wildcards",
)
parser.add_argument(
"--instruments",
type=str,
required=True,
help="Comma-separated list of instrument symbols (e.g., COIN:EQUITY,GBTC:CRYPTO)",
)
parser.add_argument(
"--result_db",
type=str,
required=True,
help="Path to SQLite database for storing results. Use 'NONE' to disable database output.",
)
args = parser.parse_args() async def _on_config(self) -> None:
# Resolve data files (CLI takes priority over config)
instruments: List[ExchangeInstrument] = self._get_instruments()
datafiles = resolve_datafiles(
config=CvttAppConfig.instance().to_dict(),
date_pattern=App.instance().get_argument("date_pattern"),
instruments=instruments,
)
config: Dict = load_config(args.config) days = list(set([day for day, _ in datafiles]))
print(f"Found {len(datafiles)} data files to process:")
for df in datafiles:
print(f" - {df}")
# Resolve data files (CLI takes priority over config) # Create result database if needed
instruments = get_instruments(args, config) if App.instance().get_argument("result_db").upper() != "NONE":
datafiles = resolve_datafiles(config, args.date_pattern, instruments) create_result_database(App.instance().get_argument("result_db"))
days = list(set([day for day, _ in datafiles])) # Initialize a dictionary to store all trade results
print(f"Found {len(datafiles)} data files to process:") all_results: Dict[str, Dict[str, Any]] = {}
for df in datafiles: is_config_stored = False
print(f" - {df}") # Process each data file
# Create result database if needed results = PairResearchResult(config=CvttAppConfig.instance().to_dict())
if args.result_db.upper() != "NONE": for day in sorted(days):
args.result_db = expand_filename(args.result_db) md_datafiles = [datafile for md_day, datafile in datafiles if md_day == day]
create_result_database(args.result_db) if not all([os.path.exists(datafile) for datafile in md_datafiles]):
print(f"WARNING: insufficient data files: {md_datafiles}")
exit(1)
print(f"\n====== Processing {day} ======")
# Initialize a dictionary to store all trade results if not is_config_stored:
all_results: Dict[str, Dict[str, Any]] = {} store_config_in_database(
is_config_stored = False db_path=App.instance().get_argument("result_db"),
# Process each data file config_file_path=App.instance().get_argument("config"),
config=CvttAppConfig.instance().to_dict(),
datafiles=datafiles,
instruments=instruments,
)
is_config_stored = True
results = PairResearchResult(config=config) CvttAppConfig.instance().set_value("datafiles", md_datafiles)
for day in sorted(days): pt_strategy = PtResearchStrategy(
md_datafiles = [datafile for md_day, datafile in datafiles if md_day == day] config=CvttAppConfig.instance(),
if not all([os.path.exists(datafile) for datafile in md_datafiles]):
print(f"WARNING: insufficient data files: {md_datafiles}")
continue
print(f"\n====== Processing {day} ======")
if not is_config_stored:
store_config_in_database(
db_path=args.result_db,
config_file_path=args.config,
config=config,
datafiles=datafiles,
instruments=instruments, instruments=instruments,
) )
is_config_stored = True pt_strategy.run()
results.add_day_results(
day=day,
trades=pt_strategy.day_trades(),
outstanding_positions=pt_strategy.outstanding_positions(),
)
pt_strategy = PtResearchStrategy( results.analyze_pair_performance()
config=config, datafiles=md_datafiles, instruments=instruments
)
pt_strategy.run()
results.add_day_results(
day=day,
trades=pt_strategy.day_trades(),
outstanding_positions=pt_strategy.outstanding_positions(),
)
def _get_instruments(self) -> List[ExchangeInstrument]:
res: List[ExchangeInstrument] = []
results.analyze_pair_performance() for inst in App.instance().get_argument("instruments").split(","):
instrument_type = inst.split(":")[0]
exchange_id = inst.split(":")[1]
instrument_id = inst.split(":")[2]
exch_inst: ExchangeInstrument = Instruments.instance().get_exch_inst(
exch_id=exchange_id, inst_id=instrument_id, src=f"{self.fname()}"
)
exch_inst.user_data_["instrument_type"] = instrument_type
res.append(exch_inst)
return res
if args.result_db.upper() != "NONE": async def run(self) -> None:
print(f"\nResults stored in database: {args.result_db}")
else: if App.instance().get_argument("result_db").upper() != "NONE":
print("No results to display.") print(
f'\nResults stored in database: {App.instance().get_argument("result_db")}'
)
else:
print("No results to display.")
if __name__ == "__main__": if __name__ == "__main__":
main() Runner()
App.instance().run()

File diff suppressed because one or more lines are too long

View File

@ -1,94 +0,0 @@
import glob
import os
from typing import Dict, List, Optional
import pandas as pd
from pairs_trading.lib.pt_trading.fit_method import PairsTradingFitMethod
def resolve_datafiles(config: Dict, cli_datafiles: Optional[str] = None) -> List[str]:
"""
Resolve the list of data files to process.
CLI datafiles take priority over config datafiles.
Supports wildcards in config but not in CLI.
"""
if cli_datafiles:
# CLI override - comma-separated list, no wildcards
datafiles = [f.strip() for f in cli_datafiles.split(",")]
# Make paths absolute relative to data directory
data_dir = config.get("data_directory", "./data")
resolved_files = []
for df in datafiles:
if not os.path.isabs(df):
df = os.path.join(data_dir, df)
resolved_files.append(df)
return resolved_files
# Use config datafiles with wildcard support
config_datafiles = config.get("datafiles", [])
data_dir = config.get("data_directory", "./data")
resolved_files = []
for pattern in config_datafiles:
if "*" in pattern or "?" in pattern:
# Handle wildcards
if not os.path.isabs(pattern):
pattern = os.path.join(data_dir, pattern)
matched_files = glob.glob(pattern)
resolved_files.extend(matched_files)
else:
# Handle explicit file path
if not os.path.isabs(pattern):
pattern = os.path.join(data_dir, pattern)
resolved_files.append(pattern)
return sorted(list(set(resolved_files))) # Remove duplicates and sort
def create_pairs(
datafiles: List[str],
fit_method: PairsTradingFitMethod,
config: Dict,
instruments: List[Dict[str, str]],
) -> List:
from pt_trading.trading_pair import TradingPair
from tools.data_loader import load_market_data
all_indexes = range(len(instruments))
unique_index_pairs = [(i, j) for i in all_indexes for j in all_indexes if i < j]
pairs = []
# Update config to use the specified instruments
config_copy = config.copy()
config_copy["instruments"] = instruments
market_data_df = pd.DataFrame()
extra_minutes = 0
if "execution_price" in config_copy:
extra_minutes = config_copy["execution_price"]["shift"]
for datafile in datafiles:
md_df = load_market_data(
datafile=datafile,
instruments=instruments,
db_table_name=config_copy["market_data_loading"][instruments[0]["instrument_type"]]["db_table_name"],
trading_hours=config_copy["trading_hours"],
extra_minutes=extra_minutes,
)
market_data_df = pd.concat([market_data_df, md_df])
if len(set(market_data_df["symbol"])) != 2: # both symbols must be present for a pair
print(f"WARNING: insufficient data in files: {datafiles}")
return []
for a_index, b_index in unique_index_pairs:
symbol_a=instruments[a_index]["symbol"]
symbol_b=instruments[b_index]["symbol"]
pair = fit_method.create_trading_pair(
config=config_copy,
market_data=market_data_df,
symbol_a=symbol_a,
symbol_b=symbol_b,
)
pairs.append(pair)
return pairs

View File

@ -1,111 +0,0 @@
from __future__ import annotations
import os
from typing import Any, Dict
from pairs_trading.lib.pt_strategy.results import (PairResearchResult, create_result_database,
store_config_in_database)
from pairs_trading.lib.pt_strategy.research_strategy import PtResearchStrategy
from pairs_trading.lib.tools.filetools import resolve_datafiles
from pairs_trading.lib.tools.instruments import get_instruments
from pairs_trading.lib.tools.viz.viz_trades import visualize_trades
def main() -> None:
import argparse
from pairs_trading.lib.tools.config import expand_filename, load_config
parser = argparse.ArgumentParser(description="Run pairs trading backtest.")
parser.add_argument(
"--config", type=str, required=True, help="Path to the configuration file."
)
parser.add_argument(
"--date_pattern",
type=str,
required=True,
help="Date YYYYMMDD, allows * and ? wildcards",
)
parser.add_argument(
"--instruments",
type=str,
required=True,
help="Comma-separated list of instrument symbols (e.g., COIN:EQUITY,GBTC:CRYPTO)",
)
parser.add_argument(
"--result_db",
type=str,
required=False,
default="NONE",
help="Path to SQLite database for storing results. Use 'NONE' to disable database output.",
)
args = parser.parse_args()
config: Dict = load_config(args.config)
# Resolve data files (CLI takes priority over config)
instruments = get_instruments(args, config)
datafiles = resolve_datafiles(config, args.date_pattern, instruments)
days = list(set([day for day, _ in datafiles]))
print(f"Found {len(datafiles)} data files to process:")
for df in datafiles:
print(f" - {df}")
# Create result database if needed
if args.result_db.upper() != "NONE":
args.result_db = expand_filename(args.result_db)
create_result_database(args.result_db)
# Initialize a dictionary to store all trade results
all_results: Dict[str, Dict[str, Any]] = {}
is_config_stored = False
# Process each data file
results = PairResearchResult(config=config)
for day in sorted(days):
md_datafiles = [datafile for md_day, datafile in datafiles if md_day == day]
if not all([os.path.exists(datafile) for datafile in md_datafiles]):
print(f"WARNING: insufficient data files: {md_datafiles}")
continue
print(f"\n====== Processing {day} ======")
if not is_config_stored:
store_config_in_database(
db_path=args.result_db,
config_file_path=args.config,
config=config,
datafiles=datafiles,
instruments=instruments,
)
is_config_stored = True
pt_strategy = PtResearchStrategy(
config=config, datafiles=md_datafiles, instruments=instruments
)
pt_strategy.run()
results.add_day_results(
day=day,
trades=pt_strategy.day_trades(),
outstanding_positions=pt_strategy.outstanding_positions(),
)
results.analyze_pair_performance()
visualize_trades(pt_strategy, results, day)
if args.result_db.upper() != "NONE":
print(f"\nResults stored in database: {args.result_db}")
else:
print("No results to display.")
if __name__ == "__main__":
main()