Compare commits
16 Commits
ba2a6cd2eb
...
v0.0.5
| Author | SHA1 | Date | |
|---|---|---|---|
| bf25eb7fb5 | |||
| f2a5d6a7ad | |||
| b9d479ae8c | |||
| e6ae62ebb6 | |||
| 170e48d646 | |||
| d5f00f557b | |||
| c0fabcb429 | |||
| bd6cf1d4d0 | |||
| b196863a34 | |||
| 6dd0f97d74 | |||
| 002f797751 | |||
| 4bf1d46208 | |||
| 842eb3ec62 | |||
| 69a0b19e9f | |||
| 121c85def0 | |||
| 2e32b26fad |
Vendored
+49
-144
@@ -21,196 +21,101 @@
|
|||||||
"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",
|
||||||
|
"CVTT_URL": "http://cvtt-tester-01.cvtt.vpn:23456",
|
||||||
|
"MODEL_CONFIG": "vecm"
|
||||||
},
|
},
|
||||||
"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": "PAIR SELECTOR",
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "CRYPTO OLS (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}/apps/pair_selector.py",
|
||||||
"args": [
|
"console": "integratedTerminal",
|
||||||
"--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": {
|
"env": {
|
||||||
"PYTHONPATH": "${workspaceFolder}/lib"
|
"PYTHONPATH": "${workspaceFolder}/..",
|
||||||
|
"CONFIG_SERVICE": "cloud16.cvtt.vpn:6789",
|
||||||
|
// "CVTT_URL": "http://cvtt-tester-01.cvtt.vpn:23456",
|
||||||
|
"CVTT_URL": "http://dev-server-02.cvtt.vpn:23456",
|
||||||
|
"PAIR_SELECTOR_REST_PORT": "44320"
|
||||||
},
|
},
|
||||||
"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": [
|
"args": [
|
||||||
"--config=${workspaceFolder}/configuration/ols-opt.cfg",
|
// "--config=${workspaceFolder}/configuration/pair_trader.cfg",
|
||||||
"--instruments=ADA-USDT:CRYPTO:BNBSPOT,SOL-USDT:CRYPTO:BNBSPOT",
|
"--config=http://cloud16.cvtt.vpn:6789/apps/pairs_trading/pair_selector",
|
||||||
"--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 --------",
|
||||||
},
|
},
|
||||||
|
|||||||
Vendored
+1
-1
@@ -5,6 +5,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
"workbench.colorTheme": "Noctis Minimus"
|
"workbench.colorTheme": "Dracula Theme"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Vendored
-112
@@ -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,
|
|
||||||
}
|
|
||||||
Vendored
+1
-5
@@ -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,9 +15,5 @@
|
|||||||
],
|
],
|
||||||
"python.envFile": "${workspaceFolder}/.env",
|
"python.envFile": "${workspaceFolder}/.env",
|
||||||
"python.testing.debugPort": 3000,
|
"python.testing.debugPort": 3000,
|
||||||
"files.associations": {
|
|
||||||
"*.py": "python"
|
|
||||||
},
|
|
||||||
"python.testing.promptToConfigure": false,
|
"python.testing.promptToConfigure": false,
|
||||||
"workbench.colorTheme": "Dracula Theme Soft"
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
|
from cvttpy_tools.app import App
|
||||||
|
from cvttpy_tools.base import NamedObject
|
||||||
|
from cvttpy_tools.config import CvttAppConfig
|
||||||
|
from cvttpy_tools.logger import Log
|
||||||
|
from cvttpy_tools.web.rest_service import RestService
|
||||||
|
from cvttpy_trading.trading.exchange_config import ExchangeAccounts
|
||||||
|
from cvttpy_trading.trading.instrument import ExchangeInstrument
|
||||||
|
|
||||||
|
from pairs_trading.lib.pair_selector_engine import PairSelectionEngine
|
||||||
|
|
||||||
|
|
||||||
|
class PairSelector(NamedObject):
|
||||||
|
instruments_: List[ExchangeInstrument]
|
||||||
|
engine_: PairSelectionEngine
|
||||||
|
rest_service_: RestService
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
App.instance().add_cmdline_arg("--oneshot", action="store_true", default=False)
|
||||||
|
App.instance().add_call(App.Stage.Config, self._on_config())
|
||||||
|
App.instance().add_call(App.Stage.Run, self.run())
|
||||||
|
|
||||||
|
async def _on_config(self) -> None:
|
||||||
|
cfg = CvttAppConfig.instance()
|
||||||
|
self.instruments_ = self._load_instruments(cfg)
|
||||||
|
price_field = cfg.get_value("model/stat_model_price", "close")
|
||||||
|
|
||||||
|
self.engine_ = PairSelectionEngine(
|
||||||
|
config=cfg,
|
||||||
|
instruments=self.instruments_,
|
||||||
|
price_field=price_field,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.rest_service_ = RestService(config_key="/api/REST")
|
||||||
|
self.rest_service_.add_handler("GET", "/data_quality", self._on_data_quality)
|
||||||
|
self.rest_service_.add_handler("GET", "/pair_selection", self._on_pair_selection)
|
||||||
|
|
||||||
|
def _load_instruments(self, cfg: CvttAppConfig) -> List[ExchangeInstrument]:
|
||||||
|
instruments_cfg = cfg.get_value("instruments", [])
|
||||||
|
instruments: List[ExchangeInstrument] = []
|
||||||
|
assert len(instruments_cfg) >= 2, "at least two instruments required"
|
||||||
|
for item in instruments_cfg:
|
||||||
|
if isinstance(item, str):
|
||||||
|
parts = item.split(":", 1)
|
||||||
|
if len(parts) != 2:
|
||||||
|
raise ValueError(f"invalid instrument format: {item}")
|
||||||
|
exch_acct, instrument_id = parts
|
||||||
|
elif isinstance(item, dict):
|
||||||
|
exch_acct = item.get("exch_acct", "")
|
||||||
|
instrument_id = item.get("instrument_id", "")
|
||||||
|
if not exch_acct or not instrument_id:
|
||||||
|
raise ValueError(f"invalid instrument config: {item}")
|
||||||
|
else:
|
||||||
|
raise ValueError(f"unsupported instrument entry: {item}")
|
||||||
|
|
||||||
|
exch_inst = ExchangeAccounts.instance().get_exchange_instrument(
|
||||||
|
exch_acct=exch_acct, instrument_id=instrument_id
|
||||||
|
)
|
||||||
|
assert exch_inst is not None, f"no ExchangeInstrument for {exch_acct}:{instrument_id}"
|
||||||
|
exch_inst.user_data_["exch_acct"] = exch_acct
|
||||||
|
instruments.append(exch_inst)
|
||||||
|
return instruments
|
||||||
|
|
||||||
|
async def run(self) -> None:
|
||||||
|
oneshot = App.instance().get_argument("oneshot", False)
|
||||||
|
while True:
|
||||||
|
await self.engine_.run_once()
|
||||||
|
if oneshot:
|
||||||
|
break
|
||||||
|
sleep_for = self.engine_.sleep_seconds_until_next_cycle()
|
||||||
|
await asyncio.sleep(sleep_for)
|
||||||
|
|
||||||
|
async def _on_data_quality(self, request: web.Request) -> web.Response:
|
||||||
|
fmt = request.query.get("format", "html").lower()
|
||||||
|
quality = self.engine_.quality_dicts()
|
||||||
|
if fmt == "json":
|
||||||
|
return web.json_response(quality)
|
||||||
|
return web.Response(text=self._render_quality_html(quality), content_type="text/html")
|
||||||
|
|
||||||
|
async def _on_pair_selection(self, request: web.Request) -> web.Response:
|
||||||
|
fmt = request.query.get("format", "html").lower()
|
||||||
|
pairs = self.engine_.pair_dicts()
|
||||||
|
if fmt == "json":
|
||||||
|
return web.json_response(pairs)
|
||||||
|
return web.Response(text=self._render_pairs_html(pairs), content_type="text/html")
|
||||||
|
|
||||||
|
def _render_quality_html(self, quality: List[Dict[str, Any]]) -> str:
|
||||||
|
rows = "".join(
|
||||||
|
f"<tr>"
|
||||||
|
f"<td>{q.get('instrument','')}</td>"
|
||||||
|
f"<td>{q.get('record_count','')}</td>"
|
||||||
|
f"<td>{q.get('latest_tstamp','')}</td>"
|
||||||
|
f"<td>{q.get('status','')}</td>"
|
||||||
|
f"<td>{q.get('reason','')}</td>"
|
||||||
|
f"</tr>"
|
||||||
|
for q in sorted(quality, key=lambda x: str(x.get("instrument", "")))
|
||||||
|
)
|
||||||
|
return f"""
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset='utf-8'/>
|
||||||
|
<title>Data Quality</title>
|
||||||
|
<style>
|
||||||
|
body {{ font-family: Arial, sans-serif; margin: 20px; }}
|
||||||
|
table {{ border-collapse: collapse; width: 100%; }}
|
||||||
|
th, td {{ border: 1px solid #ccc; padding: 8px; text-align: left; }}
|
||||||
|
th {{ background: #f2f2f2; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Data Quality</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Instrument</th><th>Records</th><th>Latest</th><th>Status</th><th>Reason</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{rows}</tbody>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _render_pairs_html(self, pairs: List[Dict[str, Any]]) -> str:
|
||||||
|
if not pairs:
|
||||||
|
body = "<p>No pairs available. Check data quality and try again.</p>"
|
||||||
|
else:
|
||||||
|
body_rows = []
|
||||||
|
for p in pairs:
|
||||||
|
body_rows.append(
|
||||||
|
"<tr>"
|
||||||
|
f"<td>{p.get('instrument_a','')}</td>"
|
||||||
|
f"<td>{p.get('instrument_b','')}</td>"
|
||||||
|
f"<td data-value='{p.get('rank_eg',0)}'>{p.get('rank_eg','')}</td>"
|
||||||
|
f"<td data-value='{p.get('rank_adf',0)}'>{p.get('rank_adf','')}</td>"
|
||||||
|
f"<td data-value='{p.get('rank_j',0)}'>{p.get('rank_j','')}</td>"
|
||||||
|
f"<td data-value='{p.get('pvalue_eg','')}'>{p.get('pvalue_eg','')}</td>"
|
||||||
|
f"<td data-value='{p.get('pvalue_adf','')}'>{p.get('pvalue_adf','')}</td>"
|
||||||
|
f"<td data-value='{p.get('pvalue_j','')}'>{p.get('pvalue_j','')}</td>"
|
||||||
|
"</tr>"
|
||||||
|
)
|
||||||
|
body = "\n".join(body_rows)
|
||||||
|
|
||||||
|
return f"""
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset='utf-8'/>
|
||||||
|
<title>Pair Selection</title>
|
||||||
|
<style>
|
||||||
|
body {{ font-family: Arial, sans-serif; margin: 20px; }}
|
||||||
|
table {{ border-collapse: collapse; width: 100%; }}
|
||||||
|
th, td {{ border: 1px solid #ccc; padding: 8px; text-align: left; }}
|
||||||
|
th.sortable {{ cursor: pointer; background: #f2f2f2; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Pair Selection</h2>
|
||||||
|
<table id="pairs-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Instrument A</th>
|
||||||
|
<th>Instrument B</th>
|
||||||
|
<th class="sortable" data-type="num">Rank-EG</th>
|
||||||
|
<th class="sortable" data-type="num">Rank-ADF</th>
|
||||||
|
<th class="sortable" data-type="num">Rank-J</th>
|
||||||
|
<th>EG p-value</th>
|
||||||
|
<th>ADF p-value</th>
|
||||||
|
<th>Johansen pseudo p</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{body}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<script>
|
||||||
|
(function() {{
|
||||||
|
const table = document.getElementById('pairs-table');
|
||||||
|
if (!table) return;
|
||||||
|
const getValue = (cell) => {{
|
||||||
|
const val = cell.getAttribute('data-value');
|
||||||
|
const num = parseFloat(val);
|
||||||
|
return isNaN(num) ? val : num;
|
||||||
|
}};
|
||||||
|
const toggleSort = (index, isNumeric) => {{
|
||||||
|
const tbody = table.querySelector('tbody');
|
||||||
|
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||||||
|
const th = table.querySelectorAll('th')[index];
|
||||||
|
const dir = th.getAttribute('data-dir') === 'asc' ? 'desc' : 'asc';
|
||||||
|
th.setAttribute('data-dir', dir);
|
||||||
|
rows.sort((a, b) => {{
|
||||||
|
const va = getValue(a.children[index]);
|
||||||
|
const vb = getValue(b.children[index]);
|
||||||
|
if (isNumeric && !isNaN(va) && !isNaN(vb)) {{
|
||||||
|
return dir === 'asc' ? va - vb : vb - va;
|
||||||
|
}}
|
||||||
|
return dir === 'asc'
|
||||||
|
? String(va).localeCompare(String(vb))
|
||||||
|
: String(vb).localeCompare(String(va));
|
||||||
|
}});
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
rows.forEach(r => tbody.appendChild(r));
|
||||||
|
}};
|
||||||
|
table.querySelectorAll('th.sortable').forEach((th, idx) => {{
|
||||||
|
th.addEventListener('click', () => toggleSort(idx, th.dataset.type === 'num'));
|
||||||
|
}});
|
||||||
|
}})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
App()
|
||||||
|
CvttAppConfig()
|
||||||
|
PairSelector()
|
||||||
|
App.instance().run()
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
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:
|
||||||
|
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.aggr_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()
|
||||||
@@ -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()
|
|
||||||
@@ -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,
|
||||||
|
|
||||||
@@ -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
@@ -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",
|
||||||
}
|
# }
|
||||||
}
|
}
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
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_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
|
|
||||||
|
|
||||||
MdSummaryCallbackT = Callable[[List[MdSummary]], None]
|
|
||||||
|
|
||||||
class MdSummaryCollector(NamedObject):
|
|
||||||
sender_: RESTSender
|
|
||||||
exch_acct_: str
|
|
||||||
instrument_id_: str
|
|
||||||
interval_sec_: int
|
|
||||||
history_depth_sec_: int
|
|
||||||
|
|
||||||
history_: List[MdSummary]
|
|
||||||
callbacks_: List[MdSummaryCallbackT]
|
|
||||||
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_ = []
|
|
||||||
self.callbacks_ = []
|
|
||||||
self.timer_ = None
|
|
||||||
|
|
||||||
def add_callback(self, cb: MdSummaryCallbackT) -> None:
|
|
||||||
self.callbacks_.append(cb)
|
|
||||||
|
|
||||||
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()
|
|
||||||
)
|
|
||||||
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]:
|
|
||||||
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
|
|
||||||
self.history_ = self.get_history()
|
|
||||||
self.run_callbacks()
|
|
||||||
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:
|
|
||||||
Log.warning(f"{self.fname()}: did not get last update")
|
|
||||||
return
|
|
||||||
if not self.is_empty() and last.ts_ns_ <= self.history_[-1].ts_ns_:
|
|
||||||
Log.info(f"{self.fname()}: Received {last}. Already Have: {self.history_[-1]}")
|
|
||||||
return
|
|
||||||
self.history_.append(last)
|
|
||||||
self.run_callbacks()
|
|
||||||
|
|
||||||
def run_callbacks(self) -> None:
|
|
||||||
[cb(self.history_) for cb in self.callbacks_]
|
|
||||||
|
|
||||||
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"})
|
|
||||||
# config = Config(json_src={"cvtt_base_url": "http://dev-server-02.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,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _calback(history: List[MdSummary]) -> None:
|
|
||||||
Log.info(f"MdSummary Hist Length is {len(history)}. Last summary: {history[-1] if len(history) > 0 else '[]'}")
|
|
||||||
|
|
||||||
mdsc.add_callback(_calback)
|
|
||||||
|
|
||||||
async def __run() -> None:
|
|
||||||
Log.info("Starting...")
|
|
||||||
await mdsc.start()
|
|
||||||
while True:
|
|
||||||
await asyncio.sleep(5)
|
|
||||||
|
|
||||||
asyncio.run(__run())
|
|
||||||
pass
|
|
||||||
@@ -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())
|
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from typing import Dict, Any, List, Optional, Set
|
||||||
|
|
||||||
|
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 NanosT, current_seconds
|
||||||
|
from cvttpy_tools.settings.cvtt_types import InstrumentIdT, IntervalSecT
|
||||||
|
from cvttpy_tools.web.rest_client import RESTSender
|
||||||
|
# ---
|
||||||
|
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, MdSummary, MdSummaryCallbackT
|
||||||
|
from cvttpy_trading.trading.exchange_config import ExchangeAccounts
|
||||||
|
# ---
|
||||||
|
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
Log.info(f"DEBUG *** {self.exch_inst_.base_asset_id_}: {res[-1].tstamp_}")
|
||||||
|
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:
|
||||||
|
ALLOW_LAG_SEC = 1
|
||||||
|
curr_sec = int(current_seconds())
|
||||||
|
return (curr_sec - curr_sec % self.interval_sec_) + self.interval_sec_ + ALLOW_LAG_SEC
|
||||||
|
|
||||||
|
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].aggr_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
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
```python
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Dict
|
||||||
|
import time
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from cvttpy_tools.base import NamedObject
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
@@ -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_tools.web.rest_client import RESTSender
|
||||||
|
# ---
|
||||||
|
from cvttpy_trading.trading.trading_instructions import TradingInstructions
|
||||||
|
# ---
|
||||||
|
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}"
|
||||||
|
)
|
||||||
|
|
||||||
@@ -0,0 +1,391 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from statsmodels.tsa.stattools import adfuller, coint
|
||||||
|
from statsmodels.tsa.vector_ar.vecm import coint_johansen
|
||||||
|
# ---
|
||||||
|
from cvttpy_tools.base import NamedObject
|
||||||
|
from cvttpy_tools.config import Config
|
||||||
|
from cvttpy_tools.logger import Log
|
||||||
|
from cvttpy_tools.timeutils import NanoPerSec, SecPerHour, current_nanoseconds
|
||||||
|
from cvttpy_tools.web.rest_client import RESTSender
|
||||||
|
# ---
|
||||||
|
from cvttpy_trading.trading.instrument import ExchangeInstrument
|
||||||
|
from cvttpy_trading.trading.mkt_data.md_summary import MdTradesAggregate, MdSummary
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class InstrumentQuality(NamedObject):
|
||||||
|
instrument_: ExchangeInstrument
|
||||||
|
record_count_: int
|
||||||
|
latest_tstamp_: Optional[pd.Timestamp]
|
||||||
|
status_: str
|
||||||
|
reason_: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PairStats(NamedObject):
|
||||||
|
instrument_a_: ExchangeInstrument
|
||||||
|
instrument_b_: ExchangeInstrument
|
||||||
|
pvalue_eg_: Optional[float]
|
||||||
|
pvalue_adf_: Optional[float]
|
||||||
|
pvalue_j_: Optional[float]
|
||||||
|
trace_stat_j_: Optional[float]
|
||||||
|
rank_eg_: int = 0
|
||||||
|
rank_adf_: int = 0
|
||||||
|
rank_j_: int = 0
|
||||||
|
composite_rank_: int = 0
|
||||||
|
|
||||||
|
def as_dict(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"instrument_a": self.instrument_a_.instrument_id(),
|
||||||
|
"instrument_b": self.instrument_b_.instrument_id(),
|
||||||
|
"pvalue_eg": self.pvalue_eg_,
|
||||||
|
"pvalue_adf": self.pvalue_adf_,
|
||||||
|
"pvalue_j": self.pvalue_j_,
|
||||||
|
"trace_stat_j": self.trace_stat_j_,
|
||||||
|
"rank_eg": self.rank_eg_,
|
||||||
|
"rank_adf": self.rank_adf_,
|
||||||
|
"rank_j": self.rank_j_,
|
||||||
|
"composite_rank": self.composite_rank_,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DataFetcher(NamedObject):
|
||||||
|
sender_: RESTSender
|
||||||
|
interval_sec_: int
|
||||||
|
history_depth_sec_: int
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
base_url: str,
|
||||||
|
interval_sec: int,
|
||||||
|
history_depth_sec: int,
|
||||||
|
) -> None:
|
||||||
|
self.sender_ = RESTSender(base_url=base_url)
|
||||||
|
self.interval_sec_ = interval_sec
|
||||||
|
self.history_depth_sec_ = history_depth_sec
|
||||||
|
|
||||||
|
def fetch(self, exch_acct: str, inst: ExchangeInstrument) -> List[MdTradesAggregate]:
|
||||||
|
rqst_data = {
|
||||||
|
"exch_acct": exch_acct,
|
||||||
|
"instrument_id": inst.instrument_id(),
|
||||||
|
"interval_sec": self.interval_sec_,
|
||||||
|
"history_depth_sec": self.history_depth_sec_,
|
||||||
|
}
|
||||||
|
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()}: error {response.status_code} for {inst.details_short()}: {response.text}")
|
||||||
|
return []
|
||||||
|
mdsums: List[MdSummary] = MdSummary.from_REST_response(response=response)
|
||||||
|
return [
|
||||||
|
mdsum.create_md_trades_aggregate(
|
||||||
|
exch_acct=exch_acct, exch_inst=inst, interval_sec=self.interval_sec_
|
||||||
|
)
|
||||||
|
for mdsum in mdsums
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class QualityChecker(NamedObject):
|
||||||
|
interval_sec_: int
|
||||||
|
|
||||||
|
def __init__(self, interval_sec: int) -> None:
|
||||||
|
self.interval_sec_ = interval_sec
|
||||||
|
|
||||||
|
def evaluate(self, inst: ExchangeInstrument, aggr: List[MdTradesAggregate]) -> InstrumentQuality:
|
||||||
|
if len(aggr) == 0:
|
||||||
|
return InstrumentQuality(
|
||||||
|
instrument_=inst,
|
||||||
|
record_count_=0,
|
||||||
|
latest_tstamp_=None,
|
||||||
|
status_="FAIL",
|
||||||
|
reason_="no records",
|
||||||
|
)
|
||||||
|
|
||||||
|
aggr_sorted = sorted(aggr, key=lambda a: a.aggr_time_ns_)
|
||||||
|
|
||||||
|
latest_ts = pd.to_datetime(aggr_sorted[-1].aggr_time_ns_, unit="ns", utc=True)
|
||||||
|
now_ts = pd.Timestamp.utcnow()
|
||||||
|
recency_cutoff = now_ts - pd.Timedelta(seconds=2 * self.interval_sec_)
|
||||||
|
if latest_ts <= recency_cutoff:
|
||||||
|
return InstrumentQuality(
|
||||||
|
instrument_=inst,
|
||||||
|
record_count_=len(aggr_sorted),
|
||||||
|
latest_tstamp_=latest_ts,
|
||||||
|
status_="FAIL",
|
||||||
|
reason_=f"stale: latest {latest_ts} <= cutoff {recency_cutoff}",
|
||||||
|
)
|
||||||
|
|
||||||
|
gaps_ok, reason = self._check_gaps(aggr_sorted)
|
||||||
|
status = "PASS" if gaps_ok else "FAIL"
|
||||||
|
return InstrumentQuality(
|
||||||
|
instrument_=inst,
|
||||||
|
record_count_=len(aggr_sorted),
|
||||||
|
latest_tstamp_=latest_ts,
|
||||||
|
status_=status,
|
||||||
|
reason_=reason,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _check_gaps(self, aggr: List[MdTradesAggregate]) -> Tuple[bool, str]:
|
||||||
|
NUM_TRADES_THRESHOLD = 50
|
||||||
|
if len(aggr) < 2:
|
||||||
|
return True, "ok"
|
||||||
|
|
||||||
|
interval_ns = self.interval_sec_ * NanoPerSec
|
||||||
|
for idx in range(1, len(aggr)):
|
||||||
|
prev = aggr[idx - 1]
|
||||||
|
curr = aggr[idx]
|
||||||
|
delta = curr.aggr_time_ns_ - prev.aggr_time_ns_
|
||||||
|
missing_intervals = int(delta // interval_ns) - 1
|
||||||
|
if missing_intervals <= 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
prev_nt = prev.num_trades_
|
||||||
|
next_nt = curr.num_trades_
|
||||||
|
estimate = self._approximate_num_trades(prev_nt, next_nt)
|
||||||
|
if estimate > NUM_TRADES_THRESHOLD:
|
||||||
|
return False, (
|
||||||
|
f"gap of {missing_intervals} interval(s), est num_trades={estimate} > {NUM_TRADES_THRESHOLD}"
|
||||||
|
)
|
||||||
|
return True, "ok"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _approximate_num_trades(prev_nt: int, next_nt: int) -> float:
|
||||||
|
if prev_nt is None and next_nt is None:
|
||||||
|
return 0.0
|
||||||
|
if prev_nt is None:
|
||||||
|
return float(next_nt)
|
||||||
|
if next_nt is None:
|
||||||
|
return float(prev_nt)
|
||||||
|
return (prev_nt + next_nt) / 2.0
|
||||||
|
|
||||||
|
|
||||||
|
class PairAnalyzer(NamedObject):
|
||||||
|
price_field_: str
|
||||||
|
interval_sec_: int
|
||||||
|
|
||||||
|
def __init__(self, price_field: str, interval_sec: int) -> None:
|
||||||
|
self.price_field_ = price_field
|
||||||
|
self.interval_sec_ = interval_sec
|
||||||
|
|
||||||
|
def analyze(self, series: Dict[ExchangeInstrument, pd.DataFrame]) -> List[PairStats]:
|
||||||
|
instruments = list(series.keys())
|
||||||
|
results: List[PairStats] = []
|
||||||
|
for i in range(len(instruments)):
|
||||||
|
for j in range(i + 1, len(instruments)):
|
||||||
|
inst_a = instruments[i]
|
||||||
|
inst_b = instruments[j]
|
||||||
|
df_a = series[inst_a][["tstamp", "price"]].rename(
|
||||||
|
columns={"price": "price_a"}
|
||||||
|
)
|
||||||
|
df_b = series[inst_b][["tstamp", "price"]].rename(
|
||||||
|
columns={"price": "price_b"}
|
||||||
|
)
|
||||||
|
merged = pd.merge(df_a, df_b, on="tstamp", how="inner").sort_values(
|
||||||
|
"tstamp"
|
||||||
|
)
|
||||||
|
stats = self._compute_stats(inst_a, inst_b, merged)
|
||||||
|
if stats:
|
||||||
|
results.append(stats)
|
||||||
|
self._rank(results)
|
||||||
|
return results
|
||||||
|
|
||||||
|
def _compute_stats(
|
||||||
|
self,
|
||||||
|
inst_a: ExchangeInstrument,
|
||||||
|
inst_b: ExchangeInstrument,
|
||||||
|
merged: pd.DataFrame,
|
||||||
|
) -> Optional[PairStats]:
|
||||||
|
if len(merged) < 2:
|
||||||
|
return None
|
||||||
|
px_a = merged["price_a"].astype(float)
|
||||||
|
px_b = merged["price_b"].astype(float)
|
||||||
|
|
||||||
|
std_a = float(px_a.std())
|
||||||
|
std_b = float(px_b.std())
|
||||||
|
if std_a == 0 or std_b == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
z_a = (px_a - float(px_a.mean())) / std_a
|
||||||
|
z_b = (px_b - float(px_b.mean())) / std_b
|
||||||
|
|
||||||
|
p_eg: Optional[float]
|
||||||
|
p_adf: Optional[float]
|
||||||
|
p_j: Optional[float]
|
||||||
|
trace_stat: Optional[float]
|
||||||
|
|
||||||
|
try:
|
||||||
|
p_eg = float(coint(z_a, z_b)[1])
|
||||||
|
except Exception as exc:
|
||||||
|
Log.warning(f"{self.fname()}: EG failed for {inst_a.details_short()}/{inst_b.details_short()}: {exc}")
|
||||||
|
p_eg = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
spread = z_a - z_b
|
||||||
|
p_adf = float(adfuller(spread, maxlag=1, regression="c")[1])
|
||||||
|
except Exception as exc:
|
||||||
|
Log.warning(f"{self.fname()}: ADF failed for {inst_a.details_short()}/{inst_b.details_short()}: {exc}")
|
||||||
|
p_adf = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = np.column_stack([z_a, z_b])
|
||||||
|
res = coint_johansen(data, det_order=0, k_ar_diff=1)
|
||||||
|
trace_stat = float(res.lr1[0])
|
||||||
|
cv10, cv5, cv1 = res.cvt[0]
|
||||||
|
if trace_stat > cv1:
|
||||||
|
p_j = 0.01
|
||||||
|
elif trace_stat > cv5:
|
||||||
|
p_j = 0.05
|
||||||
|
elif trace_stat > cv10:
|
||||||
|
p_j = 0.10
|
||||||
|
else:
|
||||||
|
p_j = 1.0
|
||||||
|
except Exception as exc:
|
||||||
|
Log.warning(f"{self.fname()}: Johansen failed for {inst_a.details_short()}/{inst_b.details_short()}: {exc}")
|
||||||
|
p_j = None
|
||||||
|
trace_stat = None
|
||||||
|
|
||||||
|
return PairStats(
|
||||||
|
instrument_a_=inst_a,
|
||||||
|
instrument_b_=inst_b,
|
||||||
|
pvalue_eg_=p_eg,
|
||||||
|
pvalue_adf_=p_adf,
|
||||||
|
pvalue_j_=p_j,
|
||||||
|
trace_stat_j_=trace_stat,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _rank(self, results: List[PairStats]) -> None:
|
||||||
|
self._assign_ranks(results, key=lambda r: r.pvalue_eg_, attr="rank_eg_")
|
||||||
|
self._assign_ranks(results, key=lambda r: r.pvalue_adf_, attr="rank_adf_")
|
||||||
|
self._assign_ranks(results, key=lambda r: r.pvalue_j_, attr="rank_j_")
|
||||||
|
for res in results:
|
||||||
|
res.composite_rank_ = res.rank_eg_ + res.rank_adf_ + res.rank_j_
|
||||||
|
results.sort(key=lambda r: r.composite_rank_)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _assign_ranks(
|
||||||
|
results: List[PairStats], key, attr: str
|
||||||
|
) -> None:
|
||||||
|
values = [key(r) for r in results]
|
||||||
|
sorted_vals = sorted([v for v in values if v is not None])
|
||||||
|
for res in results:
|
||||||
|
val = key(res)
|
||||||
|
if val is None:
|
||||||
|
setattr(res, attr, len(sorted_vals) + 1)
|
||||||
|
continue
|
||||||
|
rank = 1 + sum(1 for v in sorted_vals if v < val)
|
||||||
|
setattr(res, attr, rank)
|
||||||
|
|
||||||
|
|
||||||
|
class PairSelectionEngine(NamedObject):
|
||||||
|
config_: object
|
||||||
|
instruments_: List[ExchangeInstrument]
|
||||||
|
price_field_: str
|
||||||
|
fetcher_: DataFetcher
|
||||||
|
quality_: QualityChecker
|
||||||
|
analyzer_: PairAnalyzer
|
||||||
|
interval_sec_: int
|
||||||
|
history_depth_sec_: int
|
||||||
|
data_quality_cache_: List[InstrumentQuality]
|
||||||
|
pair_results_cache_: List[PairStats]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
config: Config,
|
||||||
|
instruments: List[ExchangeInstrument],
|
||||||
|
price_field: str,
|
||||||
|
) -> None:
|
||||||
|
self.config_ = config
|
||||||
|
self.instruments_ = instruments
|
||||||
|
self.price_field_ = price_field
|
||||||
|
|
||||||
|
interval_sec = int(config.get_value("interval_sec", 0))
|
||||||
|
history_depth_sec = int(config.get_value("history_depth_hours", 0)) * SecPerHour
|
||||||
|
base_url = config.get_value("cvtt_base_url", None)
|
||||||
|
assert interval_sec > 0, "interval_sec must be > 0"
|
||||||
|
assert history_depth_sec > 0, "history_depth_sec must be > 0"
|
||||||
|
assert base_url, "cvtt_base_url must be set"
|
||||||
|
|
||||||
|
self.fetcher_ = DataFetcher(
|
||||||
|
base_url=base_url,
|
||||||
|
interval_sec=interval_sec,
|
||||||
|
history_depth_sec=history_depth_sec,
|
||||||
|
)
|
||||||
|
self.quality_ = QualityChecker(interval_sec=interval_sec)
|
||||||
|
self.analyzer_ = PairAnalyzer(price_field=price_field, interval_sec=interval_sec)
|
||||||
|
|
||||||
|
self.interval_sec_ = interval_sec
|
||||||
|
self.history_depth_sec_ = history_depth_sec
|
||||||
|
|
||||||
|
self.data_quality_cache_ = []
|
||||||
|
self.pair_results_cache_ = []
|
||||||
|
|
||||||
|
async def run_once(self) -> None:
|
||||||
|
quality_results: List[InstrumentQuality] = []
|
||||||
|
price_series: Dict[ExchangeInstrument, pd.DataFrame] = {}
|
||||||
|
|
||||||
|
for inst in self.instruments_:
|
||||||
|
exch_acct = inst.user_data_.get("exch_acct") or inst.exchange_id_
|
||||||
|
aggr = self.fetcher_.fetch(exch_acct=exch_acct, inst=inst)
|
||||||
|
q = self.quality_.evaluate(inst, aggr)
|
||||||
|
quality_results.append(q)
|
||||||
|
if q.status_ != "PASS":
|
||||||
|
continue
|
||||||
|
df = self._to_dataframe(aggr, inst)
|
||||||
|
if len(df) > 0:
|
||||||
|
price_series[inst] = df
|
||||||
|
self.data_quality_cache_ = quality_results
|
||||||
|
self.pair_results_cache_ = self.analyzer_.analyze(price_series)
|
||||||
|
|
||||||
|
def _to_dataframe(self, aggr: List[MdTradesAggregate], inst: ExchangeInstrument) -> pd.DataFrame:
|
||||||
|
rows: List[Dict[str, Any]] = []
|
||||||
|
for item in aggr:
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"tstamp": pd.to_datetime(item.aggr_time_ns_, unit="ns", utc=True),
|
||||||
|
"price": self._extract_price(item, inst),
|
||||||
|
"num_trades": item.num_trades_,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
df = pd.DataFrame(rows)
|
||||||
|
return df.sort_values("tstamp").reset_index(drop=True)
|
||||||
|
|
||||||
|
def _extract_price(self, aggr: MdTradesAggregate, inst: ExchangeInstrument) -> float:
|
||||||
|
price_field = self.price_field_
|
||||||
|
# MdTradesAggregate inherits hist bar with fields open_, high_, low_, close_, vwap_
|
||||||
|
field_map = {
|
||||||
|
"open": aggr.open_,
|
||||||
|
"high": aggr.high_,
|
||||||
|
"low": aggr.low_,
|
||||||
|
"close": aggr.close_,
|
||||||
|
"vwap": aggr.vwap_,
|
||||||
|
}
|
||||||
|
raw = field_map.get(price_field, aggr.close_)
|
||||||
|
return inst.get_price(raw)
|
||||||
|
|
||||||
|
def sleep_seconds_until_next_cycle(self) -> float:
|
||||||
|
now_ns = current_nanoseconds()
|
||||||
|
interval_ns = self.interval_sec_ * NanoPerSec
|
||||||
|
next_boundary = (now_ns // interval_ns + 1) * interval_ns
|
||||||
|
return max(0.0, (next_boundary - now_ns) / NanoPerSec)
|
||||||
|
|
||||||
|
def quality_dicts(self) -> List[Dict[str, Any]]:
|
||||||
|
res: List[Dict[str, Any]] = []
|
||||||
|
for q in self.data_quality_cache_:
|
||||||
|
res.append(
|
||||||
|
{
|
||||||
|
"instrument": q.instrument_.instrument_id(),
|
||||||
|
"record_count": q.record_count_,
|
||||||
|
"latest_tstamp": q.latest_tstamp_.isoformat() if q.latest_tstamp_ else None,
|
||||||
|
"status": q.status_,
|
||||||
|
"reason": q.reason_,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def pair_dicts(self) -> List[Dict[str, Any]]:
|
||||||
|
return [p.as_dict() for p in self.pair_results_cache_]
|
||||||
@@ -1,356 +1,350 @@
|
|||||||
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 NanosT, SecPerHour, current_nanoseconds, NanoPerSec, format_nanos_utc
|
||||||
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 pairs_trading.lib.pt_strategy.live.ti_sender import TradingInstructionsSender
|
|
||||||
from pairs_trading.lib.pt_strategy.model_data_policy import ModelDataPolicy
|
from pairs_trading.lib.pt_strategy.model_data_policy import ModelDataPolicy
|
||||||
from pairs_trading.lib.pt_strategy.pt_market_data import RealTimeMarketData
|
|
||||||
from pairs_trading.lib.pt_strategy.pt_model import Prediction
|
from pairs_trading.lib.pt_strategy.pt_model import Prediction
|
||||||
from pairs_trading.lib.pt_strategy.trading_pair import PairState, TradingPair
|
from pairs_trading.lib.pt_strategy.trading_pair import LiveTradingPair
|
||||||
|
from pairs_trading.apps.pair_trader import PairTrader
|
||||||
"""
|
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
|
||||||
|
allowed_md_lag_sec_: int
|
||||||
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
config: Dict[str, Any],
|
config: Config,
|
||||||
instruments: List[Dict[str, str]],
|
pairs_trader: PairTrader,
|
||||||
ti_sender: TradingInstructionsSender,
|
|
||||||
):
|
):
|
||||||
|
|
||||||
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"
|
||||||
|
|
||||||
|
self.allowed_md_lag_sec_ = self.config_.get_value("allowed_md_lag_sec", 3)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
market_data_df: pd.DataFrame = self._create_md_df(hist_aggr=hist_aggr)
|
||||||
|
if len(market_data_df) == 0:
|
||||||
|
Log.warning(f"{self.fname()} Unable to create market data df")
|
||||||
|
return
|
||||||
|
|
||||||
async def on_mkt_data_update(self, aggr: JsonDictT) -> None:
|
|
||||||
market_data_df = await self.pt_mkt_data_.on_mkt_data_update(update=aggr)
|
|
||||||
if market_data_df is not None:
|
|
||||||
self.trading_pair_.market_data_ = market_data_df
|
self.trading_pair_.market_data_ = 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(
|
prediction = self.trading_pair_.run(
|
||||||
market_data_df, self.model_data_policy_.advance()
|
market_data_df, self.model_data_policy_.advance()
|
||||||
)
|
)
|
||||||
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
|
||||||
)
|
)
|
||||||
|
|
||||||
trading_instructions: List[TradingInstruction] = (
|
trading_instructions: List[TradingInstructions] = (
|
||||||
self._create_trading_instructions(
|
self._create_trading_instructions(
|
||||||
prediction=prediction, last_row=market_data_df.iloc[-1]
|
prediction=prediction, last_row=market_data_df.iloc[-1]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if len(trading_instructions) > 0:
|
if trading_instructions is not None:
|
||||||
await self._send_trading_instructions(trading_instructions)
|
await self._send_trading_instructions(trading_instructions)
|
||||||
# trades = self._create_trades(prediction=prediction, last_row=market_data_df.iloc[-1])
|
|
||||||
# URGENT implement this
|
def _is_md_actual(self, hist_aggr: List[MdTradesAggregate]) -> bool:
|
||||||
pass
|
if len(hist_aggr) == 0:
|
||||||
|
Log.warning(f"{self.fname()} list of aggregates IS EMPTY")
|
||||||
|
return False
|
||||||
|
|
||||||
|
curr_ns = current_nanoseconds()
|
||||||
|
|
||||||
|
# MAYBE check market data length
|
||||||
|
|
||||||
|
# at 18:05:01 we should see data for 18:04:00
|
||||||
|
lag_sec = (curr_ns - hist_aggr[-1].aggr_time_ns_) / NanoPerSec - self.interval_sec()
|
||||||
|
if lag_sec > self.allowed_md_lag_sec_:
|
||||||
|
Log.warning(
|
||||||
|
f"{self.fname()} {hist_aggr[-1].exch_inst_.details_short()}"
|
||||||
|
f" Lagging {int(lag_sec)} > {self.allowed_md_lag_sec_} seconds:"
|
||||||
|
f"\n{len(hist_aggr)} records"
|
||||||
|
f"\n{hist_aggr[-1].exch_inst_.base_asset_id_}: {hist_aggr[-1].tstamp()}"
|
||||||
|
f"\n{hist_aggr[-2].exch_inst_.base_asset_id_}: {hist_aggr[-2].tstamp()}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
Log.info(
|
||||||
|
f"{self.fname()} {hist_aggr[-1].exch_inst_.details_short()}"
|
||||||
|
f" Lag {int(lag_sec)} <= {self.allowed_md_lag_sec_} seconds"
|
||||||
|
f"\n{len(hist_aggr)} records"
|
||||||
|
f"\n{hist_aggr[-1].exch_inst_.base_asset_id_}: {hist_aggr[-1].tstamp()}"
|
||||||
|
f"\n{hist_aggr[-2].exch_inst_.base_asset_id_}: {hist_aggr[-2].tstamp()}"
|
||||||
|
)
|
||||||
|
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.aggr_time_ns_, unit="ns", utc=True),
|
||||||
|
"time_ns": aggr.aggr_time_ns_,
|
||||||
|
"symbol": exch_inst.instrument_id().split("-", 1)[1],
|
||||||
|
"exchange_id": exch_inst.exchange_id_,
|
||||||
|
"instrument_id": exch_inst.instrument_id(),
|
||||||
|
"open": exch_inst.get_price(aggr.open_),
|
||||||
|
"high": exch_inst.get_price(aggr.high_),
|
||||||
|
"low": exch_inst.get_price(aggr.low_),
|
||||||
|
"close": exch_inst.get_price(aggr.close_),
|
||||||
|
"volume": exch_inst.get_quantity(aggr.volume_),
|
||||||
|
"num_trades": aggr.num_trades_,
|
||||||
|
"vwap": exch_inst.get_price(aggr.vwap_),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
source_md_df = pd.DataFrame(
|
||||||
|
rows,
|
||||||
|
columns=[
|
||||||
|
"tstamp",
|
||||||
|
"time_ns",
|
||||||
|
"symbol",
|
||||||
|
"exchange_id",
|
||||||
|
"instrument_id",
|
||||||
|
"open",
|
||||||
|
"high",
|
||||||
|
"low",
|
||||||
|
"close",
|
||||||
|
"volume",
|
||||||
|
"num_trades",
|
||||||
|
"vwap",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# automatic sorting
|
||||||
|
source_md_df.sort_values(
|
||||||
|
by=["time_ns", "symbol"],
|
||||||
|
ascending=True,
|
||||||
|
inplace=True,
|
||||||
|
kind="mergesort", # stable sort
|
||||||
|
)
|
||||||
|
|
||||||
|
source_md_df.reset_index(drop=True, inplace=True)
|
||||||
|
|
||||||
|
pt_mkt_data = LiveMarketData(config=self.config_, instruments=self.instruments_)
|
||||||
|
pt_mkt_data.origin_mkt_data_df_ = source_md_df
|
||||||
|
pt_mkt_data.set_market_data()
|
||||||
|
|
||||||
|
return pt_mkt_data.market_data_df_
|
||||||
|
|
||||||
|
def interval_sec(self) -> IntervalSecT:
|
||||||
|
return self.interval_sec_
|
||||||
|
|
||||||
|
def history_depth_sec(self) -> IntervalSecT:
|
||||||
|
return self.history_depth_sec_
|
||||||
|
|
||||||
async def _send_trading_instructions(
|
async def _send_trading_instructions(
|
||||||
self, trading_instructions: List[TradingInstruction]
|
self, trading_instructions: List[TradingInstructions]
|
||||||
) -> None:
|
) -> None:
|
||||||
pass # URGENT implement _send_trading_instructions
|
for ti in trading_instructions:
|
||||||
|
Log.info(f"{self.fname()} Sending trading instructions {ti}")
|
||||||
|
await self.pairs_trader_.ti_sender_.send_trading_instructions(ti)
|
||||||
|
|
||||||
def _create_trading_instructions(
|
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():
|
|
||||||
if abs_scaled_disequilibrium <= self.close_threshold_:
|
elif abs_scaled_disequilibrium <= self.close_threshold_ or pair.to_stop_close_conditions(predicted_row=last_row):
|
||||||
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]:
|
||||||
|
diseqlbrm = prediction.disequilibrium_
|
||||||
scaled_disequilibrium = prediction.scaled_disequilibrium_
|
scaled_disequilibrium = prediction.scaled_disequilibrium_
|
||||||
|
if diseqlbrm > 0:
|
||||||
if scaled_disequilibrium > 0:
|
side_a = -1
|
||||||
side_a = "SELL"
|
side_b = 1
|
||||||
trd_inst_a = TradingInstruction(
|
|
||||||
type=TradingInstructionType.TARGET_POSITION,
|
|
||||||
exch_instr=pair.get_instrument_a(),
|
|
||||||
specifics={"side": "SELL", "strength": -1},
|
|
||||||
)
|
|
||||||
side_b = "BUY"
|
|
||||||
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
|
|
||||||
df.loc[len(df)] = {
|
|
||||||
"time": tstamp,
|
|
||||||
"symbol": pair.symbol_a_,
|
|
||||||
"side": side_a,
|
|
||||||
"action": "OPEN",
|
|
||||||
"price": px_a,
|
|
||||||
"disequilibrium": diseqlbrm,
|
|
||||||
"signed_scaled_disequilibrium": scaled_disequilibrium,
|
|
||||||
"scaled_disequilibrium": abs(scaled_disequilibrium),
|
|
||||||
# "pair": pair,
|
|
||||||
}
|
|
||||||
df.loc[len(df)] = {
|
|
||||||
"time": tstamp,
|
|
||||||
"symbol": pair.symbol_b_,
|
|
||||||
"side": side_b,
|
|
||||||
"action": "OPEN",
|
|
||||||
"price": px_b,
|
|
||||||
"disequilibrium": diseqlbrm,
|
|
||||||
"scaled_disequilibrium": abs(scaled_disequilibrium),
|
|
||||||
"signed_scaled_disequilibrium": scaled_disequilibrium,
|
|
||||||
# "pair": pair,
|
|
||||||
}
|
|
||||||
return df
|
|
||||||
|
|
||||||
def _create_close_trade_instructions(
|
def _create_close_trade_instructions(
|
||||||
self, pair: TradingPair, row: pd.Series #, prediction: Prediction
|
self, pair: LiveTradingPair, row: pd.Series
|
||||||
) -> List[TradingInstruction]:
|
) -> List[TradingInstructions]:
|
||||||
return [] # URGENT implement _create_close_trade_instructions
|
ti_a: Optional[TradingInstructions] = TradingInstructions(
|
||||||
|
book=self.pairs_trader_.book_id_,
|
||||||
def _handle_outstanding_positions(self) -> Optional[pd.DataFrame]:
|
strategy_id=self.__class__.__name__,
|
||||||
trades = None
|
ti_type=TradingInstructions.Type.TARGET_POSITION,
|
||||||
pair = self.trading_pair_
|
issued_ts_ns=current_nanoseconds(),
|
||||||
|
data=TargetPositionSignal(
|
||||||
# Outstanding positions
|
strength=0,
|
||||||
if pair.user_data_["state"] == PairState.OPEN:
|
exchange_id=pair.get_instrument_a().exchange_id_,
|
||||||
print(f"{pair}: *** Position is NOT CLOSED. ***")
|
base_asset=pair.get_instrument_a().base_asset_id_,
|
||||||
# outstanding positions
|
quote_asset=pair.get_instrument_a().quote_asset_id_,
|
||||||
if self.config_["close_outstanding_positions"]:
|
user_data={}
|
||||||
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:
|
if not ti_a:
|
||||||
trades["status"] = PairState.CLOSE_POSITION.name
|
return []
|
||||||
print(f"CLOSE_POSITION TRADES:\n{trades}")
|
ti_b: Optional[TradingInstructions] = TradingInstructions(
|
||||||
pair.user_data_["state"] = PairState.CLOSE_POSITION
|
book=self.pairs_trader_.book_id_,
|
||||||
pair.on_close_trades(trades)
|
strategy_id=self.__class__.__name__,
|
||||||
else:
|
ti_type=TradingInstructions.Type.TARGET_POSITION,
|
||||||
pair.add_outstanding_position(
|
issued_ts_ns=current_nanoseconds(),
|
||||||
symbol=pair.symbol_a_,
|
data=TargetPositionSignal(
|
||||||
open_side=pair.user_data_["open_side_a"],
|
strength=0,
|
||||||
open_px=pair.user_data_["open_px_a"],
|
exchange_id=pair.get_instrument_b().exchange_id_,
|
||||||
open_tstamp=pair.user_data_["open_tstamp"],
|
base_asset=pair.get_instrument_b().base_asset_id_,
|
||||||
last_mkt_data_row=pair.market_data_.iloc[-1],
|
quote_asset=pair.get_instrument_b().quote_asset_id_,
|
||||||
|
user_data={}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
pair.add_outstanding_position(
|
if not ti_b:
|
||||||
symbol=pair.symbol_b_,
|
return []
|
||||||
open_side=pair.user_data_["open_side_b"],
|
return [ti_a, ti_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_
|
|
||||||
scaled_disequilibrium = prediction.scaled_disequilibrium_
|
|
||||||
px_a = row[f"{colname_a}"]
|
|
||||||
px_b = row[f"{colname_b}"]
|
|
||||||
|
|
||||||
# creating the trades
|
|
||||||
df = self._trades_df()
|
|
||||||
|
|
||||||
print(f"OPEN_TRADES: {row["tstamp"]} {scaled_disequilibrium=}")
|
|
||||||
if diseqlbrm > 0:
|
|
||||||
side_a = "SELL"
|
|
||||||
side_b = "BUY"
|
|
||||||
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 _create_close_trades(
|
|
||||||
self, pair: TradingPair, row: pd.Series, prediction: Optional[Prediction] = None
|
|
||||||
) -> Optional[pd.DataFrame]:
|
|
||||||
colname_a, colname_b = pair.exec_prices_colnames()
|
|
||||||
|
|
||||||
tstamp = row["tstamp"]
|
|
||||||
if prediction is not None:
|
|
||||||
diseqlbrm = prediction.disequilibrium_
|
|
||||||
signed_scaled_disequilibrium = prediction.scaled_disequilibrium_
|
|
||||||
scaled_disequilibrium = abs(prediction.scaled_disequilibrium_)
|
|
||||||
else:
|
|
||||||
diseqlbrm = 0.0
|
|
||||||
signed_scaled_disequilibrium = 0.0
|
|
||||||
scaled_disequilibrium = 0.0
|
|
||||||
px_a = row[f"{colname_a}"]
|
|
||||||
px_b = row[f"{colname_b}"]
|
|
||||||
|
|
||||||
# creating the trades
|
|
||||||
df = self._trades_df()
|
|
||||||
|
|
||||||
# create opening trades
|
|
||||||
df.loc[len(df)] = {
|
|
||||||
"time": tstamp,
|
|
||||||
"symbol": pair.symbol_a_,
|
|
||||||
"side": pair.user_data_["close_side_a"],
|
|
||||||
"action": "CLOSE",
|
|
||||||
"price": px_a,
|
|
||||||
"disequilibrium": diseqlbrm,
|
|
||||||
"scaled_disequilibrium": scaled_disequilibrium,
|
|
||||||
"signed_scaled_disequilibrium": signed_scaled_disequilibrium,
|
|
||||||
# "pair": pair,
|
|
||||||
}
|
|
||||||
df.loc[len(df)] = {
|
|
||||||
"time": tstamp,
|
|
||||||
"symbol": pair.symbol_b_,
|
|
||||||
"side": pair.user_data_["close_side_b"],
|
|
||||||
"action": "CLOSE",
|
|
||||||
"price": px_b,
|
|
||||||
"disequilibrium": diseqlbrm,
|
|
||||||
"scaled_disequilibrium": scaled_disequilibrium,
|
|
||||||
"signed_scaled_disequilibrium": signed_scaled_disequilibrium,
|
|
||||||
# "pair": pair,
|
|
||||||
}
|
|
||||||
del pair.user_data_["close_side_a"]
|
|
||||||
del pair.user_data_["close_side_b"]
|
|
||||||
|
|
||||||
del pair.user_data_["open_tstamp"]
|
|
||||||
del pair.user_data_["open_px_a"]
|
|
||||||
del pair.user_data_["open_px_b"]
|
|
||||||
del pair.user_data_["open_side_a"]
|
|
||||||
del pair.user_data_["open_side_b"]
|
|
||||||
return df
|
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
```python
|
|
||||||
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 pairs_trading.lib.pt_strategy.live.live_strategy import PtLiveStrategy
|
|
||||||
from pairs_trading.lib.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()
|
|
||||||
```
|
|
||||||
@@ -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)
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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
|
||||||
|
if not self.is_real_time_:
|
||||||
print(self.count_, end="\r")
|
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("min_training_size"))
|
self.min_training_size_ = cast(int, config.get_value("model/min_training_size"))
|
||||||
self.max_training_size_ = cast(int, config.get("max_training_size"))
|
self.max_training_size_ = cast(int, config.get_value("model/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.")
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
+148
-154
@@ -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,73 +111,61 @@ 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()
|
|
||||||
|
|
||||||
def _set_market_data(self, ) -> None:
|
|
||||||
if self.is_execution_price_:
|
|
||||||
self.market_data_df_ = pd.DataFrame(
|
|
||||||
self._transform_dataframe(self.origin_mkt_data_df_)[["tstamp"] + self.colnames() + self.orig_exec_prices_colnames()]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.market_data_df_ = pd.DataFrame(
|
|
||||||
self._transform_dataframe(self.origin_mkt_data_df_)[["tstamp"] + self.colnames()]
|
|
||||||
)
|
|
||||||
|
|
||||||
self.market_data_df_ = self.market_data_df_.dropna().reset_index(drop=True)
|
|
||||||
self.market_data_df_["tstamp"] = pd.to_datetime(self.market_data_df_["tstamp"])
|
|
||||||
self.market_data_df_ = self.market_data_df_.sort_values("tstamp")
|
|
||||||
self._set_execution_price_data()
|
|
||||||
|
|
||||||
def _transform_dataframe(self, df: pd.DataFrame) -> pd.DataFrame:
|
|
||||||
df_selected: pd.DataFrame
|
|
||||||
if self.is_execution_price_:
|
|
||||||
execution_price_column = self.config_["execution_price"]["column"]
|
|
||||||
|
|
||||||
df_selected = pd.DataFrame(
|
|
||||||
df[["tstamp", "symbol", self.stat_model_price_, execution_price_column]]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
df_selected = pd.DataFrame(
|
|
||||||
df[["tstamp", "symbol", self.stat_model_price_]]
|
|
||||||
)
|
|
||||||
|
|
||||||
result_df = pd.DataFrame(df_selected["tstamp"]).drop_duplicates().reset_index(drop=True)
|
|
||||||
|
|
||||||
# For each unique symbol, add a corresponding stat_model_price column
|
|
||||||
symbols = df_selected["symbol"].unique()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
for symbol in symbols:
|
|
||||||
# Filter rows for this symbol
|
|
||||||
df_symbol = df_selected[df_selected["symbol"] == symbol].reset_index(
|
|
||||||
drop=True
|
drop=True
|
||||||
)
|
)
|
||||||
|
self.set_market_data()
|
||||||
|
self._set_execution_price_data()
|
||||||
|
|
||||||
# Create column name like "close-COIN"
|
def _set_execution_price_data(self) -> None:
|
||||||
|
if not self.is_execution_price_:
|
||||||
|
return
|
||||||
|
if not self.config_.key_exists("execution_price"):
|
||||||
|
self.market_data_df_[f"exec_price_{self.symbol_a_}"] = self.market_data_df_[
|
||||||
|
f"{self.stat_model_price_}_{self.symbol_a_}"
|
||||||
|
]
|
||||||
|
self.market_data_df_[f"exec_price_{self.symbol_b_}"] = self.market_data_df_[
|
||||||
|
f"{self.stat_model_price_}_{self.symbol_b_}"
|
||||||
|
]
|
||||||
|
return
|
||||||
|
execution_price_column = self.config_.get_value("execution_price")["column"]
|
||||||
|
execution_price_shift = self.config_.get_value("execution_price")["shift"]
|
||||||
|
self.market_data_df_[f"exec_price_{self.symbol_a_}"] = self.market_data_df_[
|
||||||
|
f"{execution_price_column}_{self.symbol_a_}"
|
||||||
|
].shift(-execution_price_shift)
|
||||||
|
self.market_data_df_[f"exec_price_{self.symbol_b_}"] = self.market_data_df_[
|
||||||
|
f"{execution_price_column}_{self.symbol_b_}"
|
||||||
|
].shift(-execution_price_shift)
|
||||||
|
self.market_data_df_ = self.market_data_df_.dropna().reset_index(drop=True)
|
||||||
|
|
||||||
|
def md_columns(self) -> List[str]:
|
||||||
|
# @abstractmethod
|
||||||
|
if self.is_execution_price_:
|
||||||
|
return ["tstamp", "symbol", self.stat_model_price_, self.execution_price_column_]
|
||||||
|
else:
|
||||||
|
return ["tstamp", "symbol", self.stat_model_price_]
|
||||||
|
|
||||||
|
def rename_columns(self, selected_symbol_df: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
# @abstractmethod
|
||||||
|
symbol = selected_symbol_df.iloc[0]["symbol"]
|
||||||
new_price_column = f"{self.stat_model_price_}_{symbol}"
|
new_price_column = f"{self.stat_model_price_}_{symbol}"
|
||||||
if self.is_execution_price_:
|
if self.is_execution_price_:
|
||||||
new_execution_price_column = f"{self.execution_price_column_}_{symbol}"
|
new_execution_price_column = f"{self.execution_price_column_}_{symbol}"
|
||||||
@@ -117,113 +173,51 @@ class ResearchMarketData(PtMarketData):
|
|||||||
# Create temporary dataframe with timestamp and price
|
# Create temporary dataframe with timestamp and price
|
||||||
temp_df = pd.DataFrame(
|
temp_df = pd.DataFrame(
|
||||||
{
|
{
|
||||||
"tstamp": df_symbol["tstamp"],
|
"tstamp": selected_symbol_df["tstamp"],
|
||||||
new_price_column: df_symbol[self.stat_model_price_],
|
new_price_column: selected_symbol_df[self.stat_model_price_],
|
||||||
new_execution_price_column: df_symbol[execution_price_column],
|
new_execution_price_column: selected_symbol_df[self.execution_price_column_],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
temp_df = pd.DataFrame(
|
temp_df = pd.DataFrame(
|
||||||
{
|
{
|
||||||
"tstamp": df_symbol["tstamp"],
|
"tstamp": selected_symbol_df["tstamp"],
|
||||||
new_price_column: df_symbol[self.stat_model_price_],
|
new_price_column: selected_symbol_df[self.stat_model_price_],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
return temp_df
|
||||||
|
|
||||||
# Join with our result dataframe
|
def tranform_df_target_colnames(self):
|
||||||
result_df = pd.merge(result_df, temp_df, on="tstamp", how="left")
|
# @abstractmethod
|
||||||
result_df = result_df.reset_index(
|
return self.colnames() + self.orig_exec_prices_colnames()
|
||||||
drop=True
|
|
||||||
) # do not dropna() since irrelevant symbol would affect dataset
|
|
||||||
|
|
||||||
return result_df.dropna()
|
|
||||||
|
|
||||||
def _set_execution_price_data(self) -> None:
|
|
||||||
if "execution_price" not in self.config_:
|
|
||||||
self.market_data_df_[f"exec_price_{self.symbol_a_}"] = self.market_data_df_[f"{self.stat_model_price_}_{self.symbol_a_}"]
|
|
||||||
self.market_data_df_[f"exec_price_{self.symbol_b_}"] = self.market_data_df_[f"{self.stat_model_price_}_{self.symbol_b_}"]
|
|
||||||
return
|
|
||||||
execution_price_column = self.config_["execution_price"]["column"]
|
|
||||||
execution_price_shift = self.config_["execution_price"]["shift"]
|
|
||||||
self.market_data_df_[f"exec_price_{self.symbol_a_}"] = self.market_data_df_[f"{execution_price_column}_{self.symbol_a_}"].shift(-execution_price_shift)
|
|
||||||
self.market_data_df_[f"exec_price_{self.symbol_b_}"] = self.market_data_df_[f"{execution_price_column}_{self.symbol_b_}"].shift(-execution_price_shift)
|
|
||||||
self.market_data_df_ = self.market_data_df_.dropna().reset_index(drop=True)
|
|
||||||
|
|
||||||
def colnames(self) -> List[str]:
|
|
||||||
return [
|
|
||||||
f"{self.stat_model_price_}_{self.symbol_a_}",
|
|
||||||
f"{self.stat_model_price_}_{self.symbol_b_}",
|
|
||||||
]
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
'''
|
)
|
||||||
|
return temp_df
|
||||||
|
|
||||||
|
def tranform_df_target_colnames(self):
|
||||||
pass
|
# @abstractmethod
|
||||||
|
return self.colnames()
|
||||||
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 pd.DataFrame()
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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({
|
||||||
|
|||||||
@@ -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,7 +201,8 @@ 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,
|
"symbol": symbol,
|
||||||
"open_side": open_side,
|
"open_side": open_side,
|
||||||
"open_px": open_px,
|
"open_px": open_px,
|
||||||
@@ -188,12 +211,16 @@ class TradingPair:
|
|||||||
"last_px": last_px,
|
"last_px": last_px,
|
||||||
"last_tstamp": last_mkt_data_row["tstamp"],
|
"last_tstamp": last_mkt_data_row["tstamp"],
|
||||||
"last_value": last_px * shares,
|
"last_value": last_px * shares,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def run(self, market_data: pd.DataFrame, data_params: DataWindowParams) -> Prediction: # type: ignore[assignment]
|
class LiveTradingPair(TradingPair):
|
||||||
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 __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
@@ -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:
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+67
-34
@@ -1,53 +1,67 @@
|
|||||||
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."
|
||||||
parser = argparse.ArgumentParser(description="Run pairs trading backtest.")
|
# )
|
||||||
parser.add_argument(
|
App.instance().add_cmdline_arg(
|
||||||
"--config", type=str, required=True, help="Path to the configuration file."
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--date_pattern",
|
"--date_pattern",
|
||||||
type=str,
|
type=str,
|
||||||
required=True,
|
required=True,
|
||||||
help="Date YYYYMMDD, allows * and ? wildcards",
|
help="Date YYYYMMDD, allows * and ? wildcards",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
App.instance().add_cmdline_arg(
|
||||||
"--instruments",
|
"--instruments",
|
||||||
type=str,
|
type=str,
|
||||||
required=True,
|
required=True,
|
||||||
help="Comma-separated list of instrument symbols (e.g., COIN:EQUITY,GBTC:CRYPTO)",
|
help="Comma-separated list of instrument symbols (e.g., COIN:EQUITY,GBTC:CRYPTO)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
App.instance().add_cmdline_arg(
|
||||||
"--result_db",
|
"--result_db",
|
||||||
type=str,
|
type=str,
|
||||||
required=True,
|
required=True,
|
||||||
help="Path to SQLite database for storing results. Use 'NONE' to disable database output.",
|
help="Path to SQLite database for storing results. Use 'NONE' to disable database output.",
|
||||||
)
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
App.instance().add_call(stage=App.Stage.Config, func=self._on_config())
|
||||||
|
App.instance().add_call(stage=App.Stage.Run, func=self.run())
|
||||||
config: Dict = load_config(args.config)
|
|
||||||
|
|
||||||
|
async def _on_config(self) -> None:
|
||||||
# Resolve data files (CLI takes priority over config)
|
# Resolve data files (CLI takes priority over config)
|
||||||
instruments = get_instruments(args, config)
|
instruments: List[ExchangeInstrument] = self._get_instruments()
|
||||||
datafiles = resolve_datafiles(config, args.date_pattern, instruments)
|
datafiles = resolve_datafiles(
|
||||||
|
config=CvttAppConfig.instance(),
|
||||||
|
date_pattern=App.instance().get_argument("date_pattern"),
|
||||||
|
instruments=instruments,
|
||||||
|
)
|
||||||
|
|
||||||
days = list(set([day for day, _ in datafiles]))
|
days = list(set([day for day, _ in datafiles]))
|
||||||
print(f"Found {len(datafiles)} data files to process:")
|
print(f"Found {len(datafiles)} data files to process:")
|
||||||
@@ -55,35 +69,36 @@ def main() -> None:
|
|||||||
print(f" - {df}")
|
print(f" - {df}")
|
||||||
|
|
||||||
# Create result database if needed
|
# Create result database if needed
|
||||||
if args.result_db.upper() != "NONE":
|
if App.instance().get_argument("result_db").upper() != "NONE":
|
||||||
args.result_db = expand_filename(args.result_db)
|
create_result_database(App.instance().get_argument("result_db"))
|
||||||
create_result_database(args.result_db)
|
|
||||||
|
|
||||||
# Initialize a dictionary to store all trade results
|
# Initialize a dictionary to store all trade results
|
||||||
all_results: Dict[str, Dict[str, Any]] = {}
|
all_results: Dict[str, Dict[str, Any]] = {}
|
||||||
is_config_stored = False
|
is_config_stored = False
|
||||||
# Process each data file
|
# Process each data file
|
||||||
|
|
||||||
results = PairResearchResult(config=config)
|
results = PairResearchResult(config=CvttAppConfig.instance())
|
||||||
for day in sorted(days):
|
for day in sorted(days):
|
||||||
md_datafiles = [datafile for md_day, datafile in datafiles if md_day == day]
|
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]):
|
if not all([os.path.exists(datafile) for datafile in md_datafiles]):
|
||||||
print(f"WARNING: insufficient data files: {md_datafiles}")
|
print(f"WARNING: insufficient data files: {md_datafiles}")
|
||||||
continue
|
exit(1)
|
||||||
print(f"\n====== Processing {day} ======")
|
print(f"\n====== Processing {day} ======")
|
||||||
|
|
||||||
if not is_config_stored:
|
if not is_config_stored:
|
||||||
store_config_in_database(
|
store_config_in_database(
|
||||||
db_path=args.result_db,
|
db_path=App.instance().get_argument("result_db"),
|
||||||
config_file_path=args.config,
|
config_file_path=App.instance().get_argument("config"),
|
||||||
config=config,
|
config=CvttAppConfig.instance(),
|
||||||
datafiles=datafiles,
|
datafiles=datafiles,
|
||||||
instruments=instruments,
|
instruments=instruments,
|
||||||
)
|
)
|
||||||
is_config_stored = True
|
is_config_stored = True
|
||||||
|
|
||||||
|
CvttAppConfig.instance().set_value("datafiles", md_datafiles)
|
||||||
pt_strategy = PtResearchStrategy(
|
pt_strategy = PtResearchStrategy(
|
||||||
config=config, datafiles=md_datafiles, instruments=instruments
|
config=CvttAppConfig.instance(),
|
||||||
|
instruments=instruments,
|
||||||
)
|
)
|
||||||
pt_strategy.run()
|
pt_strategy.run()
|
||||||
results.add_day_results(
|
results.add_day_results(
|
||||||
@@ -92,15 +107,33 @@ def main() -> None:
|
|||||||
outstanding_positions=pt_strategy.outstanding_positions(),
|
outstanding_positions=pt_strategy.outstanding_positions(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
results.analyze_pair_performance()
|
results.analyze_pair_performance()
|
||||||
|
|
||||||
|
def _get_instruments(self) -> List[ExchangeInstrument]:
|
||||||
|
res: List[ExchangeInstrument] = []
|
||||||
|
|
||||||
if args.result_db.upper() != "NONE":
|
for inst in App.instance().get_argument("instruments").split(","):
|
||||||
print(f"\nResults stored in database: {args.result_db}")
|
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
|
||||||
|
|
||||||
|
async def run(self) -> None:
|
||||||
|
|
||||||
|
if App.instance().get_argument("result_db").upper() != "NONE":
|
||||||
|
print(
|
||||||
|
f'\nResults stored in database: {App.instance().get_argument("result_db")}'
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
print("No results to display.")
|
print("No results to display.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
Runner()
|
||||||
|
App.instance().run()
|
||||||
|
|||||||
+5188
-4519
File diff suppressed because one or more lines are too long
@@ -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
|
|
||||||
@@ -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()
|
|
||||||
Reference in New Issue
Block a user