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": "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=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)",
"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-opt.cfg",
"--instruments=ADA-USDT:CRYPTO:BNBSPOT,SOL-USDT:CRYPTO:BNBSPOT",
"--date_pattern=20250605",
"--instruments=CRYPTO:BNBSPOT:PAIR-ADA-USDT,CRYPTO:BNBSPOT:PAIR-SOL-USDT",
"--date_pattern=20250910",
"--result_db=${workspaceFolder}/research/results/crypto/%T.vecm-opt.ADA-SOL.20250605.crypto_results.db",
],
"env": {
"PYTHONPATH": "${workspaceFolder}/lib"
"PYTHONPATH": "${workspaceFolder}/..",
"CONFIG_SERVICE": "cloud16.cvtt.vpn:6789"
},
"console": "integratedTerminal"
},
// {
// "name": "CRYPTO VECM (expanding)",
// "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-exp.cfg",
// "--instruments=ADA-USDT:CRYPTO:BNBSPOT,SOL-USDT:CRYPTO:BNBSPOT",
// "--config=${workspaceFolder}/configuration/vecm.cfg",
// "--instruments=COIN:EQUITY:ALPACA,MSTR:EQUITY:ALPACA",
// "--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": {
// "PYTHONPATH": "${workspaceFolder}/lib"
// },
// "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 --------",
},

View File

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

View File

@ -90,8 +90,7 @@ class PairsTrader(NamedObject):
strategy_config = self.config_.get_subconfig("strategy_config", Config({}))
self.live_strategy_ = PtLiveStrategy(
config=strategy_config,
instruments=self.instruments_,
pairs_trader=self
pairs_trader=self,
)
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)
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
async def run(self) -> None:
Log.info(f"{self.fname()} ...")
pass

View File

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

View File

@ -22,11 +22,11 @@
},
"dis-equilibrium_open_trshld": 1.75,
"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": "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.EGOptimizedWndDataPolicy",
# "model_data_policy_class": "pairs_trading.lib.pt_strategy.model_data_policy.ADFOptimizedWndDataPolicy",
"model_data_policy_class": "pairs_trading.lib.pt_strategy.model_data_policy.JohansenOptdWndDataPolicy",
"min_training_size": 60,
"max_training_size": 150,

View File

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

View File

