Розробка моделі виявлення wash trading

Проєктуємо та розробляємо блокчейн-рішення повного циклу: від архітектури смарт-контрактів до запуску DeFi-протоколів, NFT-маркетплейсів та криптобірж. Аудит безпеки, токеноміка, інтеграція з наявною інфраструктурою.
Показано 1 з 1Усі 1306 послуг
Розробка моделі виявлення wash trading
Складний
від 2 тижнів до 3 місяців
Часті запитання

Напрямки блокчейн-розробки

Етапи блокчейн-розробки

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1306
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1218
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    920
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1147
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    610
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    885

Розробка моделі виявлення wash trading

Wash trading — штучне створення обсягу торгів через одночасне купівлю та продаж одного активу однією особою або групою координованих осіб. За даними Chainalysis, у 2023 році понад 50% обсягу торгів на кількох NFT маркетплейсах було wash trading. На централізованих біржах масштаб ще більший. Модель виявлення повинна працювати з on-chain даними, виявляти закономірності та надавати інтерпретовані результати.

Типи wash trading у Web3

Розуміння різновидів визначає вибір ознак моделі:

Self-trading: той самий гаманець купує і продає сам собі або через ланцюг афільованих адрес.

Circular trading: A продає B, B продає C, C продає A. Актив повертається до первісного власника.

Layered wash trading: складні ланцюги через 5-10 адрес для приховування зв'язків. Використовується для розкачування NFT перед продажем справжньому покупцю за завищеною ціною.

Airdrop farming: wash trading для накопичення обсягу торгів для майбутніх airdrop. Це було масовим на Blur у 2023 році.

Fee rebate abuse: отримання повернень від біржі через штучний обсяг.

Граф-аналіз: основний інструмент

Ключовий метод виявлення — побудова графа транзакцій та пошук циклів і кластерів:

Побудова графа транзакцій

import networkx as nx
from collections import defaultdict
from dataclasses import dataclass
from typing import List, Dict, Set, Tuple
import pandas as pd

@dataclass
class Transfer:
    tx_hash: str
    from_address: str
    to_address: str
    token_id: int  # для NFT
    price: float
    timestamp: int
    block_number: int

def build_transaction_graph(transfers: List[Transfer]) -> nx.DiGraph:
    """
    Будуємо спрямований граф: вузли = адреси, ребра = трансферти.
    Вага ребра = обсяг торгів між адресами.
    """
    G = nx.DiGraph()

    for t in transfers:
        if G.has_edge(t.from_address, t.to_address):
            G[t.from_address][t.to_address]['volume'] += t.price
            G[t.from_address][t.to_address]['count'] += 1
            G[t.from_address][t.to_address]['txs'].append(t.tx_hash)
        else:
            G.add_edge(
                t.from_address,
                t.to_address,
                volume=t.price,
                count=1,
                txs=[t.tx_hash]
            )

    return G

def detect_cycles(G: nx.DiGraph, max_length: int = 6) -> List[List[str]]:
    """
    Знаходимо цикли в графі — ознака wash trading.
    max_length обмежує глибину пошуку для продуктивності.
    """
    cycles = []

    # simple_cycles з NetworkX — алгоритм Джонсона, O((n+e)(c+1))
    for cycle in nx.simple_cycles(G):
        if len(cycle) <= max_length:
            cycles.append(cycle)

    return cycles

Кластеризація афільованих адрес

Адреси з одного кластера (керовані однією особою) можна виявити через аналіз:

  • Однаковий джерело фінансування (отримали ETH з однієї адреси)
  • Закономірності синхронізації активності в часі
  • Спільні стратегії gas price
