xync-client 0.0.155__py3-none-any.whl → 0.0.156.dev18__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/Agent.py CHANGED
@@ -1,15 +1,15 @@
1
+ import logging
1
2
  from abc import abstractmethod
2
- from asyncio.tasks import gather
3
+ from asyncio import create_task, sleep
3
4
  from collections import defaultdict
5
+ from typing import Literal
4
6
 
5
- from playwright.async_api import async_playwright
6
7
  from pydantic import BaseModel
7
8
  from pyro_client.client.file import FileClient
8
9
  from x_client import df_hdrs
9
10
  from x_client.aiohttp import Client as HttpClient
10
11
  from xync_bot import XyncBot
11
12
  from xync_client.Abc.PmAgent import PmAgentClient
12
- from xync_schema.enums import UserStatus
13
13
 
14
14
  from xync_client.Abc.InAgent import BaseInAgentClient
15
15
 
@@ -19,7 +19,7 @@ from xync_schema.models import OrderStatus, Coin, Cur, Ad, AdStatus, Actor, Agen
19
19
  from xync_schema.xtype import BaseAd
20
20
 
21
21
  from xync_client.Abc.Ex import BaseExClient
22
- from xync_client.Abc.xtype import CredExOut, BaseOrderReq, BaseAdUpdate, AdUpd
22
+ from xync_client.Abc.xtype import CredExOut, BaseOrderReq, BaseAdUpdate, AdUpd, GetAds
23
23
  from xync_client.Gmail import GmClient
24
24
 
25
25
 
@@ -30,15 +30,20 @@ class BaseAgentClient(HttpClient, BaseInAgentClient):
30
30
  fbot: FileClient
31
31
  ex_client: BaseExClient
32
32
  pm_clients: dict[int, PmAgentClient] # {pm_id: PmAgentClient}
33
+ api: HttpClient
34
+ cred_x2e: dict[int, int] = {}
35
+ cred_e2x: dict[int, int] = {}
33
36
 
34
37
  def __init__(
35
38
  self,
36
- agent: Agent,
39
+ agent: Agent, # agent.actor.person.user
37
40
  ex_client: BaseExClient,
38
41
  fbot: FileClient,
39
42
  bbot: XyncBot,
43
+ pm_clients: dict[int, PmAgentClient] = None,
40
44
  headers: dict[str, str] = df_hdrs,
41
45
  cookies: dict[str, str] = None,
46
+ proxy: models.Proxy = None,
42
47
  ):
43
48
  self.bbot = bbot
44
49
  self.fbot = fbot
@@ -47,34 +52,282 @@ class BaseAgentClient(HttpClient, BaseInAgentClient):
47
52
  self.gmail = agent.actor.person.user.gmail and GmClient(agent.actor.person.user)
48
53
  self.ex_client: BaseExClient = ex_client
49
54
  self.pm_clients: dict[int, PmAgentClient] = defaultdict()
