Smart Contract
TraderflowScores
A.bb12a6da563a5e8e.TraderflowScores
1/*
2
3 The TradeScores object creates a append only store for historical trade and equity data.
4 This data can be processed to determine performance information for a given trade account.
5 */
6
7access(all) contract TraderflowScores {
8
9 access(all) enum TradeType: UInt8 {
10 access(all) case Short
11 access(all) case Long
12 }
13
14 access(all) struct Equity {
15 access(all) let timestamp: UFix64
16 access(all) let value: UFix64
17
18 init(_value: UFix64) {
19 self.timestamp = getCurrentBlock().timestamp
20 self.value = _value
21 }
22 }
23
24 access(all) struct TradeMetadata {
25 access(all) let score: UInt32
26 access(all) let drawdown: UInt32
27 access(all) let winrate: UInt32
28 access(all) let tradeCount: UInt64
29 access(all) let equity: UFix64
30 access(all) let average_profit: UFix64
31 access(all) let average_loss: UFix64
32 access(all) let average_long_profit_ema150: UFix64
33 access(all) let average_long_loss_ema150: UFix64
34 access(all) let average_short_profit_ema150: UFix64
35 access(all) let average_short_loss_ema150: UFix64
36 access(all) let achievement_provisional: Bool
37 access(all) let achievement_bear: Bool
38 access(all) let achievement_bull: Bool
39 access(all) let achievement_piggyban: Bool
40 access(all) let achievement_scales: Bool
41 access(all) let achievement_robot: Bool
42 access(all) let achievement_bank: Bool
43 access(all) let achievement_moneybags: Bool
44 access(all) let achievement_safe: Bool
45 access(all) let achievement_crown1: Bool
46 access(all) let achievement_crown2: Bool
47 access(all) let achievement_diamond1: Bool
48 access(all) let achievement_diamond2: Bool
49 access(all) let achievement_onfire: Bool
50
51 init (_score: UFix64, _drawdown: UFix64, _winrate: UFix64, _tradeCount: UInt64, _equity: UFix64, _average_profit:UFix64, _average_loss:UFix64, _average_long_profit_ema150:UFix64, _average_long_loss_ema150:UFix64, _average_short_profit_ema150:UFix64, _average_short_loss_ema150:UFix64, _achievement_provisional: Bool, _achievement_bear: Bool, _achievement_bull: Bool, _achievement_piggyban: Bool, _achievement_scales: Bool, _achievement_robot: Bool, _achievement_bank: Bool, _achievement_moneybags: Bool, _achievement_safe: Bool, _achievement_crown1: Bool, _achievement_crown2: Bool, _achievement_diamond1: Bool, _achievement_diamond2: Bool, _achievement_onfire: Bool) {
52 self.score = UInt32(_score*100.0)
53 self.drawdown = UInt32(_drawdown*100.0)
54 self.winrate = UInt32(_winrate*100.0)
55 self.tradeCount = _tradeCount
56 self.equity = _equity
57 self.average_profit = _average_profit
58 self.average_loss = _average_loss
59 self.average_long_profit_ema150 = _average_long_profit_ema150
60 self.average_long_loss_ema150 = _average_long_loss_ema150
61 self.average_short_profit_ema150 = _average_short_profit_ema150
62 self.average_short_loss_ema150 = _average_short_loss_ema150
63 self.achievement_provisional = _achievement_provisional
64 self.achievement_bear = _achievement_bear
65 self.achievement_bull = _achievement_bull
66 self.achievement_piggyban = _achievement_piggyban
67 self.achievement_scales = _achievement_scales
68 self.achievement_robot = _achievement_robot
69 self.achievement_bank = _achievement_bank
70 self.achievement_moneybags = _achievement_moneybags
71 self.achievement_safe = _achievement_safe
72 self.achievement_crown1 = _achievement_crown1
73 self.achievement_crown2 = _achievement_crown2
74 self.achievement_diamond1 = _achievement_diamond1
75 self.achievement_diamond2 = _achievement_diamond2
76 self.achievement_onfire = _achievement_onfire
77 }
78
79 access(all) view fun equal(md:TradeMetadata):Bool {
80 if self.score != md.score { return false }
81 if self.drawdown != md.drawdown { return false }
82 if self.winrate != md.winrate { return false }
83 if self.achievement_provisional != md.achievement_provisional { return false }
84 if self.achievement_bear != md.achievement_bear { return false }
85 if self.achievement_bull != md.achievement_bull { return false }
86 if self.achievement_piggyban != md.achievement_piggyban { return false }
87 if self.achievement_scales != md.achievement_scales { return false }
88 if self.achievement_robot != md.achievement_robot { return false }
89 if self.achievement_bank != md.achievement_bank { return false }
90 if self.achievement_moneybags != md.achievement_moneybags { return false }
91 if self.achievement_safe != md.achievement_safe { return false }
92 if self.achievement_crown1 != md.achievement_crown1 { return false }
93 if self.achievement_crown2 != md.achievement_crown2 { return false }
94 if self.achievement_diamond1 != md.achievement_diamond1 { return false }
95 if self.achievement_diamond2 != md.achievement_diamond2 { return false }
96 if self.achievement_onfire != md.achievement_onfire { return false }
97 return true
98 }
99 }
100
101 access(all) struct TradeMetadataRebuild {
102 access(all) let tbv: TradeMetadata
103 access(all) let rebuild: Bool
104 init(_metadata: TradeMetadata, _rebuild: Bool) {
105 self.tbv = _metadata
106 self.rebuild = _rebuild
107 }
108 }
109
110 access(all) struct Trade {
111 access(all) let onchain: UFix64
112 access(all) let symbol: String
113 access(all) let tradeType: TradeType
114 access(all) let openPrice: Fix64
115 access(all) let openTime: UInt64
116 access(all) let closePrice: Fix64
117 access(all) let closeTime: UInt64
118 access(all) let stopLoss: Fix64
119 access(all) let takeProfit: Fix64
120 access(all) let profit: Fix64
121 access(all) let equity: UFix64
122 access(all) let ticket: UInt64
123
124 init(_symbol: String, _tradeType: TradeType, _openPrice: Fix64, _openTime: UInt64, _closePrice: Fix64, _closeTime: UInt64, _stopLoss: Fix64, _takeProfit: Fix64, _profit: Fix64, _equity: UFix64, _ticket: UInt64) {
125 self.onchain = getCurrentBlock().timestamp
126 self.symbol = _symbol
127 self.tradeType = _tradeType
128 self.openPrice = _openPrice
129 self.openTime = _openTime
130 self.closePrice = _closePrice
131 self.closeTime = _closeTime
132 self.stopLoss = _stopLoss
133 self.takeProfit = _takeProfit
134 self.profit = _profit
135 self.equity = _equity
136 self.ticket = _ticket
137 }
138 }
139
140 access(all) struct TradeScores {
141 /* Log of trades and counts */
142 access(self) var historical: [Trade]
143 access(contract) var positive_long_total: UInt
144 access(contract) var negative_long_total: UInt
145 access(contract) var positive_long_run: UInt
146 access(contract) var negative_long_run: UInt
147 access(contract) var positive_short_total: UInt
148 access(contract) var negative_short_total: UInt
149 access(contract) var positive_short_run: UInt
150 access(contract) var negative_short_run: UInt
151
152 /* Moving average of % profit */
153 access(contract) var average_long_profit_ema150: UFix64
154 access(contract) var average_long_loss_ema150: UFix64
155 access(contract) var average_short_profit_ema150: UFix64
156 access(contract) var average_short_loss_ema150: UFix64
157
158 /* Log of equity and totals */
159 access(self) var historical_equity: [Equity]
160 access(contract) var equity_max: UFix64
161
162
163 init() {
164 self.historical=[]
165 self.historical_equity=[]
166 self.positive_long_total = 0
167 self.negative_long_total = 0
168 self.positive_long_run = 0
169 self.negative_long_run = 0
170 self.positive_short_total = 0
171 self.negative_short_total = 0
172 self.positive_short_run = 0
173 self.negative_short_run = 0
174 self.equity_max = 0.0
175 self.average_long_profit_ema150 = 0.0
176 self.average_long_loss_ema150 = 0.0
177 self.average_short_profit_ema150 = 0.0
178 self.average_short_loss_ema150 = 0.0
179 }
180
181 access(all) view fun findOpen(_symbol: String, _ticket: UInt64):Trade? {
182 var pos:Int = self.historical.length-1
183 var openTrade: Trade? = nil
184
185 while (pos>0) {
186 var trade:Trade = self.historical[pos]
187
188 if (_symbol == trade.symbol && trade.ticket == _ticket) {
189 if trade.closeTime == 0 {
190 return trade
191 }
192 }
193 pos = pos - 1
194 }
195 return nil
196 }
197
198 access(all) view fun equityMinMaxBetween( start:UFix64, end:UFix64 ): [UFix64] {
199 var min:UFix64 = UFix64.max
200 var max:UFix64 = 0.0
201 var cnt:Int = self.historical_equity.length
202
203 while (cnt>0) {
204 cnt = cnt - 1
205 var eq = self.historical_equity[cnt]
206 if (eq.timestamp < end) {
207 if (eq.timestamp < start) {
208 cnt=0
209 break
210 } else {
211 if eq.value < max {
212 max = eq.value
213 }
214 if min > eq.value {
215 min = eq.value
216 }
217 }
218 }
219 }
220
221 return [min,max]
222 }
223
224 access(all) fun pushEquity(_equity: UFix64): TradeMetadataRebuild {
225 let oldMetadata: TraderflowScores.TradeMetadata = self.metadata();
226 var eq:Equity = Equity(_value:_equity)
227
228 self.historical_equity.append(eq)
229
230 if (self.equity_max < _equity) {
231 self.equity_max = _equity
232 }
233
234 /* Determine if the NFT needs to be rebuilt */
235 let newMetadata: TraderflowScores.TradeMetadata = self.metadata()
236 return TradeMetadataRebuild(_metadata: newMetadata, _rebuild: !oldMetadata.equal(md:newMetadata))
237 }
238
239 access(all) fun pushTrade(_trade: Trade): TradeMetadataRebuild {
240 let oldMetadata: TraderflowScores.TradeMetadata = self.metadata();
241
242 self.pushEquity(_equity: _trade.equity)
243
244 /* Calculate the running totals for completed trades */
245 if (_trade.closeTime != 0) { // Has the trade completed
246 if (_trade.tradeType == TradeType.Long) { // Is the trade long
247 if (_trade.profit > 0.0) {
248 self.positive_long_total = self.positive_long_total +1
249 self.positive_long_run = self.positive_long_run + 1
250 self.negative_long_run = 0
251 } else {
252 self.negative_long_total = self.negative_long_total +1
253 self.negative_long_run = self.positive_long_run + 1
254 self.positive_long_run = 0
255 }
256 } else if (_trade.tradeType == TradeType.Short) {
257 if (_trade.profit > 0.0) {
258 self.positive_short_total = self.positive_short_total +1
259 self.positive_short_run = self.positive_short_run + 1
260 self.negative_short_run = 0
261 } else {
262 self.negative_short_total = self.negative_short_total +1
263 self.negative_short_run = self.positive_short_run + 1
264 self.positive_short_run = 0
265 }
266 }
267
268 var open: Trade? = self.findOpen(_symbol: _trade.symbol, _ticket: _trade.ticket)
269 if ( open != nil ) {
270 var start: UFix64 = open!.onchain // Start timestamp
271 var end: UFix64 = _trade.onchain // End timestamp
272
273 /* Find minimum and maximum equity value for the duration of the open trade
274 var minmax :[UFix64]= self.equityMinMaxBetween(start: start, end: end)
275 _trade.minEquity = minmax[0]
276 _trade.maxEquity = minmax[1]*/
277
278 /* Calculate 150 trade exponential moving average percentage for profit and loss */
279 let ema = 2.0/150.0
280 let invEma = 1.0-ema
281 var profitPercent:Fix64 = _trade.profit / Fix64(open!.equity)
282 if _trade.profit>0.0 {
283 if _trade.tradeType == TradeType.Short {
284 if (self.positive_short_total==1) {
285 self.average_short_profit_ema150 = UFix64(profitPercent)
286 } else {
287 self.average_short_profit_ema150 = self.average_short_profit_ema150*invEma + UFix64(profitPercent)*ema
288 }
289 } else if _trade.tradeType == TradeType.Long {
290 if (self.positive_long_total==1) {
291 self.average_long_profit_ema150 = UFix64(profitPercent)
292 } else {
293 self.average_long_profit_ema150 = self.average_long_profit_ema150*invEma + UFix64(profitPercent)*ema
294 }
295 }
296
297 } else if _trade.profit<0.0 {
298 var lossPercent:UFix64 = UFix64(-profitPercent)
299 if _trade.tradeType == TradeType.Short {
300 if (self.negative_short_total==1) {
301 self.average_short_loss_ema150 = lossPercent
302 } else {
303 self.average_short_loss_ema150 = self.average_short_loss_ema150*invEma + lossPercent*ema
304 }
305 } else if _trade.tradeType == TradeType.Long {
306 if (self.negative_long_total == 1) {
307 self.average_long_loss_ema150 = lossPercent
308 } else {
309 self.average_long_loss_ema150 = self.average_long_loss_ema150*invEma + lossPercent*ema
310 }
311 }
312 }
313 }
314 }
315
316 /* Every trade is added to the historical data to allow for verification */
317 self.historical.append(_trade)
318
319 /* Determine if the NFT needs to be rebuilt */
320 let newMetadata = self.metadata()
321 return TradeMetadataRebuild(_metadata: newMetadata, _rebuild: !oldMetadata.equal(md:newMetadata))
322 }
323
324
325
326 /* INTERMEDIATE CALCULATIONS */
327
328 /* Return last recorded equity value */
329 access(all) view fun Equity(): UFix64 {
330 let len: Int = self.historical_equity.length
331 if len == 0 { return 0.0 }
332 else { return self.historical_equity[len-1].value }
333 }
334
335 access(all) view fun DrawDown(): UFix64 {
336 let equity: UFix64 = self.Equity()
337 if equity == 0.0 { return 0.0 }
338 else { return self.equity_max / equity }
339 }
340
341 access(all) view fun WinRate(): UFix64 {
342 var ptotal: UFix64 = UFix64(self.positive_long_total+self.positive_short_total)
343 var total: UFix64 = ptotal + UFix64(self.negative_long_total+self.negative_short_total)
344 if total == 0.0 || ptotal == 0.0 { return 0.0 }
345 else { return ptotal / total }
346 }
347
348 access(all) fun AverageProfitAndLoss(): [UFix64] {
349 var aveP: Fix64 = 0.0
350 var aveL: Fix64 = 0.0
351 var avePC: Fix64 = 0.0
352 var aveLC: Fix64 = 0.0
353
354 for trade in self.historical {
355 if trade.profit>0.0 {
356 aveP = aveP + trade.profit
357 avePC = avePC + 1.0
358 } else if trade.profit<0.0 {
359 aveL = aveL - trade.profit
360 aveLC = aveLC + 1.0
361 }
362 }
363 var avePnL:[UFix64] = []
364 if avePC == 0.0 { avePnL.append(0.0) }
365 else { avePnL.append(UFix64(aveP/avePC)) }
366 if aveLC == 0.0 { avePnL.append(0.0) }
367 else { avePnL.append(UFix64(aveL/aveLC)) }
368 return avePnL
369 }
370
371 access(all) fun Score(): UFix64 {
372 var pl: [UFix64] = self.AverageProfitAndLoss()
373 var win: UFix64 = self.WinRate()
374 if (pl[0]==0.0) { pl[0]=1.0 }
375 if (pl[1]==0.0) { pl[1]=1.0 }
376 var score: UFix64 = (pl[0]/pl[1])*win;
377 if (score > 79.999999) {
378
379 }
380 return score
381 }
382
383 /* ACHIEVEMENTS */
384 access(all) view fun Provisional_Achievement(): Bool {
385 /* Duration of 60 days in seconds */
386 let sixty_days:UFix64 = 60.0*24.0*60.0*60.0
387
388 /* If a user has less than 50 trades they are provisional */
389 if self.historical.length < 50 { return true }
390
391 /* Additionally if they have less than 60 days of trades on chain they are provisional */
392 if self.historical[0].onchain + sixty_days < getCurrentBlock().timestamp { return true }
393
394 return false
395 }
396
397 access(all) view fun Bear_Achievement(): Bool {
398 return self.positive_short_total > 25
399 }
400
401 access(all) view fun Bull_Achievement(): Bool {
402 return self.positive_long_total > 25
403 }
404
405 access(all) view fun Piggybank_Achievement(): Bool {
406 return (self.positive_long_total + self.positive_short_total) > 50
407 }
408
409 access(all) view fun Scales_Achievement(): Bool {
410 return (self.positive_short_total>25) && (self.positive_long_total>25)
411 }
412
413 access(all) view fun Robot_Achievement(): Bool {
414 return (self.positive_long_total + self.positive_short_total) > 100
415 }
416
417 access(all) view fun Bank_Achievement(): Bool {
418 return self.Equity() > 1000.0
419 }
420
421 access(all) view fun Moneybags_Achievement(): Bool {
422 return self.Equity() > 10000.0
423 }
424
425 access(all) view fun Safe_Achievement(): Bool {
426 return self.DrawDown() < 0.10
427 }
428
429 access(all) view fun Crown1_Achievement(): Bool {
430 return true
431 }
432
433 access(all) view fun Crown2_Achievement(): Bool {
434 return false
435 }
436
437 access(all) view fun Diamond1_Achievement(): Bool {
438 return true
439 }
440
441 access(all) view fun Diamond2_Achievement(): Bool {
442 return false
443 }
444
445 access(all) view fun OnFire_Achievement(): Bool {
446 return self.positive_long_run > 10 || self.positive_short_run > 10
447 }
448
449 access(all) fun metadata(): TradeMetadata {
450 var pl: [UFix64] = self.AverageProfitAndLoss()
451 return TradeMetadata(
452 _score: self.Score(),
453 _drawdown: self.DrawDown(),
454 _winrate: self.WinRate(),
455 _tradeCount: UInt64(self.historical.length),
456 _equity: self.Equity(),
457 _average_profit: pl[0],
458 _average_loss: pl[1],
459 _average_long_profit_ema150: self.average_long_profit_ema150,
460 _average_long_loss_ema150: self.average_long_loss_ema150,
461 _average_short_profit_ema150: self.average_short_profit_ema150,
462 _average_short_loss_ema150: self.average_short_loss_ema150,
463 _achievement_provisional: self.Provisional_Achievement(),
464 _achievement_bear: self.Bear_Achievement(),
465 _achievement_bull: self.Bull_Achievement(),
466 _achievement_piggyban: self.Piggybank_Achievement(),
467 _achievement_scales: self.Scales_Achievement(),
468 _achievement_robot: self.Robot_Achievement(),
469 _achievement_bank: self.Bank_Achievement(),
470 _achievement_moneybags: self.Moneybags_Achievement(),
471 _achievement_safe: self.Safe_Achievement(),
472 _achievement_crown1: self.Crown1_Achievement(),
473 _achievement_crown2: self.Crown2_Achievement(),
474 _achievement_diamond1: self.Diamond1_Achievement(),
475 _achievement_diamond2: self.Diamond2_Achievement(),
476 _achievement_onfire: self.OnFire_Achievement()
477 )
478 }
479 }
480}
481