def cluster_addresses(
    addresses: List[str],
    funding_map: Dict[str, str],  # address -> funding source
    time_correlations: Dict[Tuple[str, str], float]
) -> List[Set[str]]:
    """
    Union-Find для об'єднання афільованих адрес у кластери.
    """
    parent = {addr: addr for addr in addresses}

    def find(x):
        if parent[x] != x:
            parent[x] = find(parent[x])
        return parent[x]

    def union(x, y):
        parent[find(x)] = find(y)

    # Об'єднуємо адреси з одним джерелом фінансування
    funding_groups = defaultdict(list)
    for addr, source in funding_map.items():
        funding_groups[source].append(addr)

    for source, addrs in funding_groups.items():
        for i in range(1, len(addrs)):
            union(addrs[0], addrs[i])

    # Об'єднуємо за високою кореляцією активності
    CORRELATION_THRESHOLD = 0.85
    for (addr1, addr2), corr in time_correlations.items():
        if corr >= CORRELATION_THRESHOLD:
            union(addr1, addr2)

    # Збираємо кластери
    clusters = defaultdict(set)
    for addr in addresses:
        clusters[find(addr)].add(addr)

    return [cluster for cluster in clusters.values() if len(cluster) > 1]

Ознаки для ML моделі

Крім граф-аналізу будуємо feature vector для кожної торгової пари або адреси:

Часові ознаки

def compute_temporal_features(
    trades: pd.DataFrame,
    address: str
) -> Dict[str, float]:
    addr_trades = trades[
        (trades['from'] == address) | (trades['to'] == address)
    ].sort_values('timestamp')

    features = {}

    # Середній інтервал між торгами (у секундах)
    if len(addr_trades) > 1:
        intervals = addr_trades['timestamp'].diff().dropna()
        features['mean_trade_interval'] = intervals.mean()
        features['std_trade_interval'] = intervals.std()
        # Малий STD = підозрило регулярні інтервали
        features['regularity_score'] = 1 / (1 + features['std_trade_interval'])
    else:
        features['mean_trade_interval'] = 0
        features['std_trade_interval'] = 0
        features['regularity_score'] = 0

    # Частка торгів у позаробочий час (2-6 AM UTC)
    addr_trades['hour'] = pd.to_datetime(
        addr_trades['timestamp'], unit='s'
    ).dt.hour
    off_hours = addr_trades[addr_trades['hour'].between(2, 6)]
    features['off_hours_ratio'] = len(off_hours) / max(len(addr_trades), 1)

    return features

Економічні ознаки

def compute_economic_features(
    trades: pd.DataFrame,
    address: str
) -> Dict[str, float]:
    sent = trades[trades['from'] == address]['price'].sum()
    received = trades[trades['to'] == address]['price'].sum()

    features = {}

    # Чистий P&L: wash trader зазвичай має P&L близько 0 (тільки газ)
    features['net_pnl'] = received - sent
    features['total_volume'] = sent + received
    features['pnl_to_volume_ratio'] = abs(features['net_pnl']) / max(features['total_volume'], 1)
    # Низьке значення = підозріле (нема справжнього прибутку/збитку)

    # Кількість унікальних контрагентів
    counterparts = set(trades[trades['from'] == address]['to'].tolist() +
                       trades[trades['to'] == address]['from'].tolist())
    features['unique_counterparts'] = len(counterparts)

    # Концентрація обсягу з одним контрагентом
    if len(counterparts) > 0:
        volumes_by_counterpart = trades.groupby('to')['price'].sum()
        max_concentration = volumes_by_counterpart.max() / max(sent, 1)
        features['max_counterpart_concentration'] = max_concentration

    return features

NFT-специфічні ознаки

def compute_nft_features(
    trades: pd.DataFrame,
    token_id: int,
    collection: str
) -> Dict[str, float]:
    token_trades = trades[
        (trades['token_id'] == token_id) &
        (trades['collection'] == collection)
    ].sort_values('timestamp')

    features = {}

    # Кількість змін власництва
    features['ownership_changes'] = len(token_trades)

    # Повернення до попередніх власників
    owners_seen = set()
    revisits = 0
    for _, row in token_trades.iterrows():
        if row['to'] in owners_seen:
            revisits += 1
        owners_seen.add(row['to'])
    features['ownership_revisit_rate'] = revisits / max(len(token_trades), 1)

    # Зростання ціни проти ринкового еталону
    if len(token_trades) >= 2:
        price_growth = (token_trades.iloc[-1]['price'] /
                        token_trades.iloc[0]['price'] - 1)
        features['price_growth'] = price_growth
        # Аномальне зростання ціни при високому обсязі = підозріле
    else:
        features['price_growth'] = 0

    return features