50
- super().__init__(self.actor.ex.host_p2p, headers, cookies)
51
-
52
- async def start(self, debug: bool = False):
53
- tasks = []
54
- if not self.is_started:
55
- if self.agent.status & 1: # race
56
- tasks.append(self.start_race())
57
-
58
- if self.agent.status & 2: # in agent
59
- if not self.pm_clients:
60
- pm_agents = await models.PmAgent.filter(
61
- active=True,
62
- auth__isnull=False,
63
- user__status=UserStatus.ACTIVE,
64
- ).prefetch_related("pm", "user__gmail")
65
- # payeer_cl = Client(actor.person.user.username_id)
66
- pw = await async_playwright().start()
67
- browser = await pw.chromium.launch(
68
- channel="chrome-beta" if debug else "chromium-headless-shell", headless=not debug
69
- )
70
- self.pm_clients = {pma.pm_id: pma.client(browser, self.bbot) for pma in pm_agents}
71
- [tasks.append(pmcl.start()) for pmcl in self.pm_clients.values()]
72
- # tasks.append(self.start_listen())
73
-
74
- if self.agent.status & 4: # for further
75
- ...
76
- self.is_started = True
77
- return await gather(*tasks)
55
+ super().__init__(self.actor.ex.host_p2p, headers, cookies, proxy) # and proxy.str()
56
+ # start
57
+ create_task(self.start())
58
+
59
+ async def x2e_cred(self, cred_id: int) -> int: # cred.exid
60
+ if not self.cred_x2e.get(cred_id):
61
+ self.cred_x2e[cred_id] = (await models.CredEx.get(cred_id=cred_id)).exid
62
+ self.cred_e2x[self.cred_x2e[cred_id]] = cred_id
63
+ return self.cred_x2e[cred_id]
64
+
65
+ async def e2x_cred(self, exid: int) -> int: # cred.id
66
+ if not self.cred_e2x.get(exid):
67
+ self.cred_e2x[exid] = (await models.CredEx.get(exid=exid, ex=self.ex_client.ex)).cred_id
68
+ self.cred_x2e[self.cred_e2x[exid]] = exid
69
+ return self.cred_e2x[exid]
70
+
71
+ async def start(self):
72
+ if self.agent.status & 1: # щас пока юзаем статус чисто как бул актив
73
+ for race in await models.Race.filter(started=True, road__ad__maker_id=self.agent.actor_id).prefetch_related(
74
+ "road__ad__pair_side__pair__cur", "road__credexs__cred"
75
+ ):
76
+ create_task(self.racing(race))
77
+
78
+ async def racing(self, race: models.Race):
79
+ pair = race.road.ad.pair_side.pair
80
+ taker_side: int = not race.road.ad.pair_side.is_sell
81
+ # конвертим наши параметры гонки в ex-овые для конкретной биржи текущего агента
82
+ coinex: models.CoinEx = await models.CoinEx.get(coin_id=pair.coin_id, ex=self.actor.ex).prefetch_related("coin")
83
+ curex: models.CurEx = await models.CurEx.get(cur_id=pair.cur_id, ex=self.actor.ex).prefetch_related("cur")
84
+ creds = [c.cred for c in race.road.credexs]
85
+ pm_ids = [pm.id for pm in race.road.ad.pms]
86
+ pmexs: list[models.PmEx] = [pmex for pm in race.road.ad.pms for pmex in pm.pmexs if pmex.ex_id == 4]
87
+ post_pm_ids = {c.cred.ovr_pm_id for c in race.road.credexs if c.cred.ovr_pm_id}
88
+ post_pmexs = set(await models.PmEx.filter(pm_id__in=post_pm_ids, ex=self.actor.ex).prefetch_related("pm"))
89
+
90
+ k = (-1) ** taker_side # on_buy=1, on_sell=-1
91
+ sleep_sec = 3 # 1 if set(pms) & {"volet"} and coinex.coin_id == 1 else 5
92
+ _lstat, volume = None, 0
93
+
94
+ # погнали цикл гонки
95
+ while self.actor.person.user.status > 0: # todo: separate agents, not whole user.activity
96
+ # подгружаем из бд обновления по текущей гонке
97
+ await race.refresh_from_db()
98
+ if not race.started: # пока выключена
99
+ await sleep(5)
100
+ continue
101
+
102
+ # конверт бд int фильтровочной суммы в float конкретной биржи
103
+ amt = race.filter_amount * 10**-curex.cur.scale if race.filter_amount else None
104
+ ceils = await self.get_ceils(coinex, curex, pmexs, 0.003, 0, amt, post_pmexs)
105
+ race.ceil = int(ceils[taker_side] * 10**curex.scale)
106
+ await race.save()
107
+
108
+ last_vol = volume
109
+ if taker_side: # гонка в стакане продажи - мы покупаем монету за ФИАТ
110
+ fiat = max(await models.Fiat.filter(cred_id__in=[c.id for c in creds]), key=lambda x: x.amount)
111
+ volume = (fiat.amount * 10**-curex.cur.scale) / (race.road.ad.price * 10**-curex.scale)
112
+ else: # гонка в стакане покупки - мы продаем МОНЕТУ за фиат
113
+ asset = await models.Asset.get(addr__actor=self.actor, addr__coin_id=coinex.coin_id)
114
+ volume = asset.free * 10**-coinex.scale
115
+ volume = str(round(volume, coinex.scale))
116
+ get_ads_req = GetAds(
117
+ coin_id=pair.coin_id, cur_id=pair.cur_id, is_sell=bool(taker_side), pm_ids=pm_ids, amount=amt, limit=50
118
+ )
119
+ try:
120
+ ads: list[Ad] = await self.ex_client.ads(get_ads_req)
121
+ except Exception:
122
+ await sleep(1)
123
+ ads: list[Ad] = await self.ads(coinex, curex, taker_side, pmexs, amt, 50, race.vm_filter, post_pmexs)
124
+
125
+ self.overprice_filter(ads, race.ceil * 10**-curex.scale, k) # обрезаем сверху все ads дороже нашего потолка
126
+
127
+ if not ads:
128
+ print(coinex.exid, curex.exid, taker_side, "no ads!")
129
+ await sleep(15)
130
+ continue
131
+ # определяем наше текущее место в уже обрезанном списке ads
132
+ if not (cur_plc := [i for i, ad in enumerate(ads) if int(ad.userId) == self.actor.exid]):
133
+ logging.warning(f"No racing in {pmexs[0].name} {'-' if taker_side else '+'}{coinex.exid}/{curex.exid}")
134
+ await sleep(15)
135
+ continue
136
+ (cur_plc,) = cur_plc # может упасть если в списке > 1 наш ad
137
+ [(await self.ex_client.cond_load(ad, race.road.ad.pair_side, True))[0] for ad in ads[:cur_plc]]
138
+ # rivals = [
139
+ # (await models.RaceStat.update_or_create({"place": plc, "price": ad.price, "premium": ad.premium}, ad=ad))[
140
+ # 0
141
+ # ]
142
+ # for plc, ad in enumerate(rads)
143
+ # ]
144
+ mad: Ad = ads.pop(cur_plc)
145
+ # if (
146
+ # not (lstat := lstat or await race.stats.order_by("-created_at").first())
147
+ # or lstat.place != cur_plc
148
+ # or lstat.price != float(mad.price)
149
+ # or set(rivals) != set(await lstat.rivals)
150
+ # ):
151
+ # lstat = await models.RaceStat.create(race=race, place=cur_plc, price=mad.price, premium=mad.premium)
152
+ # await lstat.rivals.add(*rivals)
153
+ if not ads:
154
+ await sleep(60)
155
+ continue
156
+ if not (cad := self.get_cad(ads, race.ceil * 10**-curex.scale, k, race.target_place, cur_plc)):
157
+ continue
158
+ new_price = round(float(cad.price) - k * step(mad, cad, curex.scale), curex.scale)
159
+ if (
160
+ float(mad.price) == new_price and volume == last_vol
161
+ ): # Если место уже нужное или нужная цена и так уже стоит
162
+ print(
163
+ f"{'v' if taker_side else '^'}{mad.price}",
164
+ end=f"[{race.ceil * 10**-curex.scale}+{cur_plc}] ",
165
+ flush=True,
166
+ )
167
+ await sleep(sleep_sec)
168
+ continue
169
+ if cad.priceType: # Если цена конкурента плавающая, то повышаем себе не цену, а %
170
+ new_premium = (float(mad.premium) or float(cad.premium)) - k * step(mad, cad, 2)
171
+ # if float(mad.premium) == new_premium: # Если нужный % и так уже стоит
172
+ # if mad.priceType and cur_plc != race.target_place:
173
+ # new_premium -= k * step(mad, cad, 2)
174
+ # elif volume == last_vol:
175
+ # print(end="v" if taker_side else "^", flush=True)
176
+ # await sleep(sleep_sec)
177
+ # continue
178
+ mad.premium = str(round(new_premium, 2))
179
+ mad.priceType = cad.priceType
180
+ mad.quantity = volume
181
+ mad.maxAmount = str(2_000_000 if curex.cur_id == 1 else 40_000)
182
+ # req = AdUpdateRequest.model_validate(
183
+ # {
184
+ # **mad.model_dump(),
185
+ # "price": str(round(new_price, curex.scale)),
186
+ # "paymentIds": [str(cx.exid) for cx in race.road.credexs],
187
+ # }
188
+ # )
189
+ # try:
190
+ # print(
191
+ # f"c{race.ceil * 10**-curex.scale}+{cur_plc} {coinex.coin.ticker}{'-' if taker_side else '+'}{req.price}{curex.cur.ticker}"
192
+ # f"{[pm.norm for pm in race.road.ad.pms]}{f'({req.premium}%)' if req.premium != '0' else ''} "
193
+ # f"t{race.target_place} ;",
194
+ # flush=True,
195
+ # )
196
+ # _res = self.ad_upd(req)
197
+ # except FailedRequestError as e:
198
+ # if ExcCode(e.status_code) == ExcCode.FixPriceLimit:
199
+ # if limits := re.search(
200
+ # r"The fixed price set is lower than ([0-9]+\.?[0-9]{0,2}) or higher than ([0-9]+\.?[0-9]{0,2})",
201
+ # e.message,
202
+ # ):
203
+ # req.price = limits.group(1 if taker_side else 2)
204
+ # if req.price != mad.price:
205
+ # _res = self.ad_upd(req)
206
+ # else:
207
+ # raise e
208
+ # elif ExcCode(e.status_code) == ExcCode.InsufficientBalance:
209
+ # asset = await models.Asset.get(addr__actor=self.actor, addr__coin_id=coinex.coin_id)
210
+ # req.quantity = str(round(asset.free * 10**-coinex.scale, coinex.scale))
211
+ # _res = self.ad_upd(req)
212
+ # elif ExcCode(e.status_code) == ExcCode.RareLimit:
213
+ # if not (
214
+ # sads := [
215
+ # ma
216
+ # for ma in self.my_ads(False)
217
+ # if (
218
+ # ma.currencyId == curex.exid
219
+ # and ma.tokenId == coinex.exid
220
+ # and taker_side != ma.side
221
+ # and set(ma.payments) == set([pe.exid for pe in pmexs])
222
+ # )
223
+ # ]
224
+ # ):
225
+ # logging.error(f"Need reserve Ad {'sell' if taker_side else 'buy'} {coinex.exid}/{curex.exid}")
226
+ # await sleep(90)
227
+ # continue
228
+ # self.ad_del(ad_id=int(mad.id))
229
+ # req.id = sads[0].id
230
+ # req.actionType = "ACTIVE"
231
+ # self.api.update_ad(**req.model_dump())
232
+ # logging.warning(f"Ad#{mad.id} recreated")
233
+ # # elif ExcCode(e.status_code) == ExcCode.Timestamp:
234
+ # # await sleep(3)
235
+ # else:
236
+ # raise e
237
+ # except (ReadTimeoutError, ConnectionDoesNotExistError):
238
+ # logging.warning("Connection failed. Restarting..")
239
+ await sleep(6)
240
+
241
+ async def get_books(
242
+ self,
243
+ coinex: models.CoinEx,
244
+ curex: models.CurEx,
245
+ pmexs: list[models.PmEx],
246
+ amount: int,
247
+ post_pmexs: list[models.PmEx] = None,
248
+ ) -> tuple[list[Ad], list[Ad]]:
249
+ buy: list[Ad] = await self.ads(coinex, curex, False, pmexs, amount, 40, False, post_pmexs)
250
+ sell: list[Ad] = await self.ads(coinex, curex, True, pmexs, amount, 30, False, post_pmexs)
251
+ return buy, sell
252
+
253
+ async def get_spread(
254
+ self, bb: list[Ad], sb: list[Ad], perc: float, place: int = 0
255
+ ) -> tuple[tuple[float, float], float, int] | None:
256
+ if len(bb) and len(sb):
257
+ buy_price, sell_price = float(bb[place].price), float(sb[place].price)
258
+ half_spread = (buy_price - sell_price) / (buy_price + sell_price)
259
+ if half_spread * 2 < perc:
260
+ return await self.get_spread(bb, sb, perc, place)
261
+ return (buy_price, sell_price), half_spread, place
262
+ return None
263
+
264
+ async def get_ceils(
265
+ self,
266
+ coinex: models.CoinEx,
267
+ curex: models.CurEx,
268
+ pmexs: list[models.PmEx],
269
+ min_prof=0.02,
270
+ place: int = 0,
271
+ amount: int = None,
272
+ post_pmexs: set[models.PmEx] = None,
273
+ ) -> tuple[float, float]: # todo: refact to Pairex
274
+ for pmc_id in {pmx.pm_id for pmx in pmexs} | set(self.pm_clients.keys()):
275
+ if ceils := self.pm_clients[pmc_id].get_ceils():
276
+ return ceils
277
+ bb, sb = await self.get_books(coinex, curex, pmexs, amount, post_pmexs)
278
+ perc = list(post_pmexs or pmexs)[0].pm.fee * 0.0001 + min_prof
279
+ (bf, sf), _hp, _zplace = await self.get_spread(bb, sb, perc, place)
280
+ mdl = (bf + sf) / 2 # middle price
281
+ bc, sc = mdl + mdl * (perc / 2), mdl - mdl * (perc / 2)
282
+ return bc, sc
283
+
284
+ async def mad_upd(self, mad: Ad, attrs: dict, cxids: list[str]):
285
+ if not [setattr(mad, k, v) for k, v in attrs.items() if getattr(mad, k) != v]:
286
+ print(end="v" if mad.side else "^", flush=True)
287
+ return await sleep(5)
288
+ # req = AdUpdateRequest.model_validate({**mad.model_dump(), "paymentIds": cxids})
289
+ # try:
290
+ # return self.ad_upd(req)
291
+ # except FailedRequestError as e:
292
+ # if ExcCode(e.status_code) == ExcCode.FixPriceLimit:
293
+ # if limits := re.search(
294
+ # r"The fixed price set is lower than ([0-9]+\.?[0-9]{0,2}) or higher than ([0-9]+\.?[0-9]{0,2})",
295
+ # e.message,
296
+ # ):
297
+ # return await self.mad_upd(mad, {"price": limits.group(1 if mad.side else 2)}, cxids)
298
+ # elif ExcCode(e.status_code) == ExcCode.RareLimit:
299
+ # await sleep(180)
300
+ # else:
301
+ # raise e
302
+ # except (ReadTimeoutError, ConnectionDoesNotExistError):
303
+ # logging.warning("Connection failed. Restarting..")
304
+ # print("-" if mad.side else "+", end=req.price, flush=True)
305
+ await sleep(60)
306
+
307
+ def overprice_filter(self, ads: list[Ad], ceil: float, k: Literal[-1, 1]):
308
+ # вырезаем ads с ценами выше потолка
309
+ if ads and (ceil - float(ads[0].price)) * k > 0:
310
+ if int(ads[0].userId) != self.actor.exid:
311
+ ads.pop(0)
312
+ self.overprice_filter(ads, ceil, k)
313
+
314
+ def get_cad(self, ads: list[Ad], ceil: float, k: Literal[-1, 1], target_place: int, cur_plc: int) -> Ad:
315
+ if not ads:
316
+ return None
317
+ # чью цену будем обгонять, предыдущей или слещующей объявы?
318
+ # cad: Ad = ads[place] if cur_plc > place else ads[cur_plc]
319
+ # переделал пока на жесткую установку целевого места, даже если текущее выше:
320
+ if len(ads) <= target_place:
321
+ logging.error(f"target place {target_place} not found in ads {len(ads)}-lenght list")
322
+ target_place = len(ads) - 1
323
+ cad: Ad = ads[target_place]
324
+ # а цена обгоняемой объявы не выше нашего потолка?
325
+ if (float(cad.price) - ceil) * k <= 0:
326
+ # тогда берем следующую
327
+ ads.pop(target_place)
328
+ cad = self.get_cad(ads, ceil, k, target_place, cur_plc)
329
+ # todo: добавить фильтр по лимитам min-max
330
+ return cad
78
331
 
