Coverage for GuiSessionViewer.py: 0%
345 statements
« prev ^ index » next coverage.py v7.6.3, created at 2024-10-14 11:07 +0000
« prev ^ index » next coverage.py v7.6.3, created at 2024-10-14 11:07 +0000
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
4# Copyright 2008-2011 Steffen Schaumburg
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Affero General Public License as published by
7# the Free Software Foundation, version 3 of the License.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16# In the "official" distribution you can find the license in agpl-3.0.txt.
18from __future__ import print_function
19from __future__ import division
21from past.utils import old_div
22# import L10n
23# _ = L10n.get_translation()
25import sys
26import traceback
27from time import time, strftime, localtime, gmtime
29from PyQt5.QtCore import Qt
30from PyQt5.QtGui import QStandardItem, QStandardItemModel
31from PyQt5.QtWidgets import QFrame, QLabel, QScrollArea, QSplitter, QTableView, QVBoxLayout
33import matplotlib
34from matplotlib.figure import Figure
35from matplotlib.backends.backend_qt5agg import FigureCanvas
36from mplfinance.original_flavor import candlestick_ochl
37from numpy import diff, nonzero, sum, cumsum, max, min, append
39try:
40 calluse = not "matplotlib" in sys.modules
41 import matplotlib
43 if calluse:
44 try:
45 matplotlib.use("qt5agg")
46 except ValueError as e:
47 print(e)
48 from matplotlib.figure import Figure
49 from matplotlib.backends.backend_qt5agg import FigureCanvas
50 from mplfinance.original_flavor import candlestick_ochl
52 from numpy import diff, nonzero, sum, cumsum, max, min, append
54except ImportError as inst:
55 print(("""Failed to load numpy and/or matplotlib in Session Viewer"""))
56 print("ImportError: %s" % inst.args)
58import Database
59import Filters
60# import Charset
62import GuiHandViewer
63import logging
65log = logging.getLogger("sessionViewer")
66DEBUG = False
69class GuiSessionViewer(QSplitter):
70 def __init__(self, config, querylist, mainwin, owner, colors, debug=True):
71 QSplitter.__init__(self, mainwin)
72 self.debug = debug
73 self.conf = config
74 self.sql = querylist
75 self.window = mainwin
76 self.owner = owner
77 self.colors = colors
79 self.liststore = None
81 self.MYSQL_INNODB = 2
82 self.PGSQL = 3
83 self.SQLITE = 4
85 self.fig = None
86 self.canvas = None
87 self.ax = None
88 self.graphBox = None
90 # create new db connection to avoid conflicts with other threads
91 self.db = Database.Database(self.conf, sql=self.sql)
92 self.cursor = self.db.cursor
94 settings = {}
95 settings.update(self.conf.get_db_parameters())
96 settings.update(self.conf.get_import_parameters())
97 settings.update(self.conf.get_default_paths())
99 # text used on screen stored here so that it can be configured
100 self.filterText = {"handhead": ("Hand Breakdown for all levels listed above")}
102 filters_display = {
103 "Heroes": True,
104 "Sites": True,
105 "Games": True,
106 "Currencies": True,
107 "Limits": True,
108 "LimitSep": True,
109 "LimitType": True,
110 "Type": True,
111 "UseType": "ring",
112 "Seats": True,
113 "SeatSep": False,
114 "Dates": True,
115 "Groups": False,
116 "GroupsAll": False,
117 "Button1": True,
118 "Button2": False,
119 }
121 self.filters = Filters.Filters(self.db, display=filters_display)
122 self.filters.registerButton1Name("_Refresh")
123 self.filters.registerButton1Callback(self.refreshStats)
125 scroll = QScrollArea()
126 scroll.setWidget(self.filters)
128 self.columns = [
129 (1.0, "SID"),
130 (1.0, "Hands"),
131 (0.5, "Start"),
132 (0.5, "End"),
133 (1.0, "Rate"),
134 (1.0, "Open"),
135 (1.0, "Close"),
136 (1.0, "Low"),
137 (1.0, "High"),
138 (1.0, "Range"),
139 (1.0, "Profit"),
140 ]
142 self.detailFilters = []
144 self.stats_frame = QFrame()
145 self.stats_frame.setLayout(QVBoxLayout())
146 self.view = None
147 heading = QLabel(self.filterText["handhead"])
148 heading.setAlignment(Qt.AlignCenter)
149 self.stats_frame.layout().addWidget(heading)
151 self.main_vbox = QSplitter(Qt.Vertical)
153 self.graphBox = QFrame()
154 self.graphBox.setStyleSheet(f'background-color: {self.colors["background"]}')
155 self.graphBox.setLayout(QVBoxLayout())
157 self.addWidget(scroll)
158 self.addWidget(self.main_vbox)
159 self.setStretchFactor(0, 0)
160 self.setStretchFactor(1, 1)
161 self.main_vbox.addWidget(self.graphBox)
162 self.main_vbox.addWidget(self.stats_frame)
164 def refreshStats(self, checkState):
165 if self.view:
166 self.stats_frame.layout().removeWidget(self.view)
167 self.view.setParent(None)
168 self.fillStatsFrame(self.stats_frame)
170 def fillStatsFrame(self, frame):
171 sites = self.filters.getSites()
172 heroes = self.filters.getHeroes()
173 siteids = self.filters.getSiteIds()
174 games = self.filters.getGames()
175 currencies = self.filters.getCurrencies()
176 limits = self.filters.getLimits()
177 seats = self.filters.getSeats()
178 sitenos = []
179 playerids = []
181 for site in sites:
182 sitenos.append(siteids[site])
183 _hname = str(heroes[site])
184 result = self.db.get_player_id(self.conf, site, _hname)
185 if result is not None:
186 playerids.append(result)
188 if not sitenos:
189 print(("No sites selected - defaulting to PokerStars"))
190 sitenos = [2]
191 if not games:
192 print(("No games found"))
193 return
194 if not currencies:
195 print(("No currencies found"))
196 return
197 if not playerids:
198 print(("No player ids found"))
199 return
200 if not limits:
201 print(("No limits found"))
202 return
204 self.createStatsPane(frame, playerids, sitenos, games, currencies, limits, seats)
206 def createStatsPane(self, frame, playerids, sitenos, games, currencies, limits, seats):
207 starttime = time()
209 (results, quotes) = self.generateDatasets(playerids, sitenos, games, currencies, limits, seats)
211 if DEBUG:
212 for x in quotes:
213 print("start %s\tend %s \thigh %s\tlow %s" % (x[1], x[2], x[3], x[4]))
215 self.generateGraph(quotes)
217 self.addTable(frame, results)
219 self.db.rollback()
220 print(("Stats page displayed in %4.2f seconds") % (time() - starttime))
222 def generateDatasets(self, playerids, sitenos, games, currencies, limits, seats):
223 if DEBUG:
224 print("DEBUG: Starting generateDatasets")
225 THRESHOLD = 1800 # Min # of secs between consecutive hands before being considered a new session
226 PADDING = 5 # Additional time in minutes to add to a session, session startup, shutdown etc
228 q = self.sql.query["sessionStats"]
229 start_date, end_date = self.filters.getDates()
230 q = q.replace("<datestest>", " BETWEEN '" + start_date + "' AND '" + end_date + "'")
232 for m in list(self.filters.display.items()):
233 if m[0] == "Games" and m[1]:
234 if len(games) > 0:
235 gametest = str(tuple(games))
236 gametest = gametest.replace("L", "")
237 gametest = gametest.replace(",)", ")")
238 gametest = gametest.replace("u'", "'")
239 gametest = "AND gt.category in %s" % gametest
240 else:
241 gametest = "AND gt.category IS NULL"
242 q = q.replace("<game_test>", gametest)
244 limittest = self.filters.get_limits_where_clause(limits)
245 q = q.replace("<limit_test>", limittest)
247 currencytest = str(tuple(currencies))
248 currencytest = currencytest.replace(",)", ")")
249 currencytest = currencytest.replace("u'", "'")
250 currencytest = "AND gt.currency in %s" % currencytest
251 q = q.replace("<currency_test>", currencytest)
253 if seats:
254 q = q.replace("<seats_test>", "AND h.seats BETWEEN " + str(seats["from"]) + " AND " + str(seats["to"]))
255 else:
256 q = q.replace("<seats_test>", "AND h.seats BETWEEN 0 AND 100")
258 nametest = str(tuple(playerids))
259 nametest = nametest.replace("L", "")
260 nametest = nametest.replace(",)", ")")
261 q = q.replace("<player_test>", nametest)
262 q = q.replace("<ampersand_s>", "%s")
264 if DEBUG:
265 hands = [
266 ("10000", 10),
267 ("10000", 20),
268 ("10000", 30),
269 ("20000", -10),
270 ("20000", -20),
271 ("20000", -30),
272 ("30000", 40),
273 ("40000", 0),
274 ("50000", -40),
275 ("60000", 10),
276 ("60000", 30),
277 ("60000", -20),
278 ("70000", -20),
279 ("70000", 10),
280 ("70000", 30),
281 ("80000", -10),
282 ("80000", -30),
283 ("80000", 20),
284 ("90000", 20),
285 ("90000", -10),
286 ("90000", -30),
287 ("100000", 30),
288 ("100000", -50),
289 ("100000", 30),
290 ("110000", -20),
291 ("110000", 50),
292 ("110000", -20),
293 ("120000", -30),
294 ("120000", 50),
295 ("120000", -30),
296 ("130000", 20),
297 ("130000", -50),
298 ("130000", 20),
299 ("140000", 40),
300 ("140000", -40),
301 ("150000", -40),
302 ("150000", 40),
303 ("160000", -40),
304 ("160000", 80),
305 ("160000", -40),
306 ]
307 else:
308 self.db.cursor.execute(q)
309 hands = self.db.cursor.fetchall()
311 hands = list(hands)
313 if not hands:
314 return ([], [])
316 hands.insert(0, (hands[0][0], 0))
318 times = [int(x[0]) for x in hands]
319 profits = [float(x[1]) for x in hands]
320 diffs = diff(times)
321 diffs2 = append(diffs, THRESHOLD + 1)
322 index = nonzero(diffs2 > THRESHOLD)
323 if len(index[0]) > 0:
324 pass
325 else:
326 index = [[0]]
327 pass
329 first_idx = 1
330 quotes = []
331 results = []
332 cum_sum = old_div(cumsum(profits), 100)
333 sid = 1
335 total_hands = 0
336 total_time = 0
337 global_open = None
338 global_lwm = None
339 global_hwm = None
341 self.times = []
342 for i in range(len(index[0])):
343 last_idx = index[0][i]
344 hds = last_idx - first_idx + 1
345 if hds > 0:
346 stime = strftime("%d/%m/%Y %H:%M", localtime(times[first_idx]))
347 etime = strftime("%d/%m/%Y %H:%M", localtime(times[last_idx]))
348 self.times.append((times[first_idx] - PADDING * 60, times[last_idx] + PADDING * 60))
349 minutesplayed = old_div((times[last_idx] - times[first_idx]), 60)
350 minutesplayed = minutesplayed + PADDING
351 if minutesplayed == 0:
352 minutesplayed = 1
353 hph = hds * 60 / minutesplayed
354 end_idx = last_idx + 1
355 won = old_div(sum(profits[first_idx:end_idx]), 100.0)
356 hwm = max(cum_sum[first_idx - 1 : end_idx])
357 lwm = min(cum_sum[first_idx - 1 : end_idx])
358 open = old_div((sum(profits[:first_idx])), 100)
359 close = old_div((sum(profits[:end_idx])), 100)
361 total_hands = total_hands + hds
362 total_time = total_time + minutesplayed
363 if global_lwm is None or global_lwm > lwm:
364 global_lwm = lwm
365 if global_hwm is None or global_hwm < hwm:
366 global_hwm = hwm
367 if global_open is None:
368 global_open = open
369 global_stime = stime
371 results.append(
372 [
373 sid,
374 hds,
375 stime,
376 etime,
377 hph,
378 "%.2f" % open,
379 "%.2f" % close,
380 "%.2f" % lwm,
381 "%.2f" % hwm,
382 "%.2f" % (hwm - lwm),
383 "%.2f" % won,
384 ]
385 )
386 quotes.append((sid, open, close, hwm, lwm))
387 first_idx = end_idx
388 sid = sid + 1
389 else:
390 print("hds <= 0")
391 global_close = close
392 global_etime = etime
393 results.append([""] * 11)
394 results.append(
395 [
396 ("all"),
397 total_hands,
398 global_stime,
399 global_etime,
400 total_hands * 60 // total_time,
401 "%.2f" % global_open,
402 "%.2f" % global_close,
403 "%.2f" % global_lwm,
404 "%.2f" % global_hwm,
405 "%.2f" % (global_hwm - global_lwm),
406 "%.2f" % (global_close - global_open),
407 ]
408 )
410 return (results, quotes)
412 def clearGraphData(self):
413 try:
414 try:
415 if self.canvas:
416 self.graphBox.layout().removeWidget(self.canvas)
417 self.canvas.setParent(None)
418 except (AttributeError, RuntimeError) as e:
419 # Handle specific exceptions here if you expect them
420 log.error(f"Error during canvas cleanup: {e}")
421 pass
423 if self.fig is not None:
424 self.fig.clear()
425 self.fig = Figure(figsize=(5, 4), dpi=100)
426 self.fig.patch.set_facecolor(self.colors["background"])
428 if self.canvas is not None:
429 self.canvas.destroy()
431 self.canvas = FigureCanvas(self.fig)
432 self.canvas.setParent(self)
433 except Exception as e:
434 # Catch all other exceptions and log for better debugging
435 err = traceback.extract_tb(sys.exc_info()[2])[-1]
436 log.error(f"Error: {err[2]}({err[1]}): {e}")
437 raise
439 def generateGraph(self, quotes):
440 self.clearGraphData()
441 sitenos = []
442 playerids = []
444 sites = self.filters.getSites()
445 heroes = self.filters.getHeroes()
446 siteids = self.filters.getSiteIds()
447 limits = self.filters.getLimits()
449 # graphops = self.filters.getGraphOps()
451 names = ""
453 for site in sites:
454 sitenos.append(siteids[site])
455 _hname = heroes.get(site, "")
456 if not _hname:
457 raise ValueError(f"Hero name not found for site {site}")
458 result = self.db.get_player_id(self.conf, site, _hname)
459 if result is not None:
460 playerids.append(int(result))
461 names = names + "\n" + _hname + " on " + site
463 if not sitenos:
464 print(("No sites selected - defaulting to PokerStars"))
465 self.db.rollback()
466 return
468 if not playerids:
469 print(("No player ids found"))
470 self.db.rollback()
471 return
473 if not limits:
474 print(("No limits found"))
475 self.db.rollback()
476 return
478 self.ax = self.fig.add_subplot(111)
479 self.ax.tick_params(axis="x", colors=self.colors["foreground"])
480 self.ax.tick_params(axis="y", colors=self.colors["foreground"])
481 self.ax.spines["left"].set_color(self.colors["foreground"])
482 self.ax.spines["right"].set_color(self.colors["foreground"])
483 self.ax.spines["top"].set_color(self.colors["foreground"])
484 self.ax.spines["bottom"].set_color(self.colors["foreground"])
485 self.ax.set_title((("Session graph for ring games") + names), color=self.colors["foreground"])
486 self.ax.set_facecolor(self.colors["background"])
487 self.ax.set_xlabel(("Sessions"), fontsize=12, color=self.colors["foreground"])
488 self.ax.set_ylabel("$", color=self.colors["foreground"])
489 self.ax.grid(color=self.colors["grid"], linestyle=":", linewidth=0.2)
491 candlestick_ochl(
492 self.ax, quotes, width=0.50, colordown=self.colors["line_down"], colorup=self.colors["line_up"], alpha=1.00
493 )
494 self.graphBox.layout().addWidget(self.canvas)
495 self.canvas.draw()
497 def addTable(self, frame, results):
498 colxalign, colheading = list(range(2))
500 self.liststore = QStandardItemModel(0, len(self.columns))
501 self.liststore.setHorizontalHeaderLabels([column[colheading] for column in self.columns])
502 for row in results:
503 listrow = [QStandardItem(str(r)) for r in row]
504 for item in listrow:
505 item.setEditable(False)
506 self.liststore.appendRow(listrow)
508 self.view = QTableView()
509 self.view.setModel(self.liststore)
510 self.view.verticalHeader().hide()
511 self.view.setSelectionBehavior(QTableView.SelectRows)
512 frame.layout().addWidget(self.view)
513 self.view.doubleClicked.connect(self.row_activated)
515 def row_activated(self, index):
516 if index.row() < len(self.times):
517 replayer = None
518 for tabobject in self.owner.threads:
519 if isinstance(tabobject, GuiHandViewer.GuiHandViewer):
520 replayer = tabobject
521 self.owner.tab_hand_viewer(None)
522 break
523 if replayer is None:
524 self.owner.tab_hand_viewer(None)
525 for tabobject in self.owner.threads:
526 if isinstance(tabobject, GuiHandViewer.GuiHandViewer):
527 replayer = tabobject
528 break
529 reformat = lambda t: strftime("%Y-%m-%d %H:%M:%S+00:00", gmtime(t))
530 handids = replayer.get_hand_ids_from_date_range(
531 reformat(self.times[index.row()][0]), reformat(self.times[index.row()][1])
532 )
533 print("handids:", handids)
534 replayer.reload_hands(handids)
537if __name__ == "__main__":
538 import Configuration
540 config = Configuration.Config()
542 settings = {}
544 settings.update(config.get_db_parameters())
545 settings.update(config.get_import_parameters())
546 settings.update(config.get_default_paths())
548 from PyQt5.QtWidgets import QApplication, QMainWindow
550 app = QApplication([])
551 import SQL
553 sql = SQL.Sql(db_server=settings["db-server"])
555 colors = {
556 "background": "#19232D",
557 "foreground": "#9DA9B5",
558 "grid": "#4D4D4D",
559 "line_up": "g",
560 "line_down": "r",
561 "line_showdown": "b",
562 "line_nonshowdown": "m",
563 "line_ev": "orange",
564 "line_hands": "c",
565 }
567 i = GuiSessionViewer(config, sql, None, None, colors)
568 main_window = QMainWindow()
569 main_window.setCentralWidget(i)
570 main_window.show()
571 main_window.resize(1400, 800)
572 app.exec_()