@ -23,11 +23,11 @@
},
"dis-equilibrium_open_trshld": 1.75,
"dis-equilibrium_close_trshld": 1.0,
"model_class": "pt_strategy.models.VECMModel",
"model_class": "pairs_trading.lib.pt_strategy.models.VECMModel",
"training_size": 120,
"model_data_policy_class": "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.RollingWindowDataPolicy",
# "model_data_policy_class": "pairs_trading.lib.pt_strategy.model_data_policy.OptimizedWindowDataPolicy",
# "min_training_size": 60,
# "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": {
"CRYPTO": {
"data_directory": "./data/crypto",
"db_table_name": "md_1min_bars",
"instrument_id_pfx": "PAIR-",
"data_directory": "./data/crypto",
"db_table_name": "md_1min_bars",
"instrument_id_pfx": "PAIR-",
},
"EQUITY": {
"data_directory": "./data/equity",
@ -24,11 +31,11 @@
"dis-equilibrium_open_trshld": 1.75,
"dis-equilibrium_close_trshld": 1.0,
"model_class": "pt_strategy.models.VECMModel",
"model_class": "pairs_trading.lib.pt_strategy.models.VECMModel",
# "training_size": 120,
# "model_data_policy_class": "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.RollingWindowDataPolicy",
"model_data_policy_class": "pairs_trading.lib.pt_strategy.model_data_policy.ADFOptimizedWndDataPolicy",
"min_training_size": 60,
"max_training_size": 150,

View File

@ -10,19 +10,23 @@ import pandas as pd
from cvttpy_tools.base import NamedObject
from cvttpy_tools.app import App
from cvttpy_tools.config import Config
from cvttpy_tools.settings.cvtt_types import IntervalSecT
from cvttpy_tools.timeutils import SecPerHour
from cvttpy_tools.settings.cvtt_types import BookIdT, IntervalSecT
from cvttpy_tools.timeutils import SecPerHour, current_nanoseconds
from cvttpy_tools.logger import Log
# ---
from cvttpy_trading.trading.instrument import ExchangeInstrument
from cvttpy_trading.trading.mkt_data.md_summary import MdTradesAggregate
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.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.lib.pt_strategy.pt_market_data import LiveMarketData
"""
@ -51,7 +55,7 @@ class PtLiveStrategy(NamedObject):
open_threshold_: float
close_threshold_: float
trading_pair_: TradingPair
trading_pair_: LiveTradingPair
model_data_policy_: ModelDataPolicy
pairs_trader_: PairsTrader
@ -60,28 +64,29 @@ class PtLiveStrategy(NamedObject):
# for presentation: history of prediction values and trading signals
predictions_df_: pd.DataFrame
trading_signals_df_: pd.DataFrame
# book_: CvttBook
def __init__(
self,
config: Config,
instruments: List[ExchangeInstrument],
pairs_trader: PairsTrader,
):
self.trading_pair_ = TradingPair(
config=cast(Dict[str, Any], config.data()),
instruments=[{"instrument_id": ei.instrument_id()} for ei in instruments],
self.pairs_trader_ = pairs_trader
self.trading_pair_ = LiveTradingPair(
config=config,
instruments=self.pairs_trader_.instruments_,
)
self.predictions_df_ = pd.DataFrame()
self.trading_signals_df_ = pd.DataFrame()
self.pairs_trader_ = pairs_trader
# self.book_ = book
import copy
# modified config must be passed to PtMarketData
self.config_ = Config(json_src=copy.deepcopy(config.data()))
self.instruments_ = instruments
self.instruments_ = self.pairs_trader_.instruments_
App.instance().add_call(
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()
self.model_data_policy_ = ModelDataPolicy.create(
self.config_, is_real_time=True, pair=self.trading_pair_
)
self.open_threshold_ = self.config_.get_value(
"dis-equilibrium_open_trshld", 0.0
)
@ -121,13 +123,22 @@ class PtLiveStrategy(NamedObject):
if not self._is_md_actual(hist_aggr=hist_aggr):
return
market_data_df: Optional[pd.DataFrame] = self._create_md_pdf(hist_aggr=hist_aggr)
if market_data_df is None:
market_data_df: pd.DataFrame = self._create_md_df(hist_aggr=hist_aggr)
if len(market_data_df) == 0:
Log.warning(f"{self.fname()} Unable to create market data df")
return
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(
market_data_df, self.model_data_policy_.advance()
)
@ -135,7 +146,7 @@ class PtLiveStrategy(NamedObject):
[self.predictions_df_, prediction.to_df()], ignore_index=True
)
trading_instructions: Optional[TradingInstructions] = (
trading_instructions: List[TradingInstructions] = (
self._create_trading_instructions(
prediction=prediction, last_row=market_data_df.iloc[-1]
)
@ -144,10 +155,74 @@ class PtLiveStrategy(NamedObject):
await self._send_trading_instructions(trading_instructions)
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]:
return None # URGENT _create_md_pdf
def _create_md_df(self, hist_aggr: List[MdTradesAggregate]) -> pd.DataFrame:
"""
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:
return self.interval_sec_
@ -156,271 +231,110 @@ class PtLiveStrategy(NamedObject):
return self.history_depth_sec_
async def _send_trading_instructions(
self, trading_instructions: TradingInstructions
self, trading_instructions: List[TradingInstructions]
) -> None:
await self.pairs_trader_.ti_sender_.send_trading_instructions(trading_instructions)
pass # URGENT _send_trading_instructions
for ti in 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(
self, prediction: Prediction, last_row: pd.Series
) -> Optional[TradingInstructions]:
) -> List[TradingInstructions]:
trd_instructions: List[TradingInstructions] = []
pair = self.trading_pair_
res: Optional[TradingInstructions]
scaled_disequilibrium = prediction.scaled_disequilibrium_
abs_scaled_disequilibrium = abs(scaled_disequilibrium)
if pair.is_closed():
if abs_scaled_disequilibrium >= self.open_threshold_:
trd_instructions = self._create_open_trade_instructions(
pair, row=last_row, prediction=prediction
)
elif pair.is_open():
if abs_scaled_disequilibrium <= self.close_threshold_:
trd_instructions = self._create_close_trade_instructions(
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
)
if abs_scaled_disequilibrium >= self.open_threshold_:
trd_instructions = self._create_open_trade_instructions(
pair, row=last_row, prediction=prediction
)
elif abs_scaled_disequilibrium <= self.close_threshold_ or pair.to_stop_close_conditions(predicted_row=last_row):
trd_instructions = self._create_close_trade_instructions(
pair, row=last_row # , prediction=prediction
)
return trd_instructions
def _strength(self, scaled_disequilibrium) -> float:
# URGENT PtLiveStrategy._strength()
return 1.0
def _create_open_trade_instructions(
self, pair: TradingPair, row: pd.Series, prediction: Prediction
) -> Optional[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"]
self, pair: LiveTradingPair, row: pd.Series, prediction: Prediction
) -> List[TradingInstructions]:
diseqlbrm = prediction.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:
side_a = "SELL"
side_b = "BUY"
side_a = -1
side_b = 1
else:
side_a = "BUY"
side_b = "SELL"
side_a = 1
side_b = -1
# 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
ti_a: 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_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
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,
}
return df
def _create_close_trades(
self, pair: TradingPair, row: pd.Series, prediction: Optional[Prediction] = None
) -> Optional[pd.DataFrame]:
colname_a, colname_b = pair.exec_prices_colnames()
tstamp = row["tstamp"]
if prediction is not None:
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
def _create_close_trade_instructions(
self, pair: LiveTradingPair, row: pd.Series
) -> List[TradingInstructions]:
ti_a: 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=0,
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=0,
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]

