Simas Razinskas

Realaus laiko prekybos kopijavimas grandinėje veikiančioje pavedimų knygoje

Grandinės sandorių kopijavimas per kelias sekundes, keičiant dydį, tikrinant slydimą ir šalinant dublikatus taip, kad pakartojimai nevirstų dvigubu vykdymu.

Paskelbta 2026-06-30Peržiūrėta 2026-07-02

Trumpai

Problema
Beveik realaus laiko prekybos kopijavimas turėjo greitai aptikti šaltinio sandorius, bet nekartoti tos pačios transakcijos ir nepriimti blogų užpildymų.
Apribojimas
Procesas priklausė nuo atsinaujinančių RPC duomenų, asinchroninio atsiskaitymo, skirtingų balansų ir neribotos trukmės su ribota atmintimi.
Sukurta sistema
Apklausos ir vykdymo kilpa su proporciniu dydžiu, slydimo ribomis, transakcijų hash deduplikacija ir ribotais atminties rinkiniais.
Rezultatas
Kiekvienas šaltinio sandoris sukuria daugiausia vieną sekėjo veiksmą, nesaugūs užpildymai atmetami, o procesas gali veikti nuolat be atminties dreifo.
Kur tai pritaikoma
Aktualu automatizavimo sistemoms, kur išorinė būsena keičiasi greitai, o kiekvienas pakartojimas turi turėti idempotentiškumą ir rizikos kontrolę.
Technologijos
Python · Web3 RPC · on-chain CLOB

Kodėl tai svarbu

Jei jūsų komanda planuoja sistemą su tais pačiais gedimo scenarijais — būtent nuo tokio mąstymo prasideda pirmas pokalbis.

Apžvalga

Pirmiau aprašytas apribojimas — visa užduotis: RPC ryšys, per kurį šis procesas skaito duomenis, atsinaujina pagal savo, o ne pagal prekiautojo grafiką; grandinė, nuo kurios jis priklauso, atsiskaito už sandorius asinchroniškai; sąskaita, į kurią kopijuojama, niekada neturi tokio paties balanso kaip sąskaita, iš kurios kopijuojama; o pats procesas turi veikti neribotą laiką, naudodamas atmintį, kuriai neleidžiama augti be ribų. Tarp dviejų sąskaitų nėra sinchroninio rankos paspaudimo, nėra bendros knygos, ir nėra natūralaus taško, kuriame kilpa galėtų saugiai sustoti ir vėliau tęstis nuo švarios būsenos. Viskas toliau sukurta iš vienos apklausos kilpos ir kelių aplink ją glaudžiai apvyniotų apsaugų — dydžio nustatymo, slydimo vartų, dublikatų patikros ir ribos, kiek kilpai leidžiama atsiminti. Nė viena iš šių apsaugų pati savaime nėra egzotiška; disciplina slypi tame, kad nė vienai iš jų neleidžiama nusileisti spaudžiant greitai kintančiai rinkai.

Sandorių aptikimas apklausa trumpesniu nei trijų sekundžių intervalu

Nauji sandoriai aptinkami apklausiant šaltinio sąskaitos veiklą grandinėje trumpesniu nei trijų sekundžių intervalu — pakankamai trumpu, kad sekėjo veiksmas vis dar nusileistų arti sandorio, kurį jis kopijuoja, bet pakankamai ilgu, kad kilpa nesmogtų į RPC galinį tašką kas akimirką. Kiekviena apklausa užduoda tą patį klausimą — ką ši sąskaita atliko nuo paskutinės patikros, ir kiekvienas atsakymas peržiūrimas eilės tvarka, o ne laikomas vienu vieninteliu nauju sandoriu. Rinkos judesys arba tinklo trikdis, atidedantis vieną apklausą iki kitos, gali lengvai atskleisti daugiau nei vieną sandorį viename atsakyme, ir kilpa tai turi traktuoti kaip įprastą, o ne kaip pakraštinį atvejį.

Pats intervalas — suderintas kompromisas, o ne vieną kartą pasirinkta ir pamiršta konstanta. Trumpesnis intervalas sumažina aptikimo vėlavimą, bet padaugina RPC apkrovą, o su ribotu užklausų skaičiumi galiniu tašku rizikuoja sukelti būtent tą pristabdymą, kuris tik padidintų vėlavimą, kurį jis bando sumažinti. Ilgesnis intervalas sumažina apkrovą, bet išplečia langą, kuriame sekėjo sandoris nusileidžia pastebimai vėliau už tą, kurį jis kopijuoja. Trys sekundės — būtent ta pusiausvyra, tinkanti šiai apkrovai: pakankamai greita, kad sekėjo sandoris vis dar būtų vykdomas rinkoje, kuri dar nebus nuėjusi toli, ir patogiai tilpstanti į tai, ką standartinis RPC tiekėjas toleruoja be atskiro prenumeratos lygio.

Kodėl ne prenumeruoti push/webhook srautą

Akivaizdi apklausos alternatyva — push srautas: WebSocket prenumerata arba webhook, suveikiantis tą pačią akimirką, kai atsiranda naujas sandoris, visiškai panaikinant intervalą ir teoriškai sumažinantis aptikimo vėlavimą iki to, ką leidžia pats tinklas. Praktiškai trys grandinės infrastruktūros savybės šią teoriją padaro silpnesnę, nei atrodo.

Šviežumas iš tikrųjų negarantuojamas push pranešimu: įvykis suveikia pagal laikiną grandinės vaizdą, ir blokas vis dar gali persitvarkyti (reorg) jau po to, kai įvykis buvo pristatytas, todėl prenumeratai vis tiek reikia tos pačios „ar tai jau galutinė būsena“ patikros, kurią būtų atlikusi apklausa — skirtumas tik tas, kad ši patikra atliekama jau po veiksmo, o ne prieš jį. Tiekėjų palaikymas nevienodas: ne kiekvienas RPC galinis taškas siūlo patikimą prenumeratos kanalą, tie, kurie siūlo, dažnai jį slepia už brangesnio lygio, o prenumeratos garantijos dėl pristatymo tvarkos ir be spragų pristatymo dažniausiai silpnesnės ir menkiau dokumentuotos nei to paties tiekėjo paprastas užklausos-atsakymo API. Ir būtent prisijungimo iš naujo etape push srauto paprastumas tyliai išnyksta — nutrūkusį WebSocket ryšį reikia aptikti, atverti iš naujo ir, svarbiausia, atstatyti tai, kas įvyko per spragą, o tai reiškia, kad klientas galiausiai parašo atkūrimo kelią, kuris pats yra apklausa: „kas pasikeitė nuo paskutinio įvykio, kurį šis procesas tikrai gavo?“

Sistema, kuriai vis tiek reikia palaikyti šį apklausos formos atkūrimo kelią, mažai ką laimi, palaikydama dar ir atskirą, pastovios būsenos push kelią greta jo. Apklausa fiksuotu, trumpu intervalu naudoja tą patį mechanizmą tiek įprastam atvejui, tiek atkūrimo atvejui — tai vienu mažiau dalyku, kurį reikia išlaikyti teisingą spaudžiant laikui.

Proporcingas pozicijos dydžio nustatymas

Kopijuojamas sandoris niekada neatitinka absoliutaus šaltinio sandorio dydžio; jis nustatomas kaip sekėjo sąskaitos paties balanso dalis, naudojant tą pačią dalį, kurią šaltinio prekiautojas skyrė iš savo balanso. Dvi sąskaitos beveik niekada neturi panašių balansų — šaltinio sąskaita, prekiaujanti didele pozicija, ir sekėjo sąskaita su dalimi to kapitalo neišvengiamai išsiskirs, jei sekėjas tiesiog nukopijuos absoliutų dydį — arba pernelyg apkraudamas mažą sąskaitą, arba, priešingai, palikdamas didžiąją dalies didelės sąskaitos kapitalo nepanaudotą.

Proporcingas dydžio nustatymas išlaiko stabilų ryšį tarp dviejų sąskaitų nepriklausomai nuo jų absoliutaus dydžio: prekiautojas, skiriantis dešimt procentų savo portfelio pozicijai, sukuria sekėjo veiksmą, skiriantį apytiksliai dešimt procentų sekėjo portfelio, kad ir koks būtų to sekėjo balansas. Pats balansas — kintantis dydis: indėliai, išėmimai ir paties sekėjo ankstesni nukopijuoti sandoriai jį keičia tarp apklausų, todėl dydžio nustatymas turi skaityti dabartinį sekėjo balansą tą pačią akimirką, kai vykdomas kopijuojamas veiksmas, o ne anksčiau kilpoje užfiksuotą reikšmę. Skaitymas iš naujo kiekvieną kartą kainuoja šiek tiek daugiau RPC srauto mainais į svarbesnę garantiją: sekėjas niekada neprekiauja pagal balanso reikšmę, kuri jau pasenusi tuo metu, kai pavedimas pasiekia grandinę.

Slydimo (slippage) apribojimas

Kiekvienas kopijuojamas veiksmas prieš vykdymą pereina tikėtino slydimo patikrą, palyginant kainą, kuria greičiausiai užsipildys pavedimų knyga, su kaina, kuria įvykdytas šaltinio sandoris. Jei šis skirtumas didesnis už fiksuotą toleranciją, veiksmas iškart atmetamas — ne perskaičiuojamas mažesniu dydžiu, ne bandomas dar kartą, o tiesiog atsisakomas.

Tai sąmoningas asimetrijos pasirinkimas: praleistas sandoris sekėjui kainuoja prarastą galimybę, o blogas užpildymas — tiesiogiai prarastus pinigus, ir šie du dalykai nėra vienodai atkuriami. Pavedimų knyga, kuri nuo šaltinio sandorio įvykdymo suplonėjo — nes ją jau paveikė paties šaltinio prekiautojo užpildymas, arba tiesiog todėl, kad rinka greitai juda — sekėjo pavedimą to paties dydžio įvykdys pastebimai blogesne kaina, ir kopijuoti tokį užpildymą be vartų reikštų, kad sistema tyliai priima sekėjo vardu nuostolį, kuriam sekėjas niekada nepritarė.

expected_slippage = estimate_slippage(order_book, side, size)
if expected_slippage > MAX_ALLOWED_SLIPPAGE:
    reject_trade(trade, reason="slippage_exceeded")
    return
execute_trade(trade, size)

Slydimo vartai — atsisakyti užpildymo, o ne jį priimti

Vartai sąmoningai grubūs: viena riba, patikrinama vieną kartą, tiesiai prieš vykdymą, o ne laipsniškas atsakas, bandantis išgelbėti dalinį užpildymą. Grubius vartus lengviau suprasti spaudžiant laikui nei gudrius, o grubių vartų gedimo scenarijus — praleistas sandoris — būtent tas, kurį ši sistema sukurta rinktis.

Transakcijų hash deduplikacija

Kiekvienas šaltinio sąskaitos įvykdytas sandoris atsiskaito su transakcijos maiša (hash), ir būtent ši maiša yra vienintelis patikimas šaltinis, ar procesas jau į ją reagavo. Prieš aptiktam sandoriui leidžiant sukelti sekėjo veiksmą, jo maiša patikrinama pagal atmintyje laikomą jau apdorotų maišų rinkinį; jei ji jau yra, sandoris praleidžiamas be tolesnio apdorojimo, nepriklausomai nuo to, kaip jis buvo aptiktas.

if trade.tx_hash in seen_hashes:
    return  # already handled, retry or replay
seen_hashes.add(trade.tx_hash)
execute_follower_trade(trade)

Deduplikacijos patikra, dėl kurios pakartojimai saugūs

Būtent ši patikra leidžia likusiai sistemai saugiai kartotis. Apklausa, persidengianti su ankstesniu bloko intervalu, nes kilpa pasileido iš naujo po gedimo; prisijungimas iš naujo, pakartotinai gaunantis sandorius, jau matytus prieš ryšiui nutrūkstant; rankinis pakartotinis paleidimas po operatoriaus įsikišimo — nė vienas iš šių atvejų nėra specialus, kurį kodas turėtų atskirai aptikti ir apdoroti, nes maišos patikra visus juos suveda į tą patį rezultatą, kaip ir sandorio, kuris niekada nebuvo pamatytas du kartus.

Garantija, kurią tai sukuria, yra tiksliai vieną kartą atliktas veiksmas kiekvienam šaltinio sandoriui, o ne tiksliai vienas aptikimas. Kilpa gali pamatyti — ir mato — tą patį sandorį daugiau nei kartą, kartais net sąmoningai, nes apklausos langai sąmoningai persidengia, kad sandoris arti lango ribos niekada nebūtų praleistas. Būtent deduplikacijos patikra, o ne aptikimo žingsnis, neša korektiškumo garantiją.

Riboti darbiniai rinkiniai atmintyje

Visų kada nors pamatytų transakcijų maišų rinkinys, laikomas amžinai, padarytų ankstesnio skyriaus deduplikacijos patikrą trivialiai teisingą, bet taip pat leistų proceso atminčiai augti be ribų tiek laiko, kiek jis veikia — o automatizavimo procesui be numatyto perkrovimo tai reiškia neribotai. Darbinis rinkinys turi būti ribotas, tai reiškia, kad jis turi pamiršti nebereikalingas maišas, ir sunkumas — pasirinkti pamiršimo taisyklę, kuri niekada neišmestų maišos, kurios deduplikacijos patikrai dar gali prireikti.

Sandoris rinkinyje turi išbūti tik tiek, kiek jis realiai gali vėl pasirodyti — plačiausio sąmoningai apklausos kilpos įvesto langų persidengimo plotis, pridėjus pakankamą maržą, kad būtų amortizuota prisijungimo iš naujo spraga, o sandoris nebūtų suprastas kaip naujas. Peržengus šį langą, nebeegzistavusi maiša daugiau nebeatsiranda, ir laikymas ją ilgiau neduoda jokio papildomo saugumo — tik atmintį, kurios procesas niekada nepanaudos.

Praktiškai tai reiškia, kad rinkinys šalinamas nuolatiniu pagrindu — senos įrašai pašalinami, kai jie iškrenta už svarbaus lango ribų, o ne rinkinys išvalomas visas iš karto pagal laikmatį, kas vėl atvertų tą pačią dvigubo vykdymo riziką, kurią deduplikacijos patikra ir turėjo uždaryti. Ta pati ribojimo logika taikoma bet kokiai kitai kilpos kaupiamai su sandoriu susijusiai būsenai — laukiantiems patvirtinimams, pakartojimų skaitikliams — dėl tos pačios priežasties: neribota trukmė bet kokį neribotą rinkinį paverčia lėtu nuotėkiu, o lėtas nuotėkis procese, kurio niekas nestebi kas minutę, anksčiau ar vėliau tampa prastova.

Kompromisai ir apribojimai

  • Trijų sekundžių apklausos intervalas — pagal projektą apatinė aptikimo vėlavimo riba; šaltinio sandoris gali likti nepastebėtas beveik tiek laiko, kol sekėjas pradės reaguoti, o tai push srautas galėtų sutrumpinti prisijungimo iš naujo ir šviežumo sudėtingumo kaina, aptarta aukščiau.
  • Proporcingas dydžio nustatymas vis tiek priklauso nuo balanso reikšmės, nuskaitytos kopijuojamo veiksmo akimirką; jei sekėjo balansas pasikeičia tarp sandorio aptikimo ir jo nuskaitymo dydžiui nustatyti, nukopijuota dalis nėra tobulai sinchronizuota su dalimi, kurią realiai skyrė šaltinio prekiautojas.
  • Slydimo vartai, suderinti pakankamai atsargiai, kad niekada nekopijuotų blogo užpildymo, dėl to paties principo atmes ir kai kuriuos užpildymus, kurie iš tikrųjų būtų buvę tinkami — abu gedimo scenarijai tiesiogiai konkuruoja tarpusavyje, ir nėra vienos ribos, panaikinančios abu.
  • Transakcijų hash deduplikacija patikima tik tiek, kiek patikima maiša, kurią praneša grandinė ir RPC tiekėjas; tiekėjas, grąžinantis nenuoseklią ar nekanoninę maišą tam pačiam sandoriui, tyliai apeitų deduplikacijos patikrą, nesukeldamas jokios klaidos.
  • Riboto darbinio rinkinio šalinimo langas — suderintas parametras, o ne fiksuotas dėsnis; augant sandorių apimčiai ar prisijungimų iš naujo dažniui, langas, kuris atrodė dosnus mažoje apimtyje, gali imti nukirpti teisėtus pakartojimus, todėl jį reikia periodiškai peržiūrėti, o ne palikti kaip vieną kartą nustatytą reikšmę.
  • Nė vienas iš šių mechanizmų nevertina ketinimo. Sistema kopijuoja sandorius; ji neturi jokio įžvalgumo, kodėl šaltinio prekiautojas priėmė vieną ar kitą sprendimą, o proporcingai ir su slydimo apribojimu nukopijuotas blogas sprendimas tebėra blogas sprendimas — tik priimtas du kartus.

Šaltiniai ir tolesnis skaitymas

Susijusios atvejų analizės

Sistemos, kurias su šia sieja bendras apribojimas — pakartojimai, izoliacija, kaina, atkūrimas.

Technologijos: Python · Web3 RPC · on-chain CLOB← Visos atvejų analizės

Turite sistemą, kuri privalo veikti?

Rezervuokite pokalbį — susidėliosime, ką ji turi daryti, kur ji lūš ir kokią mažiausią versiją verta kurti pirmiausia.