Skip to content

Commit f5ed71f

Browse files
Merge pull request #34 from Smilelife1177/dis_ads
01_statistic
2 parents d188f05 + 6c30773 commit f5ed71f

3 files changed

Lines changed: 182 additions & 2 deletions

File tree

gui.py

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
import json
33
import time
44
import math
5+
from trade_stats import record_last_closed_trade
56
from PyQt6.QtWidgets import QMainWindow, QVBoxLayout, QWidget, QPushButton, QLineEdit, QLabel, QDoubleSpinBox, QSlider, QComboBox, QCheckBox, QMessageBox, QTabWidget, QToolButton
67
from PyQt6.QtCore import QTimer, Qt
78
from PyQt6.QtGui import QIcon
89
from logic import get_account_balance, get_funding_data, get_current_price, get_next_funding_time, place_market_order, get_symbol_info, place_limit_close_order, update_ping, initialize_client, close_all_positions, get_optimal_limit_price, get_candle_open_price, place_stop_loss_order, get_order_execution_price
910
from PyQt6.QtWidgets import QTabBar
1011
from PyQt6.QtWebEngineWidgets import QWebEngineView
1112
from PyQt6.QtCore import QUrl
12-
from PyQt6.QtWidgets import QScrollArea, QHBoxLayout
13+
from PyQt6.QtWidgets import QScrollArea, QHBoxLayout, QTableWidget, QTableWidgetItem
1314
from PyQt6.QtWebEngineCore import QWebEngineProfile, QWebEnginePage
1415

1516
class FundingTraderApp(QMainWindow):
@@ -91,7 +92,30 @@ class FundingTraderApp(QMainWindow):
9192
"price_validation_warning_text": "Значна розбіжність цін для {symbol}: Вхід={entry_price:.6f}, Свічка={candle_price:.6f}, Перед фандингом={pre_funding_price:.6f}. Використовується {selected_price:.6f}."
9293
}
9394
}
94-
95+
#
96+
def update_stats_table(self):
97+
import csv
98+
if not os.path.exists("trade_stats.csv"):
99+
self.stats_table.clear()
100+
return
101+
102+
with open("trade_stats.csv", 'r', encoding='utf-8') as file:
103+
reader = csv.reader(file)
104+
data = list(reader)
105+
if not data:
106+
return
107+
108+
headers = data[0]
109+
self.stats_table.setColumnCount(len(headers))
110+
self.stats_table.setHorizontalHeaderLabels(headers)
111+
self.stats_table.setRowCount(len(data) - 1)
112+
113+
for row_idx, row in enumerate(data[1:]):
114+
for col_idx, val in enumerate(row):
115+
self.stats_table.setItem(row_idx, col_idx, QTableWidgetItem(val))
116+
117+
self.stats_table.resizeColumnsToContents()
118+
#
95119
def __init__(self, session, testnet, exchange):
96120
super().__init__()
97121
self.language = "en"
@@ -175,7 +199,23 @@ def __init__(self, session, testnet, exchange):
175199
exchange=exchange,
176200
settings=initial_settings
177201
)
202+
#
203+
# Add Statistics tab
204+
stats_tab = QWidget()
205+
stats_layout = QVBoxLayout(stats_tab)
178206

207+
self.stats_table = QTableWidget()
208+
self.stats_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) # Read-only
209+
stats_layout.addWidget(self.stats_table)
210+
211+
refresh_stats_button = QPushButton(self.translations[self.language]["refresh_button"]) # Reuse refresh translation or add new
212+
refresh_stats_button.clicked.connect(self.update_stats_table)
213+
stats_layout.addWidget(refresh_stats_button)
214+
215+
self.tab_widget.addTab(stats_tab, "Statistics" if self.language == "en" else "Статистика")
216+
217+
self.update_stats_table()
218+
#
179219
for tab_data in self.tab_data_list:
180220
self.update_tab_funding_data(tab_data)
181221