View File

@ -12,8 +12,8 @@ from cvttpy_tools.config import Config
@dataclass
class DataWindowParams:
training_size: int
training_start_index: int
training_size_: int
training_start_index_: int
class ModelDataPolicy(ABC):
@ -24,16 +24,9 @@ class ModelDataPolicy(ABC):
def __init__(self, config: Config, *args: Any, **kwargs: Any):
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(
training_size=config.get_value("training_size", 120),
training_start_index=0,
training_size_=config.get_value("training_size", 120),
training_start_index_=0,
)
self.count_ = 0
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:
super().advance(mkt_data_df)
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:
self.current_data_params_.training_start_index += 1
self.current_data_params_.training_start_index_ += 1
return self.current_data_params_
@ -111,12 +104,12 @@ class OptimizedWndDataPolicy(ModelDataPolicy, ABC):
if self.is_real_time_:
self.end_index_ = len(self.mkt_data_df_) - 1
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:
self.end_index_ = len(self.mkt_data_df_) - 1
self.current_data_params_.training_start_index = self.end_index_ - self.max_training_size_
if self.current_data_params_.training_start_index < 0:
self.current_data_params_.training_start_index = 0
self.current_data_params_.training_start_index_ = self.end_index_ - self.max_training_size_
if self.current_data_params_.training_start_index_ < 0:
self.current_data_params_.training_start_index_ = 0
col_a, col_b = self.pair_.colnames()
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])
if eg_pvalue < last_pvalue:
last_pvalue = eg_pvalue
result.training_size = trn_size
result.training_start_index = start_index
result.training_size_ = trn_size
result.training_start_index_ = start_index
# print(
# 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:
last_pvalue = adf_pvalue
result.training_size = trn_size
result.training_start_index = start_index
result.training_size_ = trn_size
result.training_start_index_ = start_index
# print(
# f"*** DEBUG *** end_index={self.end_index_},"
@ -247,8 +240,8 @@ class JohansenOptdWndDataPolicy(OptimizedWndDataPolicy):
continue
if best_trn_size > 0:
result.training_size = best_trn_size
result.training_start_index = best_start_index
result.training_size_ = best_trn_size
result.training_start_index_ = best_start_index
else:
print("*** WARNING: No valid cointegration window found.")

View File

@ -1,157 +1,61 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional
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_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
class PtMarketData():
config_: Dict[str, Any]
class PtMarketData(NamedObject, ABC):
config_: Config
origin_mkt_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.origin_mkt_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):
current_index_: int
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
@abstractmethod
def md_columns(self) -> List[str]: ...
def has_next(self) -> bool:
return self.current_index_ < len(self.market_data_df_)
@abstractmethod
def rename_columns(self, symbol_df: pd.DataFrame) -> pd.DataFrame: ...
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("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"]
@abstractmethod
def tranform_df_target_colnames(self) -> List[str]: ...
extra_minutes: int
extra_minutes = self.execution_price_shift_
for datafile in datafiles:
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()]
)
def set_market_data(self) -> None:
self.market_data_df_ = pd.DataFrame(
self._transform_dataframe(self.origin_mkt_data_df_)[
["tstamp"] + self.tranform_df_target_colnames()
]
)
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_ = 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]:
return [
@ -159,15 +63,161 @@ class ResearchMarketData(PtMarketData):
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]:
return [
f"{self.execution_price_column_}_{self.symbol_a_}",
f"{self.execution_price_column_}_{self.symbol_b_}",
]
] if self.is_execution_price_ else []
def exec_prices_colnames(self) -> List[str]:
return [
f"exec_price_{self.symbol_a_}",
f"exec_price_{self.symbol_b_}",
]
class LiveMarketData(PtMarketData):
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 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.trading_pair import TradingPair
@ -13,10 +16,10 @@ class PairsTradingModel(ABC):
...
@staticmethod
def create(config: Dict[str, Any]) -> PairsTradingModel:
def create(config: Config) -> PairsTradingModel:
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
module_name, class_name = model_class_name.rsplit(".", 1)
module = importlib.import_module(module_name)

