xync-db 0.4.2.dev13__tar.gz → 0.4.4__tar.gz
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_db-0.4.2.dev13 → xync_db-0.4.4}/PKG-INFO +1 -1
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/migrations/models/0001_initial.py +2 -2
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/models/__init__.py +87 -1
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/typs/db/ad.py +2 -1
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/.gitignore +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/README.md +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/pyproject.toml +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/__init__.py +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/config.py +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/enums.py +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/exceptions.py +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/graph.py +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/i18n.py +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/logo.png +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/migrations/CLAUDE.md +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/migrations/__init__.py +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/migrations/models/0002_triggers.py +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/migrations/models/0003_files_seed.py +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/migrations/models/0004_all_seed.py +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/migrations/models/0005_credex_del.py +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/migrations/models/0006_credex_orphan.py +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/migrations/models/0007_agent_auth_notify.py +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/migrations/models/__init__.py +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/migrations/models/seed/__init__.py +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/migrations/models/seed/a_actor_seed.py +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/migrations/models/seed/a_tg_seed.py +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/migrations/models/seed/b_htx_seed.py +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/migrations/models/seed/xync_files.sql +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/models/CLAUDE.md +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/py.typed +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/shared.py +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/typs/__init__.py +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/typs/db/__init__.py +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/typs/db/actor.py +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/typs/db/common.py +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/typs/db/cred.py +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/typs/db/order.py +0 -0
- {xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/typs/db/pm.py +0 -0
|
@@ -10,7 +10,7 @@ from tortoise.fields.db_defaults import Now
|
|
|
10
10
|
from tortoise.migrations import operations as ops
|
|
11
11
|
from tortoise.migrations.constraints import CheckConstraint
|
|
12
12
|
from x_auth.enums import Lang, PeerType, Role
|
|
13
|
-
from x_model.field import DatetimeSecField, UInt1Field, UInt2Field, UInt4Field, UInt8Field,
|
|
13
|
+
from x_model.field import DatetimeSecField, UInt1Field, UInt2Field, UInt4Field, UInt8Field, UniqBinaryField
|
|
14
14
|
|
|
15
15
|
from xync_db.enums import (
|
|
16
16
|
AbuserType,
|
|
@@ -1587,7 +1587,7 @@ class Migration(migrations.Migration):
|
|
|
1587
1587
|
("detail", fields.CharField(max_length=255)),
|
|
1588
1588
|
("name", fields.CharField(null=True, max_length=127)),
|
|
1589
1589
|
("extra", fields.CharField(null=True, max_length=255)),
|
|
1590
|
-
("qr",
|
|
1590
|
+
("qr", fields.BinaryField(null=True)),
|
|
1591
1591
|
(
|
|
1592
1592
|
"person",
|
|
1593
1593
|
fields.ForeignKeyField(
|
|
@@ -675,6 +675,59 @@ class User(TgUser, TsTrait):
|
|
|
675
675
|
tx.id = UUID(bytes=tx.pack)
|
|
676
676
|
return await tx.sign(sign)
|
|
677
677
|
|
|
678
|
+
async def _dedup_hot_by_slot(self, mads: list[MyAd]) -> list[MyAd]:
|
|
679
|
+
"""Группирует объявы по (биржа, направление, слот) и оставляет ПО ОДНОЙ на группу. Выбор внутри
|
|
680
|
+
группы (стратегия выдачи hot тейкеру):
|
|
681
|
+
1) сильнейший критерий — агент-мейкер, с которым у юзера ДОЛЬШЕ всего не было завершённых сделок
|
|
682
|
+
(ни одной → этот агент первым);
|
|
683
|
+
2) при равенстве — у кого больший остаток в объявлении (quantity монеты / amount фиата).
|
|
684
|
+
Направление = пара+сторона; группа скоупится биржей (PairSide глобальна — одна на coin+cur для всех
|
|
685
|
+
бирж). Слот считает `Ad.slot_of` по `CurEx.minimum` (свой на каждую биржу+валюту). Объявы вне слотов
|
|
686
|
+
(битый min_fiat) отбрасываем. Ждёт mads с prefetch ad__maker__ex и ad__pair_side__pair__cur."""
|
|
687
|
+
if not mads:
|
|
688
|
+
return mads
|
|
689
|
+
maker_ids = {m.ad.maker_id for m in mads}
|
|
690
|
+
# все акторы юзера (на всех биржах) — его сторона в любой сделке (как тейкер ИЛИ как мейкер)
|
|
691
|
+
user_actor_ids = set(await Actor.filter(person__user_id=self.id).values_list("id", flat=True))
|
|
692
|
+
# давность последней ЗАВЕРШЁННОЙ сделки юзера с каждым агентом-мейкером (по агенту-actor'у)
|
|
693
|
+
last_deal: dict[int, datetime] = {}
|
|
694
|
+
if user_actor_ids:
|
|
695
|
+
for mk_id, tk_id, deal_ts in (
|
|
696
|
+
await Order.filter(status=OrderStatus.completed)
|
|
697
|
+
.filter(
|
|
698
|
+
Q(ad__maker_id__in=maker_ids, taker_id__in=user_actor_ids)
|
|
699
|
+
| Q(taker_id__in=maker_ids, ad__maker_id__in=user_actor_ids)
|
|
700
|
+
)
|
|
701
|
+
.values_list("ad__maker_id", "taker_id", "created_at")
|
|
702
|
+
):
|
|
703
|
+
cp = mk_id if mk_id in maker_ids else tk_id # контрагент = сторона агента-мейкера
|
|
704
|
+
# сравниваем только со значением того же источника (БД) — без naive/aware-микса с sentinel
|
|
705
|
+
if deal_ts and ((prev := last_deal.get(cp)) is None or deal_ts > prev):
|
|
706
|
+
last_deal[cp] = deal_ts
|
|
707
|
+
minimums = {
|
|
708
|
+
(cx.ex_id, cx.cur_id): cx.minimum
|
|
709
|
+
for cx in await CurEx.filter(
|
|
710
|
+
ex_id__in={m.ad.maker.ex_id for m in mads},
|
|
711
|
+
cur_id__in={m.ad.pair_side.pair.cur_id for m in mads},
|
|
712
|
+
)
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
def rank(m: MyAd) -> tuple:
|
|
716
|
+
ts = last_deal.get(m.ad.maker_id)
|
|
717
|
+
qty = m.ad.quantity or m.ad.amount or 0
|
|
718
|
+
# (0,…) «ни разу не торговали» < любого (1,…); внутри — старее сделка раньше; затем больший остаток
|
|
719
|
+
return (1 if ts else 0, ts or datetime.min, -qty)
|
|
720
|
+
|
|
721
|
+
best: dict[tuple, MyAd] = {}
|
|
722
|
+
for m in mads:
|
|
723
|
+
mn = minimums.get((m.ad.maker.ex_id, m.ad.pair_side.pair.cur_id))
|
|
724
|
+
if (slot := Ad.slot_of(m.ad.min_fiat, m.ad.filtered, mn)) is None:
|
|
725
|
+
continue # неклассифицируемая объява — не показываем
|
|
726
|
+
key = (m.ad.maker.ex_id, m.ad.pair_side_id, slot)
|
|
727
|
+
if key not in best or rank(m) < rank(best[key]):
|
|
728
|
+
best[key] = m
|
|
729
|
+
return list(best.values())
|
|
730
|
+
|
|
678
731
|
# доступные hot объявления для юзера с его текущими балансами # noqa: RUF003 - intentional Russian
|
|
679
732
|
# mine=True — наоборот: только собственные hot-объявления юзера, без балансовой фильтрации
|
|
680
733
|
async def get_hot_ads(
|
|
@@ -689,6 +742,8 @@ class User(TgUser, TsTrait):
|
|
|
689
742
|
# один Q: исключаем строго пересечение (Tortoise's exclude(**kwargs) отрицал бы условия порознь)
|
|
690
743
|
adq = adq.exclude(Q(hex__isnull=True, ad__maker__ex_id__in=SHARE_HEX_EX_IDS))
|
|
691
744
|
bads = await adq.filter(ad__pair_side__is_sell=False)
|
|
745
|
+
if not mine: # тейкеру — по 1 объяве на (биржа, направление, слот); свои (settings) отдаём все
|
|
746
|
+
bads = await self._dedup_hot_by_slot(bads)
|
|
692
747
|
# остаток каждой монеты (asset.free) у акторов персон этого юзера, по бирже и монете # noqa: RUF003
|
|
693
748
|
coin_balances: dict[int, dict[int, int]] = {}
|
|
694
749
|
for ex_id, coin_id, total in (
|
|
@@ -712,6 +767,7 @@ class User(TgUser, TsTrait):
|
|
|
712
767
|
minimums = {cx.cur_id: cx.minimum for cx in curexs}
|
|
713
768
|
balances = {cur_id: b for cur_id, b in balances.items() if minimums.get(cur_id) and b >= minimums[cur_id]}
|
|
714
769
|
sads = await adq.filter(ad__pair_side__is_sell=True, ad__pair_side__pair__cur_id__in=balances.keys()).all()
|
|
770
|
+
sads = await self._dedup_hot_by_slot(sads) # not mine — по 1 объяве на (биржа, направление, слот)
|
|
715
771
|
|
|
716
772
|
return bads, sads, balances, coin_balances
|
|
717
773
|
|
|
@@ -1201,6 +1257,29 @@ class Ad(Model, TsTrait):
|
|
|
1201
1257
|
"price",
|
|
1202
1258
|
}
|
|
1203
1259
|
|
|
1260
|
+
@staticmethod
|
|
1261
|
+
def slot_of(min_fiat: int, filtered: bool, minimum: int) -> tuple[int, int] | None:
|
|
1262
|
+
"""Слот hot-объявы — пара `(множитель_мин_лимита, фильтрованность)` по её нижнему лимиту и
|
|
1263
|
+
наличию доп. условия для тейкера. Минимальный лимит валюты на бирже свой — `CurEx.minimum`.
|
|
1264
|
+
Слоты в одном направлении (пара+сторона, на каждой бирже):
|
|
1265
|
+
(1, 1) — «1»: мин. лимит, С условием (ОБЯЗАТЕЛЬНЫЙ — его всегда размещает каждый мейкер);
|
|
1266
|
+
(1, 0) — «1nf»: мин. лимит, БЕЗ условия;
|
|
1267
|
+
(10, 1) — «10»: от 10 мин. лимитов;
|
|
1268
|
+
(60, 1) — «60»: от 60 мин. лимитов.
|
|
1269
|
+
Диапазоны 5..10 / 23..60 ловят объявы, ужатые нехваткой баланса (real_min_fiat = min(k*min,
|
|
1270
|
+
cur_reserve) дал множитель меньше номинала слота). Слоты 10/60 нормализуем как фильтрованные —
|
|
1271
|
+
нефильтрован лишь «1nf». `None` — min_fiat не попадает ни в один слот (битая/чужая объява)."""
|
|
1272
|
+
if not minimum:
|
|
1273
|
+
return None
|
|
1274
|
+
k = int(min_fiat / minimum)
|
|
1275
|
+
if k == 1:
|
|
1276
|
+
return 1, int(filtered)
|
|
1277
|
+
if 5 <= k <= 10:
|
|
1278
|
+
return 10, 1
|
|
1279
|
+
if 23 <= k <= 60:
|
|
1280
|
+
return 60, 1
|
|
1281
|
+
return None
|
|
1282
|
+
|
|
1204
1283
|
@staticmethod
|
|
1205
1284
|
async def _sync_pms(ad: Ad, pms: Collection[Pm] | None) -> None:
|
|
1206
1285
|
"""Синк m2m pms (clear+add), только если набор передан и отличается от текущего.
|
|
@@ -1259,6 +1338,13 @@ class Ad(Model, TsTrait):
|
|
|
1259
1338
|
await cls._sync_pms(ad, pms) # новая локальная заготовка exid=0
|
|
1260
1339
|
return ad, True
|
|
1261
1340
|
|
|
1341
|
+
@classmethod
|
|
1342
|
+
async def remove(cls, id: int) -> None:
|
|
1343
|
+
"""ЕДИНСТВЕННЫЙ writer удаления строки Ad (парный к upsert). Физически сносит объяву — каскадом
|
|
1344
|
+
уходят MyAd/Race, m2m (ad_pm/myad_cred) и ордера по ней (on_delete=CASCADE → потеря истории).
|
|
1345
|
+
Звать ТОЛЬКО после успешного удаления на бирже (controllers/hot.del_ad)."""
|
|
1346
|
+
await cls.filter(id=id).delete()
|
|
1347
|
+
|
|
1262
1348
|
class Meta:
|
|
1263
1349
|
table_description = "P2P Advertisements"
|
|
1264
1350
|
unique_together = (("exid", "maker"),)
|
|
@@ -1309,7 +1395,7 @@ class MyAd(Model): # Road
|
|
|
1309
1395
|
@classmethod
|
|
1310
1396
|
# запрос для получения объяв всех агентов этой биржи для разогрева
|
|
1311
1397
|
def hot_mads_query(cls, ex_ids: list[int], active: bool = True):
|
|
1312
|
-
fltr = dict(credexs__cred__ovr_pm_id=0,
|
|
1398
|
+
fltr = dict(credexs__cred__ovr_pm_id=0, ad__maker__agent__status__in=[2, 3])
|
|
1313
1399
|
if active:
|
|
1314
1400
|
fltr["ad__status"] = AdStatus.active
|
|
1315
1401
|
if ex_ids:
|
|
@@ -26,7 +26,8 @@ class _Ad(BaseModel):
|
|
|
26
26
|
|
|
27
27
|
@model_validator(mode="after")
|
|
28
28
|
def amount_quantity(self):
|
|
29
|
-
if
|
|
29
|
+
if self.status.value and self.quantity is None and self.amount is None:
|
|
30
|
+
# если объява не активна, то можно допустить что не указаны ни quantity, ни amount
|
|
30
31
|
raise ValueError("either amount or quantity is required")
|
|
31
32
|
# elif self.amount and not self.quantity:
|
|
32
33
|
# self.quantity = self.amount / self.price
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{xync_db-0.4.2.dev13 → xync_db-0.4.4}/src/xync_db/migrations/models/0007_agent_auth_notify.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|