@@ -199,6 +239,8 @@ def add_new_tab(self, session=None, testnet=None, exchange=None, settings=None):
199239
def create_tab_ui(self, layout, session, testnet, exchange, settings=None):
200240
default_settings = {
201241
"selected_symbol": "HYPERUSDT",
242+
"position_open": False,
243+
"update_count": 0,
202244
"funding_interval_hours": 1.0 if exchange == "Bybit" else 8.0,
203245
"entry_time_seconds": 5.0,
204246
"qty": 45.0,
@@ -752,6 +794,8 @@ def capture_tab_funding_price(self, tab_data, symbol, side):
752794

753795
# Отримання трьох цін
754796
entry_price = get_order_execution_price(tab_data["session"], symbol, tab_data["open_order_id"], tab_data["exchange"])
797+
if entry_price:
798+
tab_data["position_open"] = True
755799
candle_price = get_candle_open_price(tab_data["session"], symbol, tab_data["exchange"])
756800
pre_funding_price = tab_data["pre_funding_price"]
757801

@@ -961,6 +1005,29 @@ def update_tab_funding_data(self, tab_data, retry_count=3, retry_delay=2):
9611005
tab_data["ping_label"].setText(f"{self.translations[self.language]['ping_label'].split(':')[0]}: Error")
9621006
tab_data["ping_label"].setStyleSheet("color: red;")
9631007

1008+
tab_data["update_count"] = tab_data.get("update_count", 0) + 1
1009+
if tab_data.get("position_open", False) and tab_data["update_count"] % 10 == 0: # Check every 10 updates (~10s)
1010+
try:
1011+
symbol = tab_data["selected_symbol"]
1012+
if tab_data["exchange"] == "Bybit":
1013+
pos_response = tab_data["session"].get_positions(category="linear", symbol=symbol)
1014+
if pos_response["retCode"] == 0:
1015+
positions = pos_response["result"]["list"]
1016+
position = next((p for p in positions if p["symbol"] == symbol and float(p["size"]) > 0), None)
1017+
if not position:
1018+
record_last_closed_trade(tab_data["session"], tab_data["exchange"], symbol)
1019+
tab_data["position_open"] = False
1020+
self.update_stats_table() # Refresh stats table automatically
1021+
else: # Binance (partial support)
1022+
pos_response = tab_data["session"].get_position_information(symbol=symbol)
1023+
position = next((p for p in pos_response if p["symbol"] == symbol and abs(float(p["positionAmt"])) > 0), None)
1024+
if not position:
1025+
record_last_closed_trade(tab_data["session"], tab_data["exchange"], symbol)
1026+
tab_data["position_open"] = False
1027+
self.update_stats_table()
1028+
except Exception as e:
1029+
print(f"Error checking position status: {e}")
1030+
9641031
#
9651032
def update_tab_funding_web_view(self, tab_data):
9661033
if tab_data not in self.tab_data_list:

logic.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from dotenv import load_dotenv
44
import math
55
import time
6+
from trade_stats import record_last_closed_trade
67
from pybit.unified_trading import HTTP
78
from binance.client import Client as BinanceClient
89

@@ -389,6 +390,10 @@ def close_all_positions(session, exchange, symbol=None):
389390
timeInForce="GTC",
390391
reduceOnly=True
391392
)
393+
if response_order["retCode"] == 0:
394+
print(f"Closed position for {position_symbol}: {response_order['result']}")
395+
time.sleep(1) # Wait for execution
396+
record_last_closed_trade(session, exchange, position_symbol)
392397
if response_order["retCode"] == 0:
393398
print(f"Closed position for {position_symbol}: {response_order['result']}")
394399
else:

