Qubx 0.0.1__cp311-cp311-manylinux_2_35_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of Qubx might be problematic. Click here for more details.
- qubx/__init__.py +164 -0
- qubx/_nb_magic.py +69 -0
- qubx/core/__init__.py +0 -0
- qubx/core/basics.py +224 -0
- qubx/core/lookups.py +152 -0
- qubx/core/series.cpython-311-x86_64-linux-gnu.so +0 -0
- qubx/core/series.pxd +94 -0
- qubx/core/series.pyx +763 -0
- qubx/core/strategy.py +89 -0
- qubx/core/utils.cpython-311-x86_64-linux-gnu.so +0 -0
- qubx/core/utils.pyx +54 -0
- qubx/data/readers.py +387 -0
- qubx/math/__init__.py +1 -0
- qubx/math/stats.py +42 -0
- qubx/ta/__init__.py +0 -0
- qubx/ta/indicators.cpython-311-x86_64-linux-gnu.so +0 -0
- qubx/ta/indicators.pyx +258 -0
- qubx/utils/__init__.py +3 -0
- qubx/utils/_pyxreloader.py +271 -0
- qubx/utils/charting/mpl_helpers.py +182 -0
- qubx/utils/marketdata/binance.py +212 -0
- qubx/utils/misc.py +234 -0
- qubx/utils/pandas.py +206 -0
- qubx/utils/time.py +145 -0
- qubx-0.0.1.dist-info/METADATA +39 -0
- qubx-0.0.1.dist-info/RECORD +27 -0
- qubx-0.0.1.dist-info/WHEEL +4 -0
qubx/__init__.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
from qubx.utils import set_mpl_theme, runtime_env, install_pyx_recompiler_for_dev
|
|
2
|
+
install_pyx_recompiler_for_dev()
|
|
3
|
+
|
|
4
|
+
from loguru import logger
|
|
5
|
+
import os, sys, stackprinter
|
|
6
|
+
from qubx.core.lookups import InstrumentsLookup
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def formatter(record):
|
|
10
|
+
end = record["extra"].get("end", "\n")
|
|
11
|
+
fmt = "<lvl>{message}</lvl>%s" % end
|
|
12
|
+
if record["level"].name in {"WARNING", "SNAKY"}:
|
|
13
|
+
fmt = "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - %s" % fmt
|
|
14
|
+
|
|
15
|
+
prefix = "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> [ <level>%s</level> ] " % record["level"].icon
|
|
16
|
+
|
|
17
|
+
if record["exception"] is not None:
|
|
18
|
+
# stackprinter.set_excepthook(style='darkbg2')
|
|
19
|
+
record["extra"]["stack"] = stackprinter.format(record["exception"], style="darkbg")
|
|
20
|
+
fmt += "\n{extra[stack]}\n"
|
|
21
|
+
|
|
22
|
+
if record["level"].name in {"TEXT"}:
|
|
23
|
+
prefix = ""
|
|
24
|
+
|
|
25
|
+
return prefix + fmt
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
config = {
|
|
29
|
+
"handlers": [ {"sink": sys.stdout, "format": "{time} - {message}"}, ],
|
|
30
|
+
"extra": {"user": "someone"},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
logger.configure(**config)
|
|
35
|
+
logger.remove(None)
|
|
36
|
+
logger.add(sys.stdout, format=formatter, colorize=True)
|
|
37
|
+
logger = logger.opt(colors=True)
|
|
38
|
+
|
|
39
|
+
lookup = InstrumentsLookup()
|
|
40
|
+
|
|
41
|
+
# registering magic for jupyter notebook
|
|
42
|
+
if runtime_env() in ['notebook', 'shell']:
|
|
43
|
+
from IPython.core.magic import (Magics, magics_class, line_magic, line_cell_magic)
|
|
44
|
+
from IPython import get_ipython
|
|
45
|
+
|
|
46
|
+
@magics_class
|
|
47
|
+
class QubxMagics(Magics):
|
|
48
|
+
# process data manager
|
|
49
|
+
__manager = None
|
|
50
|
+
|
|
51
|
+
@line_magic
|
|
52
|
+
def qubxd(self, line: str):
|
|
53
|
+
self.qubx_setup('dark')
|
|
54
|
+
|
|
55
|
+
@line_magic
|
|
56
|
+
def qubxl(self, line: str):
|
|
57
|
+
self.qubx_setup('light')
|
|
58
|
+
|
|
59
|
+
@line_magic
|
|
60
|
+
def qubx_setup(self, line: str):
|
|
61
|
+
"""
|
|
62
|
+
QUBE framework initialization
|
|
63
|
+
"""
|
|
64
|
+
import os
|
|
65
|
+
|
|
66
|
+
tpl_path = os.path.join(os.path.dirname(__file__), "_nb_magic.py")
|
|
67
|
+
# print("TPL:", tpl_path)
|
|
68
|
+
with open(tpl_path, 'r', encoding="utf8") as myfile:
|
|
69
|
+
s = myfile.read()
|
|
70
|
+
|
|
71
|
+
exec(s, self.shell.user_ns)
|
|
72
|
+
|
|
73
|
+
# setup more funcy mpl theme instead of ugly default
|
|
74
|
+
if line:
|
|
75
|
+
if 'dark' in line.lower():
|
|
76
|
+
set_mpl_theme('dark')
|
|
77
|
+
|
|
78
|
+
elif 'light' in line.lower():
|
|
79
|
+
set_mpl_theme('light')
|
|
80
|
+
|
|
81
|
+
# install additional plotly helpers
|
|
82
|
+
# from qube.charting.plot_helpers import install_plotly_helpers
|
|
83
|
+
# install_plotly_helpers()
|
|
84
|
+
|
|
85
|
+
def _get_manager(self):
|
|
86
|
+
if self.__manager is None:
|
|
87
|
+
import multiprocessing as m
|
|
88
|
+
self.__manager = m.Manager()
|
|
89
|
+
return self.__manager
|
|
90
|
+
|
|
91
|
+
@line_cell_magic
|
|
92
|
+
def proc(self, line, cell=None):
|
|
93
|
+
"""
|
|
94
|
+
Run cell in separate process
|
|
95
|
+
|
|
96
|
+
>>> %%proc x, y as MyProc1
|
|
97
|
+
>>> x.set('Hello')
|
|
98
|
+
>>> y.set([1,2,3,4])
|
|
99
|
+
|
|
100
|
+
"""
|
|
101
|
+
import multiprocessing as m
|
|
102
|
+
import time, re
|
|
103
|
+
|
|
104
|
+
# create ext args
|
|
105
|
+
name = None
|
|
106
|
+
if line:
|
|
107
|
+
# check if custom process name was provided
|
|
108
|
+
if ' as ' in line:
|
|
109
|
+
line, name = line.split('as')
|
|
110
|
+
if not name.isspace():
|
|
111
|
+
name = name.strip()
|
|
112
|
+
else:
|
|
113
|
+
print('>>> Process name must be specified afer "as" keyword !')
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
ipy = get_ipython()
|
|
117
|
+
for a in [x for x in re.split('[\ ,;]', line.strip()) if x]:
|
|
118
|
+
ipy.push({a: self._get_manager().Value(None, None)})
|
|
119
|
+
|
|
120
|
+
# code to run
|
|
121
|
+
lines = '\n'.join([' %s' % x for x in cell.split('\n')])
|
|
122
|
+
|
|
123
|
+
def fn():
|
|
124
|
+
result = get_ipython().run_cell(lines)
|
|
125
|
+
|
|
126
|
+
# send errors to parent
|
|
127
|
+
if result.error_before_exec:
|
|
128
|
+
raise result.error_before_exec
|
|
129
|
+
|
|
130
|
+
if result.error_in_exec:
|
|
131
|
+
raise result.error_in_exec
|
|
132
|
+
|
|
133
|
+
t_start = str(time.time()).replace('.', '_')
|
|
134
|
+
f_id = f'proc_{t_start}' if name is None else name
|
|
135
|
+
if self._is_task_name_already_used(f_id):
|
|
136
|
+
f_id = f"{f_id}_{t_start}"
|
|
137
|
+
|
|
138
|
+
task = m.Process(target=fn, name=f_id)
|
|
139
|
+
task.start()
|
|
140
|
+
print(' -> Task %s is started' % f_id)
|
|
141
|
+
|
|
142
|
+
def _is_task_name_already_used(self, name):
|
|
143
|
+
import multiprocessing as m
|
|
144
|
+
for p in m.active_children():
|
|
145
|
+
if p.name == name:
|
|
146
|
+
return True
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
@line_magic
|
|
150
|
+
def list_proc(self, line):
|
|
151
|
+
import multiprocessing as m
|
|
152
|
+
for p in m.active_children():
|
|
153
|
+
print(p.name)
|
|
154
|
+
|
|
155
|
+
@line_magic
|
|
156
|
+
def kill_proc(self, line):
|
|
157
|
+
import multiprocessing as m
|
|
158
|
+
for p in m.active_children():
|
|
159
|
+
if line and p.name.startswith(line):
|
|
160
|
+
p.terminate()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# - registering magic here
|
|
164
|
+
get_ipython().register_magics(QubxMagics)
|
qubx/_nb_magic.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
""""
|
|
2
|
+
Here stuff we want to have in every Jupyter notebook after calling %qube magic
|
|
3
|
+
"""
|
|
4
|
+
import importlib_metadata
|
|
5
|
+
|
|
6
|
+
import qubx
|
|
7
|
+
from qubx.utils import runtime_env
|
|
8
|
+
from qubx.utils.misc import add_project_to_system_path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def np_fmt_short():
|
|
12
|
+
# default np output is 75 columns so extend it a bit and suppress scientific fmt for small floats
|
|
13
|
+
np.set_printoptions(linewidth=240, suppress=True)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def np_fmt_reset():
|
|
17
|
+
# reset default np printing options
|
|
18
|
+
np.set_printoptions(edgeitems=3, infstr='inf', linewidth=75, nanstr='nan', precision=8,
|
|
19
|
+
suppress=False, threshold=1000, formatter=None)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if runtime_env() in ['notebook', 'shell']:
|
|
23
|
+
|
|
24
|
+
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
25
|
+
# -- all imports below will appear in notebook after calling %%alphalab magic ---
|
|
26
|
+
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
27
|
+
|
|
28
|
+
# - - - - Common stuff - - - -
|
|
29
|
+
import numpy as np
|
|
30
|
+
import pandas as pd
|
|
31
|
+
from datetime import time, timedelta
|
|
32
|
+
from tqdm.auto import tqdm
|
|
33
|
+
|
|
34
|
+
# - - - - TA stuff and indicators - - - -
|
|
35
|
+
# - - - - Portfolio analysis - - - -
|
|
36
|
+
# - - - - Simulator stuff - - - -
|
|
37
|
+
# - - - - Learn stuff - - - -
|
|
38
|
+
# - - - - Charting stuff - - - -
|
|
39
|
+
from matplotlib import pyplot as plt
|
|
40
|
+
from qubx.utils.charting.mpl_helpers import fig, subplot, sbp
|
|
41
|
+
# - - - - Utils - - - -
|
|
42
|
+
|
|
43
|
+
# - setup short numpy output format
|
|
44
|
+
np_fmt_short()
|
|
45
|
+
|
|
46
|
+
# - add project home to system path
|
|
47
|
+
add_project_to_system_path()
|
|
48
|
+
|
|
49
|
+
# - check current version
|
|
50
|
+
try:
|
|
51
|
+
version = importlib_metadata.version('qube2')
|
|
52
|
+
except:
|
|
53
|
+
version = 'Dev'
|
|
54
|
+
|
|
55
|
+
# some new logo
|
|
56
|
+
if not hasattr(qubx.QubxMagics, '__already_initialized__'):
|
|
57
|
+
from qubx.utils.misc import (green, yellow, cyan, magenta, white, blue, red)
|
|
58
|
+
|
|
59
|
+
print(
|
|
60
|
+
f"""
|
|
61
|
+
{red("╻")}
|
|
62
|
+
{green("┏┓ ╻ ")} {red("┃")} {yellow("┏┓")} {cyan("Quantitative Backtesting Environment")}
|
|
63
|
+
{green("┃┃ ┓┏ ┣┓ ┏┓")} {red("┃")} {yellow("┏┛")}
|
|
64
|
+
{green("┗┻ ┗┻ ┗┛ ┗ ")} {red("┃")} {yellow("┗━")} (c) 2024, ver. {magenta(version.rstrip())}
|
|
65
|
+
{red("╹")}
|
|
66
|
+
"""
|
|
67
|
+
)
|
|
68
|
+
qubx.QubxMagics.__already_initialized__ = True
|
|
69
|
+
|
qubx/core/__init__.py
ADDED
|
File without changes
|
qubx/core/basics.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Callable, Dict, List, Optional, Union
|
|
3
|
+
import numpy as np
|
|
4
|
+
import math
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from qubx.core.series import Quote, Trade, time_as_nsec
|
|
7
|
+
from qubx.core.utils import time_to_str, time_delta_to_str, recognize_timeframe
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
dt_64 = np.datetime64
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class FuturesInfo:
|
|
15
|
+
contract_type: Optional[str] = None # contract type
|
|
16
|
+
delivery_date: Optional[datetime] = None # delivery date
|
|
17
|
+
onboard_date: Optional[datetime] = None # futures contract size
|
|
18
|
+
contract_size: float = 1.0 # futures contract size
|
|
19
|
+
maint_margin: float = 0.0 # maintanance margin
|
|
20
|
+
required_margin: float = 0.0 # required margin
|
|
21
|
+
liquidation_fee: float = 0.0 # liquidation cost
|
|
22
|
+
|
|
23
|
+
def __str__(self) -> str:
|
|
24
|
+
return f"{self.contract_type} ({self.contract_size}) {self.onboard_date.isoformat()} -> {self.delivery_date.isoformat()}"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Instrument:
|
|
29
|
+
symbol: str # instrument's name
|
|
30
|
+
market_type: str # market type (CRYPTO, STOCK, FX, etc)
|
|
31
|
+
exchange: str # exchange id
|
|
32
|
+
base: str # base symbol
|
|
33
|
+
quote: str # quote symbol
|
|
34
|
+
margin_symbol: str # margin asset
|
|
35
|
+
min_tick: float = 0.0 # tick size - minimal price change
|
|
36
|
+
min_size_step: float = 0.0 # minimal position change step size
|
|
37
|
+
min_size: float = 0.0 # minimal allowed position size
|
|
38
|
+
|
|
39
|
+
# - futures section
|
|
40
|
+
futures_info: Optional[FuturesInfo] = None
|
|
41
|
+
|
|
42
|
+
_aux_instrument: Optional['Instrument'] = None # instrument used for conversion to main asset basis
|
|
43
|
+
# | let's say we trade BTC/ETH with main account in USDT
|
|
44
|
+
# | so we need to use ETH/USDT for convert profits/losses to USDT
|
|
45
|
+
_tick_precision: int = field(repr=False ,default=-1) #type: check
|
|
46
|
+
_size_precision: int = field(repr=False ,default=-1)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def is_futures(self) -> bool:
|
|
50
|
+
return self.futures_info is not None
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def price_precision(self):
|
|
54
|
+
if self._tick_precision < 0:
|
|
55
|
+
self._tick_precision = int(abs(np.log10(self.min_tick)))
|
|
56
|
+
return self._tick_precision
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def size_precision(self):
|
|
60
|
+
if self._size_precision < 0:
|
|
61
|
+
self._size_precision = int(abs(np.log10(self.min_size_step)))
|
|
62
|
+
return self._size_precision
|
|
63
|
+
|
|
64
|
+
def __str__(self) -> str:
|
|
65
|
+
return f"{self.exchange}:{self.symbol} [{self.market_type} {str(self.futures_info) if self.futures_info else 'SPOT ' + self.base + '/' + self.quote }]"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class Signal:
|
|
70
|
+
"""
|
|
71
|
+
Class for presenting signals generated by strategy
|
|
72
|
+
"""
|
|
73
|
+
instrument: Instrument
|
|
74
|
+
signal: float
|
|
75
|
+
price: Optional[float] = None
|
|
76
|
+
stop: Optional[float] = None
|
|
77
|
+
take: Optional[float] = None
|
|
78
|
+
group: Optional[str] = None
|
|
79
|
+
comment: Optional[str] = None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TransactionCostsCalculator:
|
|
83
|
+
def __init__(self, maker: float, taker: float):
|
|
84
|
+
self.maker = maker
|
|
85
|
+
self.taker = taker
|
|
86
|
+
|
|
87
|
+
def get_execution_fees(self, instrument: Instrument, exec_price: float, amount: float, crossed_market=False, conversion_rate=1.0):
|
|
88
|
+
if crossed_market:
|
|
89
|
+
return abs(amount * exec_price) * self.taker / conversion_rate
|
|
90
|
+
else:
|
|
91
|
+
return abs(amount * exec_price) * self.maker / conversion_rate
|
|
92
|
+
|
|
93
|
+
def get_overnight_fees(self, instrument: Instrument, amount: float):
|
|
94
|
+
return 0.0
|
|
95
|
+
|
|
96
|
+
def get_funding_rates_fees(self, instrument: Instrument, amount: float):
|
|
97
|
+
return 0.0
|
|
98
|
+
|
|
99
|
+
def __repr__(self):
|
|
100
|
+
return f'<TCC: {self.maker * 100:.4f} / {self.taker * 100:.4f}>'
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
ZERO_COSTS = TransactionCostsCalculator(0.0, 0.0)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class Position:
|
|
107
|
+
instrument: Instrument # instrument for this poisition
|
|
108
|
+
quantity: float = 0.0 # quantity positive for long and negative for short
|
|
109
|
+
tcc: TransactionCostsCalculator # transaction costs calculator
|
|
110
|
+
pnl: float = 0.0 # total cumulative position PnL in portfolio basic funds currency
|
|
111
|
+
r_pnl: float = 0.0 # total cumulative position PnL in portfolio basic funds currency
|
|
112
|
+
market_value: float = 0.0 # position's market value in quote currency
|
|
113
|
+
market_value_funds: float = 0.0 # position market value in portfolio funded currency
|
|
114
|
+
position_avg_price: float = 0.0 # average position price
|
|
115
|
+
position_avg_price_funds: float = 0.0 # average position price
|
|
116
|
+
commissions: float = 0.0 # cumulative commissions paid for this position
|
|
117
|
+
|
|
118
|
+
last_update_time: Optional[int] = None # when price updated or position changed
|
|
119
|
+
last_update_price: Optional[float] = None # last update price (actually instrument's price) in quoted currency
|
|
120
|
+
|
|
121
|
+
# - helpers for position processing
|
|
122
|
+
_formatter: str
|
|
123
|
+
_prc_formatter: str
|
|
124
|
+
_qty_multiplier: float = 1.0
|
|
125
|
+
|
|
126
|
+
def __init__(self, instrument: Instrument, tcc: TransactionCostsCalculator,
|
|
127
|
+
quantity=0.0, average_price=0.0, aux_price=1.0,
|
|
128
|
+
) -> None:
|
|
129
|
+
self.instrument = instrument
|
|
130
|
+
self.tcc = tcc
|
|
131
|
+
|
|
132
|
+
# - size/price formaters
|
|
133
|
+
# time [symbol] qty
|
|
134
|
+
self._formatter = f'%s [{instrument.exchange}:{instrument.symbol}] %{instrument.size_precision+8}.{instrument.size_precision}f'
|
|
135
|
+
# pos_avg_px pnl | mkt_price mkt_value
|
|
136
|
+
self._formatter += f'%10.{instrument.price_precision}f %+10.4f | %s %10.2f'
|
|
137
|
+
self._prc_formatter = f"%.{instrument.price_precision}f"
|
|
138
|
+
if instrument.is_futures:
|
|
139
|
+
self._qty_multiplier = instrument.futures_info.contract_size
|
|
140
|
+
|
|
141
|
+
if quantity != 0.0 and average_price > 0.0:
|
|
142
|
+
self.quantity = quantity
|
|
143
|
+
raise ValueError("[TODO] Position: restore state by quantity and avg price !!!!")
|
|
144
|
+
|
|
145
|
+
def _price(self, update: Union[Quote, Trade]) -> float:
|
|
146
|
+
if isinstance(update, Quote):
|
|
147
|
+
return update.bid if np.sign(self.quantity) > 0 else update.ask
|
|
148
|
+
elif isinstance(update, Trade):
|
|
149
|
+
return update.price
|
|
150
|
+
raise ValueError(f"Unknown update type: {type(update)}")
|
|
151
|
+
|
|
152
|
+
def update_position(self, timestamp: dt_64, position: float, exec_price: float, aggressive=True, conversion_rate:float=1) -> float:
|
|
153
|
+
# - realized PnL of this fill
|
|
154
|
+
deal_pnl = 0
|
|
155
|
+
quantity = self.quantity
|
|
156
|
+
|
|
157
|
+
if quantity != position:
|
|
158
|
+
pos_change = position - quantity
|
|
159
|
+
direction = np.sign(pos_change)
|
|
160
|
+
prev_direction = np.sign(quantity)
|
|
161
|
+
|
|
162
|
+
# how many shares are closed/open
|
|
163
|
+
qty_closing = min(abs(self.quantity), abs(pos_change)) * direction if prev_direction != direction else 0
|
|
164
|
+
qty_opening = pos_change if prev_direction == direction else pos_change - qty_closing
|
|
165
|
+
|
|
166
|
+
# - extract realized part of PnL
|
|
167
|
+
if qty_closing != 0:
|
|
168
|
+
deal_pnl = qty_closing * (self.position_avg_price - exec_price)
|
|
169
|
+
quantity += qty_closing
|
|
170
|
+
# - reset average price to 0 if smaller than minimal price change to avoid cumulative error
|
|
171
|
+
if abs(quantity) < self.instrument.min_size_step:
|
|
172
|
+
quantity = 0.0
|
|
173
|
+
self.position_avg_price = 0.0
|
|
174
|
+
|
|
175
|
+
# - if it has something to add to position let's update price and cost
|
|
176
|
+
if qty_opening != 0:
|
|
177
|
+
qa_open, qas = abs(qty_opening), abs(quantity)
|
|
178
|
+
self.position_avg_price = (qa_open * exec_price + qas * self.position_avg_price) / (qas + qa_open)
|
|
179
|
+
|
|
180
|
+
# - update position and position's price
|
|
181
|
+
self.position_avg_price_funds = self.position_avg_price / conversion_rate
|
|
182
|
+
self.quantity = position
|
|
183
|
+
|
|
184
|
+
# - convert PnL to fund currency
|
|
185
|
+
self.r_pnl += deal_pnl / conversion_rate
|
|
186
|
+
|
|
187
|
+
# - update pnl
|
|
188
|
+
self._update_market_price(time_as_nsec(timestamp), exec_price, conversion_rate)
|
|
189
|
+
|
|
190
|
+
# - calculate transaction costs
|
|
191
|
+
comms = self.tcc.get_execution_fees(self.instrument, exec_price, pos_change, aggressive, conversion_rate)
|
|
192
|
+
self.commissions += comms
|
|
193
|
+
|
|
194
|
+
return deal_pnl
|
|
195
|
+
|
|
196
|
+
def update_market_price(self, price: Union[Quote, Trade], conversion_rate:float=1) -> float:
|
|
197
|
+
return self._update_market_price(price.time, self._price(price), conversion_rate)
|
|
198
|
+
|
|
199
|
+
def _update_market_price(self, timestamp: dt_64, price: float, conversion_rate:float) -> float:
|
|
200
|
+
self.last_update_time = timestamp
|
|
201
|
+
self.last_update_price = price
|
|
202
|
+
|
|
203
|
+
if not np.isnan(price):
|
|
204
|
+
self.pnl = self.quantity * (price - self.position_avg_price) / conversion_rate + self.r_pnl
|
|
205
|
+
self.market_value = self.quantity * self.last_update_price * self._qty_multiplier
|
|
206
|
+
|
|
207
|
+
# calculate mkt value in funded currency
|
|
208
|
+
self.market_value_funds = self.market_value / conversion_rate
|
|
209
|
+
return self.pnl
|
|
210
|
+
|
|
211
|
+
def total_pnl(self, conversion_rate:float=1.0) -> float:
|
|
212
|
+
pnl = self.r_pnl
|
|
213
|
+
if not np.isnan(self.last_update_price):
|
|
214
|
+
pnl += self.quantity * (self.last_update_price - self.position_avg_price) / conversion_rate
|
|
215
|
+
return pnl
|
|
216
|
+
|
|
217
|
+
@staticmethod
|
|
218
|
+
def _t2s(t) -> str:
|
|
219
|
+
return np.datetime64(t, 'ns').astype('datetime64[ms]').item().strftime('%Y-%m-%d %H:%M:%S') if t else '---'
|
|
220
|
+
|
|
221
|
+
def __str__(self):
|
|
222
|
+
_mkt_price = (self._prc_formatter % self.last_update_price) if self.last_update_price else "---"
|
|
223
|
+
return self._formatter % (Position._t2s(self.last_update_time), self.quantity, self.position_avg_price_funds,self.pnl, _mkt_price, self.market_value_funds)
|
|
224
|
+
|
qubx/core/lookups.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import glob, re
|
|
2
|
+
import json, os, dataclasses
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from qubx.core.basics import Instrument, FuturesInfo
|
|
7
|
+
from qubx.utils.marketdata.binance import get_binance_symbol_info_for_type
|
|
8
|
+
from qubx import logger
|
|
9
|
+
from qubx.utils.misc import makedirs, get_local_qubx_folder
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _InstrumentEncoder(json.JSONEncoder):
|
|
13
|
+
def default(self, obj):
|
|
14
|
+
if dataclasses.is_dataclass(obj):
|
|
15
|
+
return {k:v for k,v in dataclasses.asdict(obj).items() if not k.startswith('_')}
|
|
16
|
+
if isinstance(obj, (datetime)):
|
|
17
|
+
return obj.isoformat()
|
|
18
|
+
return super().default(obj)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _InstrumentDecoder(json.JSONDecoder):
|
|
22
|
+
def _preprocess(d, ks):
|
|
23
|
+
fi = d.get(ks)
|
|
24
|
+
if fi:
|
|
25
|
+
fi['delivery_date'] = datetime.strptime(fi.get('delivery_date', '5000-01-01T00:00:00'),'%Y-%m-%dT%H:%M:%S')
|
|
26
|
+
fi['onboard_date'] = datetime.strptime(fi.get('onboard_date', '1970-01-01T00:00:00'),'%Y-%m-%dT%H:%M:%S')
|
|
27
|
+
return d | {ks: FuturesInfo(**fi) if fi else None}
|
|
28
|
+
|
|
29
|
+
def decode(self, json_string):
|
|
30
|
+
obj = super(_InstrumentDecoder, self).decode(json_string)
|
|
31
|
+
if isinstance(obj, dict):
|
|
32
|
+
return Instrument(**_InstrumentDecoder._preprocess(obj, 'futures_info'))
|
|
33
|
+
elif isinstance(obj, list):
|
|
34
|
+
return [Instrument(**_InstrumentDecoder._preprocess(o, 'futures_info')) for o in obj]
|
|
35
|
+
return obj
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class InstrumentsLookup:
|
|
39
|
+
lookup: Dict[str, Instrument]
|
|
40
|
+
path: str
|
|
41
|
+
|
|
42
|
+
def __init__(self, path: str=makedirs(get_local_qubx_folder(), 'instruments')) -> None:
|
|
43
|
+
self.path = path
|
|
44
|
+
if not self.load():
|
|
45
|
+
self.refresh()
|
|
46
|
+
self.load()
|
|
47
|
+
|
|
48
|
+
def load(self) -> bool:
|
|
49
|
+
self.lookup = {}
|
|
50
|
+
data_exists = False
|
|
51
|
+
for fs in glob.glob(self.path + '/*.json'):
|
|
52
|
+
try:
|
|
53
|
+
with open(fs, 'r') as f:
|
|
54
|
+
instrs = json.load(f, cls=_InstrumentDecoder)
|
|
55
|
+
for i in instrs:
|
|
56
|
+
self.lookup[f"{i.exchange}:{i.symbol}"] = i
|
|
57
|
+
data_exists = True
|
|
58
|
+
except Exception as ex:
|
|
59
|
+
logger.warning(ex)
|
|
60
|
+
|
|
61
|
+
return data_exists
|
|
62
|
+
|
|
63
|
+
def find(self, exchange: str, base: str, quote: str) -> Optional[Instrument]:
|
|
64
|
+
for i in self.lookup.values():
|
|
65
|
+
if i.exchange == exchange and (
|
|
66
|
+
(i.base == base and i.quote == quote) or (i.base == quote and i.quote == base)
|
|
67
|
+
):
|
|
68
|
+
return i
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
def find_aux_instrument_for(self, instrument: Instrument, base_currency: str) -> Optional[Instrument]:
|
|
72
|
+
"""
|
|
73
|
+
Tries to find aux instrument (for conversions to funded currency)
|
|
74
|
+
for example:
|
|
75
|
+
ETHBTC -> BTCUSDT for base_currency USDT
|
|
76
|
+
EURGBP -> GBPUSD for base_currency USD
|
|
77
|
+
...
|
|
78
|
+
"""
|
|
79
|
+
base_currency = base_currency.upper()
|
|
80
|
+
if instrument.quote != base_currency and instrument._aux_instrument is None:
|
|
81
|
+
return self.find(instrument.exchange, instrument.quote, base_currency)
|
|
82
|
+
return instrument._aux_instrument
|
|
83
|
+
|
|
84
|
+
def __getitem__(self, spath: str) -> List[Instrument]:
|
|
85
|
+
res = []
|
|
86
|
+
c = re.compile(spath)
|
|
87
|
+
for k, v in self.lookup.items():
|
|
88
|
+
if re.match(c, k):
|
|
89
|
+
res.append(v)
|
|
90
|
+
return res
|
|
91
|
+
|
|
92
|
+
def refresh(self):
|
|
93
|
+
for mn in dir(self):
|
|
94
|
+
if mn.startswith('_update_'):
|
|
95
|
+
getattr(self, mn)(self.path)
|
|
96
|
+
|
|
97
|
+
def _update_kraken(self, path: str):
|
|
98
|
+
# TODO
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
def _update_dukas(self, path: str):
|
|
102
|
+
instruments = [
|
|
103
|
+
Instrument('EURUSD', 'FX', 'DUKAS', 'EUR', 'USD', 'USD', 0.00001, 1, 1000),
|
|
104
|
+
Instrument('GBPUSD', 'FX', 'DUKAS', 'GBP', 'USD', 'USD', 0.00001, 1, 1000),
|
|
105
|
+
Instrument('USDJPY', 'FX', 'DUKAS', 'USD', 'JPY', 'USD', 0.001, 1, 1000),
|
|
106
|
+
Instrument('USDCAD', 'FX', 'DUKAS', 'USD', 'CAD', 'USD', 0.00001, 1, 1000),
|
|
107
|
+
Instrument('AUDUSD', 'FX', 'DUKAS', 'AUD', 'USD', 'USD', 0.00001, 1, 1000),
|
|
108
|
+
Instrument('USDPLN', 'FX', 'DUKAS', 'USD', 'PLN', 'USD', 0.00001, 1, 1000),
|
|
109
|
+
Instrument('EURGBP', 'FX', 'DUKAS', 'EUR', 'GBP', 'USD', 0.00001, 1, 1000),
|
|
110
|
+
# TODO: addd all or find how to get it from site
|
|
111
|
+
]
|
|
112
|
+
logger.info(f'Updates {len(instruments)} for DUKASCOPY')
|
|
113
|
+
with open(os.path.join(path, f'dukas.json'), 'w') as f:
|
|
114
|
+
json.dump(instruments, f, cls=_InstrumentEncoder)
|
|
115
|
+
|
|
116
|
+
def _update_binance(self, path: str):
|
|
117
|
+
infos = get_binance_symbol_info_for_type(['UM', 'CM', 'SPOT'])
|
|
118
|
+
for exchange, info in infos.items():
|
|
119
|
+
instruments = []
|
|
120
|
+
for s in info['symbols']:
|
|
121
|
+
tick_size, size_step = None, None
|
|
122
|
+
for i in s['filters']:
|
|
123
|
+
if i['filterType'] == 'PRICE_FILTER':
|
|
124
|
+
tick_size = float(i['tickSize'])
|
|
125
|
+
if i['filterType'] == 'LOT_SIZE':
|
|
126
|
+
size_step = float(i['stepSize'])
|
|
127
|
+
|
|
128
|
+
fut_info = None
|
|
129
|
+
if 'contractType' in s:
|
|
130
|
+
fut_info = FuturesInfo(
|
|
131
|
+
s.get('contractType', 'UNKNOWN'),
|
|
132
|
+
datetime.fromtimestamp(s.get('deliveryDate', 0)/1000.0),
|
|
133
|
+
datetime.fromtimestamp(s.get('onboardDate', 0)/1000.0),
|
|
134
|
+
float(s.get('contractSize', 1)),
|
|
135
|
+
float(s.get('maintMarginPercent', 0)),
|
|
136
|
+
float(s.get('requiredMarginPercent', 0)),
|
|
137
|
+
float(s.get('liquidationFee', 0)),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
instruments.append(
|
|
141
|
+
Instrument(
|
|
142
|
+
s['symbol'], 'CRYPTO', exchange.upper(), s['baseAsset'], s['quoteAsset'], s.get('marginAsset', None),
|
|
143
|
+
tick_size, size_step,
|
|
144
|
+
min_size=size_step, # TODO: not sure about minimal position for Binance
|
|
145
|
+
futures_info=fut_info
|
|
146
|
+
))
|
|
147
|
+
|
|
148
|
+
logger.info(f'Loaded {len(instruments)} for {exchange}')
|
|
149
|
+
|
|
150
|
+
with open(os.path.join(path, f'{exchange}.json'), 'w') as f:
|
|
151
|
+
json.dump(instruments, f, cls=_InstrumentEncoder)
|
|
152
|
+
|
|
Binary file
|