Skip to content

Commit b795740

Browse files
committed
first released
1 parent 1f3becf commit b795740

23 files changed

+4060
-0
lines changed

CITATION.cff

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
cff-version: 1.2.0
2+
title: Tmatic
3+
message: 'If you use this software, please cite it as below.'
4+
type: software
5+
authors:
6+
- given-names: Eugene
7+
family-names: Romensky
8+
repository-code: 'https://github.com/evgrmn/tmatic'
9+
abstract: >-
10+
This software is designed for trading on the Bitmex.com
11+
marketplace and allows you to control trade balances and
12+
make transactions manually and automatically.
13+
version: 24.1.0
14+
date-released: '2024-01-27'

README.md

Lines changed: 375 additions & 0 deletions
Large diffs are not rendered by default.

algo/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
*
2+
!__init__.py
3+
!.gitignore
4+
!init.py

algo/__init__.py

Whitespace-only changes.

algo/init.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#Robot strategy initialization file
2+
3+
4+
def init_algo():
5+
pass

bots/__init__.py

Whitespace-only changes.

bots/init.py

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
from datetime import datetime, timedelta
2+
from typing import Tuple, Union
3+
4+
import functions as function
5+
from bots.variables import Variables as bot
6+
from common.variables import Variables as var
7+
8+
9+
def load_robots(db: str) -> dict:
10+
"""
11+
This function loads robot settings from the SQL database from the 'robots'
12+
table. Since all transactions for the current account are saved in the
13+
database, the function also checks all robots that have open positions,
14+
but are not in the 'robots' table. Such robots are also loaded. Their
15+
status is indicated as 'NOT DEFINED'. The program must see orders and
16+
trades made from the standard exchange web interface, so reserved robot
17+
names are provided for each instrument that match the symbol of the
18+
instrument. Using this names, transactions are taken into account and
19+
financial results are calculated in the database for each instrument
20+
separately from the financial results of individual robots. The status of
21+
such robots is 'RESERVED'.
22+
"""
23+
qwr = "select * from " + db + ".robots where SYMBOL in ("
24+
for num, symbol in enumerate(var.symbol_list):
25+
if num == 0:
26+
c = ""
27+
else:
28+
c = ", "
29+
qwr = qwr + c + "'" + symbol + "'"
30+
qwr = qwr + ") order by SORT"
31+
var.cursor_mysql.execute(qwr)
32+
for robot in var.cursor_mysql.fetchall():
33+
emi = robot["EMI"]
34+
bot.robots[emi] = robot
35+
bot.robots[emi]["STATUS"] = "WORK"
36+
37+
# Searching for unclosed positions by robots that are not in the 'robots' table
38+
39+
var.cursor_mysql.execute(
40+
"select SYMBOL, EMI, POS from (select emi, SYMBOL, \
41+
sum(CASE WHEN SIDE = 0 THEN QTY WHEN SIDE = 1 THEN \
42+
-QTY ELSE 0 END) POS from "
43+
+ db
44+
+ ".coins where \
45+
account = %s and SIDE <> -1 group by EMI, \
46+
SYMBOL) res where POS <> 0",
47+
var.user_id,
48+
)
49+
defuncts = var.cursor_mysql.fetchall()
50+
for defunct in defuncts:
51+
for emi in bot.robots:
52+
if defunct["EMI"] == emi:
53+
break
54+
else:
55+
if defunct["SYMBOL"] in var.symbol_list:
56+
status = "NOT DEFINED"
57+
else:
58+
status = "NOT IN LIST"
59+
bot.robots[defunct["EMI"]] = {
60+
"SYMBOL": defunct["SYMBOL"],
61+
"POS": int(defunct["POS"]),
62+
"EMI": defunct["EMI"],
63+
"STATUS": status,
64+
"TIMEFR": None,
65+
"CAPITAL": None,
66+
}
67+
68+
# Adding RESERVED robots
69+
70+
for symbol in var.symbol_list:
71+
var.cursor_mysql.execute(
72+
"select SYMBOL, EMI, POS from (select emi, SYMBOL, \
73+
sum(CASE WHEN SIDE = 0 THEN QTY WHEN SIDE = 1 THEN \
74+
-QTY ELSE 0 END) POS from "
75+
+ db
76+
+ ".coins where \
77+
account = %s and SIDE <> -1 group by EMI, \
78+
SYMBOL) res where EMI = %s",
79+
(var.user_id, symbol),
80+
)
81+
reserved = var.cursor_mysql.fetchall()
82+
if symbol not in bot.robots:
83+
if not reserved:
84+
pos = 0
85+
else:
86+
pos = int(reserved[0]["POS"])
87+
if symbol in var.symbol_list or pos != 0:
88+
bot.robots[symbol] = {
89+
"SYMBOL": symbol,
90+
"POS": pos,
91+
"EMI": symbol,
92+
"STATUS": "RESERVED",
93+
"TIMEFR": None,
94+
"CAPITAL": None,
95+
}
96+
97+
# Loading all transactions and calculating financial results for each robot
98+
99+
for emi in bot.robots:
100+
function.add_symbol(symbol=bot.robots[emi]["SYMBOL"])
101+
var.cursor_mysql.execute(
102+
"SELECT IFNULL(sum(SUMREAL), 0) SUMREAL, IFNULL(sum(QTY), 0) \
103+
POS, IFNULL(sum(abs(QTY)), 0) VOL, IFNULL(sum(COMMISS), 0) \
104+
COMMISS, IFNULL(max(TTIME), '1900-01-01 01:01:01') LTIME \
105+
FROM (SELECT SUMREAL, (CASE WHEN SIDE = 0 THEN QTY \
106+
WHEN SIDE = 1 THEN -QTY ELSE 0 END) QTY, \
107+
COMMISS, TTIME FROM "
108+
+ db
109+
+ ".coins WHERE EMI = %s \
110+
AND ACCOUNT = %s) aa",
111+
(emi, var.user_id),
112+
)
113+
data = var.cursor_mysql.fetchall()
114+
for row in data:
115+
for col in row:
116+
bot.robots[emi][col] = row[col]
117+
if col == "POS" or col == "VOL":
118+
bot.robots[emi][col] = int(bot.robots[emi][col])
119+
if col == "COMMISS" or col == "SUMREAL":
120+
bot.robots[emi][col] = float(bot.robots[emi][col])
121+
if col == "LTIME":
122+
bot.robots[emi][col] = datetime.strptime(
123+
str(bot.robots[emi][col]), "%Y-%m-%d %H:%M:%S"
124+
)
125+
bot.robots[emi]["PNL"] = 0
126+
bot.robots[emi]["FRAME"] = bot.robots[emi]["SYMBOL"] + str(
127+
bot.robots[emi]["TIMEFR"]
128+
)
129+
bot.robots[emi]["lotSize"] = (
130+
var.instruments[bot.robots[emi]["SYMBOL"]]["lotSize"]
131+
/ var.instruments[bot.robots[emi]["SYMBOL"]]["myMultiplier"]
132+
)
133+
if bot.robots[emi]["SYMBOL"] not in bot.full_symbol_list:
134+
bot.full_symbol_list.append(bot.robots[emi]["SYMBOL"])
135+
136+
return bot.robots
137+
138+
139+
def download_data(
140+
time: datetime, target: datetime, symbol: str, timeframe: str
141+
) -> Tuple[Union[list, None], Union[datetime, None]]:
142+
res = list()
143+
while target > time:
144+
data = var.ws.trade_bucketed(symbol=symbol, time=time, timeframe=timeframe)
145+
if data:
146+
last = time
147+
time = datetime.strptime(
148+
str(data[-1]["timestamp"][:19]), "%Y-%m-%dT%H:%M:%S"
149+
)
150+
if last == time:
151+
return res, time
152+
res += data
153+
print(
154+
"----> downloaded trade/bucketed, time: "
155+
+ str(time)
156+
+ ", rows downloaded:",
157+
len(res),
158+
)
159+
else:
160+
message = (
161+
"When downloading trade/bucketed data NoneType was recieved "
162+
+ str(data)
163+
)
164+
var.logger.error(message)
165+
return None, None
166+
var.ws.logNumFatal = 0
167+
168+
return res, time
169+
170+
171+
def load_frames(
172+
robot: dict, frames: dict, framing: dict, timeframe: str
173+
) -> Union[dict, None]:
174+
"""
175+
Loading usual candlestick data from the exchange server. Data is recorded
176+
in files for each algorithm. Every time you reboot the files are
177+
overwritten.
178+
"""
179+
filename = "data/" + timeframe + "_EMI" + robot["EMI"] + ".txt"
180+
with open(filename, "w"):
181+
pass
182+
target = datetime.utcnow()
183+
time = target - timedelta(days=bot.missing_days_number)
184+
delta = timedelta(minutes=robot["TIMEFR"] - target.minute % robot["TIMEFR"])
185+
target += delta
186+
target = target.replace(second=0, microsecond=0)
187+
188+
# Loading timeframe data
189+
190+
res, time = download_data(
191+
time=time,
192+
target=target,
193+
symbol=framing[timeframe]["symbol"],
194+
timeframe=var.timefrs[robot["TIMEFR"]],
195+
)
196+
if not res:
197+
return None
198+
199+
# The 'frames' array is filled with timeframe data.
200+
201+
for num, row in enumerate(res):
202+
utc = datetime.strptime(
203+
row["timestamp"][0:19], "%Y-%m-%dT%H:%M:%S"
204+
) - timedelta(minutes=robot["TIMEFR"])
205+
frames[timeframe].append(
206+
{
207+
"date": (utc.year - 2000) * 10000 + utc.month * 100 + utc.day,
208+
"time": utc.hour * 10000 + utc.minute * 100,
209+
"bid": float(row["open"]),
210+
"ask": float(row["open"]),
211+
"hi": float(row["high"]),
212+
"lo": float(row["low"]),
213+
"funding": 0,
214+
"datetime": utc,
215+
}
216+
)
217+
if num < len(res[:-1]) - 1:
218+
function.save_timeframes_data(
219+
emi=robot["EMI"],
220+
timeframe=timeframe,
221+
frame=frames[timeframe][-1],
222+
)
223+
framing[timeframe]["time"] = utc
224+
225+
message = "Downloaded missing data from the exchange for " + timeframe
226+
var.logger.info(message)
227+
function.info_display(message)
228+
229+
return frames
230+
231+
232+
def init_timeframes() -> Union[dict, None]:
233+
for emi in bot.robots:
234+
# Initialize candlestick timeframe data using 'TIMEFR' fields
235+
# expressed in minutes.
236+
if bot.robots[emi]["TIMEFR"]:
237+
time = datetime.utcnow()
238+
try:
239+
bot.frames[bot.robots[emi]["FRAME"]]
240+
bot.framing[bot.robots[emi]["FRAME"]]["robots"].append(emi)
241+
except KeyError:
242+
bot.frames[bot.robots[emi]["FRAME"]] = []
243+
bot.framing[bot.robots[emi]["FRAME"]] = {
244+
"symbol": bot.robots[emi]["SYMBOL"],
245+
"timefr": bot.robots[emi]["TIMEFR"],
246+
"time": time,
247+
"robots": [],
248+
"open": 0,
249+
"trigger": 0,
250+
}
251+
bot.framing[bot.robots[emi]["FRAME"]]["robots"].append(emi)
252+
for num, symbol in enumerate(var.symbol_list):
253+
if symbol == bot.framing[bot.robots[emi]["FRAME"]]["symbol"]:
254+
bot.framing[bot.robots[emi]["FRAME"]]["SYMBOL"] = num
255+
break
256+
res = load_frames(
257+
robot=bot.robots[emi],
258+
frames=bot.frames,
259+
framing=bot.framing,
260+
timeframe=bot.robots[emi]["FRAME"],
261+
)
262+
if not res:
263+
message = (
264+
"The emi " + emi + " candle timeframe data was not loaded!"
265+
)
266+
var.logger.error(message)
267+
return None
268+
269+
return bot.frames
270+
271+
272+
def delete_unused_robot() -> None:
273+
"""
274+
Deleting unused robots (if any)
275+
"""
276+
emi_in_orders = set()
277+
for val in var.orders.values():
278+
emi_in_orders.add(val["emi"])
279+
for emi in bot.robots.copy():
280+
if bot.robots[emi]["STATUS"] in ("WORK", "OFF"):
281+
pass
282+
elif emi in var.symbol_list:
283+
bot.robots[emi]["STATUS"] = "RESERVED"
284+
elif bot.robots[emi]["POS"] == 0 and emi not in emi_in_orders:
285+
function.info_display("Robot EMI=" + emi + ". Deleting from 'robots'")
286+
del bot.robots[emi]

bots/variables.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from collections import OrderedDict
2+
3+
from common.variables import Variables as var
4+
5+
6+
class Variables:
7+
full_symbol_list = var.symbol_list.copy()
8+
robots = OrderedDict()
9+
frames = dict()
10+
framing = dict()
11+
emi_list = list()
12+
robo = dict()
13+
robots_status = dict()
14+
missing_days_number = 2

common/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)