trade_stats.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import csv
2+
from datetime import datetime
3+
import os
4+
import time
5+
6+
CSV_FILE = "trade_stats.csv"
7+
8+
HEADERS = ["Дата_Час", "Процент", "Фандинг", "Прибиль", "Доход", "Комисия", "Обєм", "В-сделке", "Тикер"]
9+
10+
def initialize_csv():
11+
if not os.path.exists(CSV_FILE):
12+
with open(CSV_FILE, 'w', newline='', encoding='utf-8') as file:
13+
writer = csv.writer(file)
14+
writer.writerow(HEADERS)
15+
16+
def record_last_closed_trade(session, exchange, symbol):
17+
initialize_csv()
18+
if exchange != "Bybit":
19+
print(f"Automatic trade stats recording is currently supported only for Bybit. Skipping for {exchange}.")
20+
return # For now, implement only for Bybit to avoid complexity with Binance
21+
22+
try:
23+
# Get the last closed PNL
24+
response_pnl = session.get_closed_pnl(category="linear", symbol=symbol, limit=1)
25+
if response_pnl["retCode"] != 0 or not response_pnl["result"]["list"]:
26+
print(f"Error fetching closed PNL for {symbol}: {response_pnl.get('retMsg', 'No data')}")
27+
return
28+
29+
pnl = response_pnl["result"]["list"][0]
30+
created_time = int(pnl["createdTime"]) # ms
31+
updated_time = int(pnl["updatedTime"]) # ms
32+
date_time = datetime.fromtimestamp(updated_time / 1000).strftime("%Y-%m-%d %H:%M")
33+
ticker = pnl["symbol"]
34+
qty = float(pnl["qty"])
35+
avg_entry_price = float(pnl["avgEntryPrice"])
36+
avg_exit_price = float(pnl["avgExitPrice"])
37+
leverage = float(pnl.get("leverage", 1.0)) # Default to 1 if not present
38+
side = pnl["side"]
39+
cum_entry_value = float(pnl["cumEntryValue"])
40+
cum_exit_value = float(pnl["cumExitValue"])
41+
cum_entry_fee = float(pnl.get("cumEntryFee", 0.0))
42+
cum_exit_fee = float(pnl.get("cumExitFee", 0.0))
43+
closed_pnl = float(pnl["closedPnl"])
44+
45+
# Gross profit
46+
gross_profit = cum_exit_value - cum_entry_value
47+
48+
# Fees
49+
fee = cum_entry_fee + cum_exit_fee
50+
51+
# Income (net PNL)
52+
income = closed_pnl
53+
54+
# Percent (leveraged profit percentage)
55+
if qty > 0 and avg_entry_price > 0:
56+
if side == "Buy":
57+
percent = ((avg_exit_price - avg_entry_price) / avg_entry_price) * 100 * leverage
58+
else:
59+
percent = ((avg_entry_price - avg_exit_price) / avg_entry_price) * 100 * leverage
60+
else:
61+
percent = 0.0
62+
63+
# Funding fees during the position
64+
funding = 0.0
65+
response_funding = session.get_income_history(
66+
category="linear",
67+
symbol=symbol,
68+
incomeType="FUNDING_FEE",
69+
startTime=created_time,
70+
endTime=updated_time,
71+
limit=50
72+
)
73+
if response_funding["retCode"] == 0:
74+
funding = sum(float(item["income"]) for item in response_funding["result"]["list"])
75+
76+
# Volume (entry value)
77+
volume = cum_entry_value
78+
79+
# Time in trade
80+
time_in_trade_sec = (updated_time - created_time) / 1000
81+
if time_in_trade_sec < 60:
82+
time_str = f"{int(time_in_trade_sec)}с"
83+
elif time_in_trade_sec < 3600:
84+
time_str = f"{int(time_in_trade_sec / 60)}м"
85+
else:
86+
time_str = f"{int(time_in_trade_sec / 3600)}г"
87+
88+
# Format values as in example
89+
values = [
90+
f"{percent:.2f}%",
91+
f"{funding:.2f} $",
92+
f"{gross_profit:.2f} $",
93+
f"{income:.2f} $",
94+
f"{fee:.2f} $",
95+
f"{volume:.0f} $",
96+
time_str,
97+
ticker
98+
]
99+
100+
with open(CSV_FILE, 'a', newline='', encoding='utf-8') as file:
101+
writer = csv.writer(file)
102+
row = [date_time] + values
103+
writer.writerow(row)
104+
105+
print(f"Trade stats recorded for {symbol}: {values}")
106+
107+
except Exception as e:
108+
print(f"Error recording trade stats for {symbol}: {e}")

0 commit comments

Comments
 (0)