Compare commits

..

13 Commits

Author SHA1 Message Date
Oleg Sheynin d5f00f557b progress 2026-01-15 02:05:35 +00:00
Oleg Sheynin c0fabcb429 progress 2026-01-12 21:26:15 +00:00
Oleg Sheynin bd6cf1d4d0 progress. Initial untested version 2026-01-11 18:17:05 +00:00
Oleg Sheynin b196863a34 progress 2026-01-11 13:33:58 +00:00
oleg 6dd0f97d74 dev progress 2026-01-01 22:18:02 +00:00
oleg 002f797751 dev progress 2026-01-01 22:12:04 +00:00
oleg 4bf1d46208 dev progress 2026-01-01 18:36:18 +00:00
oleg 842eb3ec62 dev progress 2026-01-01 01:36:31 +00:00
oleg 69a0b19e9f dev progress 2025-12-31 08:03:26 +00:00
oleg 121c85def0 dev progress 2025-12-30 10:52:33 +00:00
Oleg Sheynin 2e32b26fad dev progress 2025-12-28 19:30:00 +00:00
Oleg Sheynin ba2a6cd2eb progress 2025-12-23 03:14:41 +00:00
Oleg Sheynin 8b115cee75 renewed development 2025-12-22 23:58:41 +00:00
46 changed files with 6895 additions and 6646 deletions
+1
View File
@@ -0,0 +1 @@
source /home/oleg/.pyenv/python3.12-venv/bin/activate
+40 -153
View File
@@ -21,196 +21,83 @@
"name": "-------- Live Pair Trading --------", "name": "-------- Live Pair Trading --------",
}, },
{ {
"name": "PAIRS TRADER", "name": "PAIR TRADER",
"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}/bin/pairs_trader.py", "program": "${workspaceFolder}/apps/pair_trader.py",
"console": "integratedTerminal", "console": "integratedTerminal",
"env": { "env": {
"PYTHONPATH": "${workspaceFolder}/.." "PYTHONPATH": "${workspaceFolder}/..",
"CONFIG_SERVICE": "cloud16.cvtt.vpn:6789",
"MODEL_CONFIG": "vecm",
"CVTT_URL": "http://cvtt-tester-01.cvtt.vpn:23456",
// "CVTT_URL": "http://dev-server-02.cvtt.vpn:23456",
}, },
"args": [ "args": [
"--config=${workspaceFolder}/configuration/pairs_trader.cfg", // "--config=${workspaceFolder}/configuration/pair_trader.cfg",
"--pair=PAIR-ADA-USDT:BNBSPOT,PAIR-SOL-USDT:BNBSPOT", "--config=http://cloud16.cvtt.vpn:6789/apps/pairs_trading/pair_trader",
"--book_id=TSTBOOK_PT_20260113",
"--instrument_A=COINBASE_AT:PAIR-ADA-USD",
"--instrument_B=COINBASE_AT:PAIR-SOL-USD",
], ],
}, },
{ {
"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", "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.cfg", "--config=http://cloud16.cvtt.vpn:6789/apps/pairs_trading/backtest",
"--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=20250911",
"--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",
"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",
"--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",
"MODEL_CONFIG": "vecm-opt"
}, },
"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 --------",
}, },
+6 -6
View File
@@ -1,10 +1,10 @@
{ {
"folders": [ "folders": [
{ {
"path": ".." "path": ".."
} }
], ],
"settings": { "settings": {
"workbench.colorTheme": "Noctis Minimus" "workbench.colorTheme": "Dracula Theme"
} }
} }
-112
View File
@@ -1,112 +0,0 @@
{
"PythonVersion": "3.12",
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
// ===========================================================
"workbench.activityBar.orientation": "vertical",
// ===========================================================
// "markdown.styles": [
// "/home/oleg/develop/cvtt2/.vscode/light-theme.css"
// ],
"markdown.preview.background": "#ffffff",
"markdown.preview.textEditorTheme": "light",
"markdown-pdf.styles": [
"/home/oleg/develop/cvtt2/.vscode/light-theme.css"
],
"editor.detectIndentation": false,
// Configure editor settings to be overridden for [yaml] language.
"[yaml]": {
"editor.insertSpaces": true,
"editor.tabSize": 4,
},
"pylint.args": [
"--disable=missing-docstring"
, "--disable=invalid-name"
, "--disable=too-few-public-methods"
, "--disable=broad-exception-raised"
, "--disable=broad-exception-caught"
, "--disable=pointless-string-statement"
, "--disable=unused-argument"
, "--disable=line-too-long"
, "--disable=import-outside-toplevel"
, "--disable=fixme"
, "--disable=protected-access"
, "--disable=logging-fstring-interpolation"
],
// ===== TESTING CONFIGURATION =====
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"python.testing.pytestArgs": [
"-v",
"--tb=short",
"--disable-warnings"
],
"python.testing.envVars": {
"PYTHONPATH": "${workspaceFolder}/lib:${workspaceFolder}/.."
},
"python.testing.cwd": "${workspaceFolder}",
"python.testing.autoTestDiscoverOnSaveEnabled": true,
"python.testing.pytestPath": "/home/oleg/.pyenv/python3.12-venv/bin/pytest",
"python.testing.promptToConfigure": false,
"python.testing.pytest.enabled": true,
// Python interpreter settings
"python.defaultInterpreterPath": "/home/oleg/.pyenv/python3.12-venv/bin/python3.12",
// Environment variables for Python execution
"python.envFile": "${workspaceFolder}/.vscode/.env",
"python.terminal.activateEnvironment": false,
"python.terminal.activateEnvInCurrentTerminal": false,
// Global environment variables for VS Code Python extension
"terminal.integrated.env.linux": {
"PYTHONPATH": "/home/oleg/develop/:${env:PYTHONPATH}"
},
"pylint.enabled": true,
"github.copilot.enable": false,
"markdown.extension.print.theme": "dark",
"python.analysis.extraPaths": [
"${workspaceFolder}/..",
"${workspaceFolder}/lib"
],
// Try enabling regular Python language server alongside CursorPyright
"python.languageServer": "None",
"python.analysis.diagnosticMode": "workspace",
"workbench.colorTheme": "Atom One Dark",
"cursorpyright.analysis.enable": false,
"cursorpyright.analysis.extraPaths": [
"${workspaceFolder}/..",
"${workspaceFolder}/lib"
],
// Enable quick fixes for unused imports
"python.analysis.autoImportCompletions": true,
"python.analysis.fixAll": ["source.unusedImports"],
"python.analysis.typeCheckingMode": "basic",
// Enable code actions for CursorPyright
"cursorpyright.analysis.autoImportCompletions": true,
"cursorpyright.analysis.typeCheckingMode": "off",
"cursorpyright.reportUnusedImport": "warning",
"cursorpyright.reportUnusedVariable": "warning",
"cursorpyright.analysis.diagnosticMode": "workspace",
// Force enable code actions
"editor.lightBulb.enabled": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.fixAll": "explicit",
"source.unusedImports": "explicit"
},
// Enable Python-specific code actions
"python.analysis.completeFunctionParens": true,
"python.analysis.addImport.exactMatchOnly": false,
"workbench.tree.indent": 24,
}
+1 -8
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}",
@@ -15,12 +15,5 @@
], ],
"python.envFile": "${workspaceFolder}/.env", "python.envFile": "${workspaceFolder}/.env",
"python.testing.debugPort": 3000, "python.testing.debugPort": 3000,
"python.linting.enabled": true,
"python.linting.pylintEnabled": false,
"python.linting.mypyEnabled": true,
"files.associations": {
"*.py": "python"
},
"python.testing.promptToConfigure": false, "python.testing.promptToConfigure": false,
"workbench.colorTheme": "Dracula Theme Soft"
} }
+2
View File
@@ -0,0 +1,2 @@
cvttpy_tools: 1.3.4
cvttpy_trading: 2.4.1
+1
View File
@@ -0,0 +1 @@
0.0.2
+170
View File
@@ -0,0 +1,170 @@
from __future__ import annotations
import asyncio
from typing import Callable, Coroutine, Dict, List
import aiohttp.web as web
from cvttpy_tools.app import App
from cvttpy_tools.config import Config
from cvttpy_tools.base import NamedObject
from cvttpy_tools.config import CvttAppConfig
from cvttpy_tools.logger import Log
from cvttpy_tools.settings.cvtt_types import BookIdT
from cvttpy_tools.web.rest_service import RestService
# ---
from cvttpy_trading.trading.instrument import ExchangeInstrument
from cvttpy_trading.trading.mkt_data.md_summary import MdTradesAggregate
from cvttpy_trading.trading.exchange_config import ExchangeAccounts
# ---
from pairs_trading.lib.live.mkt_data_client import CvttRestMktDataClient
'''
config http://cloud16.cvtt.vpn/apps/pairs_trading
'''
HistMdCbT = Callable[[List[MdTradesAggregate]], Coroutine]
UpdateMdCbT = Callable[[MdTradesAggregate], Coroutine]
class PairTrader(NamedObject):
config_: CvttAppConfig
instruments_: List[ExchangeInstrument]
book_id_: BookIdT
live_strategy_: "PtLiveStrategy" #type: ignore
ti_sender_: "TradingInstructionsSender" #type: ignore
pricer_client_: CvttRestMktDataClient
rest_service_: RestService
latest_history_: Dict[ExchangeInstrument, List[MdTradesAggregate]]
def __init__(self) -> None:
self.instruments_ = []
self.latest_history_ = {}
App.instance().add_cmdline_arg(
"--instrument_A",
type=str,
required=True,
help=(
" Instrument A in pair (e.g., COINBASE_AT:PAIR-BTC-USD)"
),
)
App.instance().add_cmdline_arg(
"--instrument_B",
type=str,
required=True,
help=(
" Instrument B in pair (e.g., COINBASE_AT:PAIR-ETH-USD)"
),
)
App.instance().add_cmdline_arg(
"--book_id",
type=str,
required=True,
help="Book ID"
)
App.instance().add_call(App.Stage.Config, self._on_config())
App.instance().add_call(App.Stage.Run, self.run())
async def _on_config(self) -> None:
self.config_ = CvttAppConfig.instance()
self.book_id_ = App.instance().get_argument(name="book_id")
# ------- PARSE INSTRUMENTS -------
instr_list: List[str] = []
instr_str = App.instance().get_argument("instrument_A", "")
assert instr_str != "", "Missing insrument A"
instr_list.append(instr_str)
instr_str = App.instance().get_argument("instrument_B", "")
assert instr_str != "", "Missing insrument B"
instr_list.append(instr_str)
for instr in instr_list:
instr_parts = instr.split(":")
if len(instr_parts) != 2:
raise ValueError(f"Invalid pair format: {instr}")
exch_acct = instr_parts[0]
instrument_id = instr_parts[1]
exch_inst = ExchangeAccounts.instance().get_exchange_instrument(exch_acct=exch_acct, instrument_id=instrument_id)
assert exch_inst is not None, f"No ExchangeInstrument for {instr}"
exch_inst.user_data_["exch_acct"] = exch_acct
self.instruments_.append(exch_inst)
Log.info(f"{self.fname()} Instruments: {self.instruments_[0].details_short()} <==> {self.instruments_[1].details_short()}")
# ------- CREATE STRATEGY -------
from pairs_trading.lib.pt_strategy.live.live_strategy import PtLiveStrategy
strategy_config = CvttAppConfig.instance() #self.config_.get_subconfig("strategy_config", Config({}))
self.live_strategy_ = PtLiveStrategy(
config=strategy_config,
pairs_trader=self,
)
Log.info(f"{self.fname()} Strategy created: {self.live_strategy_}")
model_name = self.config_.get_value("model/name", "?model/name?")
self.config_.set_value("strategy_id", f"{self.live_strategy_.__class__.__name__}:{model_name}")
# # ------- CREATE PRICER CLIENT -------
self.pricer_client_ = CvttRestMktDataClient(config=self.config_)
Log.info(f"{self.fname()} MD client created: {self.pricer_client_}")
# ------- CREATE TRADER CLIENT -------
from pairs_trading.lib.live.ti_sender import TradingInstructionsSender
self.ti_sender_ = TradingInstructionsSender(config=self.config_, pairs_trader=self)
Log.info(f"{self.fname()} TI sender created: {self.ti_sender_}")
# # ------- CREATE REST SERVER -------
self.rest_service_ = RestService(
config_key=f"/api/REST"
)
# --- Strategy Handlers
self.rest_service_.add_handler(
method="POST",
url="/api/strategy",
handler=self._on_api_request,
)
async def subscribe_md(self) -> None:
from functools import partial
for exch_inst in self.instruments_:
exch_acct = exch_inst.user_data_.get("exch_acct", "?exch_acct?")
instrument_id = exch_inst.instrument_id()
await self.pricer_client_.add_subscription(
exch_acct=exch_acct,
instrument_id=instrument_id,
interval_sec=self.live_strategy_.interval_sec(),
history_depth_sec=self.live_strategy_.history_depth_sec(),
callback=partial(self._on_md_summary, exch_inst=exch_inst)
)
async def _on_md_summary(self, history: List[MdTradesAggregate], exch_inst: ExchangeInstrument) -> None:
# URGENT before calling stragegy, make sure that **BOTH** instruments market data is combined.
Log.info(f"{self.fname()}: got {exch_inst.details_short()} data")
self.latest_history_[exch_inst] = history
if len(self.latest_history_) == 2:
from itertools import chain
all_aggrs = sorted(list(chain.from_iterable(self.latest_history_.values())), key=lambda X: X.time_ns_)
await self.live_strategy_.on_mkt_data_hist_snapshot(hist_aggr=all_aggrs)
self.latest_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()} ...")
while True:
await asyncio.sleep(0.1)
pass
if __name__ == "__main__":
App()
CvttAppConfig()
PairTrader()
App.instance().run()
-103
View File
@@ -1,103 +0,0 @@
from __future__ import annotations
from functools import partial
from typing import Dict, List
from cvttpy_tools.settings.cvtt_types import JsonDictT
from cvttpy_tools.app import App
from cvttpy_tools.base import NamedObject
from cvttpy_tools.config import CvttAppConfig
from cvttpy_tools.logger import Log
from pairs_trading.lib.pt_strategy.live.live_strategy import PtLiveStrategy
from pairs_trading.lib.pt_strategy.live.pricer_md_client import PtMktDataClient
from pairs_trading.lib.pt_strategy.live.ti_sender import TradingInstructionsSender
# import sys
# print("PYTHONPATH directories:")
# for path in sys.path:
# print(path)
class PairTradingRunner(NamedObject):
config_: CvttAppConfig
instruments_: List[JsonDictT]
live_strategy_: PtLiveStrategy
pricer_client_: PtMktDataClient
def __init__(self) -> None:
self.instruments_ = []
App.instance().add_cmdline_arg(
"--pair",
type=str,
required=True,
help=(
"Comma-separated pair of instrument symbols"
" with exchange config name"
" (e.g., PAIR-BTC-USD:BNBSPOT,PAIR-ETH-USD:BNBSPOT)"
),
)
App.instance().add_call(App.Stage.Config, self._on_config())
App.instance().add_call(App.Stage.Run, self.run())
async def _on_config(self) -> None:
self.config_ = CvttAppConfig.instance()
# ------- PARSE INSTRUMENTS -------
instr_str = App.instance().get_argument("pair", "")
if not instr_str:
raise ValueError("Pair is required")
instr_list = instr_str.split(",")
for instr in instr_list:
instr_parts = instr.split(":")
if len(instr_parts) != 2:
raise ValueError(f"Invalid pair format: {instr}")
instrument_id = instr_parts[0]
exchange_config_name = instr_parts[1]
self.instruments_.append({
"exchange_config_name": exchange_config_name,
"instrument_id": instrument_id
})
assert len(self.instruments_) == 2, "Only two instruments are supported"
Log.info(f"{self.fname()} Instruments: {self.instruments_}")
# # ------- CREATE TI (trading instructions) CLIENT -------
# ti_config = self.config_.get_subconfig("ti_config", {})
# self.ti_sender_ = TradingInstructionsSender(config=ti_config)
# Log.info(f"{self.fname()} TI client created: {self.ti_sender_}")
# ------- CREATE CVTT CLIENT -------
ti_config = self.config_.get_subconfig("ti_config", {})
self.ti_sender_ = TradingInstructionsSender(config=ti_config)
Log.info(f"{self.fname()} TI client created: {self.ti_sender_}")
# ------- CREATE STRATEGY -------
strategy_config = self.config_.get_value("strategy_config", {})
self.live_strategy_ = PtLiveStrategy(
config=strategy_config,
instruments=self.instruments_,
ti_sender=self.ti_sender_
)
Log.info(f"{self.fname()} Strategy created: {self.live_strategy_}")
# # ------- CREATE PRICER CLIENT -------
# pricer_config = self.config_.get_subconfig("pricer_config", {})
# self.pricer_client_ = PtMktDataClient(
# live_strategy=self.live_strategy_,
# pricer_config=pricer_config
# )
# Log.info(f"{self.fname()} CVTT Pricer client created: {self.pricer_client_}")
async def run(self) -> None:
Log.info(f"{self.fname()} ...")
pass
if __name__ == "__main__":
App()
CvttAppConfig()
PairTradingRunner()
App.instance().run()
Executable
+183
View File
@@ -0,0 +1,183 @@
#!/usr/bin/env bash
# ---------------- Settings
repo=git@cloud21.cvtt.vpn:/works/git/cvtt2/research/pairs_trading.git
dist_root=/home/cvttdist/software/cvtt2
dist_user=cvttdist
dist_host="cloud21.cvtt.vpn"
dist_ssh_port="22"
dist_locations="cloud21.cvtt.vpn:22 hs01.cvtt.vpn:22"
version_file="VERSION"
prj=pairs_trading
brnch=master
interactive=N
# ---------------- Settings
# ---------------- cmdline
usage() {
echo "Usage: $0 [-b <branch (master)> -i (interactive)"
exit 1
}
while getopts "b:i" opt; do
case ${opt} in
b )
brnch=$OPTARG
;;
i )
interactive=Y
;;
\? )
echo "Invalid option: -$OPTARG" >&2
usage
;;
: )
echo "Option -$OPTARG requires an argument." >&2
usage
;;
esac
done
# ---------------- cmdline
confirm() {
if [ "${interactive}" == "Y" ]; then
echo "--------------------------------"
echo -n "Press <Enter> to continue" && read
fi
}
if [ "${interactive}" == "Y" ]; then
echo -n "Enter project [${prj}]: "
read project
if [ "${project}" == "" ]
then
project=${prj}
fi
else
project=${prj}
fi
# repo=${git_repo_arr[${project}]}
if [ -z ${repo} ]; then
echo "ERROR: Project repository for ${project} not found"
exit -1
fi
echo "Project repo: ${repo}"
if [ "${interactive}" == "Y" ]; then
echo -n "Enter branch to build release from [${brnch}]: "
read branch
if [ "${branch}" == "" ]
then
branch=${brnch}
fi
else
branch=${brnch}
fi
tmp_dir=$(mktemp -d)
function cleanup {
cd ${HOME}
rm -rf ${tmp_dir}
}
trap cleanup EXIT
prj_dir="${tmp_dir}/${prj}"
cmd_arr=()
Cmd="git clone ${repo} ${prj_dir}"
cmd_arr+=("${Cmd}")
Cmd="cd ${prj_dir}"
cmd_arr+=("${Cmd}")
if [ "${interactive}" == "Y" ]; then
echo "------------------------------------"
echo "The following commands will execute:"
echo "------------------------------------"
for cmd in "${cmd_arr[@]}"
do
echo ${cmd}
done
fi
confirm
for cmd in "${cmd_arr[@]}"
do
echo ${cmd} && eval ${cmd}
done
Cmd="git checkout ${branch}"
echo ${Cmd} && eval ${Cmd}
if [ "${?}" != "0" ]; then
echo "ERROR: Branch ${branch} is not found"
cd ${HOME} && rm -rf ${tmp_dir}
exit -1
fi
release_version=$(cat ${version_file} | awk -F',' '{print $1}')
whats_new=$(cat ${version_file} | awk -F',' '{print $2}')
echo "--------------------------------"
echo "Version file: ${version_file}"
echo "Release version: ${release_version}"
confirm
version_tag="v${release_version}"
version_comment="'${version_tag} ${project} ${branch} $(date +%Y-%m-%d)\n${whats_new}'"
cmd_arr=()
Cmd="git tag -a ${version_tag} -m ${version_comment}"
cmd_arr+=("${Cmd}")
Cmd="git push origin --tags"
cmd_arr+=("${Cmd}")
Cmd="rm -rf .git"
cmd_arr+=("${Cmd}")
SourceLoc=../${project}
dist_path="${dist_root}/${project}/${release_version}"
for dist_loc in ${dist_locations}; do
dhp=(${dist_loc//:/ })
dist_host=${dhp[0]}
dist_port=${dhp[1]}
Cmd="rsync -avzh"
Cmd="${Cmd} --rsync-path=\"mkdir -p ${dist_path}"
Cmd="${Cmd} && rsync\" -e \"ssh -p ${dist_ssh_port}\""
Cmd="${Cmd} $SourceLoc ${dist_user}@${dist_host}:${dist_path}/"
cmd_arr+=("${Cmd}")
done
if [ "${interactive}" == "Y" ]; then
echo "------------------------------------"
echo "The following commands will execute:"
echo "------------------------------------"
for cmd in "${cmd_arr[@]}"
do
echo ${cmd}
done
fi
confirm
for cmd in "${cmd_arr[@]}"
do
pwd && echo ${cmd} && eval ${cmd}
done
echo "$0 Done ${project} ${release_version}"
@@ -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": {
@@ -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,
@@ -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,
@@ -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,
+46
View File
@@ -0,0 +1,46 @@
{
"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-",
},
"EQUITY": {
"data_directory": "./data/equity",
"db_table_name": "md_1min_bars",
"instrument_id_pfx": "STOCK-",
}
},
# ====== Funding ======
"funding_per_pair": 2000.0,
# ====== Model =======
"model": @inc=http://@env{CONFIG_SERVICE}/apps/common/models/@env{MODEL_CONFIG}
# ====== Trading =======
"execution_price": {
"column": "vwap",
"shift": 1,
},
# ====== Stop Conditions ======
"stop_close_conditions": {
"profit": 2.0,
"loss": -0.5
}
# ====== End of Session Closeout ======
"close_outstanding_positions": true,
# "close_outstanding_positions": false,
"trading_hours": {
"timezone": "America/New_York",
"begin_session": "7:30:00",
"end_session": "18:30:00",
}
}
@@ -1,5 +1,5 @@
{ {
"strategy_config": @inc=file:///home/oleg/develop/pairs_trading/configuration/ols.cfg "strategy_config": @inc=file:///home/oleg/develop/pairs_trading/configuration/vecm-opt.cfg
"pricer_config": { "pricer_config": {
"pricer_url": "ws://localhost:12346/ws", "pricer_url": "ws://localhost:12346/ws",
"history_depth_sec": 86400 #"60*60*24", # use simpleeval "history_depth_sec": 86400 #"60*60*24", # use simpleeval
+37 -30
View File
@@ -1,19 +1,26 @@
{ {
"market_data_loading": { # "refdata": {
"CRYPTO": { # "assets": @inc=http://@env{CONFIG_SERVICE}/refdata/assets
"data_directory": "./data/crypto", # , "instruments": @inc=http://@env{CONFIG_SERVICE}/refdata/instruments
"db_table_name": "md_1min_bars", # , "exchange_instruments": @inc=http://@env{CONFIG_SERVICE}/refdata/exchange_instruments
"instrument_id_pfx": "PAIR-", # , "dynamic_instrument_exchanges": ["ALPACA"]
}, # , "exchanges": @inc=http://@env{CONFIG_SERVICE}/refdata/exchanges
"EQUITY": { # },
"data_directory": "./data/equity", # "market_data_loading": {
"db_table_name": "md_1min_bars", # "CRYPTO": {
"instrument_id_pfx": "STOCK-", # "data_directory": "./data/crypto",
} # "db_table_name": "md_1min_bars",
}, # "instrument_id_pfx": "PAIR-",
# },
# "EQUITY": {
# "data_directory": "./data/equity",
# "db_table_name": "md_1min_bars",
# "instrument_id_pfx": "STOCK-",
# }
# },
# ====== Funding ====== # # ====== Funding ======
"funding_per_pair": 2000.0, # "funding_per_pair": 2000.0,
# ====== Trading Parameters ====== # ====== Trading Parameters ======
"stat_model_price": "close", # "vwap" "stat_model_price": "close", # "vwap"
@@ -24,26 +31,26 @@
"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,
# ====== Stop Conditions ====== # # ====== Stop Conditions ======
"stop_close_conditions": { # "stop_close_conditions": {
"profit": 2.0, # "profit": 2.0,
"loss": -0.5 # "loss": -0.5
} # }
# ====== End of Session Closeout ====== # # ====== End of Session Closeout ======
"close_outstanding_positions": true, # "close_outstanding_positions": true,
# "close_outstanding_positions": false, # # "close_outstanding_positions": false,
"trading_hours": { # "trading_hours": {
"timezone": "America/New_York", # "timezone": "America/New_York",
"begin_session": "7:30:00", # "begin_session": "7:30:00",
"end_session": "18:30:00", # "end_session": "18:30:00",
} # }
} }
-213
View File
@@ -1,213 +0,0 @@
from __future__ import annotations
from typing import Dict, Any, List, Optional
import time
import requests
from cvttpy_tools.base import NamedObject
from cvttpy_tools.logger import Log
from cvttpy_tools.config import Config
from cvttpy_tools.timer import Timer
from cvttpy_trading.trading.mkt_data.historical_md import HistMdBar
class RESTSender(NamedObject):
session_: requests.Session
base_url_: str
def __init__(self, base_url: str) -> None:
self.base_url_ = base_url
self.session_ = requests.Session()
def is_ready(self) -> bool:
"""Checks if the server is up and responding"""
url = f"{self.base_url_}/ping"
try:
response = self.session_.get(url)
response.raise_for_status()
return True
except requests.exceptions.RequestException:
return False
def send_post(self, endpoint: str, post_body: Dict) -> requests.Response:
while not self.is_ready():
print("Waiting for FrontGateway to start...")
time.sleep(5)
url = f"{self.base_url_}/{endpoint}"
try:
return self.session_.request(
method="POST",
url=url,
json=post_body,
headers={"Content-Type": "application/json"},
)
except requests.exceptions.RequestException as excpt:
raise ConnectionError(
f"Failed to send status={excpt.response.status_code} {excpt.response.text}" # type: ignore
) from excpt
def send_get(self, endpoint: str) -> requests.Response:
while not self.is_ready():
print("Waiting for FrontGateway to start...")
time.sleep(5)
url = f"{self.base_url_}/{endpoint}"
try:
return self.session_.request(method="GET", url=url)
except requests.exceptions.RequestException as excpt:
raise ConnectionError(
f"Failed to send status={excpt.response.status_code} {excpt.response.text}" # type: ignore
) from excpt
class MdSummary(HistMdBar):
def __init__(
self,
ts_ns: int,
open: float,
high: float,
low: float,
close: float,
volume: float,
vwap: float,
num_trades: int,
):
super().__init__(ts=ts_ns)
self.open_ = open
self.high_ = high
self.low_ = low
self.close_ = close
self.volume_ = volume
self.vwap_ = vwap
self.num_trades_ = num_trades
@classmethod
def from_REST_response(cls, response: requests.Response) -> List[MdSummary]:
res: List[MdSummary] = []
jresp = response.json()
hist_data = jresp.get("historical_data", [])
for hd in hist_data:
res.append(
MdSummary(
ts_ns=hd["time_ns"],
open=hd["open"],
high=hd["high"],
low=hd["low"],
close=hd["close"],
volume=hd["volume"],
vwap=hd["vwap"],
num_trades=hd["num_trades"],
)
)
return res
class MdSummaryCollector(NamedObject):
sender_: RESTSender
exch_acct_: str
instrument_id_: str
interval_sec_: int
history_depth_sec_: int
history_: List[MdSummary]
timer_: Optional[Timer]
def __init__(
self,
sender: RESTSender,
exch_acct: str,
instrument_id: str,
interval_sec: int,
history_depth_sec: int,
) -> None:
self.sender_ = sender
self.exch_acct_ = exch_acct
self.instrument_id_ = instrument_id
self.interval_sec_ = interval_sec
self.history_depth_sec_ = history_depth_sec
self.history_depth_sec_ = []
self.timer_ = None
def rqst_data(self) -> Dict[str, Any]:
return {
"exch_acct": self.exch_acct_,
"instrument_id": self.instrument_id_,
"interval_sec": self.interval_sec_,
"history_depth_sec": self.history_depth_sec_,
}
def get_history(self) -> List[MdSummary]:
response: requests.Response = self.sender_.send_post(
endpoint="md_summary", post_body=self.rqst_data()
)
return MdSummary.from_REST_response(response=response)
def get_last(self) -> Optional[MdSummary]:
rqst_data = self.rqst_data()
rqst_data["history_depth_sec"] = self.interval_sec_
response: requests.Response = self.sender_.send_post(
endpoint="md_summary", post_body=rqst_data
)
res = MdSummary.from_REST_response(response=response)
return None if len(res) == 0 else res[-1]
async def start(self) -> None:
if self.timer_:
Log.error(f"{self.fname()}: Timer is already started")
return
self.history_ = self.get_history()
self.timer_ = Timer(
start_in_sec=self.interval_sec_,
is_periodic=True,
period_interval=self.interval_sec_,
func=self._load_new,
)
async def _load_new(self) -> None:
last: Optional[MdSummary] = self.get_last()
if not last:
# URGENT logging
return
if last.ts_ns_ <= self.history_[-1].ts_ns_:
# URGENT logging
return
self.history_.append(last)
# URGENT implement notification
def stop(self) -> None:
if self.timer_:
self.timer_.cancel()
self.timer_ = None
class CvttRESTClient(NamedObject):
config_: Config
sender_: RESTSender
def __init__(self, config: Config) -> None:
self.config_ = config
base_url = self.config_.get_value("cvtt_base_url", default="")
assert base_url
self.sender_ = RESTSender(base_url=base_url)
if __name__ == "__main__":
config = Config(json_src={"cvtt_base_url": "http://cvtt-tester-01.cvtt.vpn:23456"})
cvtt_client = CvttRESTClient(config)
mdsc = MdSummaryCollector(
sender=cvtt_client.sender_,
exch_acct="COINBASE_AT",
instrument_id="PAIR-BTC-USD",
interval_sec=60,
history_depth_sec=24 * 3600,
)
hist = mdsc.get_history()
last = mdsc.get_last()
pass
-220
View File
@@ -1,220 +0,0 @@
#!/usr/bin/env python3
import asyncio
import json
import uuid
from dataclasses import dataclass
from functools import partial
from typing import Callable, Coroutine, Dict, Optional
import websockets
from cvttpy_tools.logger import Log
from cvttpy_tools.settings.cvtt_types import JsonDictT
from websockets.asyncio.client import ClientConnection
MessageTypeT = str
SubscriptionIdT = str
MessageT = Dict
UrlT = str
CallbackT = Callable[[MessageTypeT, SubscriptionIdT, MessageT], Coroutine[None, str, None]]
@dataclass
class CvttPricesSubscription:
id_: str
exchange_config_name_: str
instrument_id_: str
interval_sec_: int
history_depth_sec_: int
is_subscribed_: bool
is_historical_: bool
callback_: CallbackT
def __init__(
self,
exchange_config_name: str,
instrument_id: str,
interval_sec: int,
history_depth_sec: int,
callback: CallbackT,
):
self.exchange_config_name_ = exchange_config_name
self.instrument_id_ = instrument_id
self.interval_sec_ = interval_sec
self.history_depth_sec_ = history_depth_sec
self.callback_ = callback
self.id_ = str(uuid.uuid4())
self.is_subscribed_ = False
self.is_historical_ = history_depth_sec > 0
class CvttWebSockClient:
ws_url_: UrlT
websocket_: Optional[ClientConnection]
is_connected_: bool
def __init__(self, url: str):
self.ws_url_ = url
self.websocket_ = None
self.is_connected_ = False
async def connect(self) -> None:
self.websocket_ = await websockets.connect(self.ws_url_)
self.is_connected_ = True
async def close(self) -> None:
if self.websocket_ is not None:
await self.websocket_.close()
self.is_connected_ = False
async def receive_message(self) -> JsonDictT:
assert self.websocket_ is not None
assert self.is_connected_
message = await self.websocket_.recv()
message_str = (
message.decode("utf-8")
if isinstance(message, bytes)
else message
)
res = json.loads(message_str)
assert res is not None
assert isinstance(res, dict)
return res
@classmethod
async def check_connection(cls, url: str) -> bool:
try:
async with websockets.connect(url) as websocket:
result = True
except Exception as e:
Log.error(f"Unable to connect to {url}: {str(e)}")
result = False
return result
class CvttPricerWebSockClient(CvttWebSockClient):
# Class members with type hints
subscriptions_: Dict[SubscriptionIdT, CvttPricesSubscription]
def __init__(self, url: str):
super().__init__(url)
self.subscriptions_ = {}
async def subscribe(
self, subscription: CvttPricesSubscription
) -> str: # returns subscription id
if not self.is_connected_:
try:
Log.info(f"Connecting to {self.ws_url_}")
await self.connect()
except Exception as e:
Log.error(f"Unable to connect to {self.ws_url_}: {str(e)}")
raise e
subscr_msg = {
"type": "subscr",
"id": subscription.id_,
"subscr_type": "MD_AGGREGATE",
"exchange_config_name": subscription.exchange_config_name_,
"instrument_id": subscription.instrument_id_,
"interval_sec": subscription.interval_sec_,
}
if subscription.is_historical_:
subscr_msg["history_depth_sec"] = subscription.history_depth_sec_
assert self.websocket_ is not None
await self.websocket_.send(json.dumps(subscr_msg))
response = await self.websocket_.recv()
response_data = json.loads(response)
if not await self.handle_subscription_response(subscription, response_data):
await self.websocket_.close()
self.is_connected_ = False
raise Exception(f"Subscription failed: {str(response)}")
self.subscriptions_[subscription.id_] = subscription
return subscription.id_
async def handle_subscription_response(
self, subscription: CvttPricesSubscription, response: dict
) -> bool:
if response.get("type") != "subscr" or response.get("id") != subscription.id_:
return False
if response.get("status") == "success":
Log.info(f"Subscription successful: {json.dumps(response)}")
return True
elif response.get("status") == "error":
Log.error(f"Subscription failed: {response.get('reason')}")
return False
return False
async def run(self) -> None:
assert self.websocket_
try:
while self.is_connected_:
try:
msg_dict: JsonDictT = await self.receive_message()
except websockets.ConnectionClosed:
Log.warning("Connection closed")
self.is_connected_ = False
break
except Exception as e:
Log.error(f"Error occurred: {str(e)}")
self.is_connected_ = False
await asyncio.sleep(5) # Wait before reconnecting
await self.process_message(msg_dict)
except Exception as e:
Log.error(f"Error occurred: {str(e)}")
self.is_connected_ = False
await asyncio.sleep(5) # Wait before reconnecting
async def process_message(self, message: Dict) -> None:
message_type = message.get("type")
if message_type in ["md_aggregate", "historical_md_aggregate"]:
subscription_id = message.get("subscr_id")
if subscription_id not in self.subscriptions_:
Log.warning(f"Unknown subscription id: {subscription_id}")
return
subscription = self.subscriptions_[subscription_id]
await subscription.callback_(message_type, subscription_id, message)
else:
Log.warning(f"Unknown message type: {message.get('type')}")
async def main() -> None:
async def on_message(message_type: MessageTypeT, subscr_id: SubscriptionIdT, message: Dict, instrument_id: str) -> None:
print(f"{message_type=} {subscr_id=} {instrument_id}")
if message_type == "md_aggregate":
aggr = message.get("md_aggregate", [])
print(f"[{aggr['tstamp'][:19]}] *** RLTM *** {message}")
elif message_type == "historical_md_aggregate":
for aggr in message.get("historical_data", []):
print(f"[{aggr['tstamp'][:19]}] *** HIST *** {aggr}")
else:
print(f"Unknown message type: {message_type}")
pricer_client = CvttPricerWebSockClient(
"ws://localhost:12346/ws"
)
await pricer_client.subscribe(CvttPricesSubscription(
exchange_config_name="COINBASE_AT",
instrument_id="PAIR-BTC-USD",
interval_sec=60,
history_depth_sec=60*60*24,
callback=partial(on_message, instrument_id="PAIR-BTC-USD")
))
await pricer_client.subscribe(CvttPricesSubscription(
exchange_config_name="COINBASE_AT",
instrument_id="PAIR-ETH-USD",
interval_sec=60,
history_depth_sec=60*60*24,
callback=partial(on_message, instrument_id="PAIR-ETH-USD")
))
await pricer_client.run()
if __name__ == "__main__":
asyncio.run(main())
+279
View File
@@ -0,0 +1,279 @@
from __future__ import annotations
import asyncio
from typing import Callable, Coroutine, Dict, Any, List, Optional, Set
import requests
from cvttpy_tools.base import NamedObject
from cvttpy_tools.app import App
from cvttpy_tools.logger import Log
from cvttpy_tools.config import Config
from cvttpy_tools.timer import Timer
from cvttpy_tools.timeutils import NanosT, current_seconds, NanoPerSec
from cvttpy_tools.settings.cvtt_types import InstrumentIdT, IntervalSecT
# ---
from cvttpy_trading.trading.mkt_data.historical_md import HistMdBar
from cvttpy_trading.trading.instrument import ExchangeInstrument
from cvttpy_trading.trading.accounting.exch_account import ExchangeAccountNameT
from cvttpy_trading.trading.mkt_data.md_summary import MdTradesAggregate
from cvttpy_trading.trading.exchange_config import ExchangeAccounts
# ---
from pairs_trading.lib.live.rest_client import RESTSender
class MdSummary(HistMdBar):
def __init__(
self,
ts_ns: int,
open: float,
high: float,
low: float,
close: float,
volume: float,
vwap: float,
num_trades: int,
):
super().__init__(ts=ts_ns)
self.open_ = open
self.high_ = high
self.low_ = low
self.close_ = close
self.volume_ = volume
self.vwap_ = vwap
self.num_trades_ = num_trades
@classmethod
def from_REST_response(cls, response: requests.Response) -> List[MdSummary]:
res: List[MdSummary] = []
jresp = response.json()
hist_data = jresp.get("historical_data", [])
for hd in hist_data:
res.append(
MdSummary(
ts_ns=hd["time_ns"],
open=hd["open"],
high=hd["high"],
low=hd["low"],
close=hd["close"],
volume=hd["volume"],
vwap=hd["vwap"],
num_trades=hd["num_trades"],
)
)
return res
def create_md_trades_aggregate(
self,
exch_acct: ExchangeAccountNameT,
exch_inst: ExchangeInstrument,
interval_sec: IntervalSecT,
) -> MdTradesAggregate:
res = MdTradesAggregate(
exch_acct=exch_acct,
exch_inst=exch_inst,
interval_ns=interval_sec * NanoPerSec,
)
res.set(mdbar=self)
return res
MdSummaryCallbackT = Callable[[List[MdTradesAggregate]], Coroutine]
class MdSummaryCollector(NamedObject):
sender_: RESTSender
exch_acct_: ExchangeAccountNameT
exch_inst_: ExchangeInstrument
interval_sec_: IntervalSecT
history_depth_sec_: IntervalSecT
history_: List[MdTradesAggregate]
callbacks_: List[MdSummaryCallbackT]
timer_: Optional[Timer]
def __init__(
self,
sender: RESTSender,
exch_acct: ExchangeAccountNameT,
instrument_id: InstrumentIdT,
interval_sec: IntervalSecT,
history_depth_sec: IntervalSecT,
) -> None:
self.sender_ = sender
self.exch_acct_ = exch_acct
exch_inst = ExchangeAccounts.instance().get_exchange_instrument(
exch_acct=exch_acct, instrument_id=instrument_id
)
assert exch_inst is not None, f"Unable to find Exchange instrument for {exch_acct}/{instrument_id}"
self.exch_inst_ = exch_inst
self.interval_sec_ = interval_sec
self.history_depth_sec_ = history_depth_sec
self.history_ = []
self.callbacks_ = []
self.timer_ = None
def add_callback(self, cb: MdSummaryCallbackT) -> None:
self.callbacks_.append(cb)
def __hash__(self):
return hash(
(
self.exch_acct_,
self.exch_inst_.instrument_id(),
self.interval_sec_,
self.history_depth_sec_,
)
)
def rqst_data(self) -> Dict[str, Any]:
return {
"exch_acct": self.exch_acct_,
"instrument_id": self.exch_inst_.instrument_id(),
"interval_sec": self.interval_sec_,
"history_depth_sec": self.history_depth_sec_,
}
def get_history(self) -> List[MdSummary]:
response: requests.Response = self.sender_.send_post(
endpoint="md_summary", post_body=self.rqst_data()
)
if response.status_code not in (200, 201):
Log.error(
f"{self.fname()}: Received error: {response.status_code} - {response.text}"
)
return []
return MdSummary.from_REST_response(response=response)
def get_last(self) -> Optional[MdSummary]:
Log.info(f"{self.fname()}: for {self.exch_inst_.details_short()}")
rqst_data = self.rqst_data()
rqst_data["history_depth_sec"] = self.interval_sec_ * 2
response: requests.Response = self.sender_.send_post(
endpoint="md_summary", post_body=rqst_data
)
if response.status_code not in (200, 201):
Log.error(
f"{self.fname()}: Received error: {response.status_code} - {response.text}"
)
return None
res = MdSummary.from_REST_response(response=response)
return None if len(res) == 0 else res[-1]
def is_empty(self) -> bool:
return len(self.history_) == 0
async def start(self) -> None:
if self.timer_:
Log.error(f"{self.fname()}: Timer is already started")
return
mdsum_hist = self.get_history()
self.history_ = [
mdsum.create_md_trades_aggregate(
exch_acct=self.exch_acct_,
exch_inst=self.exch_inst_,
interval_sec=self.interval_sec_,
)
for mdsum in mdsum_hist
]
await self.run_callbacks()
self.set_timer()
def set_timer(self):
if self.timer_:
self.timer_.cancel()
start_in = self.next_load_time() - current_seconds()
self.timer_ = Timer(
start_in_sec=start_in,
func=self._load_new,
)
Log.info(f"{self.fname()} Timer for {self.exch_inst_.details_short()} is set to run in {start_in} sec")
def next_load_time(self) -> NanosT:
curr_sec = int(current_seconds())
return (curr_sec - curr_sec % self.interval_sec_) + self.interval_sec_ + 2
async def _load_new(self) -> None:
last: Optional[MdSummary] = self.get_last()
if not last:
Log.warning(f"{self.fname()}: did not get last update")
elif not self.is_empty() and last.ts_ns_ <= self.history_[-1].time_ns_:
Log.info(
f"{self.fname()}: Received {last}. Already Have: {self.history_[-1]}"
)
else:
self.history_.append(last.create_md_trades_aggregate(exch_acct=self.exch_acct_, exch_inst=self.exch_inst_, interval_sec=self.interval_sec_))
await self.run_callbacks()
self.set_timer()
async def run_callbacks(self) -> None:
[await cb(self.history_) for cb in self.callbacks_]
def stop(self) -> None:
if self.timer_:
self.timer_.cancel()
self.timer_ = None
class CvttRestMktDataClient(NamedObject):
config_: Config
sender_: RESTSender
collectors_: Set[MdSummaryCollector]
def __init__(self, config: Config) -> None:
self.config_ = config
base_url = self.config_.get_value("cvtt_base_url", default="")
assert base_url
self.sender_ = RESTSender(base_url=base_url)
self.collectors_ = set()
async def add_subscription(
self,
exch_acct: ExchangeAccountNameT,
instrument_id: InstrumentIdT,
interval_sec: IntervalSecT,
history_depth_sec: IntervalSecT,
callback: MdSummaryCallbackT,
) -> None:
mdsc = MdSummaryCollector(
sender=self.sender_,
exch_acct=exch_acct,
instrument_id=instrument_id,
interval_sec=interval_sec,
history_depth_sec=history_depth_sec,
)
mdsc.add_callback(callback)
self.collectors_.add(mdsc)
await mdsc.start()
if __name__ == "__main__":
config = Config(json_src={"cvtt_base_url": "http://cvtt-tester-01.cvtt.vpn:23456"})
# config = Config(json_src={"cvtt_base_url": "http://dev-server-02.cvtt.vpn:23456"})
async def _calback(history: List[MdTradesAggregate]) -> None:
Log.info(
f"MdSummary Hist Length is {len(history)}. Last summary: {history[-1] if len(history) > 0 else '[]'}"
)
async def __run() -> None:
Log.info("Starting...")
cvtt_client = CvttRestMktDataClient(config)
await cvtt_client.add_subscription(
exch_acct="COINBASE_AT",
instrument_id="PAIR-BTC-USD",
interval_sec=60,
history_depth_sec=24 * 3600,
callback=_calback,
)
while True:
await asyncio.sleep(5)
asyncio.run(__run())
pass
+67
View File
@@ -0,0 +1,67 @@
from __future__ import annotations
import asyncio
from typing import Callable, Dict, Any, List, Optional
import time
import requests
from cvttpy_tools.base import NamedObject
from cvttpy_tools.logger import Log
from cvttpy_tools.config import Config
from cvttpy_tools.timer import Timer
from cvttpy_tools.timeutils import NanoPerSec, NanosT, current_nanoseconds, current_seconds
from cvttpy_trading.trading.mkt_data.historical_md import HistMdBar
class RESTSender(NamedObject):
session_: requests.Session
base_url_: str
def __init__(self, base_url: str) -> None:
self.base_url_ = base_url
self.session_ = requests.Session()
def is_ready(self) -> bool:
"""Checks if the server is up and responding"""
url = f"{self.base_url_}/ping"
try:
response = self.session_.get(url)
response.raise_for_status()
return True
except requests.exceptions.RequestException:
return False
def send_post(self, endpoint: str, post_body: Dict) -> requests.Response:
while not self.is_ready():
print("Waiting for FrontGateway to start...")
time.sleep(5)
url = f"{self.base_url_}/{endpoint}"
try:
return self.session_.request(
method="POST",
url=url,
json=post_body,
headers={"Content-Type": "application/json"},
)
except requests.exceptions.RequestException as excpt:
raise ConnectionError(
f"Failed to send status={excpt.response.status_code} {excpt.response.text}" # type: ignore
) from excpt
def send_get(self, endpoint: str) -> requests.Response:
while not self.is_ready():
print("Waiting for FrontGateway to start...")
time.sleep(5)
url = f"{self.base_url_}/{endpoint}"
try:
return self.session_.request(method="GET", url=url)
except requests.exceptions.RequestException as excpt:
raise ConnectionError(
f"Failed to send status={excpt.response.status_code} {excpt.response.text}" # type: ignore
) from excpt
+50
View File
@@ -0,0 +1,50 @@
from enum import Enum
import requests
# import aiohttp
from cvttpy_tools.base import NamedObject
from cvttpy_tools.config import Config
from cvttpy_tools.logger import Log
# ---
from cvttpy_trading.trading.trading_instructions import TradingInstructions
# ---
from pairs_trading.lib.live.rest_client import RESTSender
from pairs_trading.apps.pair_trader import PairTrader
class TradingInstructionsSender(NamedObject):
config_: Config
sender_: RESTSender
pairs_trader_: PairTrader
class TradingInstType(str, Enum):
TARGET_POSITION = "TARGET_POSITION"
DIRECT_ORDER = "DIRECT_ORDER"
MARKET_MAKING = "MARKET_MAKING"
NONE = "NONE"
def __init__(self, config: Config, pairs_trader: PairTrader) -> None:
self.config_ = config
base_url = self.config_.get_value("cvtt_base_url", default="")
assert base_url
self.sender_ = RESTSender(base_url=base_url)
self.pairs_trader_ = pairs_trader
self.book_id_ = self.pairs_trader_.book_id_
assert self.book_id_, "book_id is required"
self.strategy_id_ = config.get_value("strategy_id", "")
assert self.strategy_id_, "strategy_id is required"
async def send_trading_instructions(self, ti: TradingInstructions) -> None:
Log.info(f"{self.fname()}: sending {ti=}")
response: requests.Response = self.sender_.send_post(
endpoint="trading_instructions", post_body=ti.to_dict()
)
if response.status_code not in (200, 201):
Log.error(
f"{self.fname()}: Received error: {response.status_code} - {response.text}"
)
+278 -297
View File
@@ -1,351 +1,332 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from enum import Enum
import pandas as pd 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.config import Config
from cvttpy_tools.settings.cvtt_types import IntervalSecT
from cvttpy_tools.timeutils import SecPerHour, current_nanoseconds, NanoPerSec
from cvttpy_tools.logger import Log from cvttpy_tools.logger import Log
from cvttpy_tools.settings.cvtt_types import JsonDictT
# --- # ---
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.trading_instructions import TradingInstructions
from cvttpy_trading.trading.trading_instructions import TargetPositionSignal
# --- # ---
from pt_strategy.live.ti_sender import TradingInstructionsSender from pairs_trading.lib.pt_strategy.model_data_policy import ModelDataPolicy
from pt_strategy.model_data_policy import ModelDataPolicy from pairs_trading.lib.pt_strategy.pt_model import Prediction
from pt_strategy.pt_market_data import RealTimeMarketData from pairs_trading.lib.pt_strategy.trading_pair import LiveTradingPair
from pt_strategy.pt_model import Prediction from pairs_trading.apps.pair_trader import PairTrader
from pt_strategy.trading_pair import PairState, TradingPair from pairs_trading.lib.pt_strategy.pt_market_data import LiveMarketData
"""
--config=pair.cfg
--pair=PAIR-BTC-USDT:COINBASE_AT,PAIR-ETH-USDT:COINBASE_AT
"""
class TradingInstructionType(Enum):
TARGET_POSITION = "TARGET_POSITION"
@dataclass
class TradingInstruction(NamedObject):
type_: TradingInstructionType
exch_instr_: ExchangeInstrument
specifics_: Dict[str, Any]
class PtLiveStrategy(NamedObject): class PtLiveStrategy(NamedObject):
config_: Dict[str, Any] config_: Config
trading_pair_: TradingPair instruments_: List[ExchangeInstrument]
interval_sec_: IntervalSecT
history_depth_sec_: IntervalSecT
open_threshold_: float
close_threshold_: float
trading_pair_: LiveTradingPair
model_data_policy_: ModelDataPolicy model_data_policy_: ModelDataPolicy
pt_mkt_data_: RealTimeMarketData pairs_trader_: PairTrader
ti_sender_: TradingInstructionsSender
# for presentation: history of prediction values and trading signals # for presentation: history of prediction values and trading signals
predictions_: pd.DataFrame predictions_df_: pd.DataFrame
trading_signals_: pd.DataFrame trading_signals_df_: pd.DataFrame
def __init__( def __init__(
self, self,
config: Dict[str, Any], config: Config,
instruments: List[Dict[str, str]], pairs_trader: PairTrader,
ti_sender: TradingInstructionsSender,
): ):
# import copy
# self.config_ = Config(json_src=copy.deepcopy(config.data()))
self.config_ = config self.config_ = config
self.trading_pair_ = TradingPair(config=config, instruments=instruments)
self.predictions_ = pd.DataFrame()
self.trading_signals_ = pd.DataFrame()
self.ti_sender_ = ti_sender
import copy self.pairs_trader_ = pairs_trader
self.trading_pair_ = LiveTradingPair(
# modified config must be passed to PtMarketData config=config,
config_copy = copy.deepcopy(config) instruments=self.pairs_trader_.instruments_,
config_copy["instruments"] = instruments
self.pt_mkt_data_ = RealTimeMarketData(config=config_copy)
self.model_data_policy_ = ModelDataPolicy.create(
config, is_real_time=True, pair=self.trading_pair_
) )
self.open_threshold_ = self.config_.get("dis-equilibrium_open_trshld", 0.0) self.model_data_policy_ = ModelDataPolicy.create(
assert self.open_threshold_ > 0, "open_threshold must be greater than 0" self.config_,
self.close_threshold_ = self.config_.get("dis-equilibrium_close_trshld", 0.0) is_real_time=True,
assert self.close_threshold_ > 0, "close_threshold must be greater than 0" pair=self.trading_pair_,
)
assert (
self.model_data_policy_ is not None
), f"{self.fname()}: Unable to create ModelDataPolicy"
self.predictions_df_ = pd.DataFrame()
self.trading_signals_df_ = pd.DataFrame()
self.instruments_ = self.pairs_trader_.instruments_
App.instance().add_call(
stage=App.Stage.Config, func=self._on_config(), can_run_now=True
)
async def _on_config(self) -> None:
self.interval_sec_ = self.config_.get_value("interval_sec", 0)
assert self.interval_sec_ > 0, "interval_sec cannot be 0"
self.history_depth_sec_ = (
self.config_.get_value("history_depth_hours", 0) * SecPerHour
)
assert self.history_depth_sec_ > 0, "history_depth_hours cannot be 0"
await self.pairs_trader_.subscribe_md()
self.open_threshold_ = self.config_.get_value(
"model/disequilibrium/open_trshld", 0.0
)
self.close_threshold_ = self.config_.get_value(
"model/disequilibrium/close_trshld", 0.0
)
assert (
self.open_threshold_ > 0
), "disequilibrium/open_trshld must be greater than 0"
assert (
self.close_threshold_ > 0
), "disequilibrium/close_trshld must be greater than 0"
def __repr__(self) -> str: def __repr__(self) -> str:
return f"{self.classname()}: trading_pair={self.trading_pair_}, mdp={self.model_data_policy_.__class__.__name__}, " return f"{self.classname()}: trading_pair={self.trading_pair_}, mdp={self.model_data_policy_.__class__.__name__}, "
async def on_mkt_data_hist_snapshot(self, aggr: JsonDictT) -> None: async def on_mkt_data_hist_snapshot(
Log.info(f"on_mkt_data_hist_snapshot: {aggr}") self, hist_aggr: List[MdTradesAggregate]
await self.pt_mkt_data_.on_mkt_data_hist_snapshot(snapshot=aggr) ) -> None:
pass if not self._is_md_actual(hist_aggr=hist_aggr):
return
async def on_mkt_data_update(self, aggr: JsonDictT) -> None: market_data_df: pd.DataFrame = self._create_md_df(hist_aggr=hist_aggr)
market_data_df = await self.pt_mkt_data_.on_mkt_data_update(update=aggr) if len(market_data_df) == 0:
if market_data_df is not None: Log.warning(f"{self.fname()} Unable to create market data df")
self.trading_pair_.market_data_ = market_data_df return
self.model_data_policy_.advance()
prediction = self.trading_pair_.run( self.trading_pair_.market_data_ = market_data_df
market_data_df, self.model_data_policy_.advance()
Log.info(f"{self.fname()}: Running prediction for pair: {self.trading_pair_}")
prediction = self.trading_pair_.run(
market_data_df, self.model_data_policy_.advance()
)
self.predictions_df_ = pd.concat(
[self.predictions_df_, prediction.to_df()], ignore_index=True
)
trading_instructions: List[TradingInstructions] = (
self._create_trading_instructions(
prediction=prediction, last_row=market_data_df.iloc[-1]
) )
self.predictions_ = pd.concat( )
[self.predictions_, prediction.to_df()], ignore_index=True if trading_instructions is not None:
await self._send_trading_instructions(trading_instructions)
def _is_md_actual(self, hist_aggr: List[MdTradesAggregate]) -> bool:
curr_ns = current_nanoseconds()
LAG_THRESHOLD = 5 * NanoPerSec
if len(hist_aggr) == 0:
Log.warning(f"{self.fname()} list of aggregates IS EMPTY")
return False
# MAYBE check market data length
lag_ns = curr_ns - hist_aggr[-1].time_ns_
if lag_ns > LAG_THRESHOLD:
Log.warning(f"{self.fname()} {hist_aggr[-1].exch_inst_.details_short()} Lagging {int(lag_ns/NanoPerSec)} seconds")
return False
return True
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_),
}
) )
trading_instructions: List[TradingInstruction] = ( source_md_df = pd.DataFrame(
self._create_trading_instructions( rows,
prediction=prediction, last_row=market_data_df.iloc[-1] columns=[
) "tstamp",
) "time_ns",
if len(trading_instructions) > 0: "symbol",
await self._send_trading_instructions(trading_instructions) "exchange_id",
# trades = self._create_trades(prediction=prediction, last_row=market_data_df.iloc[-1]) "instrument_id",
# URGENT implement this "open",
pass "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_
def history_depth_sec(self) -> IntervalSecT:
return self.history_depth_sec_
async def _send_trading_instructions( async def _send_trading_instructions(
self, trading_instructions: pd.DataFrame self, trading_instructions: List[TradingInstructions]
) -> None: ) -> None:
pass 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( def _create_trading_instructions(
self, prediction: Prediction, last_row: pd.Series self, prediction: Prediction, last_row: pd.Series
) -> List[TradingInstruction]: ) -> List[TradingInstructions]:
trd_instructions: List[TradingInstructions] = []
pair = self.trading_pair_ pair = self.trading_pair_
trd_instructions: List[TradingInstruction] = []
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) -> float:
# TODO 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
) -> List[TradingInstruction]: ) -> List[TradingInstructions]:
scaled_disequilibrium = prediction.scaled_disequilibrium_
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"
# 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,
}
return df
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_["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),
exchange_id=pair.get_instrument_a().exchange_id_,
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),
exchange_id=pair.get_instrument_b().exchange_id_,
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, exchange_id=pair.get_instrument_a().exchange_id_,
} base_asset=pair.get_instrument_a().base_asset_id_,
df.loc[len(df)] = { quote_asset=pair.get_instrument_a().quote_asset_id_,
"time": tstamp, user_data={}
"symbol": pair.symbol_b_, ),
"side": side_b, )
"action": "OPEN", if not ti_a:
"price": px_b, return []
"disequilibrium": diseqlbrm, ti_b: Optional[TradingInstructions] = TradingInstructions(
"scaled_disequilibrium": abs(scaled_disequilibrium), book=self.pairs_trader_.book_id_,
"signed_scaled_disequilibrium": scaled_disequilibrium, strategy_id=self.__class__.__name__,
# "pair": pair, ti_type=TradingInstructions.Type.TARGET_POSITION,
} issued_ts_ns=current_nanoseconds(),
return df data=TargetPositionSignal(
strength=0,
def _create_close_trades( exchange_id=pair.get_instrument_b().exchange_id_,
self, pair: TradingPair, row: pd.Series, prediction: Optional[Prediction] = None base_asset=pair.get_instrument_b().base_asset_id_,
) -> Optional[pd.DataFrame]: quote_asset=pair.get_instrument_b().quote_asset_id_,
colname_a, colname_b = pair.exec_prices_colnames() user_data={}
),
tstamp = row["tstamp"] )
if prediction is not None: if not ti_b:
diseqlbrm = prediction.disequilibrium_ return []
signed_scaled_disequilibrium = prediction.scaled_disequilibrium_ return [ti_a, ti_b]
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
-85
View File
@@ -1,85 +0,0 @@
from __future__ import annotations
from functools import partial
from typing import Dict, List
from cvtt_client.mkt_data import (CvttPricerWebSockClient,
CvttPricesSubscription, MessageTypeT,
SubscriptionIdT)
from cvttpy_tools.app import App
from cvttpy_tools.base import NamedObject
from cvttpy_tools.config import Config
from cvttpy_tools.logger import Log
from cvttpy_tools.settings.cvtt_types import JsonDictT
from pt_strategy.live.live_strategy import PtLiveStrategy
from pt_strategy.trading_pair import TradingPair
"""
--config=pair.cfg
--pair=PAIR-BTC-USDT:COINBASE_AT,PAIR-ETH-USDT:COINBASE_AT
"""
class PtMktDataClient(NamedObject):
config_: Config
live_strategy_: PtLiveStrategy
pricer_client_: CvttPricerWebSockClient
subscriptions_: List[CvttPricesSubscription]
def __init__(self, live_strategy: PtLiveStrategy, pricer_config: Config):
self.config_ = pricer_config
self.live_strategy_ = live_strategy
App.instance().add_call(App.Stage.Start, self._on_start())
App.instance().add_call(App.Stage.Run, self.run())
async def _on_start(self) -> None:
pricer_url = self.config_.get_value("pricer_url")
assert pricer_url is not None, "pricer_url is not found in config"
self.pricer_client_ = CvttPricerWebSockClient(url=pricer_url)
async def _subscribe(self) -> None:
history_depth_sec = self.config_.get_value("history_depth_sec", 86400)
interval_sec = self.config_.get_value("interval_sec", 60)
pair: TradingPair = self.live_strategy_.trading_pair_
subscriptions = [CvttPricesSubscription(
exchange_config_name=instrument["exchange_config_name"],
instrument_id=instrument["instrument_id"],
interval_sec=interval_sec,
history_depth_sec=history_depth_sec,
callback=partial(
self.on_message, instrument_id=instrument["instrument_id"]
),
) for instrument in pair.instruments_]
for subscription in subscriptions:
Log.info(f"{self.fname()} Subscribing to {subscription}")
await self.pricer_client_.subscribe(subscription)
async def on_message(
self,
message_type: MessageTypeT,
subscr_id: SubscriptionIdT,
message: Dict,
instrument_id: str,
) -> None:
Log.info(f"{self.fname()}: {message_type=} {subscr_id=} {instrument_id}")
aggr: JsonDictT
if message_type == "md_aggregate":
aggr = message.get("md_aggregate", {})
await self.live_strategy_.on_mkt_data_update(aggr)
elif message_type == "historical_md_aggregate":
aggr = message.get("historical_data", {})
await self.live_strategy_.on_mkt_data_hist_snapshot(aggr)
else:
Log.info(f"Unknown message type: {message_type}")
async def run(self) -> None:
if not await CvttPricerWebSockClient.check_connection(self.pricer_client_.ws_url_):
Log.error(f"Unable to connect to {self.pricer_client_.ws_url_}")
raise Exception(f"Unable to connect to {self.pricer_client_.ws_url_}")
await self._subscribe()
await self.pricer_client_.run()
-86
View File
@@ -1,86 +0,0 @@
import time
from enum import Enum
from typing import Tuple
# import aiohttp
from cvttpy_tools.app import App
from cvttpy_tools.base import NamedObject
from cvttpy_tools.config import Config
from cvttpy_tools.logger import Log
from cvttpy_tools.timer import Timer
from cvttpy_tools.timeutils import NanoPerSec
from cvttpy_tools.web.rest_client import REST_RequestProcessor
class TradingInstructionsSender(NamedObject):
class TradingInstType(str, Enum):
TARGET_POSITION = "TARGET_POSITION"
DIRECT_ORDER = "DIRECT_ORDER"
MARKET_MAKING = "MARKET_MAKING"
NONE = "NONE"
config_: Config
ti_method_: str
ti_url_: str
health_check_method_: str
health_check_url_: str
def __init__(self, config: Config):
self.config_ = config
base_url = config.get_value("url", "ws://localhost:12346/ws")
self.book_id_ = config.get_value("book_id", "")
assert self.book_id_, "book_id is required"
self.strategy_id_ = config.get_value("strategy_id", "")
assert self.strategy_id_, "strategy_id is required"
endpoint_uri = config.get_value("ti_endpoint/url", "/trading_instructions")
endpoint_method = config.get_value("ti_endpoint/method", "POST")
health_check_uri = config.get_value("health_check_endpoint/url", "/ping")
health_check_method = config.get_value("health_check_endpoint/method", "GET")
self.ti_method_ = endpoint_method
self.ti_url_ = f"{base_url}{endpoint_uri}"
self.health_check_method_ = health_check_method
self.health_check_url_ = f"{base_url}{health_check_uri}"
App.instance().add_call(App.Stage.Start, self._set_health_check_timer(), can_run_now=True)
async def _set_health_check_timer(self) -> None:
# TODO: configurable interval
self.health_check_timer_ = Timer(is_periodic=True, period_interval=15, start_in_sec=0, func=self._health_check)
Log.info(f"{self.fname()} Health check timer set to 15 seconds")
async def _health_check(self) -> None:
rqst = REST_RequestProcessor(method=self.health_check_method_, url=self.health_check_url_)
async with rqst as (status, msg, headers):
if status != 200:
Log.error(f"{self.fname()} CVTT Service is not responding")
async def send_tgt_positions(self, strength: float, base_asset: str, quote_asset: str) -> Tuple[int, str]:
instr = {
"type": self.TradingInstType.TARGET_POSITION.value,
"book_id": self.book_id_,
"strategy_id": self.strategy_id_,
"issued_ts_ns": int(time.time() * NanoPerSec),
"data": {
"strength": strength,
"base_asset": base_asset,
"quote_asset": quote_asset,
"user_data": {},
},
}
rqst = REST_RequestProcessor(method=self.ti_method_, url=self.ti_url_, params=instr)
async with rqst as (status, msg, headers):
if status != 200:
raise ConnectionError(f"Failed to send trading instructions: {msg}")
return (status, msg)
+35 -39
View File
@@ -8,31 +8,25 @@ from typing import Any, Dict, Optional, cast
import numpy as np import numpy as np
import pandas as pd import pandas as pd
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):
config_: Dict[str, Any] config_: Config
current_data_params_: DataWindowParams current_data_params_: DataWindowParams
count_: int count_: int
is_real_time_: bool is_real_time_: bool
def __init__(self, config: Dict[str, Any], *args: Any, **kwargs: Any): def __init__(self, config: Config, *args: Any, **kwargs: Any):
self.config_ = config self.config_ = config
training_size = config.get("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("training_size", 120)
self.current_data_params_ = DataWindowParams( self.current_data_params_ = DataWindowParams(
training_size=config.get("training_size", 120), training_size_=config.get_value("model/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)
@@ -40,14 +34,15 @@ class ModelDataPolicy(ABC):
@abstractmethod @abstractmethod
def advance(self, mkt_data_df: Optional[pd.DataFrame] = None) -> DataWindowParams: def advance(self, mkt_data_df: Optional[pd.DataFrame] = None) -> DataWindowParams:
self.count_ += 1 self.count_ += 1
print(self.count_, end="\r") if not self.is_real_time_:
print(self.count_, end="\r")
return self.current_data_params_ return self.current_data_params_
@staticmethod @staticmethod
def create(config: Dict[str, Any], *args: Any, **kwargs: Any) -> ModelDataPolicy: def create(config: Config, *args: Any, **kwargs: Any) -> ModelDataPolicy:
import importlib import importlib
model_data_policy_class_name = config.get("model_data_policy_class", None) model_data_policy_class_name = config.get_value("model/model_data_policy_class", None)
assert model_data_policy_class_name is not None assert model_data_policy_class_name is not None
module_name, class_name = model_data_policy_class_name.rsplit(".", 1) module_name, class_name = model_data_policy_class_name.rsplit(".", 1)
module = importlib.import_module(module_name) module = importlib.import_module(module_name)
@@ -58,16 +53,18 @@ class ModelDataPolicy(ABC):
class RollingWindowDataPolicy(ModelDataPolicy): class RollingWindowDataPolicy(ModelDataPolicy):
def __init__(self, config: Dict[str, Any], *args: Any, **kwargs: Any): def __init__(self, config: Config, *args: Any, **kwargs: Any):
super().__init__(config, *args, **kwargs) super().__init__(config, *args, **kwargs)
self.count_ = 1 self.count_ = 1
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_ = 0
if mkt_data_df and len(mkt_data_df) > self.curren_data_params_.training_size_:
self.current_data_params_.training_start_index_ = -self.curren_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_
@@ -80,18 +77,17 @@ class OptimizedWndDataPolicy(ModelDataPolicy, ABC):
prices_a_: np.ndarray prices_a_: np.ndarray
prices_b_: np.ndarray prices_b_: np.ndarray
def __init__(self, config: Dict[str, Any], *args: Any, **kwargs: Any): def __init__(self, config: Config, *args: Any, **kwargs: Any):
super().__init__(config, *args, **kwargs) super().__init__(config, *args, **kwargs)
assert ( assert (
kwargs.get("pair") is not None kwargs.get("pair") is not None
), "pair must be provided" ), "pair must be provided"
assert ( assert (config.key_exists("model/max_training_size") and config.key_exists("model/min_training_size")
"min_training_size" in config and "max_training_size" in config ), "min_training_size and max_training_size must be provided"
), "min_training_size and max_training_size must be provided" self.min_training_size_ = cast(int, config.get_value("model/min_training_size"))
self.min_training_size_ = cast(int, config.get("min_training_size")) self.max_training_size_ = cast(int, config.get_value("model/max_training_size"))
self.max_training_size_ = cast(int, config.get("max_training_size"))
from pt_strategy.trading_pair import TradingPair from pairs_trading.lib.pt_strategy.trading_pair import TradingPair
self.pair_ = cast(TradingPair, kwargs.get("pair")) self.pair_ = cast(TradingPair, kwargs.get("pair"))
if "mkt_data" in kwargs: if "mkt_data" in kwargs:
@@ -110,12 +106,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])
@@ -133,7 +129,7 @@ class EGOptimizedWndDataPolicy(OptimizedWndDataPolicy):
# Engle-Granger cointegration test # Engle-Granger cointegration test
*** VERY SLOW *** *** VERY SLOW ***
''' '''
def __init__(self, config: Dict[str, Any], *args: Any, **kwargs: Any): def __init__(self, config: Config, *args: Any, **kwargs: Any):
super().__init__(config, *args, **kwargs) super().__init__(config, *args, **kwargs)
def optimize_window_size(self) -> DataWindowParams: def optimize_window_size(self) -> DataWindowParams:
@@ -152,8 +148,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=}"
@@ -162,7 +158,7 @@ class EGOptimizedWndDataPolicy(OptimizedWndDataPolicy):
class ADFOptimizedWndDataPolicy(OptimizedWndDataPolicy): class ADFOptimizedWndDataPolicy(OptimizedWndDataPolicy):
# Augmented Dickey-Fuller test # Augmented Dickey-Fuller test
def __init__(self, config: Dict[str, Any], *args: Any, **kwargs: Any): def __init__(self, config: Config, *args: Any, **kwargs: Any):
super().__init__(config, *args, **kwargs) super().__init__(config, *args, **kwargs)
def optimize_window_size(self) -> DataWindowParams: def optimize_window_size(self) -> DataWindowParams:
@@ -196,8 +192,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_},"
@@ -208,7 +204,7 @@ class ADFOptimizedWndDataPolicy(OptimizedWndDataPolicy):
class JohansenOptdWndDataPolicy(OptimizedWndDataPolicy): class JohansenOptdWndDataPolicy(OptimizedWndDataPolicy):
# Johansen test # Johansen test
def __init__(self, config: Dict[str, Any], *args: Any, **kwargs: Any): def __init__(self, config: Config, *args: Any, **kwargs: Any):
super().__init__(config, *args, **kwargs) super().__init__(config, *args, **kwargs)
def optimize_window_size(self) -> DataWindowParams: def optimize_window_size(self) -> DataWindowParams:
@@ -246,8 +242,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.")
+2 -2
View File
@@ -6,8 +6,8 @@ import statsmodels.api as sm
from pt_strategy.pt_model import PairsTradingModel, Prediction from pairs_trading.lib.pt_strategy.pt_model import PairsTradingModel, Prediction
from pt_strategy.trading_pair import TradingPair from pairs_trading.lib.pt_strategy.trading_pair import TradingPair
class OLSModel(PairsTradingModel): class OLSModel(PairsTradingModel):
+160 -166
View File
@@ -1,35 +1,103 @@
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 tools.data_loader import load_market_data
# ---
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(): 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("model/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]
@abstractmethod
def md_columns(self) -> List[str]: ...
@abstractmethod
def rename_columns(self, symbol_df: pd.DataFrame) -> pd.DataFrame: ...
@abstractmethod
def tranform_df_target_colnames(self) -> List[str]: ...
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")
def colnames(self) -> List[str]:
return [
f"{self.stat_model_price_}_{self.symbol_a_}",
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): class ResearchMarketData(PtMarketData):
current_index_: int current_index_: int
is_execution_price_: bool is_execution_price_: bool
def __init__(self, config: Dict[str, Any]): def __init__(self, config: Config, instruments: List[ExchangeInstrument]):
super().__init__(config) super().__init__(config, instruments)
self.current_index_ = 0 self.current_index_ = 0
self.is_execution_price_ = "execution_price" in self.config_ self.is_execution_price_ = self.config_.key_exists("execution_price")
if self.is_execution_price_: if self.is_execution_price_:
self.execution_price_column_ = self.config_["execution_price"]["column"] self.execution_price_column_ = self.config_.get_value("execution_price")["column"]
self.execution_price_shift_ = self.config_["execution_price"]["shift"] self.execution_price_shift_ = self.config_.get_value("execution_price")["shift"]
else: else:
self.execution_price_column_ = None self.execution_price_column_ = None
self.execution_price_shift_ = 0 self.execution_price_shift_ = 0
@@ -43,187 +111,113 @@ class ResearchMarketData(PtMarketData):
return result return result
def load(self) -> None: def load(self) -> None:
datafiles: List[str] = self.config_.get("datafiles", []) datafiles: List[str] = self.config_.get_value("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" 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 extra_minutes: int = self.execution_price_shift_
extra_minutes = self.execution_price_shift_
for datafile in datafiles: for datafile in datafiles:
md_df = load_market_data( md_df = load_market_data(
datafile=datafile, datafile=datafile,
instruments=instruments, instruments=self.instruments_,
db_table_name=self.config_["market_data_loading"][instruments[0]["instrument_type"]]["db_table_name"], db_table_name=self.config_.get_value("market_data_loading")[
trading_hours=self.config_["trading_hours"], 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, extra_minutes=extra_minutes,
) )
self.origin_mkt_data_df_ = pd.concat([self.origin_mkt_data_df_, md_df]) 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_.sort_values(by="tstamp")
self.origin_mkt_data_df_ = self.origin_mkt_data_df_.dropna().reset_index(drop=True) self.origin_mkt_data_df_ = self.origin_mkt_data_df_.dropna().reset_index(
self._set_market_data() drop=True
)
def _set_market_data(self, ) -> None: self.set_market_data()
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_["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() 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: def _set_execution_price_data(self) -> None:
if "execution_price" not in self.config_: if not self.is_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 return
execution_price_column = self.config_["execution_price"]["column"] if not self.config_.key_exists("execution_price"):
execution_price_shift = self.config_["execution_price"]["shift"] self.market_data_df_[f"exec_price_{self.symbol_a_}"] = self.market_data_df_[
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) f"{self.stat_model_price_}_{self.symbol_a_}"
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_[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) self.market_data_df_ = self.market_data_df_.dropna().reset_index(drop=True)
def colnames(self) -> List[str]: def md_columns(self) -> List[str]:
return [ # @abstractmethod
f"{self.stat_model_price_}_{self.symbol_a_}", if self.is_execution_price_:
f"{self.stat_model_price_}_{self.symbol_b_}", 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_}",
]
class RealTimeMarketData(PtMarketData): def __init__(self, config: Config, instruments: List[ExchangeInstrument]):
super().__init__(config, instruments)
def __init__(self, config: Dict[str, Any], *args: Any, **kwargs: Any): def md_columns(self) -> List[str]:
super().__init__(config, *args, **kwargs) # @abstractmethod
return ["tstamp", "symbol", self.stat_model_price_]
async def on_mkt_data_hist_snapshot(self, snapshot: JsonDictT) -> None: def rename_columns(self, selected_symbol_df: pd.DataFrame) -> pd.DataFrame:
# URGENT # @abstractmethod
# create origin_mkt_data_df_ from snapshot symbol = selected_symbol_df.iloc[0]["symbol"]
# verify that the data for both instruments are present new_price_column = f"{self.stat_model_price_}_{symbol}"
temp_df = pd.DataFrame(
# transform it to market_data_df_ tstamp, close_symbolA, close_symbolB {
''' "tstamp": selected_symbol_df["tstamp"],
# from cvttpy/exchanges/binance/spot/mkt_data.py new_price_column: selected_symbol_df[self.stat_model_price_],
values = {
"time_ns": time_ns,
"tstamp": format_nanos_utc(time_ns),
"exchange_id": exch_inst.exchange_id_,
"instrument_id": exch_inst.instrument_id(),
"interval_ns": interval_sec * 1_000_000_000,
"open": float(kline[1]),
"high": float(kline[2]),
"low": float(kline[3]),
"close": float(kline[4]),
"volume": float(kline[5]),
"num_trades": kline[8],
"vwap": float(kline[7]) / float(kline[5]) if float(kline[5]) > 0 else 0.0 # Calculate VWAP
}
'''
pass
async def on_mkt_data_update(self, update: JsonDictT) -> Optional[pd.DataFrame]:
# URGENT
# make sure update has both instruments
# create DataFrame tmp1 from update
# transform tmp1 into temp. datframe tmp2
# add tmp1 to origin_mkt_data_df_
# add tmp2 to market_data_df_
# return market_data_df_
'''
class MdTradesAggregate(NamedObject):
def to_dict(self) -> Dict[str, Any]:
return {
"time_ns": self.time_ns_,
"tstamp": format_nanos_utc(self.time_ns_),
"exchange_id": self.exch_inst_.exchange_id_,
"instrument_id": self.exch_inst_.instrument_id(),
"interval_ns": self.interval_ns_,
"open": self.exch_inst_.get_price(self.open_),
"high": self.exch_inst_.get_price(self.high_),
"low": self.exch_inst_.get_price(self.low_),
"close": self.exch_inst_.get_price(self.close_),
"volume": self.exch_inst_.get_quantity(self.volume_),
"vwap": self.exch_inst_.get_price(self.vwap_),
"num_trades": self.exch_inst_.get_quantity(self.num_trades_),
} }
''' )
return temp_df
return pd.DataFrame() def tranform_df_target_colnames(self):
# @abstractmethod
return self.colnames()
+7 -4
View File
@@ -3,8 +3,11 @@ 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 pt_strategy.prediction import Prediction # ---
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
class PairsTradingModel(ABC): class PairsTradingModel(ABC):
@@ -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/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)
+36 -34
View File
@@ -1,54 +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 pt_strategy.model_data_policy import ModelDataPolicy # ---
from pt_strategy.pt_market_data import ResearchMarketData from cvttpy_tools.config import Config
from pt_strategy.pt_model import Prediction # ---
from pt_strategy.trading_pair import PairState, TradingPair 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, 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 pt_strategy.model_data_policy import ModelDataPolicy from pairs_trading.lib.pt_strategy.model_data_policy import ModelDataPolicy
from 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, 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()
@@ -72,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
@@ -93,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("model/disequilibrium/open_trshld")
close_threshold = self.config_["dis-equilibrium_close_trshld"] close_threshold = self.config_.get_value("model/disequilibrium/close_trshld")
scaled_disequilibrium = prediction.scaled_disequilibrium_ scaled_disequilibrium = prediction.scaled_disequilibrium_
abs_scaled_disequilibrium = abs(scaled_disequilibrium) abs_scaled_disequilibrium = abs(scaled_disequilibrium)
@@ -143,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
@@ -159,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"],
@@ -190,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()
@@ -224,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,
@@ -235,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,
@@ -247,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()
@@ -269,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,
@@ -280,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,
+13 -18
View File
@@ -4,8 +4,12 @@ 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 pt_strategy.trading_pair import TradingPair # ---
from cvttpy_tools.config import Config
# ---
from cvttpy_trading.trading.instrument import ExchangeInstrument
# ---
from pairs_trading.lib.pt_strategy.trading_pair import TradingPair
# Recommended replacement adapters and converters for Python 3.12+ # Recommended replacement adapters and converters for Python 3.12+
# From: https://docs.python.org/3/library/sqlite3.html#sqlite3-adapter-converter-recipes # From: https://docs.python.org/3/library/sqlite3.html#sqlite3-adapter-converter-recipes
@@ -18,12 +22,10 @@ def adapt_datetime_iso(val: datetime) -> str:
"""Adapt datetime.datetime to timezone-naive ISO 8601 date.""" """Adapt datetime.datetime to timezone-naive ISO 8601 date."""
return val.isoformat() return val.isoformat()
def convert_date(val: bytes) -> date: def convert_date(val: bytes) -> date:
"""Convert ISO 8601 date to datetime.date object.""" """Convert ISO 8601 date to datetime.date object."""
return datetime.fromisoformat(val.decode()).date() return datetime.fromisoformat(val.decode()).date()
def convert_datetime(val: bytes) -> datetime: def convert_datetime(val: bytes) -> datetime:
"""Convert ISO 8601 datetime to datetime.datetime object.""" """Convert ISO 8601 datetime to datetime.datetime object."""
return datetime.fromisoformat(val.decode()) return datetime.fromisoformat(val.decode())
@@ -118,9 +120,9 @@ def create_result_database(db_path: str) -> None:
def store_config_in_database( def store_config_in_database(
db_path: str, db_path: str,
config_file_path: str, config_file_path: str,
config: Dict, config: Config,
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.
@@ -135,13 +137,13 @@ def store_config_in_database(
cursor = conn.cursor() cursor = conn.cursor()
# Convert config to JSON string # Convert config to JSON string
config_json = json.dumps(config, indent=2, default=str) config_json = json.dumps(config.data(), indent=2, default=str)
# Convert lists to comma-separated strings for storage # Convert lists to comma-separated strings for storage
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
] ]
) )
@@ -204,9 +206,9 @@ class PairResearchResult:
trades_: Dict[DayT, pd.DataFrame] trades_: Dict[DayT, pd.DataFrame]
outstanding_positions_: Dict[DayT, List[OutstandingPositionT]] outstanding_positions_: Dict[DayT, List[OutstandingPositionT]]
symbol_roundtrip_trades_: Dict[str, List[Dict[str, Any]]] symbol_roundtrip_trades_: Dict[str, List[Dict[str, Any]]]
config_: Config
def __init__(self, config: Config) -> None:
def __init__(self, config: Dict[str, Any]) -> None:
self.config_ = config self.config_ = config
self.trades_ = {} self.trades_ = {}
self.outstanding_positions_ = {} self.outstanding_positions_ = {}
@@ -218,13 +220,6 @@ class PairResearchResult:
self.trades_[day] = trades self.trades_[day] = trades
self.outstanding_positions_[day] = outstanding_positions self.outstanding_positions_[day] = outstanding_positions
# def all_trades(self) -> List[TradeT]:
# """Get all trades across all days as a flat list."""
# all_trades_list: List[TradeT] = []
# for day_trades in self.trades_.values():
# all_trades_list.extend(day_trades.to_dict(orient="records"))
# return all_trades_list
def outstanding_positions(self) -> List[OutstandingPositionT]: def outstanding_positions(self) -> List[OutstandingPositionT]:
"""Get all outstanding positions across all days as a flat list.""" """Get all outstanding positions across all days as a flat list."""
res: List[Dict[str, Any]] = [] res: List[Dict[str, Any]] = []
@@ -292,7 +287,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({
+102 -75
View File
@@ -1,13 +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 pt_strategy.model_data_policy import DataWindowParams # ---
from pt_strategy.prediction import Prediction 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
class PairState(Enum): class PairState(Enum):
@@ -19,59 +27,76 @@ class PairState(Enum):
CLOSE_STOP_PROFIT = 6 CLOSE_STOP_PROFIT = 6
def get_symbol(instrument: Dict[str, str]) -> str: class TradingPair(NamedObject, ABC):
if "symbol" in instrument: config_: Config
return instrument["symbol"] model_: Any # "PairsTradingModel"
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]
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] user_data_: Dict[str, Any]
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 pt_strategy.pt_model import PairsTradingModel
assert len(instruments) == 2, "Trading pair must have exactly 2 instruments"
self.config_ = config 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.model_ = PairsTradingModel.create(config)
self.stat_model_price_ = config["stat_model_price"] self.user_data_ = {}
self.user_data_ = { self.instruments_ = instruments
"state": PairState.INITIAL, 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]
self.stat_model_price_ = config.get_value("model/stat_model_price")
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)
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]
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__}"
) )
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.user_data_ = {
"state": PairState.INITIAL,
}
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,
@@ -79,39 +104,34 @@ class TradingPair:
PairState.CLOSE_STOP_LOSS, PairState.CLOSE_STOP_LOSS,
PairState.CLOSE_STOP_PROFIT, PairState.CLOSE_STOP_PROFIT,
] ]
def is_open(self) -> bool:
return self.user_data_["state"] == PairState.OPEN
def colnames(self) -> List[str]: def is_open(self) -> bool:
return [ return not self.is_closed()
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
@@ -136,8 +156,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
@@ -158,20 +178,22 @@ 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_.get_value("funding_per_pair") / 2
funding_per_position = self.config_["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
@@ -179,21 +201,26 @@ class TradingPair:
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 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] class LiveTradingPair(TradingPair):
return self.model_.predict(pair=self)
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
+4 -4
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:
+7 -8
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"
+8 -4
View File
@@ -1,18 +1,22 @@
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 Config
# ---
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: Config, 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.get_value(f"market_data_loading/{inst_type}/data_directory")
if "*" in pattern or "?" in pattern: if "*" in pattern or "?" in pattern:
# Handle wildcards # Handle wildcards
if not os.path.isabs(pattern): if not os.path.isabs(pattern):
-21
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
+3 -3
View File
@@ -1,4 +1,4 @@
from pt_strategy.research_strategy import PtResearchStrategy from pairs_trading.lib.pt_strategy.research_strategy import PtResearchStrategy
def visualize_prices(strategy: PtResearchStrategy, trading_date: str) -> None: def visualize_prices(strategy: PtResearchStrategy, trading_date: str) -> None:
@@ -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')
+13 -18
View File
@@ -1,13 +1,8 @@
from __future__ import annotations from __future__ import annotations
import os
from typing import Any, Dict
from pt_strategy.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 pt_strategy.research_strategy import PtResearchStrategy
from tools.filetools import resolve_datafiles
from 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('model/disequilibrium/open_trshld'),
y1=strategy.config_['dis-equilibrium_open_trshld'], y1=strategy.config_.get_value('model/disequilibrium/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('model/disequilibrium/open_trshld'),
y1=-strategy.config_['dis-equilibrium_open_trshld'], y1=-strategy.config_.get_value('model/disequilibrium/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('model/disequilibrium/close_trshld'),
y1=strategy.config_['dis-equilibrium_close_trshld'], y1=strategy.config_.get_value('model/disequilibrium/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('model/disequilibrium/close_trshld'),
y1=-strategy.config_['dis-equilibrium_close_trshld'], y1=-strategy.config_.get_value('model/disequilibrium/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
-66
View File
@@ -1,66 +0,0 @@
[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "pairs-trading"
version = "0.1.0"
description = "Pairs Trading Backtesting Framework"
requires-python = ">=3.8"
[tool.black]
line-length = 88
target-version = ['py38']
include = '\.pyi?$'
extend-exclude = '''
/(
# directories
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| build
| dist
)/
'''
[tool.flake8]
max-line-length = 88
extend-ignore = ["E203", "W503"]
exclude = [
".git",
"__pycache__",
"build",
"dist",
".venv",
".mypy_cache",
".tox"
]
[tool.mypy]
python_version = "3.8"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
warn_unreachable = true
strict_equality = true
[[tool.mypy.overrides]]
module = [
"numpy.*",
"pandas.*",
"matplotlib.*",
"seaborn.*",
"scipy.*",
"sklearn.*"
]
ignore_missing_imports = true
-25
View File
@@ -1,25 +0,0 @@
{
"include": [
"lib"
],
"exclude": [
"**/node_modules",
"**/__pycache__",
"**/.*",
"results",
"data"
],
"ignore": [],
"defineConstant": {},
"typeCheckingMode": "basic",
"useLibraryCodeForTypes": true,
"autoImportCompletions": true,
"autoSearchPaths": true,
"extraPaths": [
"lib",
".."
],
"stubPath": "./typings",
"venvPath": ".",
"venv": "python3.12-venv"
}
+1
View File
@@ -170,6 +170,7 @@ types-PyYAML>=5.4
types-redis>=3.5 types-redis>=3.5
types-requests>=2.25 types-requests>=2.25
types-retry>=0.9 types-retry>=0.9
types-seaborn>0.13.2
types-selenium>=3.141 types-selenium>=3.141
types-Send2Trash>=1.8 types-Send2Trash>=1.8
types-setuptools>=57.4 types-setuptools>=57.4
+110 -77
View File
@@ -1,106 +1,139 @@
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 pt_strategy.results import ( # ---
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, PairResearchResult,
create_result_database, create_result_database,
store_config_in_database, store_config_in_database,
) )
from pt_strategy.research_strategy import PtResearchStrategy from pairs_trading.lib.pt_strategy.research_strategy import PtResearchStrategy
from tools.filetools import resolve_datafiles from pairs_trading.lib.tools.filetools import resolve_datafiles
from tools.instruments import get_instruments
InstrumentTypeT = str
def main() -> None: class Runner(NamedObject):
import argparse def __init__(self):
App()
CvttAppConfig()
from 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(),
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())
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(),
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
-94
View File
@@ -1,94 +0,0 @@
import glob
import os
from typing import Dict, List, Optional
import pandas as pd
from 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
-111
View File
@@ -1,111 +0,0 @@
from __future__ import annotations
import os
from typing import Any, Dict
from pt_strategy.results import (PairResearchResult, create_result_database,
store_config_in_database)
from pt_strategy.research_strategy import PtResearchStrategy
from tools.filetools import resolve_datafiles
from tools.instruments import get_instruments
from tools.viz.viz_trades import visualize_trades
def main() -> None:
import argparse
from 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()