Ίδιο commit, ίδια αρχεία parquet, ίδιο μηχάνημα, δύο εκτελέσεις με διαφορά μίας ώρας. Sharpe 1.34 και Sharpe 1.19. Συνολική απόδοση 41.2% και 37.8%. Αριθμός συναλλαγών 1,418 και 1,421. Και οι δύο καμπύλες κεφαλαίου συμφωνούσαν σε κάθε δεκαδικό ψηφίο που εμφανιζόταν, για 11,205 συνεχόμενες μπάρες, πριν αποκλίνουν.
Οι τρεις πρώτοι αριθμοί είναι εκνευριστικοί. Ο τελευταίος είναι αυτός που έχει ενδιαφέρον, γιατί δείχνει ότι το πρόβλημα δεν είναι ένα πρόχειρο σφάλμα κινητής υποδιαστολής που συσσωρεύτηκε σε όλη την εκτέλεση. Κάτι διακριτό συνέβη σε μία συγκεκριμένη μπάρα και ό,τι ακολούθησε ήταν αποτέλεσμα ανατοκισμού. Αυτή η ανάρτηση αφορά τον εντοπισμό εκείνης της μπάρας και το πώς το μέγεθος αυτής της διαφοράς 0.15 επηρεάζει κάθε σάρωση παραμέτρων που έχετε κάνει ποτέ.
Η στρατηγική: διατομεακή ορμή στα 30 perpetual συμβόλαια USDⓈ-M με τον μεγαλύτερο όγκο, επανεξισορρόπηση ανά 4 ώρες, long στα πέντε κορυφαία βάσει απόδοσης 12 ωρών, short στα πέντε χαμηλότερα, ιστορικό 18 μηνών, με χρεώσεις και funding ανά σκέλος.
Εντοπισμός της μπάρας όπου αποκλίνουν οι εκτελέσεις
Αν καταγράφετε τα ίδια κεφάλαια ανά μπάρα με πλήρη ακρίβεια, αυτό παίρνει περίπου τέσσερα λεπτά. Εξάγετε τις δύο εκτελέσεις σε CSV με (bar_index, equity, open_positions_hash), φορτώστε τα αρχεία και βρείτε τον πρώτο δείκτη όπου διαφωνούν. Είχαμε ήδη γράψει αυτό το script για ένα διαφορετικό bug, και γι' αυτόν ακριβώς τον λόγο δεν πέρασα ένα πρωινό ψάχνοντάς το.
Μπάρα 11,206, 2025-03-14T08:00 UTC. Στην προηγούμενη μπάρα τα ίδια κεφάλαια ήταν πανομοιότυπα μέχρι 13 σημαντικά ψηφία. Στην 11,206 τα hash των θέσεων διαφέρουν: η εκτέλεση A είναι long SOL, η εκτέλεση B είναι long AVAX. Ίδια πλευρά, ίδιο ονομαστικό μέγεθος, διαφορετικό σύμβολο. Ό,τι ακολουθεί προκύπτει από αυτό.
Έτσι, τύπωσα τις εισόδους της κατάταξης για εκείνη τη μπάρα και στις δύο εκτελέσεις. Ήταν πανομοιότυπες. Ίδιες byte προς byte, ίδια 30 σύμβολα, ίδιες 30 βαθμολογίες. Δύο από τις βαθμολογίες ήταν 0.0. Ακριβώς μηδέν και οι δύο, επειδή και τα δύο ονόματα είχαν μια μπάρα 4 ωρών με μηδενικές συναλλαγές μέσα στο παράθυρο αναδρομής, οπότε η σχέση close προς close βγήκε ένα και ο λογάριθμος μηδέν. Δεν ήταν στρογγυλοποίηση σε μηδέν. Ήταν μηδέν.
Δύο σύμβολα ισοβάθμησαν στην πέμπτη θέση. Η επιλογή κράτησε τα πέντε κορυφαία. Το ποιο από τα δύο θα έμπαινε εξαρτιόταν από τη σειρά που τα άφηνε η ταξινόμηση, και η ταξινόμηση δεν έκανε αυτό που υπέθετα.
Ισοβαθμία, ασταθής ταξινόμηση και αυτό που αγνοούσα εδώ και δύο χρόνια
Η κατάταξη γινόταν μέσω ομαδοποιημένης συνάθροισης, και το frame που της έδινε δεδομένα προερχόταν από dict comprehension πάνω σε ένα σύνολο συμβόλων, το οποίο αναδημιουργούνταν ανά μπάρα από ένα async fetch. Η σειρά επανάληψης του συνόλου αλλάζει ανάλογα με το hash seed, και η Python τυχαιοποιεί το seed κατακερματισμού συμβολοσειρών ανά διεργασία, εκτός αν ορίσετε το PYTHONHASHSEED. Έτσι, η σειρά των γραμμών πριν από την ταξινόμηση διέφερε μεταξύ των εκτελέσεων και, με ασταθή ταξινόμηση σε κλειδί με ισοβαθμία, η ισοβαθμία έσπαγε διαφορετικά.
Τέτοιες ισοβαθμίες δεν είναι σπάνια εξαίρεση. Είναι δομικό φαινόμενο. Όπου ένα χαρακτηριστικό κορέννυται ή περικόπτεται, δημιουργούνται ακριβείς ισότητες: οι μπάρες με μηδενικό όγκο δίνουν αποδόσεις ακριβώς μηδέν, ένα περικομμένο z-score καρφώνεται στο ±3.0, ένας μετασχηματισμός σε κατάταξη με λίγες διακριτές τιμές δημιουργεί δεκάδες ισοβαθμίες, ένα Boolean φίλτρο δίνει βαθμολογία 1.0 σε ό,τι περνάει. Σε 18 μήνες με μπάρες 4 ωρών, αυτή η εκτέλεση είχε 47 μπάρες με ισοβαθμία στο όριο επιλογής. Σε 3 από αυτές άλλαξε το επιλεγμένο καλάθι. Στις υπόλοιπες, ισοβάθμησαν δύο ονόματα που ήταν ήδη και τα δύο μέσα ή και τα δύο έξω.
Για περίπου μία μέρα ήμουν πεπεισμένος ότι ο loader δεδομένων ήταν μη ντετερμινιστικός, επειδή αυτή είναι η συναρπαστική απάντηση. Δεν ήταν. Ποτέ δεν είναι. Είναι ένα σύνολο, μια ισοβαθμία και μια υπόθεση για τη σταθερότητα της ταξινόμησης που κανείς δεν κατέγραψε.
Γιατί 3 συναλλαγές αξίζουν 0.15 Sharpe
Αυτό είναι το σημείο στο οποίο αρκετοί αντιδρούν, και η απάντηση είναι ότι ένα backtest με μέγεθος θέσης ως κλάσμα των ιδίων κεφαλαίων εξαρτάται από τη διαδρομή. Αν το μέγεθος είναι 8% των τρεχόντων ιδίων κεφαλαίων ανά σκέλος, τότε η διαφορά στα ίδια κεφάλαια στη μπάρα n σημαίνει διαφορά σε κάθε ονομαστικό μέγεθος από τη μπάρα n και μετά.
Η πρώτη απόκλιση από μόνη της κόστισε ελάχιστα. Το σκέλος AVAX της εκτέλεσης B έχασε 2.1% σε εννέα ώρες· το σκέλος SOL της εκτέλεσης A κέρδισε 0.4%. Διαφορά ιδίων κεφαλαίων μετά από εκείνη τη συναλλαγή: 0.21%. Αμελητέα. Αλλά από εκεί και πέρα οι δύο εκτελέσεις δεν είναι πια η ίδια στρατηγική. Διατηρούν ελαφρώς διαφορετικά μεγέθη θέσεων, οπότε επιβαρύνονται με ελαφρώς διαφορετικές χρεώσεις funding, ενώ δύο από τις μεταγενέστερες ισοβαθμίες στο όριο έσπασαν ξανά διαφορετικά, επειδή οι βαθμολογίες που τροφοδοτούσαν την επιλογή προέρχονταν πλέον από θέσεις που διέφεραν οριακά. Η μία συνέβη στις 2025-03-27, μία μέρα πριν από μια τάση έξι ημερών που έφερε περίπου το ένα τρίτο του συνολικού PnL της εκτέλεσης. Η εκτέλεση A συμμετείχε σε όλη την κίνηση· η εκτέλεση B μπήκε μία επανεξισορρόπηση αργότερα.
Διαφορά απόδοσης: 3.4 μονάδες. Η διαφορά Sharpe είναι μεγαλύτερη απ' όσο υποδηλώνει η διαφορά απόδοσης, επειδή οι αναδιατεταγμένες συναλλαγές της εκτέλεσης B συνέπεσαν με ένα πιο ευμετάβλητο διάστημα: ο παρονομαστής αυξήθηκε ενώ ο αριθμητής μειώθηκε. Μικρή αιτία, δύο ενισχυτές.
Αν το μέγεθος θέσης σας είναι σταθερό σε ονομαστική αξία και οι είσοδοί σας δεν εξαρτώνται από τις τρέχουσες θέσεις, είστε πολύ πιο προστατευμένοι. Οι περισσότερες ενδιαφέρουσες στρατηγικές δεν ισχύουν ούτε στη μία ούτε στην άλλη περίπτωση.
Τα πέντε σημεία από όπου προκύπτει στην πράξη
| Πηγή | Σύμπτωμα | Διόρθωση |
|---|---|---|
Μη ορισμένο PYTHONHASHSEED με σειρά επανάληψης set/dict να τροφοδοτεί ταξινόμηση | Η ισοβαθμία σπάει διαφορετικά μεταξύ εκτελέσεων· πρώτη απόκλιση σε συγκεκριμένη μπάρα | Ορίστε το seed· ταξινομήστε με ρητό δευτερεύον κλειδί (σύμβολο), ώστε οι ισοβαθμίες να λύνονται ντετερμινιστικά |
Ασταθής ταξινόμηση σε κλειδί με ισοβαθμία (quicksort προεπιλογή σε NumPy/pandas) | Όπως παραπάνω, το πρόβλημα παραμένει και μετά τον ορισμό του seed | kind="stable", ή ορίστε κλειδί που επιβάλλει συνολική διάταξη |
| RNG χωρίς ορισμένο seed σε bootstrap, ανακατέματα train/test ή τεχνητό jitter εκτέλεσης | Απόκλιση σε όλη την εκτέλεση, χωρίς σαφές σημείο πρώτης διαφοράς | Ένα ρητό seed ανά στοιχείο, καταγεγραμμένο στο manifest της εκτέλεσης |
| Παράλληλη άθροιση float (σειρά άθροισης που εξαρτάται από τον αριθμό των threads) | Διαφορές στα τελευταία λίγα bits, συνήθως ακίνδυνες μέχρι να περάσουν ένα όριο σύγκρισης | Ορίστε τον αριθμό των threads για ερευνητικές εκτελέσεις· ποτέ μην συγκρίνετε floats με == σε όριο απόφασης |
| Εκδόσεις βιβλιοθηκών χωρίς κλείδωμα | Αναπαραγώγιμο σήμερα, όχι τον Νοέμβριο | Hash του lockfile στο manifest, μαζί με το hash του snapshot δεδομένων |
Η τέταρτη γραμμή έχει μικρότερη σημασία απ' όσο φοβούνται πολλοί, ενώ η πρώτη είναι αυτή που προκαλεί συνεχώς προβλήματα.
Η αναπαραγωγιμότητα bit προς bit είναι εργαλείο, όχι αρετή
Θέλετε ντετερμινισμό ώστε, όταν αλλάζετε μία γραμμή, η διαφορά στην καμπύλη κεφαλαίου να αποδίδεται σε εκείνη τη γραμμή. Αυτός είναι όλος ο λόγος. Κάθε εκτέλεση agent στο Stratmill γράφει πλέον ένα manifest με το hash του snapshot δεδομένων, το hash του lockfile και κάθε seed, ενώ μια επανεκτέλεση που δεν αναπαράγει την προηγούμενη καμπύλη bit προς bit θεωρείται αποτυχημένο build, όχι αξιοπερίεργο συμβάν.
Αλλά μόλις πετύχετε την αναπαραγωγιμότητα, σπάστε την επίτηδες. Τρέξτε το 64 φορές με 64 seeds και δείτε την κατανομή:
Το εύρος μεταβλητότητας. Ίδια στρατηγική, ίδια δεδομένα, 64 τυχαιοποιημένες εκδοχές για την επίλυση ισοβαθμιών και τη σειρά εκτέλεσης, με ορισμένα seeds. Sharpe p5 1.12, διάμεσος 1.27, p95 1.41. Πλάτος εύρους 0.29.
Τώρα επιστρέψτε στη σάρωση παραμέτρων. Η καλύτερη διαμόρφωση βαθμολογήθηκε με 1.46. Η διαμόρφωση στην 40ή θέση από τις 96 βαθμολογήθηκε με 1.31. Η διαφορά μεταξύ τους είναι 0.15, δηλαδή το μισό του εύρους. Η σάρωση δεν κατέταξε αυτές τις δύο διαμορφώσεις. Δειγματοληψία έκανε από μία τιμή από την κατανομή καθεμιάς και ταξινόμησε τις τιμές.
Αυτή η αλλαγή οπτικής επηρέασε τον τρόπο που επιλέγουμε. Το αποτέλεσμα μιας σάρωσης είναι κατάταξη μόνο όταν οι διαφορές μεταξύ των διαμορφώσεων υπερβαίνουν τη μεταβλητότητα μιας μεμονωμένης διαμόρφωσης. Σε μια στρατηγική που εξαρτάται από τη διαδρομή και έχει 1,400 συναλλαγές, η μεταβλητότητα συνήθως αρκεί για να ισοπεδώσει το πρώτο τρίτο του πίνακα κατάταξης. Όταν συμβαίνει αυτό, επιλέξτε με βάση κάτι που δεν κρύβεται μέσα στο εύρος: χαμηλότερη εναλλαγή θέσεων, λιγότερες παράμετροι, μια παραδοχή για το κόστος που θα υπερασπιζόσασταν απέναντι σε έναν δύσπιστο συνομιλητή, καλύτερη συμπεριφορά στην περίοδο walk-forward που σας αρέσει λιγότερο. Αυτά είναι ουσιαστικά κριτήρια ισοβαθμίας. Ένα πλεονέκτημα 0.15 Sharpe δεν είναι.
Ένα ακόμη πράγμα που αξίζει να κάνετε πριν εμπιστευτείτε οτιδήποτε από αυτά. Τρέξτε τώρα το backtest σας δύο φορές, συγκρίνετε τα ίδια κεφάλαια ανά μπάρα και διαπιστώστε αν ανήκετε στην κατηγορία των πανομοιότυπων bit ή στην κατηγορία του 0.15. Είναι ένα πείραμα δεκαπέντε λεπτών και σας δείχνει πόσο από το ερευνητικό σας ιστορικό μετρούσε τη στρατηγική και πόσο ένα hash seed.
← Όλα τα άρθρα


