-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathbasic_trade_stats.py
786 lines (595 loc) · 31.1 KB
/
basic_trade_stats.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
#######################################
# Code: Rich O'Regan (London) Sep 2017
#######################################
import math
import numpy as np
from backtrader import Analyzer
from backtrader.utils import AutoOrderedDict, AutoDict
class BasicTradeStats(Analyzer):
'''
Summary:
Calculate popular statistics on all closed trades.
Also calculates statistics on winning and losing trades seperately.
Statistics include, percentage of winning trades, reward risk ratio
and basic streak analysis.
Params:
- ``calcStatsAfterEveryTrade``: (default: ``False``)
### COMMENTS NEEDED ####
- ``useStandardPrint``: (default: ``False``)
### COMMENTS NEEDED ####
Methods:
- get_analysis
Returns a dictionary holding the statistics.
Statistics calculated:
From ALL trades:
total - number of all trades closed and open.
open - number of open trades.
NOTE: Obvious point but worth reminding.If a trade is open,
it is not yet known if it will be a winner or loser.
closed - number of closed trades.
- pnl
total - p&l of all trades combined.
average - average p&l from every trade taken, won or lost.
- stats
profitFactor - total of all profit / total of all losses.
winFactor - number of trades won / number of trades lost.
winRate - percentage of winning trades (also called strike rate).
rewardRiskRatio - average profit of winning trades
/ average loss from losing trades.
From WON trades:
closed - number of closed trades.
percent - winning trades as a percentage of all closed trades.
- pnl
total - total p&l of only the winning trades.
average - average p&l of winning trades.
median - median p&l of winning trades.
max - maximun p&l generated on a trade,
from set of all winning trades.
- streak
current - current length of winning streak (if any).
max - maximum length of winning streak that occurred.
average - average length of winning streaks.
median - median length of winning streaks.
From LOST trades:
Includes stats from WON above but applied to losing trades.
[This 'basictradedtats.py' was coded by Richard O'Regan (London) October 2017]
'''
# Declare parameters user can pass to this Analyzer..
params = (
# Run calculations once at end (fast) OR after every trade (slower)
('calcStatsAfterEveryTrade', False ),
# Filter stats on long or short trades only..
# 'all' = all trades
# 'long' = long trades only
# 'short' = short trades only
('filter', 'all'),
# When .print() called
# If True -> use standard BackTrader print() [this is verbose]
# If False -> instead use my clearer table format..
('useStandardPrint', False),
)
def nextstart(self):
# Called once by Backtrader first valid bar of data..
o = self.rets # User returned object..
o.all.firstStrategyTradingDate = self.datas[0].datetime.datetime(0)
self.next() # Run next() method..
def next(self):
# Called every bar of data strategy runs.
# Get last trading date strategy runs used to calc annual statistics.
# Last trading date so far, is simply the current date of this bar.
# As more bars pass by, this last date is updated to reflect latest
# last date..
self.rets.all.lastStrategyTradingDate = self.datas[0].datetime.datetime(0)
def create_analysis(self):
# Set up variables..
# Variables hidden from user..
# Depending on the input parameter user provides for 'filter',
# append 'LONG' or 'SHORT' in table heading output to user.
# Helps user identify the types of trades stats calculated on,
# either long, short or all (i.e. both)..
if self.p.filter == 'long':
self._tableLongShort = 'LONG' # Append 'LONG' to table heading..
elif self.p.filter == 'short':
self._tableLongShort = 'SHORT' # Append 'SHORT' to table heading..
elif self.p.filter == 'all':
self._tableLongShort = 'TRADES' # Blank char appended to our table..
else:
raise Exception("Parameter 'filter' must be 'long', 'short', or" +
" 'all' not '%s'." % str(self.p.filter))
self._all_pnl_list=[] # hidden from user - all trades pnl.
self._won_pnl_list=[] # hidden from user - win trades pnl.
self._lost_pnl_list=[] # hidden from user - lost trades pnl.
self._curStreak = None # Current streak type [None, 'Won', 'Lost']
self._wonStreak_list=[] # Store each won streak in list..
self._lostStreak_list=[] # Store each loss streak in list..
# Variables output to user..
o = self.rets = AutoOrderedDict() # Return user object..
# Stats applied to all trades (winners and losers)..
o.all.firstStrategyTradingDate = None
o.all.lastStrategyTradingDate = None
o.all.trades.total = 0
o.all.trades.open = 0
o.all.trades.closed = 0
o.all.pnl.total = None
o.all.pnl.average = None
o.all.streak.zScore = None
o.all.stats.profitFactor = None
o.all.stats.winFactor = None
o.all.stats.winRate = None
o.all.stats.rewardRiskRatio = None
o.all.stats.expectancyPercentEstimated = None
o.all.stats.kellyPercent = None
o.all.stats.tradesPerYear = None
o.all.stats.perTradeOpportunityPercent = None
o.all.stats.annualOpportunityPercent = None
o.all.stats.annualOpportunityCompoundedPercent = None
#o.all.stats.stake1PercentAnnualOpportunityCompoundedPercent = None
for each in ['won', 'lost']:
oWL=self.rets[each]
oWL.trades.closed = 0
oWL.trades.percent = None
oWL.pnl.total = None
oWL.pnl.average = None
oWL.pnl.median = None
oWL.pnl.max = None
oWL.streak.current = 0
oWL.streak.max = None
oWL.streak.average = None
oWL.streak.median = None
def calculate_statistics(self):
# Calculate various statistics..
# Applied to three groups;
# 1) All trades
# 2) Winning trades
# 3) Losing trades
# NOTE: To see for different types of trades, e.g. Long or Short,
# simply run strategy with Long trades only, gather stats,
# and then re-run with short trades only and gather stats.
# A system that goes long and short can be simplified to two different
# systems. likely parameters will be different also..
# This method can be called either;
# 1) After every completed trade, i.e. ran multiple times.
# 2) Once at end after all trades completed.
# Option 1) occurs if calcStatsAfterEveryTrade is set to True.
# Option 2) occurs if calcStatsAfterEveryTrade is set to False.
# NOTE: Final output after last trade will always be the same.
# regardless of which option you use. The difference is that option 1)
# allows your strategy to access these statistics whilst it is running.
# Option 1)
# Running after every trade is obviously slower but needed if your
# strategy needs to know these statstics whilst running. i.e. may adapt
# itself as profit and loss statistics change.
# example1: as winning streak increases, bet more.
# example2: if win rate drops to less than 50% (0.5) stop trading until
# win rate picks back up to 70% (0.7)..
# Option 2)
# If strategy does not need to know these information, it will be
# quicker to run statistics just once at the end. In which case
# option 2 is quicker and more efficient.
# Must be at least 1 trade to proceed..
if self._all_pnl_list!=[]:
# Set up 'pointers' to save typing long lines..
oA=self.rets.all
oW=self.rets.won
oL=self.rets.lost
oA.pnl.total = np.sum(self._all_pnl_list)
oA.pnl.average = np.mean(self._all_pnl_list)
# Calc stats seperately for winning and losing trades..
for each in ['won', 'lost']:
# Get our list. Either _won_pnl_list or _lost_pnl_list..
pnlList = eval('self._' + str(each) + '_pnl_list')
# Check list not empty, else can't calculate median e.t.c.
if pnlList!=[]:
oWL=self.rets[each]
oWL.trades.closed = np.size(pnlList)
oWL.trades.percent = len(pnlList)/len(self._all_pnl_list)*100
oWL.pnl.total = np.sum(pnlList)
# Note: Max win calculated with max() function,
# but Max loss calculated using min() function..
oWL.pnl.max = (np.max(pnlList) if each=='won' else np.min(pnlList))
oWL.pnl.average = np.mean(pnlList)
oWL.pnl.median = np.median(pnlList)
# Streak calculations..
streak = eval('self._' + str(each) + 'Streak_list')
if streak!=[]:
oWL.streak.max = np.max(streak)
oWL.streak.average = np.mean(streak)
# Can only be integer. Cast from double/float to integer
oWL.streak.median = int(np.median(streak))
# Calc key stats on ALL trades..
oA.stats.winRate = oW.trades.percent
# Can only calc following if at least 1 winner and 1 loser..
if self._won_pnl_list!=[] and self._lost_pnl_list!=[]:
oA.streak.zScore = self.zScore(oW.trades.closed,
oL.trades.closed,
len(self._wonStreak_list))
oA.stats.profitFactor = (oW.pnl.total
/ (-1 * oL.pnl.total))
oA.stats.winFactor = (oW.trades.closed
/ oL.trades.closed)
if oW.pnl.average != 0: # Check for division by zero
oA.stats.kellyPercent = oA.pnl.average / oW.pnl.average*100
if oL.pnl.average != 0: # Check for division by zero
oA.stats.rewardRiskRatio = (oW.pnl.average
/ (-1 * oL.pnl.average))
oA.stats.expectancyPercentEstimated = (oA.pnl.average
/ (-1 * oL.pnl.average) * 100)
if (oA.stats.kellyPercent is not None
and oA.stats.expectancyPercentEstimated is not None):
# This is my (Rich O'Regan) own idea for important system
# measures.. simple but I believe essence.
# You will know how works by looking at the simple calculation..
# 'perTradeOpportunityPercent'
# a system may have 3 trades a year, will get a shitty AOP &
# AOCP compared to a system with 200.. but don't ignore as
# could still be great system and combined with other similar
# systems to increase trades..
#
# 'annualOpportunityPercent'
# Equalises for effect of time. Take into account number of
# trading days system backtested over and calulate the
# 'opportunity' for whole year.
# 'annualOpportunityCompoundedPercent'
# Same as above, just assume after each trade we are able to
# compound profits.
# Note: in reality may not be possible to compound after each
# trade coz amount of contracts allowed with margin or risk..
# MORE IMPORTANTLY, may have several trades on at the same time,
# some may close at same time (e.g. a shared stop), and
# outcome wasn't known, so new trades made whilst old positions
# not yet closed, would not have had benefits of knowing we
# could compound the profits (or allow for losses)..
# WHAT DOES THE ABOVE SHIT MEAN?:
# It's a useful measure, sometimes it is precise, though in some
# system cases, perhaps with muliple overlapping trades,
# it's more of an helpful estimation..
# TO % - 'perTradeOpportunityPercent'
oA.stats.perTradeOpportunityPercent = (
(oA.stats.kellyPercent / 100) *
(oA.stats.expectancyPercentEstimated / 100) * 100)
# AO % - 'annualOpportunityPercent'
_daysStrategyRan = (oA.lastStrategyTradingDate -
oA.firstStrategyTradingDate).days
oA.stats.tradesPerYear = oA.trades.closed * 365 / _daysStrategyRan
oA.stats.annualOpportunityPercent = (oA.stats.tradesPerYear *
oA.stats.perTradeOpportunityPercent)
# AOC % - 'annualOpportunityCompoundedPercent'
_power = (oA.stats.tradesPerYear)
_value = ((oA.stats.perTradeOpportunityPercent / 100) + 1)
oA.stats.annualOpportunityCompoundedPercent = (
(np.power(_value, _power) - 1) * 100 )
# 1% stake AOC % - 'stake1PercentAnnualOpportunityCompoundedPercent'
#_1pctTradeOp = 0.01 * (oA.stats.expectancyPercentEstimated / 100)
#_1pctValue = _1pctTradeOp + 1
#oA.stats.stake1PercentAnnualOpportunityCompoundedPercent = (
# (np.power(_1pctValue, _power) - 1) * 100 )
def preparation_pre_calculation(self, trade):
# This code does the basic steps of sorting each trade into a winner or
# loser list which is then used later by 'calculate_statistics()'.
# It also sets up the lists for winner and losing streak analysis.
# NOTE: the code here runs in linear n time. There should be little
# reason to optimise. Better to optimise 'calculate_statistics()' as
# this may be running every trade in exponential O^n time.
# [If you don't know what n time or O^n times means, don't stress :) ]
if trade.justopened:
# Trade just opened, update number of trades..
self.rets.all.trades.total += 1
self.rets.all.trades.open += 1
elif trade.status == trade.Closed:
# Trade closed, updated number of trades closed..
self.rets.all.trades.open += -1
self.rets.all.trades.closed += 1
# Put each trade pnl into different buckets (lists) depending if
# they are winning or losing trades..
pnl = trade.pnlcomm
self._all_pnl_list.append(pnl) # List of all win & losing trades.
if pnl >= 0:
# Current trade is a winner..
self._won_pnl_list.append(pnl) # List of all win trades
# Update winning streak list..
if self._curStreak=='Won':
# Previous trade was also a winner..
self.rets.won.streak.current+=1
else:
# Previous trade was a loser..
self._curStreak='Won'
self._lostStreak_list.append(self.rets.lost.streak.current)
self.rets.lost.streak.current=0
self.rets.won.streak.current+=1
else:
# Current trade is a loser..
self._lost_pnl_list.append(pnl) # List of all losing trades
# Update losing streak list..
if self._curStreak=='Lost':
# Previous trade was also a loser..
self.rets.lost.streak.current+=1
else:
# Previous trade was a winner..
self._curStreak='Lost'
self._wonStreak_list.append(self.rets.won.streak.current)
self.rets.won.streak.current=0
self.rets.lost.streak.current+=1
def notify_trade(self, trade):
longMatch = trade.long and self.p.filter == 'long'
shortMatch = not trade.long and self.p.filter == 'short'
allMatch = self.p.filter == 'all'
if True in [longMatch, shortMatch, allMatch]:
self.preparation_pre_calculation(trade)
if self.p.calcStatsAfterEveryTrade:
self.calculate_statistics()
def stop(self):
# Get last trading date strategy runs.. used to calc annual statistics..
#o = self.rets # User returned object..
#o.all.lastStrategyTradingDate = self.datas[0].datetime.datetime(0)
# REMOVED: if not self.p.calcStatsAfterEveryTrade: self.calculate_statistics()
# Removed line above, if we are using dates strategy ran for, then
# notify_trade() different than next()
# I.e. most statistics identical, but to check how many trading days passed
# we may have a trade then many days till another.. because of this
# any statistics like 'annualOpportunityPercent' that rely on number of
# trading days have occured, may be out..
# By running calculate_statistics() at end regardless if we calculated
# per trade, may mean for most statistics, we ran one time extra
# unecessary, but for trading day calculations, we need to run for
# accuracy..
self.calculate_statistics() # Run every time..
# Delete all lists we created to perform calculations..
# (to save memory)
self._all_pnl_list=[] # hidden from user - all trades pnl.
self._won_pnl_list=[] # hidden from user - win trades pnl.
self._lost_pnl_list=[] # hidden from user - lost trades pnl.
self._curStreak = None # Current streak type [None, 'Won', 'Lost']
self._wonStreak_list=[] # Store each won streak in list..
self._lostStreak_list=[] # Store each loss streak in list..
self.rets._close() # Check if we need this.. £££££££££####
def zScore(self, wins, losses, streaks):
'''
Calculates the Z-Score of streaks of wins and losses from a trading system.
If system has a significant Z score then it is potentially possible to
exploit the system for extra profit.
A negative Z score means that there are fewer streaks in the trading
system than would be expected statistically. This means that winning
trades tend to follow winning trades and that losing trades tend to
follower losers.
A positive Z score means that there are more streaks in the trading
system than would be expected. This means that winners tend to follow
losers and vice versa.
A confidence level of 95% or above is generally regarded as significant
enough to exploit the apparent non-randomness of streaks in a system.
Z scores of above 1.96 and below -1.96 represent 95% confidence.
i.e. Z scores less than 1.96 and greater than -1.96 e.g. 0.87 or -1.2,
suggest that outcome of previous trade cannot be used successfully to
predict (and therfore profit) from outcome of following trade.
A score of 0.0 suggests trade outcome totally independent of previous
trade outcome.
THE CALCULATION:
Z-Score = (n*(s - 0.5) - x) / ((x*(x - n))/(n - 1))^(1/2)
Where:
s = The total number of streaks in the sequence.
w = The total number of winning trades in the sequence.
L = The total number of losing trades in the sequence.
n = w + L [i.e. total number of trades in the sequence.]
x = 2*w*L
CONFIDENCE LEVELS:
z Score of 3.0 = 99.73%
Z Score of 2.58 = 99.0%
Z Score of 2.17 = 97.0%
Z Score of 1.96 = 95.0%
Z Score of 1.64 = 90.0%
Z Score of 1.44 = 85.0%
Z Score of 1.28 = 80.0%
Z Score of 1.04 = 70.0%
LIMITATIONS:
The Z-Score does not take into account size of wins and losses in
each trade of streak. Only binary outcomes considered, i.e. the
trade either won or lost.
Apparently Serial Correlation methods can deal with magnitude and
therefore provide a more useful statistic than Z-Score.
SOURCE OF INFORMATION:
http://www.mypivots.com/dictionary/definition/233/z-score
Maths probably came from book;
The Mathematics of Money Management: Risk Analysis Techniques for
Traders by Ralph Vince.
[This 'Z-Score' function was coded by Richard O'Regan (London) October 2017]
'''
w = wins
L = losses
s = streaks
n = w + L
x = 2*w*L
denominator = math.sqrt( (x*(x - n)) / (n - 1) )
if denominator != 0: # Avoid division by zero error..
numerator = n*(s - 0.5) - x
z = numerator/denominator
return z
# Denominator was zero, therefore can't calculate..
return None
def print(self, *args, **kwargs):
'''
Overide print method to display statistics to user in a
more visually pleasing and space efficient table format.
'''
# NOTE: Since this code is probably just a one off for this Analyzer
# It is not yet a flexible general purpose method to display any data
# in any table.
# It currently have half programmable and half hardwired functionality.
# Should this nicer output need to be used in other modules, the
# code could be modified to become general purpose.
# For now time is short and I just need something specific that works:)
# If user requests standard output, print using parent class..
if self.p.useStandardPrint:
super().print(*args, **kwargs)
return
# ..else override and make look nicer..!
# Set up 'pointers' to save typing long lines..
oAt=self.rets.all.trades
oAp=self.rets.all.pnl
oAs=self.rets.all.stats
oAk=self.rets.all.streak
oWt=self.rets.won.trades
oWp=self.rets.won.pnl
oWk=self.rets.won.streak
oLt=self.rets.lost.trades
oLp=self.rets.lost.pnl
oLk=self.rets.lost.streak
dpsf=self.dpsf # Decimal Place & Significant Figure formatting..
#oWt.percent=None ### ROR remove
# Structure for output
# List of dicts #### improve comenting
d = [
{'rowType':'table-top'},
{'rowType':'row-title', 'data':
['' , 'ALL ' + self._tableLongShort,
'', self._tableLongShort + ' WON', self._tableLongShort + ' LOST']},
{'rowType':'table-seperator'},
{'rowType':'row-data', 'data':
['TRADES open', dpsf(oAt.open),
'TRADES ', '', '']},
#'%.2f' % oWt.percent if oWt.percent!=None else oWt.percent,
#('%s' if oLt.percent is None else '%.2f') % oLt.percent]},
{'rowType':'row-data', 'data':
['closed', dpsf(oAt.closed),
'closed', dpsf(oWt.closed), dpsf(oLt.closed)]},
{'rowType':'row-data', 'data':
['Win Factor', dpsf(oAs.winFactor, dp=2),
'%', dpsf(oWt.percent, dp=2), dpsf(oLt.percent, dp=2)]},
#['Win Factor','%.2f'% oAs.winFactor, '%',
#'%.2f' % oWt.percent if oWt.percent!=None else oWt.percent,
#('%s' if oLt.percent is None else '%.2f') % oLt.percent]},
{'rowType':'row-data', 'data':
['Trades per year', dpsf(oAs.tradesPerYear, dp=1),
'', '', '']},
{'rowType':'table-seperator'},
{'rowType':'row-data', 'data':
['PROFIT total', dpsf(oAp.total, dp=2),
'PROFIT total', dpsf(oWp.total, dp=2), dpsf(oLp.total, dp=2)]},
{'rowType':'row-data', 'data':
['average', dpsf(oAp.average, dp=2), 'average',
dpsf(oWp.average, dp=2), dpsf(oLp.average, dp=2)]},
{'rowType':'row-data', 'data':
['Profit Factor', dpsf(oAs.profitFactor, dp=2),
'median', dpsf(oWp.median, dp=2), dpsf(oLp.median, dp=2)]},
{'rowType':'row-data', 'data':
['Reward : Risk', dpsf(oAs.rewardRiskRatio, dp=2),
'max', dpsf(oWp.max, dp=2), dpsf(oLp.max, dp=2)]},
{'rowType':'table-seperator'},
{'rowType':'row-data', 'data':
['Kelly %', dpsf(oAs.kellyPercent, dp=1),
'STREAK current', dpsf(oWk.current), dpsf(oLk.current)]},
{'rowType':'row-data', 'data':
['Expectancy %', dpsf(oAs.expectancyPercentEstimated, dp=1),
'max' , dpsf(oWk.max), dpsf(oLk.max)]},
{'rowType':'row-data', 'data':
['TO %', dpsf(oAs.perTradeOpportunityPercent, dp=2),
'average', dpsf(oWk.average, dp=2), dpsf(oLk.average, dp=2)]},
{'rowType':'row-data', 'data':
['AO %', dpsf(oAs.annualOpportunityPercent, dp=1),
'median', dpsf(oWk.median), dpsf(oLk.median)]},
#{'rowType':'row-data', 'data':
#['Kelly %', dpsf(oAs.kellyPercent, dp=1),
#'', '', '']},
#{'rowType':'row-data', 'data':
#['TO %', dpsf(oAs.perTradeOpportunityPercent, dp=2),
#'', '', '']},
{'rowType':'row-data', 'data':
['AOC %', dpsf(oAs.annualOpportunityCompoundedPercent, dp=1),
'Z-Score', dpsf(oAk.zScore, dp=1), dpsf(oAk.zScore, dp=1)]},
#{'rowType':'row-data', 'data':
#['1% AOC %',
# dpsf(oAs.stake1PercentAnnualOpportunityCompoundedPercent, dp=2),
#'', '', '']},
{'rowType':'table-bottom'}
]
s = self.displayTable(d)
print(s)
def fixedWidthText(self, string, nChars=15, align='centre'): # ,horzChar='═'):
# Displayoutput string of exactly n chars, no more no less (for good formatting)..
# Convert input to string incase it is not e.g. an int
string = str(string)
# Pad input string with space chars either side.
# Enables us to easily justify 'left','right' e.t.c. by slicing..
_s=' '*nChars + string + ' '*nChars
if align=='left' or align=='l':
return _s[nChars : nChars + nChars]
elif align=='right' or align=='r':
return _s[len(string):nChars+len(string)]
elif align=='centre' or align=='center' or align=='c':
startIndex = nChars - (int((nChars - len(string))/2))
return _s[startIndex : startIndex + nChars]
else:
raise Exception("Parameter 'align' must be 'left', 'right', or 'center' not '%s'." % str(align))
def displayTable(self, i):
# Input is a list of dictionaries, the 5 column format hardwired to
# run specifically with this method..
fWT = self.fixedWidthText # Shortcut to text formatting function.
# Find out max width need for each of the columns.
# This enables us to customise size of table cell and ensure data fits..
cs=[0,0,0,0,0] # Store size of columm for each of 5 columns..
for d in i:
# Check for data rows..
if d['rowType'] in ['row-title','row-data','row-data2']:
# Go thro each data cell and keep track of the max text length needed to display..
for c in range(5): # There are always 5 columns (hardwired)
_l = len(str(d['data'][c]))
if _l > cs[c]: cs[c]= _l
# Display each row by joining table cells together with these chars..
(x,rx,lx,v,h)=('╬','╣','╠','║','═')
(sv, hx, srx)=(' '+v, h+x, h+'╣')
s=''
for d in i:
# Check for table formating rows..
if d['rowType']=='table-top':
s+='╔═'+'═'*cs[0]+'╦'+'═'*cs[1]+'═╗'+' ╔═'+'═'*cs[2]+'╦'+'═'*cs[3]+'═╦'+'═'*cs[4]+'═╗\n'
if d['rowType']=='table-seperator':
s+='╠═'+'═'*cs[0]+'╬'+'═'*cs[1]+'═╣'+' ╠═'+'═'*cs[2]+'╬'+'═'*cs[3]+'═╬'+'═'*cs[4]+'═╣\n'
if d['rowType']=='table-bottom':
s+='╚═'+'═'*cs[0]+'╩'+'═'*cs[1]+'═╝'+' ╚═'+'═'*cs[2]+'╩'+'═'*cs[3]+'═╩'+'═'*cs[4]+'═╝'
# Check for data rows..
if d['rowType']=='row-title':
l = d['data']
s+= (v + fWT(l[0],cs[0]) + sv + fWT(l[1],cs[1],'center') + sv + ' ' + v
+ fWT(l[2],cs[2]) + sv + fWT(l[3],cs[3],'center')
+ sv + fWT(l[4],cs[4],'center') + sv + '\n')
if d['rowType']=='row-data':
l = d['data']
s+= (v + fWT(l[0],cs[0],'right') + sv + fWT(l[1],cs[1],'left') + sv + ' ' + v
+ fWT(l[2],cs[2],'right') + sv + fWT(l[3],cs[3],'left')
+ sv + fWT(l[4],cs[4],'left') + sv + '\n')
if d['rowType']=='row-data2':
l = d['data']
s+= (v + fWT(l[0],cs[0],'center') + sv + fWT(l[1],cs[1],'left') + sv + ' ' + v
+ fWT(l[2],cs[2],'right') + sv + fWT(l[3],cs[3],'left')
+ sv + fWT(l[4],cs[4],'left') + sv + '\n')
# Return a string representing nicely formated table..
return s
def dpsf(self, n=None, dp=None, sf=None):
# Decimal Place & Significant Figure formatting..
# Logic used to display numeric values neatly:
# dp = decimal places
# sf = significant figues
# n value is None type, e.g. a variable passed that does
# not have enough data to be initialised correctly..
# Do not try to format a None type, will cause an exception,
# instead pass it straight back..
if n == None:
return 'None'
# If no dp or sf provided..
# Display but allow space for sign
#if dp == None and sf == None:
# if
# If just dp
# check i= None
# If just sf
# check s= None
# If both, take the biggest
# checks n= None
# Keep alignment if positive or negative number
# e.g. -1.23 -> '-1.23'
# e.g. 1.45 -> ' 1.45' extra space added so stays aligned..
if dp != None:
_st = f'{dp}'
# Format decimal place e.g. '%.2f' for 2dp..
_st = ('%.'+ _st +'f') % n
return _st
else:
return str(n)