Smart Contract

TraderflowScores

A.bb12a6da563a5e8e.TraderflowScores

Deployed

1d ago
Feb 27, 2026, 03:00:30 AM UTC

Dependents

0 imports
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