mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-11-02 06:16:10 +00:00
fix(trading): sell the exact raw token count the buy received (#245)
* fix(trading): sell the exact raw token count the buy received The buy read the raw token count off its transaction, kept it as a float, and the sell rebuilt it with int(amount * 10**6), which for some counts comes out one raw unit short and leaves dust in the token account. The raw count now travels on TradeResult and Position to the seller, and an exact-in buy converts its configured spend through its decimal text. * fix(trading): keep the exact token count when the spend cannot be read * fix(trading): price an exact-in buy from its spend when the spend can't be read
This commit is contained in:
@@ -29,6 +29,7 @@ individual scripts for a subset.
|
||||
| `verify_cleanup_survives_shutdown.py` | shutdown cleanup runs to completion through the cancellation that triggered it, bounded, and a cleanup that hangs or raises still lets the process exit |
|
||||
| `verify_token_queue_shutdown.py` | cancelling the token queue processor ends it without raising, and `task_done()` stays balanced on the paths that took an item |
|
||||
| `verify_pumpfun_buy_token_amount.py` | a pump.fun `buy_v2` asks for the whole sized token count with slippage on the cost ceiling, in extreme_fast_mode and on the regular path |
|
||||
| `verify_exact_token_amounts.py` | a position sells the raw token count its buy read, not a float rebuilt from it, including when the buy falls back to the wallet balance; an exact-in buy spends the configured amount to the raw unit |
|
||||
| `verify_buy_result_not_lost.py` | a landed buy is never reported failed, and a reverted one never reported landed |
|
||||
| `verify_tx_status_checks.py` | every path reads `meta.err`; `--live` replays known reverted signatures |
|
||||
| `verify_tp_sl_exit_price.py` | the tp/sl exit prices off the trigger price, and a reverted sell is retried, bounded |
|
||||
|
||||
@@ -19,6 +19,8 @@ class TradeResult:
|
||||
tx_signature: str | None = None
|
||||
error_message: str | None = None
|
||||
amount: float | None = None
|
||||
#: `amount` in raw token units, exact. None when only an estimate exists.
|
||||
amount_raw: int | None = None
|
||||
price: float | None = None
|
||||
# Why a failed trade failed, when the caller has to decide whether
|
||||
# resubmitting is safe. None on success, and on failures that predate the
|
||||
@@ -37,6 +39,7 @@ class TradeResult:
|
||||
"tx_signature": self.tx_signature,
|
||||
"error_message": self.error_message,
|
||||
"amount": self.amount,
|
||||
"amount_raw": self.amount_raw,
|
||||
"price": self.price,
|
||||
"failure_reason": (
|
||||
self.failure_reason.value if self.failure_reason else None
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Platform-aware trader implementations built on the interface system."""
|
||||
|
||||
import asyncio
|
||||
from decimal import Decimal
|
||||
from time import monotonic
|
||||
|
||||
from solders.pubkey import Pubkey
|
||||
@@ -289,8 +290,11 @@ class PlatformAwareBuyer(Trader):
|
||||
# exact-in program spends all of amount_in, so it gets the
|
||||
# configured amount and a token floor. Otherwise the token count
|
||||
# is exact (pump.fun buy_v2) and amount_in is the ceiling.
|
||||
if getattr(instruction_builder, "spends_exact_amount_in", False):
|
||||
max_quote_amount_raw = int(quote_amount * quote_unit)
|
||||
exact_in = getattr(instruction_builder, "spends_exact_amount_in", False)
|
||||
if exact_in:
|
||||
# The configured spend is a decimal like 0.003; scaling the float
|
||||
# can land one raw unit short of it.
|
||||
max_quote_amount_raw = int(Decimal(str(quote_amount)) * quote_unit)
|
||||
token_amount_raw = int(
|
||||
token_amount * (1 - self.slippage) * 10**TOKEN_DECIMALS
|
||||
)
|
||||
@@ -352,22 +356,34 @@ class PlatformAwareBuyer(Trader):
|
||||
quote_mint=quote_mint,
|
||||
)
|
||||
|
||||
if tokens_raw is not None and quote_spent is not None:
|
||||
# The exact raw count, carried to the sell: converting the float
|
||||
# back can come out a raw unit short and leave dust behind.
|
||||
amount_raw = None
|
||||
if tokens_raw is not None:
|
||||
# The token count stands on its own; a missing spend only
|
||||
# leaves the price at its estimate.
|
||||
amount_raw = tokens_raw
|
||||
actual_amount = tokens_raw / 10**TOKEN_DECIMALS
|
||||
actual_price = (quote_spent / quote_unit) / actual_amount
|
||||
logger.info(
|
||||
f"Actual tokens received: {actual_amount:.6f} "
|
||||
f"(expected: {token_amount:.6f})"
|
||||
)
|
||||
logger.info(
|
||||
f"Actual {quote_label} spent: "
|
||||
f"{quote_spent / quote_unit:.10f} {quote_label}"
|
||||
)
|
||||
logger.info(
|
||||
f"Actual price: {actual_price:.10f} {quote_label}/token"
|
||||
)
|
||||
token_amount = actual_amount
|
||||
token_price_sol = actual_price
|
||||
if quote_spent is None and exact_in and tokens_raw > 0:
|
||||
# An exact-in buy spent what it submitted; pairing the
|
||||
# actual count with the estimated price would set an
|
||||
# unreachable sell floor.
|
||||
quote_spent = max_quote_amount_raw
|
||||
if quote_spent is not None and tokens_raw > 0:
|
||||
actual_price = (quote_spent / quote_unit) / actual_amount
|
||||
logger.info(
|
||||
f"Actual {quote_label} spent: "
|
||||
f"{quote_spent / quote_unit:.10f} {quote_label}"
|
||||
)
|
||||
logger.info(
|
||||
f"Actual price: {actual_price:.10f} {quote_label}/token"
|
||||
)
|
||||
token_price_sol = actual_price
|
||||
else:
|
||||
# confirm_transaction already read meta.err, so the buy
|
||||
# executed — only the amounts could not be read back.
|
||||
@@ -379,8 +395,9 @@ class PlatformAwareBuyer(Trader):
|
||||
f"quote_spent={quote_spent}) from tx {tx_signature}; "
|
||||
f"the buy did land, falling back to the wallet balance"
|
||||
)
|
||||
balance = await self._read_token_balance(token_info)
|
||||
if balance is not None and balance > 0:
|
||||
balance_raw = await self._read_token_balance(token_info)
|
||||
if balance_raw is not None and balance_raw > 0:
|
||||
balance = balance_raw / 10**TOKEN_DECIMALS
|
||||
# The balance is cumulative, so cap it at what this buy
|
||||
# asked for: the rest was already held, and selling it
|
||||
# would liquidate an unrelated position. An exact delta
|
||||
@@ -390,7 +407,10 @@ class PlatformAwareBuyer(Trader):
|
||||
f"Token balance after buy: {balance:.6f}, selling at "
|
||||
f"most the {token_amount:.6f} this buy asked for"
|
||||
)
|
||||
token_amount = min(balance, token_amount)
|
||||
amount_raw = min(
|
||||
balance_raw, int(token_amount * 10**TOKEN_DECIMALS)
|
||||
)
|
||||
token_amount = amount_raw / 10**TOKEN_DECIMALS
|
||||
else:
|
||||
logger.warning(
|
||||
f"Token balance unreadable too; holding the expected "
|
||||
@@ -403,6 +423,7 @@ class PlatformAwareBuyer(Trader):
|
||||
platform=token_info.platform,
|
||||
tx_signature=tx_signature,
|
||||
amount=token_amount,
|
||||
amount_raw=amount_raw,
|
||||
price=token_price_sol,
|
||||
)
|
||||
else:
|
||||
@@ -430,7 +451,7 @@ class PlatformAwareBuyer(Trader):
|
||||
|
||||
return address_provider.derive_pool_address(token_info.mint)
|
||||
|
||||
async def _read_token_balance(self, token_info: TokenInfo) -> float | None:
|
||||
async def _read_token_balance(self, token_info: TokenInfo) -> int | None:
|
||||
"""Read how many of a coin the wallet actually holds.
|
||||
|
||||
Used only when a landed buy's amounts could not be parsed back out of the
|
||||
@@ -441,7 +462,7 @@ class PlatformAwareBuyer(Trader):
|
||||
token_info: Token information carrying the mint and token program
|
||||
|
||||
Returns:
|
||||
Balance in whole tokens, or None if it cannot be read
|
||||
Balance in raw token units, or None if it cannot be read
|
||||
"""
|
||||
token_program = (
|
||||
token_info.token_program_id or SystemAddresses.TOKEN_2022_PROGRAM
|
||||
@@ -454,7 +475,7 @@ class PlatformAwareBuyer(Trader):
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning(f"Could not read token balance for {token_info.mint}: {e}")
|
||||
return None
|
||||
return balance_raw / 10**TOKEN_DECIMALS
|
||||
return balance_raw
|
||||
|
||||
def _can_skip_refresh(self, token_info: TokenInfo) -> bool:
|
||||
"""Whether the pre-buy curve read can be skipped entirely.
|
||||
@@ -647,7 +668,11 @@ class PlatformAwareSeller(Trader):
|
||||
self.compute_units = compute_units or {}
|
||||
|
||||
async def execute(
|
||||
self, token_info: TokenInfo, token_amount: float, token_price: float
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
token_amount: float,
|
||||
token_price: float,
|
||||
token_amount_raw: int | None = None,
|
||||
) -> TradeResult:
|
||||
"""Execute sell operation using platform-specific implementations.
|
||||
|
||||
@@ -660,6 +685,9 @@ class PlatformAwareSeller(Trader):
|
||||
the freshest price available: a stale price above the market sets
|
||||
a floor the pool cannot pay and the sell reverts (pump.fun 6003
|
||||
TooLittleSolReceived).
|
||||
token_amount_raw: `token_amount` in raw units, exact. Pass it when the
|
||||
buy read it: rebuilt from the float, the amount can come out a raw
|
||||
unit short and leave dust in the token account.
|
||||
|
||||
Returns:
|
||||
TradeResult with operation outcome
|
||||
@@ -779,7 +807,11 @@ class PlatformAwareSeller(Trader):
|
||||
|
||||
# Use pre-known amount and price (no RPC delay)
|
||||
token_balance_decimal = token_amount
|
||||
token_balance = int(token_amount * 10**TOKEN_DECIMALS)
|
||||
token_balance = (
|
||||
token_amount_raw
|
||||
if token_amount_raw is not None
|
||||
else int(token_amount * 10**TOKEN_DECIMALS)
|
||||
)
|
||||
token_price_sol = token_price
|
||||
|
||||
logger.info(f"Token balance: {token_balance_decimal:.6f}")
|
||||
|
||||
@@ -28,6 +28,8 @@ class Position:
|
||||
entry_price: float
|
||||
quantity: float
|
||||
entry_time: datetime
|
||||
#: `quantity` in raw token units, exact; the sell uses it when known.
|
||||
quantity_raw: int | None = None
|
||||
|
||||
# Exit conditions
|
||||
take_profit_price: float | None = None
|
||||
@@ -50,6 +52,7 @@ class Position:
|
||||
take_profit_percentage: float | None = None,
|
||||
stop_loss_percentage: float | None = None,
|
||||
max_hold_time: int | None = None,
|
||||
quantity_raw: int | None = None,
|
||||
) -> "Position":
|
||||
"""Create a position from a successful buy transaction.
|
||||
|
||||
@@ -61,6 +64,7 @@ class Position:
|
||||
take_profit_percentage: Take profit percentage (0.5 = 50% profit)
|
||||
stop_loss_percentage: Stop loss percentage (0.2 = 20% loss)
|
||||
max_hold_time: Maximum hold time in seconds
|
||||
quantity_raw: `quantity` in raw token units, when the buy read it exactly
|
||||
|
||||
Returns:
|
||||
Position instance
|
||||
@@ -78,6 +82,7 @@ class Position:
|
||||
symbol=symbol,
|
||||
entry_price=entry_price,
|
||||
quantity=quantity,
|
||||
quantity_raw=quantity_raw,
|
||||
entry_time=datetime.utcnow(),
|
||||
take_profit_price=take_profit_price,
|
||||
stop_loss_price=stop_loss_price,
|
||||
|
||||
@@ -742,6 +742,7 @@ class UniversalTrader:
|
||||
symbol=token_info.symbol,
|
||||
entry_price=buy_result.price,
|
||||
quantity=buy_result.amount,
|
||||
quantity_raw=buy_result.amount_raw,
|
||||
take_profit_percentage=self.take_profit_percentage,
|
||||
stop_loss_percentage=self.stop_loss_percentage,
|
||||
max_hold_time=self.max_hold_time,
|
||||
@@ -779,7 +780,10 @@ class UniversalTrader:
|
||||
|
||||
for attempt in range(1, self.max_exit_sell_attempts + 1):
|
||||
sell_result: TradeResult = await self.seller.execute(
|
||||
token_info, token_amount=buy_result.amount, token_price=token_price
|
||||
token_info,
|
||||
token_amount=buy_result.amount,
|
||||
token_price=token_price,
|
||||
token_amount_raw=buy_result.amount_raw,
|
||||
)
|
||||
|
||||
if sell_result.success:
|
||||
@@ -1118,6 +1122,7 @@ class UniversalTrader:
|
||||
token_info,
|
||||
token_amount=position.quantity,
|
||||
token_price=price,
|
||||
token_amount_raw=position.quantity_raw,
|
||||
)
|
||||
|
||||
if sell_result.success:
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
"""Verify a position sells exactly what the buy received, and an exact-in buy spends what was configured.
|
||||
|
||||
The buy read the exact raw token count off its transaction, stored it as a float
|
||||
in whole tokens, and the sell rebuilt the raw count with `int(amount * 10**6)`.
|
||||
For some counts, at every position size, that comes out one raw unit short, so
|
||||
the sell left dust behind and the token account could not be closed without a
|
||||
burn. The same float scaling could also put an exact-in buy's spend one raw
|
||||
unit below the configured amount.
|
||||
|
||||
Offline machine checks, no network and no funds moved. Buyer and seller run
|
||||
through the real pump.fun instruction builder; instruction data is decoded:
|
||||
|
||||
A. The chosen raw count really is one the float round trip loses.
|
||||
B. A landed buy reports the transaction's raw token count as `amount_raw`,
|
||||
also when the spend could not be read back.
|
||||
C. When the amounts cannot be parsed back, `amount_raw` is the wallet's raw
|
||||
balance, capped at what the buy asked for.
|
||||
D. The sell's `sell_v2` amount is `amount_raw` exactly when it is passed.
|
||||
E. Both exit paths in the trader hand the raw amount to the seller, and the
|
||||
tp/sl position keeps it.
|
||||
F. An exact-in buy spends the configured amount to the raw unit, and when
|
||||
the spend cannot be read back, prices the position from that amount.
|
||||
|
||||
Usage:
|
||||
uv run tests/regression/verify_exact_token_amounts.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import inspect
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT / "src"))
|
||||
|
||||
from solders.pubkey import Pubkey # noqa: E402
|
||||
|
||||
from core.pubkeys import WSOL_MINT, SystemAddresses # noqa: E402
|
||||
from interfaces.core import ConfirmationStatus, Platform, TokenInfo # noqa: E402
|
||||
from platforms.pumpfun.address_provider import ( # noqa: E402
|
||||
PumpFunAddresses,
|
||||
PumpFunAddressProvider,
|
||||
)
|
||||
from platforms.pumpfun.instruction_builder import PumpFunInstructionBuilder # noqa: E402
|
||||
from trading import platform_aware, universal_trader # noqa: E402
|
||||
from trading.platform_aware import PlatformAwareBuyer, PlatformAwareSeller # noqa: E402
|
||||
from utils.idl_manager import get_idl_manager # noqa: E402
|
||||
|
||||
TRADER = Pubkey.from_string("11111111111111111111111111111112")
|
||||
MINT = Pubkey.new_unique()
|
||||
PROVIDER = PumpFunAddressProvider()
|
||||
BUILDER = PumpFunInstructionBuilder(get_idl_manager().get_parser(Platform.PUMP_FUN))
|
||||
|
||||
PRICE = 0.00000004
|
||||
# A raw count the float round trip loses; check A proves it.
|
||||
LOSSY_RAW = 65_758_096_180
|
||||
EXPECTED_TOKENS = 50_000
|
||||
|
||||
|
||||
def _token_info() -> TokenInfo:
|
||||
bonding_curve = PROVIDER.derive_pool_address(MINT)
|
||||
return TokenInfo(
|
||||
name="T",
|
||||
symbol="T",
|
||||
uri="",
|
||||
mint=MINT,
|
||||
platform=Platform.PUMP_FUN,
|
||||
bonding_curve=bonding_curve,
|
||||
associated_bonding_curve=PROVIDER.derive_associated_bonding_curve(
|
||||
MINT, bonding_curve, SystemAddresses.TOKEN_2022_PROGRAM
|
||||
),
|
||||
user=TRADER,
|
||||
creator=TRADER,
|
||||
creator_vault=PROVIDER.derive_creator_vault(TRADER),
|
||||
token_program_id=SystemAddresses.TOKEN_2022_PROGRAM,
|
||||
quote_mint=WSOL_MINT,
|
||||
state_from_event=True,
|
||||
)
|
||||
|
||||
|
||||
class _Client:
|
||||
def __init__(self, *, tokens_raw=None, balance_raw=None, spent=1_000_000) -> None:
|
||||
self.sent: list = []
|
||||
self.tokens_raw = tokens_raw
|
||||
self.balance_raw = balance_raw
|
||||
self.spent = spent
|
||||
|
||||
async def build_and_send_transaction(self, instructions, *_a, **_k) -> str:
|
||||
self.sent.append(instructions)
|
||||
return "STUB_SIGNATURE"
|
||||
|
||||
async def confirm_transaction(self, *_a, **_k) -> bool:
|
||||
return True
|
||||
|
||||
async def confirm_transaction_detailed(self, *_a, **_k):
|
||||
return ConfirmationStatus.SUCCESS
|
||||
|
||||
async def get_buy_transaction_details(self, *_a, **_k):
|
||||
if self.tokens_raw is None:
|
||||
return None, None
|
||||
return self.tokens_raw, self.spent
|
||||
|
||||
async def get_token_account_balance(self, _ata) -> int:
|
||||
return self.balance_raw
|
||||
|
||||
async def get_sell_transaction_details(self, *_a, **_k):
|
||||
return None, None
|
||||
|
||||
|
||||
class _Curve:
|
||||
async def get_pool_state(self, _pool, commitment=None) -> dict: # noqa: ARG002
|
||||
return {
|
||||
"price_per_token": PRICE,
|
||||
"quote_mint": WSOL_MINT,
|
||||
"creator": str(TRADER),
|
||||
"is_mayhem_mode": False,
|
||||
"is_cashback_coin": False,
|
||||
}
|
||||
|
||||
|
||||
async def _no_fee(_accounts: list) -> None:
|
||||
return None
|
||||
|
||||
|
||||
def _wire(builder=BUILDER) -> None:
|
||||
platform_aware.get_platform_implementations = lambda _p, _c: SimpleNamespace(
|
||||
address_provider=PROVIDER,
|
||||
instruction_builder=builder,
|
||||
curve_manager=_Curve(),
|
||||
graduated_market=None,
|
||||
)
|
||||
|
||||
|
||||
def _wallet() -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
pubkey=TRADER,
|
||||
keypair=None,
|
||||
get_associated_token_address=lambda mint, program: Pubkey.find_program_address(
|
||||
[bytes(TRADER), bytes(program), bytes(mint)],
|
||||
SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
|
||||
)[0],
|
||||
)
|
||||
|
||||
|
||||
def _buy(client: _Client, *, amount: float = 0.002, builder=BUILDER):
|
||||
_wire(builder)
|
||||
buyer = PlatformAwareBuyer(
|
||||
client,
|
||||
_wallet(),
|
||||
SimpleNamespace(calculate_priority_fee=_no_fee),
|
||||
amount=amount,
|
||||
slippage=0.5,
|
||||
max_retries=1,
|
||||
extreme_fast_token_amount=EXPECTED_TOKENS,
|
||||
extreme_fast_mode=True,
|
||||
)
|
||||
return asyncio.run(buyer.execute(_token_info()))
|
||||
|
||||
|
||||
def check_a_round_trip_is_lossy() -> bool:
|
||||
"""A: the float round trip really drops a unit for this raw count."""
|
||||
rebuilt = int((LOSSY_RAW / 10**6) * 10**6)
|
||||
ok = rebuilt == LOSSY_RAW - 1
|
||||
if not ok:
|
||||
print(f" rebuilt {rebuilt}, expected {LOSSY_RAW - 1}")
|
||||
return ok
|
||||
|
||||
|
||||
def check_b_buy_reports_exact_raw() -> bool:
|
||||
"""B: amount_raw is the transaction's raw token count, spend read or not."""
|
||||
full = _buy(_Client(tokens_raw=LOSSY_RAW))
|
||||
# No spend and no balance: only the transaction's token count is left.
|
||||
no_spend = _buy(_Client(tokens_raw=LOSSY_RAW, spent=None, balance_raw=None))
|
||||
ok = (
|
||||
full.success
|
||||
and full.amount_raw == LOSSY_RAW
|
||||
and no_spend.amount_raw == LOSSY_RAW
|
||||
)
|
||||
if not ok:
|
||||
print(f" full={full.amount_raw} no_spend={no_spend.amount_raw}")
|
||||
return ok
|
||||
|
||||
|
||||
def check_c_balance_fallback_is_raw() -> bool:
|
||||
"""C: unparsed buy falls back to the raw balance, capped at the request."""
|
||||
held = 12_345_678_901 # below the 50,000 tokens asked for
|
||||
below = _buy(_Client(tokens_raw=None, balance_raw=held))
|
||||
above = _buy(_Client(tokens_raw=None, balance_raw=EXPECTED_TOKENS * 10**6 + 999))
|
||||
ok = below.amount_raw == held and above.amount_raw == EXPECTED_TOKENS * 10**6
|
||||
if not ok:
|
||||
print(f" below={below.amount_raw} above={above.amount_raw}")
|
||||
return ok
|
||||
|
||||
|
||||
def _sell_amount(**kwargs) -> int:
|
||||
_wire()
|
||||
client = _Client()
|
||||
seller = PlatformAwareSeller(
|
||||
client,
|
||||
_wallet(),
|
||||
SimpleNamespace(calculate_priority_fee=_no_fee),
|
||||
slippage=0.5,
|
||||
max_retries=1,
|
||||
)
|
||||
asyncio.run(seller.execute(_token_info(), token_price=PRICE, **kwargs))
|
||||
sell = next(
|
||||
ix for ix in client.sent[0] if ix.program_id == PumpFunAddresses.PROGRAM
|
||||
)
|
||||
return struct.unpack_from("<Q", bytes(sell.data), 8)[0]
|
||||
|
||||
|
||||
def check_d_sell_uses_exact_raw() -> bool:
|
||||
"""D: sell_v2 sells amount_raw exactly; without it, the old floor stays."""
|
||||
exact = _sell_amount(token_amount=LOSSY_RAW / 10**6, token_amount_raw=LOSSY_RAW)
|
||||
legacy = _sell_amount(token_amount=LOSSY_RAW / 10**6)
|
||||
ok = exact == LOSSY_RAW and legacy == LOSSY_RAW - 1
|
||||
if not ok:
|
||||
print(f" exact={exact} legacy={legacy}")
|
||||
return ok
|
||||
|
||||
|
||||
def check_e_trader_passes_raw() -> bool:
|
||||
"""E: both exit paths hand the raw amount on; the position keeps it."""
|
||||
source = inspect.getsource(universal_trader)
|
||||
calls = source.count("self.seller.execute(")
|
||||
with_raw = source.count("token_amount_raw=buy_result.amount_raw") + source.count(
|
||||
"token_amount_raw=position.quantity_raw"
|
||||
)
|
||||
keeps = "quantity_raw=buy_result.amount_raw" in source
|
||||
ok = calls == 2 and with_raw == 2 and keeps
|
||||
if not ok:
|
||||
print(f" seller calls={calls} passing raw={with_raw} position keeps={keeps}")
|
||||
return ok
|
||||
|
||||
|
||||
class _ExactInBuilder:
|
||||
"""Stands in for LaunchLab's builder: exact-in, records what it is asked to spend."""
|
||||
|
||||
spends_exact_amount_in = True
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.amount_in: int | None = None
|
||||
|
||||
async def build_buy_instruction(self, _info, _user, amount_in, _min_out, _provider):
|
||||
self.amount_in = amount_in
|
||||
return []
|
||||
|
||||
def get_required_accounts_for_buy(self, *_a) -> list:
|
||||
return []
|
||||
|
||||
def get_buy_compute_unit_limit(self, *_a) -> int:
|
||||
return 100_000
|
||||
|
||||
|
||||
def check_f_exact_in_spend() -> bool:
|
||||
"""F: an exact-in buy spends the configured amount, and prices from it."""
|
||||
amount = 0.00013 # 130,000 lamports; as a float product, 129,999
|
||||
builder = _ExactInBuilder()
|
||||
_buy(_Client(tokens_raw=1), amount=amount, builder=builder)
|
||||
no_spend = _buy(
|
||||
_Client(tokens_raw=LOSSY_RAW, spent=None),
|
||||
amount=amount,
|
||||
builder=_ExactInBuilder(),
|
||||
)
|
||||
# Without the spend, the price must pair the submitted amount with the
|
||||
# tokens received, not keep the pre-trade estimate.
|
||||
price = (130_000 / 10**9) / (LOSSY_RAW / 10**6)
|
||||
ok = (
|
||||
int(amount * 10**9) == 129_999
|
||||
and builder.amount_in == 130_000
|
||||
and abs(no_spend.price - price) < price * 1e-9
|
||||
)
|
||||
if not ok:
|
||||
print(
|
||||
f" float={int(amount * 10**9)} spent={builder.amount_in} "
|
||||
f"price={no_spend.price} expected={price}"
|
||||
)
|
||||
return ok
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Run every check and report."""
|
||||
checks = [
|
||||
(
|
||||
"A: the chosen raw count is lossy through a float",
|
||||
check_a_round_trip_is_lossy,
|
||||
),
|
||||
("B: a landed buy reports the exact raw count", check_b_buy_reports_exact_raw),
|
||||
(
|
||||
"C: the balance fallback reports a raw count",
|
||||
check_c_balance_fallback_is_raw,
|
||||
),
|
||||
("D: the sell uses the exact raw count", check_d_sell_uses_exact_raw),
|
||||
("E: the trader passes the raw count to every sell", check_e_trader_passes_raw),
|
||||
(
|
||||
"F: an exact-in buy spends and prices from its amount",
|
||||
check_f_exact_in_spend,
|
||||
),
|
||||
]
|
||||
passed = 0
|
||||
for label, check in checks:
|
||||
ok = check()
|
||||
print(f"{'PASS' if ok else 'FAIL'} {label}")
|
||||
passed += ok
|
||||
print(f"\n{passed}/{len(checks)} checks passed")
|
||||
return 0 if passed == len(checks) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -93,7 +93,11 @@ class StubSeller:
|
||||
self.attempts = 0
|
||||
|
||||
async def execute(
|
||||
self, token_info: TokenInfo, token_amount: float, token_price: float
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
token_amount: float,
|
||||
token_price: float,
|
||||
token_amount_raw: int | None = None,
|
||||
) -> TradeResult:
|
||||
"""Return the next scripted sell outcome.
|
||||
|
||||
|
||||
@@ -87,7 +87,11 @@ class StubSeller:
|
||||
prices_seen: list[float] = field(default_factory=list)
|
||||
|
||||
async def execute(
|
||||
self, token_info: TokenInfo, token_amount: float, token_price: float
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
token_amount: float,
|
||||
token_price: float,
|
||||
token_amount_raw: int | None = None,
|
||||
) -> TradeResult:
|
||||
self.prices_seen.append(token_price)
|
||||
if len(self.prices_seen) <= self.fail_first:
|
||||
|
||||
@@ -98,7 +98,11 @@ class RecordingSeller:
|
||||
self.prices_seen: list[float] = []
|
||||
|
||||
async def execute(
|
||||
self, token_info: TokenInfo, token_amount: float, token_price: float
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
token_amount: float,
|
||||
token_price: float,
|
||||
token_amount_raw: int | None = None,
|
||||
) -> TradeResult:
|
||||
"""Record the attempt and return the scripted outcome.
|
||||
|
||||
|
||||
@@ -89,7 +89,11 @@ class StubSeller:
|
||||
prices_seen: list[float] = field(default_factory=list)
|
||||
|
||||
async def execute(
|
||||
self, token_info: TokenInfo, token_amount: float, token_price: float
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
token_amount: float,
|
||||
token_price: float,
|
||||
token_amount_raw: int | None = None,
|
||||
) -> TradeResult:
|
||||
self.prices_seen.append(token_price)
|
||||
if len(self.prices_seen) <= self.fail_first:
|
||||
|
||||
Reference in New Issue
Block a user