Skip to content

olp bot issue #420

Description

@mehdiOuazri

Rapport de bug : ordre SELL agressif ne se fait jamais matcher (INJ/USDC spot)

Environnement

  • injective-py version 1.16.0
  • Réseau : Mainnet
  • SDK helper : MsgBroadcasterWithPk avec MessageBasedTransactionFeeCalculator
  • Marché : INJ/USDC
    • market_id: 0xa8c14f892f7f7d2516442220a05b652d5afee3f57a5495981dfad7c99ef78e84
    • base_token (INJ) decimals: 18
    • quote_token (USDC, erc20:0xa00C59fF5a080D2b954d0c75e46E22a0c371235a) decimals: 6
    • Marché confirmé status: Active

Résumé du problème

Un ordre MsgCreateSpotLimitOrder de type SELL, placé à un prix délibérément et massivement inférieur au meilleur bid existant (jusqu'à -50% du best bid dans nos tests), ne se fait jamais matcher/exécuter, même après plusieurs minutes d'attente. L'ordre reste fillable == quantity (0% rempli) indéfiniment jusqu'à annulation manuelle.

Ce comportement est reproductible de façon systématique (testé 4 fois, avec des paramètres de prix différents, toujours le même résultat).

Étapes de reproduction

  1. Récupérer le carnet d'ordres réel via client.fetch_chain_spot_orderbook(market_id=market_id)
  2. Noter le meilleur bid (buysPriceLevel[0])
  3. Construire un ordre SELL avec un prix nettement inférieur à ce bid (donc normalement immédiatement "marketable"/exécutable contre ce bid)
  4. Envoyer via composer.msg_create_spot_limit_order(..., order_type=2) (SELL) puis broadcaster.broadcast([msg])
  5. Vérifier fetch_chain_trader_spot_orders toutes les 20 secondes pendant 3 minutes

Résultat observé (dernier test, le plus complet)

Best Bid (source chain):  $5.322  (raw: confirmé cohérent entre fetch_chain_spot_orderbook ET indexer.fetch_spot_orderbook_v2)
Notre prix SELL:          $4.790  (~10% sous le best bid, donc censé matcher immédiatement)

Transaction de placement : broadcast retourne code: 0 (accepté), la commande consomme bien du gas réel (transaction incluse dans un bloc, vérifié via fetch_tx).

Suivi sur 3 minutes (vérifications toutes les 20s via fetch_chain_trader_spot_orders) :

[20s]  fillable=5000000000000000000 / quantity=5000000000000000000
[40s]  fillable=5000000000000000000 / quantity=5000000000000000000
[60s]  fillable=5000000000000000000 / quantity=5000000000000000000
[80s]  fillable=5000000000000000000 / quantity=5000000000000000000
[100s] fillable=5000000000000000000 / quantity=5000000000000000000
[120s] fillable=5000000000000000000 / quantity=5000000000000000000
[140s] fillable=5000000000000000000 / quantity=5000000000000000000
[160s] fillable=5000000000000000000 / quantity=5000000000000000000
[180s] fillable=5000000000000000000 / quantity=5000000000000000000

Aucune évolution. L'ordre reste intégralement non rempli malgré un prix qui devrait, sur un carnet d'ordres central classique, provoquer une exécution immédiate contre le bid existant.

Vérifications effectuées pour exclure les causes évidentes

Hypothèse testée Résultat
Prix mal encodé (mauvais facteur de décimales) Exclu — comparaison en valeurs brutes (raw, sans conversion humaine) confirme que notre prix 4.7952e30 est bien inférieur au bid 5.328e30 (même échelle)
Bids "fantômes"/obsolètes dans le carnet Exclu — fetch_chain_spot_orderbook (source directe blockchain) et indexer.fetch_spot_orderbook_v2 (indexer) sont cohérents entre eux sur les mêmes niveaux de prix, avec des timestamps récents
Solde insuffisant Exclu — le SELL utilise de l'INJ déjà déposé et disponible dans le subaccount (availableBalance suffisant, confirmé avant chaque test)
Tick size non respecté Exclu — prix arrondi explicitement au tick de 0.001 avant envoi
Manque de patience / latence de bloc Exclu — attente de 3 minutes complètes (largement supérieur au temps de bloc d'Injective) sans aucune évolution
Ordre mal formé / rejeté silencieusement Exclu — transaction confirmée code: 0, incluse dans un bloc réel (height non nul, gasUsed non nul via fetch_tx), et l'ordre apparaît bien comme fillable dans fetch_chain_trader_spot_orders juste après placement

Transactions de test (dernière série, mainnet)

  • TX de placement de l'ordre SELL agressif (10% sous le bid) : 8B2AA0931D9C1968A298757F68DA35BE0E1AB905ECDFC05483A7CFEEBD2A0472
  • TX d'annulation correspondante : disponible sur demande

Code minimal de reproduction

import asyncio
from decimal import Decimal
from pyinjective.async_client_v2 import AsyncClient
from pyinjective.core.network import Network
from pyinjective.core.broadcaster import (
    MsgBroadcasterWithPk, StandardAccountBroadcasterConfig,
    MessageBasedTransactionFeeCalculator,
)
from pyinjective.wallet import PrivateKey

async def main():
network = Network.mainnet()
client = AsyncClient(network)
composer = await client.composer()

private_key = PrivateKey.from_mnemonic("VOTRE_MNEMONIC")
address = private_key.to_public_key().to_address()
sender = address.to_acc_bech32()

account_config = StandardAccountBroadcasterConfig(private_key=private_key.to_hex())
fee_calculator = MessageBasedTransactionFeeCalculator(client=client, composer=composer)
broadcaster = MsgBroadcasterWithPk(
    network=network, account_config=account_config,
    client=client, fee_calculator=fee_calculator,
)

subaccount_id = address.get_subaccount_id(1)
market_id = "0xa8c14f892f7f7d2516442220a05b652d5afee3f57a5495981dfad7c99ef78e84"

orderbook = await client.fetch_chain_spot_orderbook(market_id=market_id)
best_bid_human = Decimal(orderbook["buysPriceLevel"][0]["p"]) / Decimal(10**18)

# Prix volontairement très agressif (10% sous le bid)
aggressive_price_human = (best_bid_human * Decimal("0.9")).quantize(Decimal("0.001"))
price_correction_factor = Decimal(10) ** 12  # base_decimals(18) - quote_decimals(6)
price_corrected = aggressive_price_human * price_correction_factor

await client.fetch_account(sender)
msg = composer.msg_create_spot_limit_order(
    sender=sender, market_id=market_id, subaccount_id=subaccount_id,
    fee_recipient=sender, price=price_corrected, quantity=Decimal("5"), order_type=2,
)
result = await broadcaster.broadcast([msg])
print(result)

# Observer: l'ordre reste fillable==quantity indéfiniment
await asyncio.sleep(60)
orders = await client.fetch_chain_trader_spot_orders(market_id=market_id, subaccount_id=subaccount_id)
print(orders)

asyncio.run(main())

Question pour l'équipe Injective

Pourquoi un ordre SELL marketable (prix nettement inférieur au meilleur bid existant) ne se fait-il jamais matcher automatiquement contre ce bid, même après plusieurs minutes ? Y a-t-il :

  • Un mécanisme de traitement par lots (Frequent Batch Auction) qui nécessite une condition de déclenchement spécifique qu'on ne remplit pas ?
  • Une règle anti-manipulation ou de protection spécifique aux nouveaux comptes/subaccounts ?
  • Une étape supplémentaire dans le flux d'ordre qu'on aurait manquée dans la documentation du SDK Python ?

Merci d'avance pour votre aide !

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions