xync-client 0.0.155__py3-none-any.whl → 0.0.162__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xync_client/Abc/AdLoader.py +0 -294
- xync_client/Abc/Agent.py +326 -51
- xync_client/Abc/Ex.py +421 -12
- xync_client/Abc/Order.py +7 -14
- xync_client/Abc/xtype.py +35 -3
- xync_client/Bybit/InAgent.py +18 -447
- xync_client/Bybit/agent.py +531 -431
- xync_client/Bybit/etype/__init__.py +0 -0
- xync_client/Bybit/etype/ad.py +47 -34
- xync_client/Bybit/etype/order.py +34 -49
- xync_client/Bybit/ex.py +20 -46
- xync_client/Bybit/order.py +14 -12
- xync_client/Htx/agent.py +82 -40
- xync_client/Htx/etype/ad.py +22 -5
- xync_client/Htx/etype/order.py +194 -0
- xync_client/Htx/ex.py +16 -16
- xync_client/Mexc/agent.py +196 -13
- xync_client/Mexc/api.py +955 -336
- xync_client/Mexc/etype/ad.py +52 -1
- xync_client/Mexc/etype/order.py +131 -416
- xync_client/Mexc/ex.py +29 -19
- xync_client/Okx/1.py +14 -0
- xync_client/Okx/agent.py +39 -0
- xync_client/Okx/ex.py +8 -8
- xync_client/Pms/Payeer/agent.py +396 -0
- xync_client/Pms/Payeer/login.py +1 -63
- xync_client/Pms/Payeer/trade.py +58 -0
- xync_client/Pms/Volet/{__init__.py → agent.py} +1 -2
- xync_client/loader.py +1 -0
- {xync_client-0.0.155.dist-info → xync_client-0.0.162.dist-info}/METADATA +2 -1
- {xync_client-0.0.155.dist-info → xync_client-0.0.162.dist-info}/RECORD +33 -29
- xync_client/Pms/Payeer/__init__.py +0 -262
- xync_client/Pms/Payeer/api.py +0 -25
- {xync_client-0.0.155.dist-info → xync_client-0.0.162.dist-info}/WHEEL +0 -0
- {xync_client-0.0.155.dist-info → xync_client-0.0.162.dist-info}/top_level.txt +0 -0
xync_client/Abc/Ex.py
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import re
|
|
2
3
|
from abc import abstractmethod
|
|
3
4
|
from asyncio import sleep
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from difflib import SequenceMatcher
|
|
4
7
|
|
|
5
8
|
from aiohttp import ClientSession, ClientResponse
|
|
9
|
+
from google.protobuf.internal.wire_format import INT32_MAX
|
|
6
10
|
from msgspec import Struct
|
|
11
|
+
from pydantic import BaseModel
|
|
7
12
|
from pyro_client.client.file import FileClient
|
|
8
|
-
from tortoise.exceptions import MultipleObjectsReturned, IntegrityError
|
|
13
|
+
from tortoise.exceptions import MultipleObjectsReturned, IntegrityError, OperationalError
|
|
9
14
|
from x_client.aiohttp import Client as HttpClient
|
|
15
|
+
from xync_client.Bybit.etype.ad import AdsReq
|
|
10
16
|
from xync_schema import models
|
|
11
17
|
from xync_schema.enums import FileType
|
|
12
18
|
from xync_schema.xtype import CurEx, CoinEx, BaseAd, BaseAdIn
|
|
@@ -24,6 +30,19 @@ class BaseExClient(HttpClient, AdLoader):
|
|
|
24
30
|
bot: FileClient
|
|
25
31
|
ex: models.Ex
|
|
26
32
|
|
|
33
|
+
coin_x2e: dict[int, tuple[str, int]] = {}
|
|
34
|
+
coin_e2x: dict[str, int] = {}
|
|
35
|
+
cur_x2e: dict[int, tuple[str, int, int]] = {}
|
|
36
|
+
cur_e2x: dict[str, int] = {}
|
|
37
|
+
pm_x2e: dict[int, str] = {}
|
|
38
|
+
pm_e2x: dict[str, int] = {}
|
|
39
|
+
pairs_e2x: dict[int, dict[int, tuple[int, int]]] = defaultdict(defaultdict)
|
|
40
|
+
pairs_x2e: dict[int, tuple[int, int, int]] = defaultdict(defaultdict)
|
|
41
|
+
actor_x2e: dict[int, int] = {}
|
|
42
|
+
actor_e2x: dict[int, int] = {}
|
|
43
|
+
ad_x2e: dict[int, int] = {}
|
|
44
|
+
ad_e2x: dict[int, int] = {}
|
|
45
|
+
|
|
27
46
|
def __init__(
|
|
28
47
|
self,
|
|
29
48
|
ex: models.Ex,
|
|
@@ -64,21 +83,115 @@ class BaseExClient(HttpClient, AdLoader):
|
|
|
64
83
|
@abstractmethod
|
|
65
84
|
async def pairs(self) -> tuple[MapOfIdsList, MapOfIdsList]: ...
|
|
66
85
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
86
|
+
# converters
|
|
87
|
+
async def x2e_coin(self, coin_id: int) -> tuple[str, int]: # coinex.exid
|
|
88
|
+
if not self.coin_x2e.get(coin_id):
|
|
89
|
+
exid, scale = await models.CoinEx.get(coin_id=coin_id, ex=self.ex).values_list("exid", "scale")
|
|
90
|
+
self.coin_x2e[coin_id] = exid, scale
|
|
91
|
+
self.coin_e2x[exid] = coin_id
|
|
92
|
+
return self.coin_x2e[coin_id]
|
|
93
|
+
|
|
94
|
+
async def e2x_coin(self, exid: str) -> int: # coin.id
|
|
95
|
+
if not self.coin_e2x.get(exid):
|
|
96
|
+
coin_id, scale = await models.CoinEx.get(exid=exid, ex=self.ex).values_list("coin_id", "scale")
|
|
97
|
+
self.coin_x2e[coin_id] = exid, scale
|
|
98
|
+
self.coin_e2x[exid] = coin_id
|
|
99
|
+
return self.coin_e2x[exid]
|
|
100
|
+
|
|
101
|
+
async def x2e_cur(self, cur_id: int) -> tuple[str, int, int]: # curex.exid
|
|
102
|
+
if not self.cur_x2e.get(cur_id):
|
|
103
|
+
exid, scale, mnm = await models.CurEx.get(cur_id=cur_id, ex=self.ex).values_list("exid", "scale", "minimum")
|
|
104
|
+
self.cur_x2e[cur_id] = exid, scale, mnm
|
|
105
|
+
self.cur_e2x[exid] = cur_id
|
|
106
|
+
return self.cur_x2e[cur_id]
|
|
107
|
+
|
|
108
|
+
async def e2x_cur(self, exid: str) -> int: # cur.id
|
|
109
|
+
if not self.cur_e2x.get(exid):
|
|
110
|
+
cur_id, scale, mnm = await models.CurEx.get(exid=exid, ex=self.ex).values_list("cur_id", "scale", "minimum")
|
|
111
|
+
self.cur_e2x[exid] = cur_id
|
|
112
|
+
self.cur_x2e[cur_id] = exid, scale, mnm
|
|
113
|
+
return self.cur_e2x[exid]
|
|
114
|
+
|
|
115
|
+
async def x2e_pm(self, pm_id: int) -> str: # pmex.exid
|
|
116
|
+
if not self.pm_x2e.get(pm_id):
|
|
117
|
+
self.pm_x2e[pm_id] = (await models.PmEx.get(pm_id=pm_id, ex=self.ex)).exid
|
|
118
|
+
self.pm_e2x[self.pm_x2e[pm_id]] = pm_id
|
|
119
|
+
return self.pm_x2e[pm_id]
|
|
120
|
+
|
|
121
|
+
async def e2x_pm(self, exid: str) -> int: # pm.id
|
|
122
|
+
if not self.pm_e2x.get(exid):
|
|
123
|
+
self.pm_e2x[exid] = (await models.PmEx.get(exid=exid, ex=self.ex)).pm_id
|
|
124
|
+
self.pm_x2e[self.pm_e2x[exid]] = exid
|
|
125
|
+
return self.pm_e2x[exid]
|
|
126
|
+
|
|
127
|
+
# pair_side
|
|
128
|
+
async def ccs2pair(self, coin_id: int, cur_id: int, is_sell: bool) -> int: # ex cur+coin+is_sale -> x pair.id
|
|
129
|
+
if not self.pairs_e2x.get(coin_id, {}).get(cur_id):
|
|
130
|
+
self.pairs_e2x[coin_id][cur_id] = (
|
|
131
|
+
await models.PairSide.filter(pair__coin_id=coin_id, pair__cur_id=coin_id)
|
|
132
|
+
.order_by("is_sell")
|
|
133
|
+
.values_list("id", flat=True)
|
|
134
|
+
)
|
|
135
|
+
return self.pairs_e2x[coin_id][cur_id][int(is_sell)]
|
|
136
|
+
|
|
137
|
+
async def pair2ccs(self, xid: int) -> tuple[int, int, int]: # coinex.exid
|
|
138
|
+
if not self.pairs_x2e.get(xid):
|
|
139
|
+
ps = await models.PairSide.get(id=xid).prefetch_related("pair")
|
|
140
|
+
self.pairs_x2e[xid] = ps.pair.coin_id, ps.pair.cur_id, ps.is_sell
|
|
141
|
+
return self.pairs_x2e[xid]
|
|
142
|
+
|
|
143
|
+
async def e2x_pair(self, coin_exid: str, cur_exid: str, is_sell: bool) -> int: # ex cur+coin+is_sale -> x pair.id
|
|
144
|
+
coin_id = await self.e2x_coin(coin_exid)
|
|
145
|
+
cur_id = await self.e2x_cur(cur_exid)
|
|
146
|
+
return await self.ccs2pair(coin_id, cur_id, is_sell)
|
|
147
|
+
|
|
148
|
+
async def x2e_pair(self, pair_side_id: int) -> tuple[str, str, int]: # coinex.exid
|
|
149
|
+
coin_id, cur_id, is_sell = await self.pair2ccs(pair_side_id)
|
|
150
|
+
coin_exid = await self.x2e_coin(coin_id)
|
|
151
|
+
cur_exid = await self.x2e_cur(cur_id)
|
|
152
|
+
return coin_exid[0], cur_exid[0], is_sell
|
|
153
|
+
|
|
154
|
+
async def x2e_actor(self, actor_id: int) -> int: # actor.exid
|
|
155
|
+
if not self.actor_x2e.get(actor_id):
|
|
156
|
+
self.actor_x2e[actor_id] = (await models.Actor[actor_id]).exid
|
|
157
|
+
self.actor_e2x[self.actor_x2e[actor_id]] = actor_id
|
|
158
|
+
return self.actor_x2e[actor_id]
|
|
159
|
+
|
|
160
|
+
async def e2x_actor(self, exid: int) -> int: # actor.id
|
|
161
|
+
if not self.actor_e2x.get(exid):
|
|
162
|
+
self.actor_e2x[exid] = (await models.Actor.get(exid=exid, ex=self.ex)).id
|
|
163
|
+
self.actor_x2e[self.actor_e2x[exid]] = exid
|
|
164
|
+
return self.actor_e2x[exid]
|
|
165
|
+
|
|
166
|
+
async def x2e_ad(self, ad_id: int) -> int: # ad.exid
|
|
167
|
+
if not self.ad_x2e.get(ad_id):
|
|
168
|
+
self.ad_x2e[ad_id] = (await models.Ad[ad_id]).exid
|
|
169
|
+
self.ad_e2x[self.ad_x2e[ad_id]] = ad_id
|
|
170
|
+
return self.ad_x2e[ad_id]
|
|
171
|
+
|
|
172
|
+
async def e2x_ad(self, exid: int) -> int: # ad.id
|
|
173
|
+
if not self.ad_e2x.get(exid):
|
|
174
|
+
self.ad_e2x[exid] = (await models.Ad.get(exid=exid, maker__ex=self.ex)).id
|
|
175
|
+
self.ad_x2e[self.ad_e2x[exid]] = exid
|
|
176
|
+
return self.ad_e2x[exid]
|
|
72
177
|
|
|
73
178
|
# 24: Список объяв по (buy/sell, cur, coin, pm)
|
|
74
|
-
async def ads(self,
|
|
75
|
-
|
|
179
|
+
async def ads(self, xreq: GetAds, **kwargs) -> list[BaseAd]:
|
|
180
|
+
self.ex.etype().ad.AdsReq
|
|
181
|
+
ereq = AdsReq(
|
|
182
|
+
coin_id=(await self.x2e_coin(xreq.coin_id))[0],
|
|
183
|
+
cur_id=(await self.x2e_cur(xreq.cur_id))[0],
|
|
184
|
+
is_sell=str(int(xreq.is_sell)),
|
|
185
|
+
pm_ids=[await self.x2e_pm(pid) for pid in xreq.pm_ids],
|
|
186
|
+
# size=str(xreq.limit),
|
|
187
|
+
# page=str(xreq.page),
|
|
188
|
+
)
|
|
189
|
+
if xreq.amount:
|
|
190
|
+
ereq.amount = str(xreq.amount)
|
|
191
|
+
return await self._ads(ereq, **kwargs)
|
|
76
192
|
|
|
77
193
|
@abstractmethod
|
|
78
|
-
async def _ads(
|
|
79
|
-
self, req: GetAds, lim: int = None, vm_filter: bool = False, **kwargs
|
|
80
|
-
) -> list[BaseAd]: # {ad.id: ad}
|
|
81
|
-
...
|
|
194
|
+
async def _ads(self, ereq: BaseModel, **kwargs) -> list[BaseAd]: ...
|
|
82
195
|
|
|
83
196
|
# 42: Чужая объява по id
|
|
84
197
|
@abstractmethod
|
|
@@ -285,3 +398,299 @@ class BaseExClient(HttpClient, AdLoader):
|
|
|
285
398
|
)
|
|
286
399
|
return await self.METHS[resp.method](self, resp.url.path, bp)
|
|
287
400
|
return await super()._proc(resp, bp)
|
|
401
|
+
|
|
402
|
+
# ad cond loader
|
|
403
|
+
all_conds: dict[int, tuple[str, set[int]]] = {}
|
|
404
|
+
cond_sims: dict[int, int] = defaultdict(set)
|
|
405
|
+
rcond_sims: dict[int, set[int]] = defaultdict(set) # backward
|
|
406
|
+
tree: dict = {}
|
|
407
|
+
|
|
408
|
+
async def old_conds_load(self):
|
|
409
|
+
# пока не порешали рейс-кондишн, очищаем сиротские условия при каждом запуске
|
|
410
|
+
# [await c.delete() for c in await Cond.filter(ads__isnull=True)]
|
|
411
|
+
self.all_conds = {
|
|
412
|
+
c.id: (c.raw_txt, {a.maker.exid for a in c.ads})
|
|
413
|
+
for c in await models.Cond.all().prefetch_related("ads__maker")
|
|
414
|
+
}
|
|
415
|
+
for curr, old in await models.CondSim.filter().values_list("cond_id", "cond_rel_id"):
|
|
416
|
+
self.cond_sims[curr] = old
|
|
417
|
+
self.rcond_sims[old] |= {curr}
|
|
418
|
+
|
|
419
|
+
self.build_tree()
|
|
420
|
+
a = set()
|
|
421
|
+
|
|
422
|
+
def check_tree(tre):
|
|
423
|
+
for p, c in tre.items():
|
|
424
|
+
a.add(p)
|
|
425
|
+
check_tree(c)
|
|
426
|
+
|
|
427
|
+
for pr, ch in self.tree.items():
|
|
428
|
+
check_tree(ch)
|
|
429
|
+
if ct := set(self.tree.keys()) & a:
|
|
430
|
+
logging.exception(f"cycle cids: {ct}")
|
|
431
|
+
|
|
432
|
+
async def person_name_update(self, name: str, exid: int) -> models.Person:
|
|
433
|
+
if actor := await models.Actor.get_or_none(exid=exid, ex=self.ex).prefetch_related("person"):
|
|
434
|
+
# мб уже есть персона с этим же name, note, tg_id
|
|
435
|
+
if person := await models.Person.get_or_none(
|
|
436
|
+
name=name, note=actor.person.note, tg_id=actor.person.tg_id, id__not=actor.person_id
|
|
437
|
+
):
|
|
438
|
+
actor.person = person
|
|
439
|
+
else:
|
|
440
|
+
actor.person.name = name
|
|
441
|
+
await actor.person.save()
|
|
442
|
+
return actor.person
|
|
443
|
+
# tmp dirty fix
|
|
444
|
+
note = f"{self.ex.id}:{exid}"
|
|
445
|
+
if person := await models.Person.get_or_none(note__startswith=note, name__not=name):
|
|
446
|
+
person.name = name
|
|
447
|
+
await person.save()
|
|
448
|
+
return person
|
|
449
|
+
try:
|
|
450
|
+
return (await models.Person.update_or_create(name=name, note=note))[0]
|
|
451
|
+
except OperationalError as e:
|
|
452
|
+
raise e
|
|
453
|
+
await models.Actor.create(person=person, exid=exid, ex=self.ex)
|
|
454
|
+
return person
|
|
455
|
+
# person = await models.Person.create(note=f'{actor.ex_id}:{actor.exid}:{name}') # no person for just ads with no orders
|
|
456
|
+
# raise ValueError(f"Agent #{exid} not found")
|
|
457
|
+
|
|
458
|
+
async def ad_load(
|
|
459
|
+
self,
|
|
460
|
+
pad: BaseAd,
|
|
461
|
+
cid: int = None,
|
|
462
|
+
ps: models.PairSide = None,
|
|
463
|
+
maker: models.Actor = None,
|
|
464
|
+
coinex: models.CoinEx = None,
|
|
465
|
+
curex: models.CurEx = None,
|
|
466
|
+
rname: str = None,
|
|
467
|
+
) -> models.Ad:
|
|
468
|
+
# self.e2x_actor()
|
|
469
|
+
if not maker:
|
|
470
|
+
if not (maker := await models.Actor.get_or_none(exid=pad.userId, ex=self.ex)):
|
|
471
|
+
person = await models.Person.create(name=rname, note=f"{self.ex.id}:{pad.userId}:{pad.nickName}")
|
|
472
|
+
maker = await models.Actor.create(name=pad.nickName, person=person, exid=pad.userId, ex=self.ex)
|
|
473
|
+
if rname:
|
|
474
|
+
await self.person_name_update(rname, int(pad.userId))
|
|
475
|
+
ps = ps or await models.PairSide.get_or_none(
|
|
476
|
+
is_sell=pad.side,
|
|
477
|
+
pair__coin__ticker=pad.tokenId,
|
|
478
|
+
pair__cur__ticker=pad.currencyId,
|
|
479
|
+
).prefetch_related("pair")
|
|
480
|
+
# if not ps or not ps.pair:
|
|
481
|
+
# ... # THB/USDC: just for initial filling
|
|
482
|
+
ad_upd = models.Ad.validate(pad.model_dump(by_alias=True))
|
|
483
|
+
cur_scale = 10 ** (curex or await models.CurEx.get(cur_id=ps.pair.cur_id, ex=self.ex)).scale
|
|
484
|
+
coin_scale = 10 ** (coinex or await models.CoinEx.get(coin_id=ps.pair.coin_id, ex=self.ex)).scale
|
|
485
|
+
amt = int(float(pad.quantity) * float(pad.price) * cur_scale)
|
|
486
|
+
mxf = pad.maxAmount and int(float(pad.maxAmount) * cur_scale)
|
|
487
|
+
df_unq = ad_upd.df_unq(
|
|
488
|
+
maker_id=maker.id,
|
|
489
|
+
pair_side_id=ps.id,
|
|
490
|
+
amount=min(amt, INT32_MAX),
|
|
491
|
+
quantity=int(float(pad.quantity) * coin_scale),
|
|
492
|
+
min_fiat=int(float(pad.minAmount) * cur_scale),
|
|
493
|
+
max_fiat=min(mxf, INT32_MAX),
|
|
494
|
+
price=int(float(pad.price) * cur_scale),
|
|
495
|
+
premium=int(float(pad.premium) * 100),
|
|
496
|
+
cond_id=cid,
|
|
497
|
+
status=self.ad_status(ad_upd.status),
|
|
498
|
+
)
|
|
499
|
+
try:
|
|
500
|
+
ad_db, _ = await models.Ad.update_or_create(**df_unq)
|
|
501
|
+
except OperationalError as e:
|
|
502
|
+
raise e
|
|
503
|
+
await ad_db.pms.clear()
|
|
504
|
+
await ad_db.pms.add(*(await models.Pm.filter(pmexs__ex=self.ex, pmexs__exid__in=pad.payments)))
|
|
505
|
+
return ad_db
|
|
506
|
+
|
|
507
|
+
async def cond_load( # todo: refact from Bybit Ad format to universal
|
|
508
|
+
self,
|
|
509
|
+
ad: BaseAd,
|
|
510
|
+
ps: models.PairSide = None,
|
|
511
|
+
force: bool = False,
|
|
512
|
+
rname: str = None,
|
|
513
|
+
coinex: models.CoinEx = None,
|
|
514
|
+
curex: models.CurEx = None,
|
|
515
|
+
pms_from_cond: bool = False,
|
|
516
|
+
) -> tuple[models.Ad, bool]:
|
|
517
|
+
_sim, cid = None, None
|
|
518
|
+
ad_db = await models.Ad.get_or_none(exid=ad.id, maker__ex=self.ex).prefetch_related("cond")
|
|
519
|
+
# если точно такое условие уже есть в бд
|
|
520
|
+
if not (cleaned := clean(ad.remark)) or (cid := {oc[0]: ci for ci, oc in self.all_conds.items()}.get(cleaned)):
|
|
521
|
+
# и объява с таким ид уже есть, но у нее другое условие
|
|
522
|
+
if ad_db and ad_db.cond_id != cid:
|
|
523
|
+
# то обновляем ид ее условия
|
|
524
|
+
ad_db.cond_id = cid
|
|
525
|
+
await ad_db.save()
|
|
526
|
+
logging.info(f"{ad.nickName} upd cond#{ad_db.cond_id}->{cid}")
|
|
527
|
+
# old_cid = ad_db.cond_id # todo: solve race-condition, а пока что очищаем при каждом запуске
|
|
528
|
+
# if not len((old_cond := await Cond.get(id=old_cid).prefetch_related('ads')).ads):
|
|
529
|
+
# await old_cond.delete()
|
|
530
|
+
# logging.warning(f"Cond#{old_cid} deleted!")
|
|
531
|
+
return (ad_db or force and await self.ad_load(ad, cid, ps, coinex=coinex, curex=curex, rname=rname)), False
|
|
532
|
+
# если эта объява в таким ид уже есть в бд, но с другим условием (или без), а текущего условия еще нет в бд
|
|
533
|
+
if ad_db:
|
|
534
|
+
await ad_db.fetch_related("cond__ads", "maker")
|
|
535
|
+
if not ad_db.cond_id or (
|
|
536
|
+
# у измененного условия этой объявы есть другие объявы?
|
|
537
|
+
(rest_ads := set(ad_db.cond.ads) - {ad_db})
|
|
538
|
+
and
|
|
539
|
+
# другие объявы этого условия принадлежат другим юзерам
|
|
540
|
+
{ra.maker_id for ra in rest_ads} - {ad_db.maker_id}
|
|
541
|
+
):
|
|
542
|
+
# создадим новое условие и присвоим его только текущей объяве
|
|
543
|
+
cond = await self.cond_new(cleaned, {int(ad.userId)})
|
|
544
|
+
ad_db.cond_id = cond.id
|
|
545
|
+
await ad_db.save()
|
|
546
|
+
ad_db.cond = cond
|
|
547
|
+
return ad_db, True
|
|
548
|
+
# а если других объяв со старым условием этой обявы нет, либо они все этого же юзера
|
|
549
|
+
# обновляем условие (в тч во всех ЕГО объявах)
|
|
550
|
+
ad_db.cond.last_ver = ad_db.cond.raw_txt
|
|
551
|
+
ad_db.cond.raw_txt = cleaned
|
|
552
|
+
try:
|
|
553
|
+
await ad_db.cond.save()
|
|
554
|
+
except IntegrityError as e:
|
|
555
|
+
raise e
|
|
556
|
+
await self.cond_upd(ad_db.cond, {ad_db.maker.exid})
|
|
557
|
+
# и подправим коэфициенты похожести нового текста
|
|
558
|
+
await self.fix_rel_sims(ad_db.cond_id, cleaned)
|
|
559
|
+
return ad_db, False
|
|
560
|
+
|
|
561
|
+
cond = await self.cond_new(cleaned, {int(ad.userId)})
|
|
562
|
+
ad_db = await self.ad_load(ad, cond.id, ps, coinex=coinex, curex=curex, rname=rname)
|
|
563
|
+
ad_db.cond = cond
|
|
564
|
+
return ad_db, True
|
|
565
|
+
|
|
566
|
+
async def cond_new(self, txt: str, uids: set[int]) -> models.Cond:
|
|
567
|
+
new_cond, _ = await models.Cond.update_or_create(raw_txt=txt)
|
|
568
|
+
# и максимально похожую связь для нового условия (если есть >= 60%)
|
|
569
|
+
await self.cond_upd(new_cond, uids)
|
|
570
|
+
return new_cond
|
|
571
|
+
|
|
572
|
+
async def cond_upd(self, cond: models.Cond, uids: set[int]):
|
|
573
|
+
self.all_conds[cond.id] = cond.raw_txt, uids
|
|
574
|
+
# и максимально похожую связь для нового условия (если есть >= 60%)
|
|
575
|
+
old_cid, sim = await self.cond_get_max_sim(cond.id, cond.raw_txt, uids)
|
|
576
|
+
await self.actual_sim(cond.id, old_cid, sim)
|
|
577
|
+
|
|
578
|
+
def find_in_tree(self, cid: int, old_cid: int) -> bool:
|
|
579
|
+
if p := self.cond_sims.get(old_cid):
|
|
580
|
+
if p == cid:
|
|
581
|
+
return True
|
|
582
|
+
return self.find_in_tree(cid, p)
|
|
583
|
+
return False
|
|
584
|
+
|
|
585
|
+
async def cond_get_max_sim(self, cid: int, txt: str, uids: set[int]) -> tuple[int | None, int | None]:
|
|
586
|
+
# находим все старые тексты похожие на 90% и более
|
|
587
|
+
if len(txt) < 15:
|
|
588
|
+
return None, None
|
|
589
|
+
sims: dict[int, int] = {}
|
|
590
|
+
for old_cid, (old_txt, old_uids) in self.all_conds.items():
|
|
591
|
+
if len(old_txt) < 15 or uids == old_uids:
|
|
592
|
+
continue
|
|
593
|
+
elif not self.can_add_sim(cid, old_cid):
|
|
594
|
+
continue
|
|
595
|
+
if sim := get_sim(txt, old_txt):
|
|
596
|
+
sims[old_cid] = sim
|
|
597
|
+
# если есть, берем самый похожий из них
|
|
598
|
+
if sims:
|
|
599
|
+
old_cid, sim = max(sims.items(), key=lambda x: x[1])
|
|
600
|
+
await sleep(0.3)
|
|
601
|
+
return old_cid, sim
|
|
602
|
+
return None, None
|
|
603
|
+
|
|
604
|
+
def can_add_sim(self, cid: int, old_cid: int) -> bool:
|
|
605
|
+
if cid == old_cid:
|
|
606
|
+
return False
|
|
607
|
+
elif self.cond_sims.get(cid) == old_cid:
|
|
608
|
+
return False
|
|
609
|
+
elif self.find_in_tree(cid, old_cid):
|
|
610
|
+
return False
|
|
611
|
+
elif self.cond_sims.get(old_cid) == cid:
|
|
612
|
+
return False
|
|
613
|
+
elif cid in self.rcond_sims.get(old_cid, {}):
|
|
614
|
+
return False
|
|
615
|
+
elif old_cid in self.rcond_sims.get(cid, {}):
|
|
616
|
+
return False
|
|
617
|
+
return True
|
|
618
|
+
|
|
619
|
+
async def fix_rel_sims(self, cid: int, new_txt: str):
|
|
620
|
+
for rel_sim in await models.CondSim.filter(cond_rel_id=cid).prefetch_related("cond"):
|
|
621
|
+
if sim := get_sim(new_txt, rel_sim.cond.raw_txt):
|
|
622
|
+
rel_sim.similarity = sim
|
|
623
|
+
await rel_sim.save()
|
|
624
|
+
else:
|
|
625
|
+
await rel_sim.delete()
|
|
626
|
+
|
|
627
|
+
async def actual_cond(self):
|
|
628
|
+
for curr, old in await models.CondSim.all().values_list("cond_id", "cond_rel_id"):
|
|
629
|
+
self.cond_sims[curr] = old
|
|
630
|
+
self.rcond_sims[old] |= {curr}
|
|
631
|
+
for cid, (txt, uids) in self.all_conds.items():
|
|
632
|
+
old_cid, sim = await self.cond_get_max_sim(cid, txt, uids)
|
|
633
|
+
await self.actual_sim(cid, old_cid, sim)
|
|
634
|
+
# хз бля чо это ваще
|
|
635
|
+
# for ad_db in await models.Ad.filter(direction__pairex__ex=self.ex).prefetch_related("cond", "maker"):
|
|
636
|
+
# ad = Ad(id=str(ad_db.exid), userId=str(ad_db.maker.exid), remark=ad_db.cond.raw_txt)
|
|
637
|
+
# await self.cond_upsert(ad, force=True)
|
|
638
|
+
|
|
639
|
+
async def actual_sim(self, cid: int, old_cid: int, sim: int):
|
|
640
|
+
if not sim:
|
|
641
|
+
return
|
|
642
|
+
if old_sim := await models.CondSim.get_or_none(cond_id=cid):
|
|
643
|
+
if old_sim.cond_rel_id != old_cid:
|
|
644
|
+
if sim > old_sim.similarity:
|
|
645
|
+
logging.warning(f"R {cid}: {old_sim.similarity}->{sim} ({old_sim.cond_rel_id}->{old_cid})")
|
|
646
|
+
await old_sim.update_from_dict({"similarity": sim, "old_rel_id": old_cid}).save()
|
|
647
|
+
self._cond_sim_upd(cid, old_cid)
|
|
648
|
+
elif sim != old_sim.similarity:
|
|
649
|
+
logging.info(f"{cid}: {old_sim.similarity}->{sim}")
|
|
650
|
+
await old_sim.update_from_dict({"similarity": sim}).save()
|
|
651
|
+
else:
|
|
652
|
+
await models.CondSim.create(cond_id=cid, cond_rel_id=old_cid, similarity=sim)
|
|
653
|
+
self._cond_sim_upd(cid, old_cid)
|
|
654
|
+
|
|
655
|
+
def _cond_sim_upd(self, cid: int, old_cid: int):
|
|
656
|
+
if old_old_cid := self.cond_sims.get(cid): # если старый cid уже был в дереве:
|
|
657
|
+
self.rcond_sims[old_old_cid].remove(cid) # удаляем из обратного
|
|
658
|
+
self.cond_sims[cid] = old_cid # а в прямом он автоматом переопределится, даже если и был
|
|
659
|
+
self.rcond_sims[old_cid] |= {cid} # ну и в обратное добавим новый
|
|
660
|
+
|
|
661
|
+
def build_tree(self):
|
|
662
|
+
set(self.cond_sims.keys()) | set(self.cond_sims.values())
|
|
663
|
+
tree = defaultdict(dict)
|
|
664
|
+
# Группируем родителей по детям
|
|
665
|
+
for child, par in self.cond_sims.items():
|
|
666
|
+
tree[par] |= {child: {}} # todo: make from self.rcond_sim
|
|
667
|
+
|
|
668
|
+
# Строим дерево снизу вверх
|
|
669
|
+
def subtree(node):
|
|
670
|
+
if not node:
|
|
671
|
+
return node
|
|
672
|
+
for key in node:
|
|
673
|
+
subnode = tree.pop(key, {})
|
|
674
|
+
d = subtree(subnode)
|
|
675
|
+
node[key] |= d # actual tree rebuilding here!
|
|
676
|
+
return node # todo: refact?
|
|
677
|
+
|
|
678
|
+
# Находим корни / без родителей
|
|
679
|
+
roots = set(self.cond_sims.values()) - set(self.cond_sims.keys())
|
|
680
|
+
for root in roots:
|
|
681
|
+
_ = subtree(tree[root])
|
|
682
|
+
|
|
683
|
+
self.tree = tree
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def get_sim(s1, s2) -> int:
|
|
687
|
+
sim = SequenceMatcher(None, s1, s2).ratio() - 0.6
|
|
688
|
+
return int(sim * 10_000) if sim > 0 else 0
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def clean(s) -> str:
|
|
692
|
+
clear = r"[^\w\s.,!?;:()\-]"
|
|
693
|
+
repeat = r"(.)\1{2,}"
|
|
694
|
+
s = re.sub(clear, "", s).lower()
|
|
695
|
+
s = re.sub(repeat, r"\1", s)
|
|
696
|
+
return s.replace("\n\n", "\n").replace(" ", " ").strip(" \n/.,!?-")
|
xync_client/Abc/Order.py
CHANGED
|
@@ -1,27 +1,20 @@
|
|
|
1
1
|
from abc import abstractmethod
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from x_client.aiohttp import Client as HttpClient
|
|
5
|
-
from xync_schema.models import Order, Actor
|
|
3
|
+
from xync_schema.models import Order
|
|
6
4
|
|
|
7
5
|
from xync_client.Bybit.agent import AgentClient
|
|
8
6
|
|
|
9
7
|
|
|
10
|
-
class BaseOrderClient
|
|
8
|
+
class BaseOrderClient:
|
|
11
9
|
order: Order
|
|
12
10
|
im_maker: bool
|
|
13
11
|
im_seller: bool
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
@property
|
|
17
|
-
def type_map(self) -> dict[OrderType, str]: ...
|
|
18
|
-
|
|
19
|
-
def __init__(self, actor: Actor, order: Order):
|
|
13
|
+
def __init__(self, order: Order, agent_client: AgentClient):
|
|
20
14
|
self.order = order
|
|
21
|
-
self.im_maker = order.taker_id != actor.id # or order.ad.agent_id == agent.id
|
|
22
|
-
self.im_seller = order.ad.
|
|
23
|
-
|
|
24
|
-
self.agent_client = AgentClient(actor)
|
|
15
|
+
self.im_maker = order.taker_id != agent_client.actor.id # or order.ad.agent_id == agent.id
|
|
16
|
+
self.im_seller = order.ad.pair_side.is_sell and self.im_maker
|
|
17
|
+
self.agent_client = agent_client
|
|
25
18
|
|
|
26
19
|
# 2: [T] Отмена запроса на сделку
|
|
27
20
|
@abstractmethod
|
|
@@ -37,7 +30,7 @@ class BaseOrderClient(HttpClient):
|
|
|
37
30
|
|
|
38
31
|
# 5: [B] Перевод сделки в состояние "оплачено", c отправкой чека
|
|
39
32
|
@abstractmethod
|
|
40
|
-
async def mark_payed(self, receipt): ...
|
|
33
|
+
async def mark_payed(self, receipt: bytes = None): ...
|
|
41
34
|
|
|
42
35
|
# 6: [B] Отмена сделки
|
|
43
36
|
@abstractmethod
|
xync_client/Abc/xtype.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
from typing import Literal
|
|
1
|
+
from typing import Literal, ClassVar
|
|
2
2
|
|
|
3
|
-
from pydantic import BaseModel, model_validator
|
|
3
|
+
from pydantic import BaseModel, model_validator, model_serializer
|
|
4
4
|
from x_model.types import BaseUpd
|
|
5
5
|
from xync_schema.enums import PmType
|
|
6
6
|
from xync_schema.models import Country, Pm, Ex, CredEx
|
|
@@ -14,6 +14,28 @@ FlatDict = dict[int | str, str]
|
|
|
14
14
|
MapOfIdsList = dict[int | str, list[int | str]]
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
class RemapBase(BaseModel):
|
|
18
|
+
# Переопределяешь это в наследнике:
|
|
19
|
+
_remap: ClassVar[dict[str, dict]] = {}
|
|
20
|
+
|
|
21
|
+
@model_validator(mode="before")
|
|
22
|
+
def _map_in(cls, data):
|
|
23
|
+
data = dict(data)
|
|
24
|
+
for field, mapping in cls._remap.items():
|
|
25
|
+
if field in data:
|
|
26
|
+
data[field] = mapping.get(data[field], data[field])
|
|
27
|
+
return data
|
|
28
|
+
|
|
29
|
+
@model_serializer
|
|
30
|
+
def _map_out(self):
|
|
31
|
+
data = self.__dict__.copy()
|
|
32
|
+
for field, mapping in self._remap.items():
|
|
33
|
+
reverse = {v: k for k, v in mapping.items()}
|
|
34
|
+
if field in data:
|
|
35
|
+
data[field] = reverse.get(data[field], data[field])
|
|
36
|
+
return data
|
|
37
|
+
|
|
38
|
+
|
|
17
39
|
class PmTrait:
|
|
18
40
|
typ: PmType | None = None
|
|
19
41
|
logo: str | None = None
|
|
@@ -114,17 +136,27 @@ class GetAds(BaseModel):
|
|
|
114
136
|
coin_id: int | str
|
|
115
137
|
cur_id: int | str
|
|
116
138
|
is_sell: bool
|
|
117
|
-
pm_ids: list[int | str]
|
|
139
|
+
pm_ids: list[int | str] = []
|
|
118
140
|
amount: int | None = None
|
|
141
|
+
vm_only: bool = False
|
|
142
|
+
limit: int = 20
|
|
143
|
+
page: int = 1
|
|
144
|
+
# todo: add?
|
|
145
|
+
# canTrade: bool = False
|
|
146
|
+
# userId: str = "" # int
|
|
147
|
+
# verificationFilter
|
|
148
|
+
kwargs: dict = {}
|
|
119
149
|
|
|
120
150
|
|
|
121
151
|
class AdUpd(BaseAdUpdate, GetAds):
|
|
122
152
|
price: float
|
|
123
153
|
pm_ids: list[int | str]
|
|
124
154
|
amount: float
|
|
155
|
+
max_amount: float | None = None
|
|
125
156
|
premium: float | None = None
|
|
126
157
|
credexs: list[CredEx] | None = None
|
|
127
158
|
quantity: float | None = None
|
|
159
|
+
cond: str | None = None
|
|
128
160
|
|
|
129
161
|
class Config:
|
|
130
162
|
arbitrary_types_allowed = True
|