Модель класифікації

Збираємо ознаки та навчаємо Gradient Boosting:

from sklearn.ensemble import GradientBoostingClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import precision_recall_curve, roc_auc_score
import shap

def train_wash_trading_model(
    features_df: pd.DataFrame,
    labels: pd.Series  # 1 = wash trading, 0 = legitimate
):
    X_train, X_test, y_train, y_test = train_test_split(
        features_df, labels, test_size=0.2, stratify=labels
    )

    scaler = StandardScaler()
    X_train_scaled = scaler.fit_transform(X_train)
    X_test_scaled = scaler.transform(X_test)

    model = GradientBoostingClassifier(
        n_estimators=200,
        max_depth=5,
        learning_rate=0.05,
        subsample=0.8,
        random_state=42
    )
    model.fit(X_train_scaled, y_train)

    # SHAP для інтерпретовності
    explainer = shap.TreeExplainer(model)
    shap_values = explainer.shap_values(X_test_scaled)

    # Precision-Recall важливіша за ROC для незбалансованих даних
    y_proba = model.predict_proba(X_test_scaled)[:, 1]
    auc = roc_auc_score(y_test, y_proba)
    print(f"ROC-AUC: {auc:.3f}")

    return model, scaler, explainer

Оцінка впевненості та інтерпретація

Модель видає не бінарний результат, а score з поясненням:

@dataclass
class WashTradingAssessment:
    address: str
    wash_probability: float    # 0.0 - 1.0
    risk_level: str           # LOW / MEDIUM / HIGH / CRITICAL
    contributing_factors: List[str]  # SHAP-based пояснення
    flagged_transactions: List[str]  # конкретні підозрілі tx

def assess_address(
    address: str,
    model,
    scaler,
    explainer,
    features: Dict
) -> WashTradingAssessment:
    X = pd.DataFrame([features])
    X_scaled = scaler.transform(X)

    probability = model.predict_proba(X_scaled)[0][1]

    if probability < 0.3:
        risk_level = "LOW"
    elif probability < 0.6:
        risk_level = "MEDIUM"
    elif probability < 0.85:
        risk_level = "HIGH"
    else:
        risk_level = "CRITICAL"

    # SHAP пояснення — які ознаки найбільше вплинули
    shap_vals = explainer.shap_values(X_scaled)[0]
    top_factors = sorted(
        zip(X.columns, shap_vals),
        key=lambda x: abs(x[1]),
        reverse=True
    )[:5]

    contributing_factors = [
        f"{feat}: {'+' if val > 0 else '-'}{abs(val):.3f}"
        for feat, val in top_factors
    ]

    return WashTradingAssessment(
        address=address,
        wash_probability=probability,
        risk_level=risk_level,
        contributing_factors=contributing_factors,
        flagged_transactions=[]
    )

Джерела даних

Джерело Дані Оновлення
The Graph On-chain подій DEX/NFT Real-time
Dune Analytics Історичні дані, доступ SQL Кілька хвилин
Transpose Дані графа транзакцій Real-time API
Flipside Crypto On-chain аналітика Щодня
Вбудований indexer Власні подій Real-time

Для production моделі на DEX — власний indexer через WebSocket RPC забезпечує найменшу затримку та повний контроль над даними. Dune Analytics хороший для розробки та дослідження, але занадто повільний для real-time моніторингу.

Модель не є абсолютним детектором. Високе значення score — сигнал для ручного аналізу, не автоматичної заборони. Особливо важливо це для NFT маркетплейсів, де market-maker-и та автоматизовані трейдери можуть давати хибні спрацьовування.