Coverage for GuiSessionViewer.py: 0%

345 statements  

« 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 -*- 

3 

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. 

17 

18from __future__ import print_function 

19from __future__ import division 

20 

21from past.utils import old_div 

22# import L10n 

23# _ = L10n.get_translation() 

24 

25import sys 

26import traceback 

27from time import time, strftime, localtime, gmtime 

28 

29from PyQt5.QtCore import Qt 

30from PyQt5.QtGui import QStandardItem, QStandardItemModel 

31from PyQt5.QtWidgets import QFrame, QLabel, QScrollArea, QSplitter, QTableView, QVBoxLayout 

32 

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 

38 

39try: 

40 calluse = not "matplotlib" in sys.modules 

41 import matplotlib 

42 

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 

51 

52 from numpy import diff, nonzero, sum, cumsum, max, min, append 

53 

54except ImportError as inst: 

55 print(("""Failed to load numpy and/or matplotlib in Session Viewer""")) 

56 print("ImportError: %s" % inst.args) 

57 

58import Database 

59import Filters 

60# import Charset 

61 

62import GuiHandViewer 

63import logging 

64 

65log = logging.getLogger("sessionViewer") 

66DEBUG = False 

67 

68 

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 

78 

79 self.liststore = None 

80 

81 self.MYSQL_INNODB = 2 

82 self.PGSQL = 3 

83 self.SQLITE = 4 

84 

85 self.fig = None 

86 self.canvas = None 

87 self.ax = None 

88 self.graphBox = None 

89 

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 

93 

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()) 

98 

99 # text used on screen stored here so that it can be configured 

100 self.filterText = {"handhead": ("Hand Breakdown for all levels listed above")} 

101 

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 } 

120 

121 self.filters = Filters.Filters(self.db, display=filters_display) 

122 self.filters.registerButton1Name("_Refresh") 

123 self.filters.registerButton1Callback(self.refreshStats) 

124 

125 scroll = QScrollArea() 

126 scroll.setWidget(self.filters) 

127 

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 ] 

141 

142 self.detailFilters = [] 

143 

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) 

150 

151 self.main_vbox = QSplitter(Qt.Vertical) 

152 

153 self.graphBox = QFrame() 

154 self.graphBox.setStyleSheet(f'background-color: {self.colors["background"]}') 

155 self.graphBox.setLayout(QVBoxLayout()) 

156 

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) 

163 

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) 

169 

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 = [] 

180 

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) 

187 

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 

203 

204 self.createStatsPane(frame, playerids, sitenos, games, currencies, limits, seats) 

205 

206 def createStatsPane(self, frame, playerids, sitenos, games, currencies, limits, seats): 

207 starttime = time() 

208 

209 (results, quotes) = self.generateDatasets(playerids, sitenos, games, currencies, limits, seats) 

210 

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])) 

214 

215 self.generateGraph(quotes) 

216 

217 self.addTable(frame, results) 

218 

219 self.db.rollback() 

220 print(("Stats page displayed in %4.2f seconds") % (time() - starttime)) 

221 

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 

227 

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 + "'") 

231 

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) 

243 

244 limittest = self.filters.get_limits_where_clause(limits) 

245 q = q.replace("<limit_test>", limittest) 

246 

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) 

252 

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") 

257 

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") 

263 

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() 

310 

311 hands = list(hands) 

312 

313 if not hands: 

314 return ([], []) 

315 

316 hands.insert(0, (hands[0][0], 0)) 

317 

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 

328 

329 first_idx = 1 

330 quotes = [] 

331 results = [] 

332 cum_sum = old_div(cumsum(profits), 100) 

333 sid = 1 

334 

335 total_hands = 0 

336 total_time = 0 

337 global_open = None 

338 global_lwm = None 

339 global_hwm = None 

340 

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) 

360 

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 

370 

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 ) 

409 

410 return (results, quotes) 

411 

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 

422 

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"]) 

427 

428 if self.canvas is not None: 

429 self.canvas.destroy() 

430 

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 

438 

439 def generateGraph(self, quotes): 

440 self.clearGraphData() 

441 sitenos = [] 

442 playerids = [] 

443 

444 sites = self.filters.getSites() 

445 heroes = self.filters.getHeroes() 

446 siteids = self.filters.getSiteIds() 

447 limits = self.filters.getLimits() 

448 

449 # graphops = self.filters.getGraphOps() 

450 

451 names = "" 

452 

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 

462 

463 if not sitenos: 

464 print(("No sites selected - defaulting to PokerStars")) 

465 self.db.rollback() 

466 return 

467 

468 if not playerids: 

469 print(("No player ids found")) 

470 self.db.rollback() 

471 return 

472 

473 if not limits: 

474 print(("No limits found")) 

475 self.db.rollback() 

476 return 

477 

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) 

490 

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() 

496 

497 def addTable(self, frame, results): 

498 colxalign, colheading = list(range(2)) 

499 

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) 

507 

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) 

514 

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) 

535 

536 

537if __name__ == "__main__": 

538 import Configuration 

539 

540 config = Configuration.Config() 

541 

542 settings = {} 

543 

544 settings.update(config.get_db_parameters()) 

545 settings.update(config.get_import_parameters()) 

546 settings.update(config.get_default_paths()) 

547 

548 from PyQt5.QtWidgets import QApplication, QMainWindow 

549 

550 app = QApplication([]) 

551 import SQL 

552 

553 sql = SQL.Sql(db_server=settings["db-server"]) 

554 

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 } 

566 

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_()