View File

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

View File

@ -4,6 +4,8 @@ from datetime import date, datetime
from typing import Any, Dict, List, Optional, Tuple
import pandas as pd
from cvttpy_trading.trading.instrument import ExchangeInstrument
from pairs_trading.lib.pt_strategy.trading_pair import TradingPair
@ -120,7 +122,7 @@ def store_config_in_database(
config_file_path: str,
config: Dict,
datafiles: List[Tuple[str, str]],
instruments: List[Dict[str, str]],
instruments: List[ExchangeInstrument],
) -> None:
"""
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])
instruments_str = ", ".join(
[
f"{inst['symbol']}:{inst['instrument_type']}:{inst['exchange_id']}"
inst.details_short()
for inst in instruments
]
)
@ -292,7 +294,7 @@ class PairResearchResult:
pair_return = symbol_a_return + symbol_b_return
# 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
day_roundtrips.append({

View File

@ -1,17 +1,21 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from datetime import datetime
from enum import Enum
from typing import Any, Dict, List
import pandas as pd
# ---
from cvttpy_tools.base import NamedObject
from cvttpy_tools.config import Config
# ---
from cvttpy_trading.trading.instrument import ExchangeInstrument
# ---
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.models import PairsTradingModel
class PairState(Enum):
@ -23,62 +27,90 @@ class PairState(Enum):
CLOSE_STOP_PROFIT = 6
def get_symbol(instrument: Dict[str, str]) -> str:
if "symbol" in instrument:
return instrument["symbol"]
elif "instrument_id" in instrument:
instrument_id = instrument["instrument_id"]
instrument_pfx = instrument_id[:instrument_id.find("-") + 1]
symbol = instrument_id[len(instrument_pfx):]
instrument["symbol"] = symbol
instrument["instrument_id_pfx"] = instrument_pfx
return symbol
else:
raise ValueError(f"Invalid instrument: {instrument}, missing symbol or instrument_id")
# def get_symbol(instrument: Dict[str, str]) -> str:
# if "symbol" in instrument:
# return instrument["symbol"]
# elif "instrument_id" in instrument:
# instrument_id = instrument["instrument_id"]
# instrument_pfx = instrument_id[: instrument_id.find("-") + 1]
# symbol = instrument_id[len(instrument_pfx) :]
# instrument["symbol"] = symbol
# instrument["instrument_id_pfx"] = instrument_pfx
# return symbol
# else:
# 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
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
exch_inst_b_: ExchangeInstrument
user_data_: Dict[str, Any]
stat_model_price_: str
instruments_: List[ExchangeInstrument]
def __init__(
self,
config: Dict[str, Any],
instruments: List[Dict[str, str]],
config: Config,
instruments: List[ExchangeInstrument],
):
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:
return (
f"{self.__class__.__name__}:"
f" symbol_a={self.symbol_a_},"
f" symbol_b={self.symbol_b_},"
f" symbol_a={self.symbol_a()},"
f" symbol_b={self.symbol_b()},"
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:
return self.user_data_["state"] in [
PairState.CLOSE,
@ -86,39 +118,34 @@ class TradingPair:
PairState.CLOSE_STOP_LOSS,
PairState.CLOSE_STOP_PROFIT,
]
def is_open(self) -> bool:
return self.user_data_["state"] == PairState.OPEN
def colnames(self) -> List[str]:
return [
f"{self.stat_model_price_}_{self.symbol_a_}",
f"{self.stat_model_price_}_{self.symbol_b_}",
]
return not self.is_closed()
def exec_prices_colnames(self) -> List[str]:
return [
f"exec_price_{self.symbol_a_}",
f"exec_price_{self.symbol_b_}",
f"exec_price_{self.symbol_a()}",
f"exec_price_{self.symbol_b()}",
]
def to_stop_close_conditions(self, predicted_row: pd.Series) -> bool:
config = self.config_
if (
"stop_close_conditions" not in config
or config["stop_close_conditions"] is None
not config.key_exists("stop_close_conditions")
or config.get_value("stop_close_conditions") is None
):
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)
#
# 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}")
self.user_data_["stop_close_state"] = PairState.CLOSE_STOP_PROFIT
return True
if "loss" in config["stop_close_conditions"]:
if current_return <= config["stop_close_conditions"]["loss"]:
if "loss" in config.get_value("stop_close_conditions"):
if current_return <= config.get_value("stop_close_conditions")["loss"]:
print(f"STOP LOSS: {current_return}")
self.user_data_["stop_close_state"] = PairState.CLOSE_STOP_LOSS
return True
@ -143,8 +170,8 @@ class TradingPair:
)
return float(instrument_return) * 100.0
instrument_a_return = _single_instrument_return(self.symbol_a_)
instrument_b_return = _single_instrument_return(self.symbol_b_)
instrument_a_return = _single_instrument_return(self.symbol_a())
instrument_b_return = _single_instrument_return(self.symbol_b())
return instrument_a_return + instrument_b_return
return 0.0
@ -165,46 +192,56 @@ class TradingPair:
open_tstamp: datetime,
last_mkt_data_row: pd.Series,
) -> 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_px > 0, "Open price must be greater than 0"
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"
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]
else:
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
if open_side == "SELL":
shares = -shares
if "outstanding_positions" not in self.user_data_:
self.user_data_["outstanding_positions"] = []
self.user_data_["outstanding_positions"].append({
"symbol": symbol,
"open_side": open_side,
"open_px": open_px,
"shares": shares,
"open_tstamp": open_tstamp,
"last_px": last_px,
"last_tstamp": last_mkt_data_row["tstamp"],
"last_value": last_px * shares,
})
self.user_data_["outstanding_positions"].append(
{
"symbol": symbol,
"open_side": open_side,
"open_px": open_px,
"shares": shares,
"open_tstamp": open_tstamp,
"last_px": last_px,
"last_tstamp": last_mkt_data_row["tstamp"],
"last_value": last_px * shares,
}
)
def get_instrument_a(self) -> ExchangeInstrument:
return self.exch_inst_a_
def get_instrument_b(self) -> ExchangeInstrument:
return self.exch_inst_b_
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]
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)
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
from typing import Dict
from datetime import datetime
# ---
from cvttpy_tools.config import Config
def load_config(config_path: str) -> Dict:
with open(config_path, "r") as f:
config = hjson.load(f)
return dict(config)
def load_config(config_path: str) -> Config:
return Config(json_src=f"file://{config_path}")
def expand_filename(filename: str) -> str:

View File

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

View File

@ -1,17 +1,21 @@
import os
import glob
from typing import Dict, List, Tuple
# ---
from cvttpy_tools.config import CvttAppConfig
# ---
from cvttpy_trading.trading.instrument import ExchangeInstrument
DayT = str
DataFileNameT = str
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]]:
resolved_files: List[Tuple[DayT, DataFileNameT]] = []
for inst in instruments:
for exch_inst in instruments:
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"]
if "*" in pattern or "?" in pattern:
# 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
pair = strategy.trading_pair_
SYMBOL_A = pair.symbol_a_
SYMBOL_B = pair.symbol_b_
SYMBOL_A = pair.symbol_a()
SYMBOL_B = pair.symbol_b()
TRD_DATE = f"{trading_date[0:4]}-{trading_date[4:6]}-{trading_date[6:8]}"
plt.style.use('seaborn-v0_8')

View File

@ -1,13 +1,8 @@
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,
store_config_in_database)
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
from pairs_trading.lib.pt_strategy.results import (PairResearchResult)
from pairs_trading.lib.pt_strategy.research_strategy import PtResearchStrategy
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_
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]}"
SYMBOL_A = pair.symbol_a_
SYMBOL_B = pair.symbol_b_
SYMBOL_A = pair.symbol_a()
SYMBOL_B = pair.symbol_b()
print(f"\nCreated trading pair: {pair}")
@ -51,7 +46,7 @@ def visualize_trades(strategy: PtResearchStrategy, results: PairResearchResult,
timeline_df = pd.DataFrame({'tstamp': all_timestamps})
# 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')
# Get Symbol_A and Symbol_B market data
@ -110,8 +105,8 @@ def visualize_trades(strategy: PtResearchStrategy, results: PairResearchResult,
type="line",
x0=timeline_df['tstamp'].min(),
x1=timeline_df['tstamp'].max(),
y0=strategy.config_['dis-equilibrium_open_trshld'],
y1=strategy.config_['dis-equilibrium_open_trshld'],
y0=strategy.config_.get_value('dis-equilibrium_open_trshld'),
y1=strategy.config_.get_value('dis-equilibrium_open_trshld'),
line=dict(color="purple", width=2, dash="dot"),
opacity=0.7,
row=1, col=1
@ -121,8 +116,8 @@ def visualize_trades(strategy: PtResearchStrategy, results: PairResearchResult,
type="line",
x0=timeline_df['tstamp'].min(),
x1=timeline_df['tstamp'].max(),
y0=-strategy.config_['dis-equilibrium_open_trshld'],
y1=-strategy.config_['dis-equilibrium_open_trshld'],
y0=-strategy.config_.get_value('dis-equilibrium_open_trshld'),
y1=-strategy.config_.get_value('dis-equilibrium_open_trshld'),
line=dict(color="purple", width=2, dash="dot"),
opacity=0.7,
row=1, col=1
@ -132,8 +127,8 @@ def visualize_trades(strategy: PtResearchStrategy, results: PairResearchResult,
type="line",
x0=timeline_df['tstamp'].min(),
x1=timeline_df['tstamp'].max(),
y0=strategy.config_['dis-equilibrium_close_trshld'],
y1=strategy.config_['dis-equilibrium_close_trshld'],
y0=strategy.config_.get_value('dis-equilibrium_close_trshld'),
y1=strategy.config_.get_value('dis-equilibrium_close_trshld'),
line=dict(color="brown", width=2, dash="dot"),
opacity=0.7,
row=1, col=1
@ -143,8 +138,8 @@ def visualize_trades(strategy: PtResearchStrategy, results: PairResearchResult,
type="line",
x0=timeline_df['tstamp'].min(),
x1=timeline_df['tstamp'].max(),
y0=-strategy.config_['dis-equilibrium_close_trshld'],
y1=-strategy.config_['dis-equilibrium_close_trshld'],
y0=-strategy.config_.get_value('dis-equilibrium_close_trshld'),
y1=-strategy.config_.get_value('dis-equilibrium_close_trshld'),
line=dict(color="brown", width=2, dash="dot"),
opacity=0.7,
row=1, col=1

View File

@ -1,8 +1,18 @@
from __future__ import annotations
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 (
PairResearchResult,
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.tools.filetools import resolve_datafiles
from pairs_trading.lib.tools.instruments import get_instruments
InstrumentTypeT = str
def main() -> None:
import argparse
class Runner(NamedObject):
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.")
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=True,
help="Path to SQLite database for storing results. Use 'NONE' to disable database output.",
)
App.instance().add_call(stage=App.Stage.Config, func=self._on_config())
App.instance().add_call(stage=App.Stage.Run, func=self.run())
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)
instruments = get_instruments(args, config)
datafiles = resolve_datafiles(config, args.date_pattern, instruments)
# Create result database if needed
if App.instance().get_argument("result_db").upper() != "NONE":
create_result_database(App.instance().get_argument("result_db"))
days = list(set([day for day, _ in datafiles]))
print(f"Found {len(datafiles)} data files to process:")
for df in datafiles:
print(f" - {df}")
# Initialize a dictionary to store all trade results
all_results: Dict[str, Dict[str, Any]] = {}
is_config_stored = False
# Process each data file
# 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)
results = PairResearchResult(config=CvttAppConfig.instance().to_dict())
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}")
exit(1)
print(f"\n====== Processing {day} ======")
# Initialize a dictionary to store all trade results
all_results: Dict[str, Dict[str, Any]] = {}
is_config_stored = False
# Process each data file
if not is_config_stored:
store_config_in_database(
db_path=App.instance().get_argument("result_db"),
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)
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,
CvttAppConfig.instance().set_value("datafiles", md_datafiles)
pt_strategy = PtResearchStrategy(
config=CvttAppConfig.instance(),
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(
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()
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":
print(f"\nResults stored in database: {args.result_db}")
else:
print("No results to display.")
async def run(self) -> None:
if App.instance().get_argument("result_db").upper() != "NONE":
print(
f'\nResults stored in database: {App.instance().get_argument("result_db")}'
)
else:
print("No results to display.")
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()