79
332
  # 0: Получшение ордеров в статусе status, по монете coin, в валюте coin, в направлении is_sell: bool
80
333
  @abstractmethod
@@ -133,31 +386,23 @@ class BaseAgentClient(HttpClient, BaseInAgentClient):
133
386
  @abstractmethod
134
387
  async def my_ads(self, status: AdStatus = None) -> list[BaseAd]: ...
135
388
 
389
+ @abstractmethod
390
+ async def x2e_req_ad_upd(self, xreq: AdUpd) -> BaseAdUpdate: ...
391
+
136
392
  # 30: Создание объявления
137
393
  @abstractmethod
138
394
  async def ad_new(self, ad: BaseAd) -> Ad: ...
139
395
 
140
- async def ad_upd(self, ad_upd_req: AdUpd) -> Ad:
141
- pmex_exids = await models.PmEx.filter(ex_id=self.actor.ex_id, pm_id__in=ad_upd_req.pm_ids).values_list(
142
- "exid", flat=True
143
- )
144
- credexs = await models.CredEx.filter(
396
+ async def ad_upd(self, xreq: AdUpd) -> Ad:
397
+ xreq.credexs = await models.CredEx.filter(
145
398
  ex_id=self.actor.ex_id,
146
- cred__pmcur__pm_id__in=ad_upd_req.pm_ids,
147
- cred__pmcur__cur_id=ad_upd_req.cur_id,
399
+ cred__pmcur__pm_id__in=xreq.pm_ids,
400
+ cred__pmcur__cur_id=xreq.cur_id,
148
401
  cred__person_id=self.actor.person_id,
149
402
  ).prefetch_related("cred__pmcur")
150
- coinex = await models.CoinEx.get(coin_id=ad_upd_req.coin_id, ex=self.ex_client.ex)
151
- curex = await models.CurEx.get(cur_id=ad_upd_req.cur_id, ex=self.ex_client.ex)
152
- # override
153
- ad_upd_req.coin_id = coinex.exid
154
- ad_upd_req.cur_id = curex.exid
155
- ad_upd_req.pm_ids = pmex_exids
156
- ad_upd_req.credexs = credexs
157
- ad_upd_req.price = round(ad_upd_req.price, curex.scale)
158
- ad_upd_req.amount = round(ad_upd_req.amount, curex.scale)
159
- ad_upd_req.quantity = round(ad_upd_req.amount / ad_upd_req.price, coinex.scale)
160
- return await self._ad_upd(ad_upd_req)
403
+ # xreq.credexs = credexs
404
+ ereq = await self.x2e_req_ad_upd(xreq)
405
+ return await self._ad_upd(ereq)
161
406
 
162
407
  # 31: Редактирование объявления
163
408
  @abstractmethod
@@ -214,3 +459,20 @@ class BaseAgentClient(HttpClient, BaseInAgentClient):
214
459
  # if banks: # only for SBP
215
460
  # await cred_db.banks.add(*[await PmExBank.get(exid=b) for b in banks])
216
461
  # return True
462
+
463
+
464
+ def step_is_need(mad, cad) -> bool:
465
+ # todo: пока не решен непонятный кейс, почему то конкурент по всем параметрам слабже, но в списке ранжируется выше.
466
+ # текущая версия: recentExecuteRate округляется до целого, но на бэке байбита его дробная часть больше
467
+ return (
468
+ bool(set(cad.authTag) & {"VA2", "BA"})
469
+ or cad.recentExecuteRate > mad.recentExecuteRate
470
+ or (
471
+ cad.recentExecuteRate
472
+ == mad.recentExecuteRate # and cad.finishNum > mad.finishNum # пока прибавляем для равных
473
+ )
474
+ )
475
+
476
+
477
+ def step(mad, cad, scale: int = 2) -> float:
478
+ return float(int(step_is_need(mad, cad)) * 10**-scale).__round__(scale)