Coverage for GuiSessionViewer.py: 0%
343 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-28 16:41 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-28 16:41 +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 os
27import traceback
28from time import time, strftime, localtime, gmtime
30from PyQt5.QtCore import Qt
31from PyQt5.QtGui import (QStandardItem, QStandardItemModel)
32from PyQt5.QtWidgets import (QFrame, QHBoxLayout, QLabel, QScrollArea,
33 QSplitter, QTableView, QVBoxLayout, QWidget)
35import matplotlib
36from matplotlib.figure import Figure
37from matplotlib.backends.backend_qt5agg import FigureCanvas
38from mplfinance.original_flavor import candlestick_ochl
39from numpy import diff, nonzero, sum, cumsum, max, min, append
41try:
42 calluse = not 'matplotlib' in sys.modules
43 import matplotlib
44 if calluse:
45 try:
46 matplotlib.use('qt5agg')
47 except ValueError as e:
48 print(e)
49 from matplotlib.figure import Figure
50 from matplotlib.backends.backend_qt5agg import FigureCanvas
51 from mplfinance.original_flavor import candlestick_ochl
53 from numpy import diff, nonzero, sum, cumsum, max, min, append
55except ImportError as inst:
56 print(("""Failed to load numpy and/or matplotlib in Session Viewer"""))
57 print("ImportError: %s" % inst.args)
59import Card
60import Database
61import Filters
62import Charset
64import GuiHandViewer
66DEBUG = False
68class GuiSessionViewer(QSplitter):
69 def __init__(self, config, querylist, mainwin, owner, colors, debug=True):
70 QSplitter.__init__(self, mainwin)
71 self.debug = debug
72 self.conf = config
73 self.sql = querylist
74 self.window = mainwin
75 self.owner = owner
76 self.colors = colors
78 self.liststore = None
80 self.MYSQL_INNODB = 2
81 self.PGSQL = 3
82 self.SQLITE = 4
84 self.fig = None
85 self.canvas = None
86 self.ax = None
87 self.graphBox = None
89 # create new db connection to avoid conflicts with other threads
90 self.db = Database.Database(self.conf, sql=self.sql)
91 self.cursor = self.db.cursor
93 settings = {}
94 settings.update(self.conf.get_db_parameters())
95 settings.update(self.conf.get_import_parameters())
96 settings.update(self.conf.get_default_paths())
98 # text used on screen stored here so that it can be configured
99 self.filterText = {'handhead': ('Hand Breakdown for all levels listed above')}
101 filters_display = {"Heroes": True,
102 "Sites": True,
103 "Games": True,
104 "Currencies": True,
105 "Limits": True,
106 "LimitSep": True,
107 "LimitType": True,
108 "Type": True,
109 "UseType": 'ring',
110 "Seats": True,
111 "SeatSep": False,
112 "Dates": True,
113 "Groups": False,
114 "GroupsAll": False,
115 "Button1": True,
116 "Button2": False
117 }
119 self.filters = Filters.Filters(self.db, display=filters_display)
120 self.filters.registerButton1Name("_Refresh")
121 self.filters.registerButton1Callback(self.refreshStats)
123 scroll = QScrollArea()
124 scroll.setWidget(self.filters)
126 self.columns = [(1.0, "SID"),
127 (1.0, "Hands"),
128 (0.5, "Start"),
129 (0.5, "End"),
130 (1.0, "Rate"),
131 (1.0, "Open"),
132 (1.0, "Close"),
133 (1.0, "Low"),
134 (1.0, "High"),
135 (1.0, "Range"),
136 (1.0, "Profit")]
138 self.detailFilters = []
140 self.stats_frame = QFrame()
141 self.stats_frame.setLayout(QVBoxLayout())
142 self.view = None
143 heading = QLabel(self.filterText['handhead'])
144 heading.setAlignment(Qt.AlignCenter)
145 self.stats_frame.layout().addWidget(heading)
147 self.main_vbox = QSplitter(Qt.Vertical)
149 self.graphBox = QFrame()
150 self.graphBox.setStyleSheet(f'background-color: {self.colors["background"]}')
151 self.graphBox.setLayout(QVBoxLayout())
153 self.addWidget(scroll)
154 self.addWidget(self.main_vbox)
155 self.setStretchFactor(0, 0)
156 self.setStretchFactor(1, 1)
157 self.main_vbox.addWidget(self.graphBox)
158 self.main_vbox.addWidget(self.stats_frame)
160 def refreshStats(self, checkState):
161 if self.view:
162 self.stats_frame.layout().removeWidget(self.view)
163 self.view.setParent(None)
164 self.fillStatsFrame(self.stats_frame)
166 def fillStatsFrame(self, frame):
167 sites = self.filters.getSites()
168 heroes = self.filters.getHeroes()
169 siteids = self.filters.getSiteIds()
170 games = self.filters.getGames()
171 currencies = self.filters.getCurrencies()
172 limits = self.filters.getLimits()
173 seats = self.filters.getSeats()
174 sitenos = []
175 playerids = []
177 for site in sites:
178 sitenos.append(siteids[site])
179 _hname = Charset.to_utf8(heroes[site])
180 result = self.db.get_player_id(self.conf, site, _hname)
181 if result is not None:
182 playerids.append(result)
184 if not sitenos:
185 print(("No sites selected - defaulting to PokerStars"))
186 sitenos = [2]
187 if not games:
188 print(("No games found"))
189 return
190 if not currencies:
191 print(("No currencies found"))
192 return
193 if not playerids:
194 print(("No player ids found"))
195 return
196 if not limits:
197 print(("No limits found"))
198 return
200 self.createStatsPane(frame, playerids, sitenos, games, currencies, limits, seats)
202 def createStatsPane(self, frame, playerids, sitenos, games, currencies, limits, seats):
203 starttime = time()
205 (results, quotes) = self.generateDatasets(playerids, sitenos, games, currencies, limits, seats)
207 if DEBUG:
208 for x in quotes:
209 print("start %s\tend %s \thigh %s\tlow %s" % (x[1], x[2], x[3], x[4]))
211 self.generateGraph(quotes)
213 self.addTable(frame, results)
215 self.db.rollback()
216 print(("Stats page displayed in %4.2f seconds") % (time() - starttime))
218 def generateDatasets(self, playerids, sitenos, games, currencies, limits, seats):
219 if (DEBUG): print("DEBUG: Starting generateDatasets")
220 THRESHOLD = 1800 # Min # of secs between consecutive hands before being considered a new session
221 PADDING = 5 # Additional time in minutes to add to a session, session startup, shutdown etc
223 q = self.sql.query['sessionStats']
224 start_date, end_date = self.filters.getDates()
225 q = q.replace("<datestest>", " BETWEEN '" + start_date + "' AND '" + end_date + "'")
227 for m in list(self.filters.display.items()):
228 if m[0] == 'Games' and m[1]:
229 if len(games) > 0:
230 gametest = str(tuple(games))
231 gametest = gametest.replace("L", "")
232 gametest = gametest.replace(",)", ")")
233 gametest = gametest.replace("u'", "'")
234 gametest = "AND gt.category in %s" % gametest
235 else:
236 gametest = "AND gt.category IS NULL"
237 q = q.replace("<game_test>", gametest)
239 limittest = self.filters.get_limits_where_clause(limits)
240 q = q.replace("<limit_test>", limittest)
242 currencytest = str(tuple(currencies))
243 currencytest = currencytest.replace(",)", ")")
244 currencytest = currencytest.replace("u'", "'")
245 currencytest = "AND gt.currency in %s" % currencytest
246 q = q.replace("<currency_test>", currencytest)
248 if seats:
249 q = q.replace('<seats_test>',
250 'AND h.seats BETWEEN ' + str(seats['from']) +
251 ' AND ' + str(seats['to']))
252 else:
253 q = q.replace('<seats_test>', 'AND h.seats BETWEEN 0 AND 100')
255 nametest = str(tuple(playerids))
256 nametest = nametest.replace("L", "")
257 nametest = nametest.replace(",)", ")")
258 q = q.replace("<player_test>", nametest)
259 q = q.replace("<ampersand_s>", "%s")
261 if DEBUG:
262 hands = [
263 (u'10000', 10), (u'10000', 20), (u'10000', 30),
264 (u'20000', -10), (u'20000', -20), (u'20000', -30),
265 (u'30000', 40),
266 (u'40000', 0),
267 (u'50000', -40),
268 (u'60000', 10), (u'60000', 30), (u'60000', -20),
269 (u'70000', -20), (u'70000', 10), (u'70000', 30),
270 (u'80000', -10), (u'80000', -30), (u'80000', 20),
271 (u'90000', 20), (u'90000', -10), (u'90000', -30),
272 (u'100000', 30), (u'100000', -50), (u'100000', 30),
273 (u'110000', -20), (u'110000', 50), (u'110000', -20),
274 (u'120000', -30), (u'120000', 50), (u'120000', -30),
275 (u'130000', 20), (u'130000', -50), (u'130000', 20),
276 (u'140000', 40), (u'140000', -40),
277 (u'150000', -40), (u'150000', 40),
278 (u'160000', -40), (u'160000', 80), (u'160000', -40),
279 ]
280 else:
281 self.db.cursor.execute(q)
282 hands = self.db.cursor.fetchall()
284 hands = list(hands)
286 if (not hands):
287 return ([], [])
289 hands.insert(0, (hands[0][0], 0))
291 times = [int(x[0]) for x in hands]
292 profits = [float(x[1]) for x in hands]
293 diffs = diff(times)
294 diffs2 = append(diffs, THRESHOLD + 1)
295 index = nonzero(diffs2 > THRESHOLD)
296 if len(index[0]) > 0:
297 pass
298 else:
299 index = [[0]]
300 pass
302 first_idx = 1
303 quotes = []
304 results = []
305 cum_sum = old_div(cumsum(profits), 100)
306 sid = 1
308 total_hands = 0
309 total_time = 0
310 global_open = None
311 global_lwm = None
312 global_hwm = None
314 self.times = []
315 for i in range(len(index[0])):
316 last_idx = index[0][i]
317 hds = last_idx - first_idx + 1
318 if hds > 0:
319 stime = strftime("%d/%m/%Y %H:%M", localtime(times[first_idx]))
320 etime = strftime("%d/%m/%Y %H:%M", localtime(times[last_idx]))
321 self.times.append((times[first_idx] - PADDING * 60, times[last_idx] + PADDING * 60))
322 minutesplayed = old_div((times[last_idx] - times[first_idx]), 60)
323 minutesplayed = minutesplayed + PADDING
324 if minutesplayed == 0:
325 minutesplayed = 1
326 hph = hds * 60 / minutesplayed
327 end_idx = last_idx + 1
328 won = old_div(sum(profits[first_idx:end_idx]), 100.0)
329 hwm = max(cum_sum[first_idx - 1:end_idx])
330 lwm = min(cum_sum[first_idx - 1:end_idx])
331 open = old_div((sum(profits[:first_idx])), 100)
332 close = old_div((sum(profits[:end_idx])), 100)
334 total_hands = total_hands + hds
335 total_time = total_time + minutesplayed
336 if (global_lwm == None or global_lwm > lwm):
337 global_lwm = lwm
338 if (global_hwm == None or global_hwm < hwm):
339 global_hwm = hwm
340 if (global_open == None):
341 global_open = open
342 global_stime = stime
344 results.append([sid, hds, stime, etime, hph,
345 "%.2f" % open,
346 "%.2f" % close,
347 "%.2f" % lwm,
348 "%.2f" % hwm,
349 "%.2f" % (hwm - lwm),
350 "%.2f" % won])
351 quotes.append((sid, open, close, hwm, lwm))
352 first_idx = end_idx
353 sid = sid + 1
354 else:
355 print("hds <= 0")
356 global_close = close
357 global_etime = etime
358 results.append([''] * 11)
359 results.append([("all"), total_hands, global_stime, global_etime,
360 total_hands * 60 // total_time,
361 "%.2f" % global_open,
362 "%.2f" % global_close,
363 "%.2f" % global_lwm,
364 "%.2f" % global_hwm,
365 "%.2f" % (global_hwm - global_lwm),
366 "%.2f" % (global_close - global_open)])
368 return (results, quotes)
370 def clearGraphData(self):
372 try:
373 try:
374 if self.canvas:
375 self.graphBox.layout().removeWidget(self.canvas)
376 self.canvas.setParent(None)
377 except:
378 pass
380 if self.fig is not None:
381 self.fig.clear()
382 self.fig = Figure(figsize=(5, 4), dpi=100)
383 self.fig.patch.set_facecolor(self.colors['background'])
384 if self.canvas is not None:
385 self.canvas.destroy()
387 self.canvas = FigureCanvas(self.fig)
388 self.canvas.setParent(self)
389 except:
390 err = traceback.extract_tb(sys.exc_info()[2])[-1]
391 print(("Error:") + " " + err[2] + "(" + str(err[1]) + "): " + str(sys.exc_info()[1]))
392 raise
394 def generateGraph(self, quotes):
395 self.clearGraphData()
396 sitenos = []
397 playerids = []
399 sites = self.filters.getSites()
400 heroes = self.filters.getHeroes()
401 siteids = self.filters.getSiteIds()
402 limits = self.filters.getLimits()
404 graphops = self.filters.getGraphOps()
406 names = ""
408 for site in sites:
409 sitenos.append(siteids[site])
410 _hname = Charset.to_utf8(heroes[site])
411 result = self.db.get_player_id(self.conf, site, _hname)
412 if result is not None:
413 playerids.append(int(result))
414 names = names + "\n" + _hname + " on " + site
416 if not sitenos:
417 print(("No sites selected - defaulting to PokerStars"))
418 self.db.rollback()
419 return
421 if not playerids:
422 print(("No player ids found"))
423 self.db.rollback()
424 return
426 if not limits:
427 print(("No limits found"))
428 self.db.rollback()
429 return
431 self.ax = self.fig.add_subplot(111)
432 self.ax.tick_params(axis='x', colors=self.colors['foreground'])
433 self.ax.tick_params(axis='y', colors=self.colors['foreground'])
434 self.ax.spines['left'].set_color(self.colors['foreground'])
435 self.ax.spines['right'].set_color(self.colors['foreground'])
436 self.ax.spines['top'].set_color(self.colors['foreground'])
437 self.ax.spines['bottom'].set_color(self.colors['foreground'])
438 self.ax.set_title((("Session graph for ring games") + names), color=self.colors['foreground'])
439 self.ax.set_facecolor(self.colors['background'])
440 self.ax.set_xlabel(("Sessions"), fontsize=12, color=self.colors['foreground'])
441 self.ax.set_ylabel("$", color=self.colors['foreground'])
442 self.ax.grid(color=self.colors['grid'], linestyle=':', linewidth=0.2)
444 candlestick_ochl(self.ax, quotes, width=0.50, colordown=self.colors['line_down'], colorup=self.colors['line_up'], alpha=1.00)
445 self.graphBox.layout().addWidget(self.canvas)
446 self.canvas.draw()
448 def addTable(self, frame, results):
449 colxalign, colheading = list(range(2))
451 self.liststore = QStandardItemModel(0, len(self.columns))
452 self.liststore.setHorizontalHeaderLabels([column[colheading] for column in self.columns])
453 for row in results:
454 listrow = [QStandardItem(str(r)) for r in row]
455 for item in listrow:
456 item.setEditable(False)
457 self.liststore.appendRow(listrow)
459 self.view = QTableView()
460 self.view.setModel(self.liststore)
461 self.view.verticalHeader().hide()
462 self.view.setSelectionBehavior(QTableView.SelectRows)
463 frame.layout().addWidget(self.view)
464 self.view.doubleClicked.connect(self.row_activated)
466 def row_activated(self, index):
467 if index.row() < len(self.times):
468 replayer = None
469 for tabobject in self.owner.threads:
470 if isinstance(tabobject, GuiHandViewer.GuiHandViewer):
471 replayer = tabobject
472 self.owner.tab_hand_viewer(None)
473 break
474 if replayer is None:
475 self.owner.tab_hand_viewer(None)
476 for tabobject in self.owner.threads:
477 if isinstance(tabobject, GuiHandViewer.GuiHandViewer):
478 replayer = tabobject
479 break
480 reformat = lambda t: strftime("%Y-%m-%d %H:%M:%S+00:00", gmtime(t))
481 handids = replayer.get_hand_ids_from_date_range(reformat(self.times[index.row()][0]), reformat(self.times[index.row()][1]))
482 print('handids:', handids)
483 replayer.reload_hands(handids)
485if __name__ == '__main__':
486 import Configuration
487 config = Configuration.Config()
489 settings = {}
491 settings.update(config.get_db_parameters())
492 settings.update(config.get_import_parameters())
493 settings.update(config.get_default_paths())
495 from PyQt5.QtWidgets import QApplication, QMainWindow
496 app = QApplication([])
497 import SQL
498 sql = SQL.Sql(db_server=settings['db-server'])
500 colors = {
501 'background': '#19232D',
502 'foreground': '#9DA9B5',
503 'grid': '#4D4D4D',
504 'line_up': 'g',
505 'line_down': 'r',
506 'line_showdown': 'b',
507 'line_nonshowdown': 'm',
508 'line_ev': 'orange',
509 'line_hands': 'c'
510 }
512 i = GuiSessionViewer(config, sql, None, None, colors)
513 main_window = QMainWindow()
514 main_window.setCentralWidget(i)
515 main_window.show()
516 main_window.resize(1400, 800)
517 app.exec_()