Liquidez en Stellar: SDEX y Fondos de Liquidez
Esta sección está específicamente dirigida a la liquidez respecto al AMM y SDEX incorporados en el protocolo Stellar y no incluye información sobre contratos inteligentes.
Los usuarios pueden negociar y convertir activos en la red Stellar mediante el uso de pagos por rutas a través del exchange descentralizado de Stellar y los fondos de liquidez.
En esta sección, hablaremos sobre el SDEX y los fondos de liquidez. Para aprender cómo funcionan juntos para ejecutar transacciones, consulta nuestra Entrada de Enciclopedia de Pagos por Rutas.
SDEX
La red Stellar actúa como un exchange descentralizado y distribuido que permite a los usuarios negociar y convertir activos mediante las operaciones de Gestionar Oferta de Compra y Gestionar Oferta de Venta. El ledger de Stellar almacena tanto los saldos mantenidos por las cuentas de usuario como las órdenes que crean las cuentas de usuario para comprar o vender activos.
Libros de órdenes
Stellar utiliza libros de órdenes para operar su exchange descentralizado.
Un libro de órdenes es un registro de órdenes pendientes en una red, y cada registro se encuentra entre dos activos (trigo y ovejas, por ejemplo). El libro de órdenes para este par de activos registra cada cuenta que quiere vender trigo por ovejas y cada cuenta que quiere vender ovejas por trigo. En finanzas tradicionales, la compra se expresa como una orden de «oferta» y la venta se expresa como una orden de «demanda» (las órdenes de demanda también se llaman ofertas).
Un par de notas sobre los libros de órdenes en Stellar:
- El término “ofertas” generalmente se refiere específicamente a las órdenes de demanda. En Stellar, sin embargo, todas las órdenes se almacenan como ventas, es decir, el sistema convierte automáticamente las órdenes de oferta en órdenes de demanda. Debido a esto, los términos “oferta” y “orden” se utilizan de manera intercambiable en el ecosistema de Stellar.
- Los libros de órdenes contienen todas las órdenes que son aceptables para las partes de ambos lados para realizar un intercambio.
- Algunos activos tendrán un libro de órdenes pequeño o inexistente entre ellos. En estos casos, Stellar facilita pagos por rutas, de los cuales hablaremos más adelante.
Para ver un gráfico de libro de órdenes, consulta la Página de Wikipedia sobre Libros de Órdenes. Además, hay muchos tutoriales en video y artículos que pueden ayudarte a entender cómo funcionan los libros de órdenes con mayor detalle.
Órdenes
Una cuenta puede crear órdenes para comprar o vender activos utilizando las operaciones Gestionar Oferta de Compra, Gestionar Oferta de Venta o Orden Pasiva. La cuenta debe poseer el activo que desea intercambiar y debe confiar en el emisor del activo que está tratando de comprar.
Las órdenes en Stellar se comportan como órdenes limitadas en los mercados tradicionales. Cuando una cuenta inicia una orden, se verifica en relación con el libro de órdenes existente para ese par de activos. Si la orden enviada es una orden comercial (para una orden de compra comercial, el precio límite está a o por encima del precio de venta; para una orden de venta comercial, el precio límite está a o por debajo del precio de oferta), se llena al precio de orden existente por la cantidad disponible a ese precio. Si la orden no es comercial (es decir, no cruza una orden existente), la orden se guarda en el libro de órdenes hasta que sea consumida por otra orden, consumida por un pago por ruta, o cancelada por la cuenta que creó la orden.
Cada orden constituye una obligación de venta para el activo que se vende y una obligación de compra para el activo que se compra. Estas obligaciones se almacenan en la cuenta (para lumens) o en la línea de confianza (para otros activos) que posee la cuenta creando la orden. Cualquier operación que cause que una cuenta no pueda cumplir sus obligaciones, como enviar demasiado saldo, fallará. Esto garantiza que cualquier orden en el libro de órdenes se pueda ejecutar por completo.
Las órdenes se ejecutan bajo la prioridad de precio y tiempo, lo que significa que las órdenes se ejecutarán primero por precio; para las órdenes colocadas al mismo precio, se da prioridad a la orden que se ingresó primero y se ejecuta antes que la más nueva.
Precio y operaciones
Cada orden en Stellar se cotiza con un precio asociado y se representa como una proporción de los dos activos en la orden, uno siendo el “activo de cotización” y el otro el “activo base”. Esto es para asegurar que no haya pérdida de precisión al representar el precio de la orden (en oposición a almacenar la fracción como un número de punto flotante).
Los precios se especifican como un par denominador con ambos componentes de la fracción representados como enteros con signo de 32 bits. El numerador se considera el activo base, y el denominador se considera el activo de cotización. Al expresar un precio de “Activo A en términos de Activo B”, la cantidad de B es el denominador (y por lo tanto el activo de cotización) y A es el numerador (y por lo tanto el activo base). Como una buena regla general, es generalmente correcto pensar en el activo base que se está comprando/vendiendo (en términos del activo de cotización).
Gestionar Oferta de Compra
Al crear una orden de compra en Stellar a través de la operación Gestionar Oferta de Compra, el precio se especifica como 1 unidad de la moneda base (el activo que se compra), en términos del activo de cotización (el activo que se está vendiendo). Por ejemplo, si estás comprando 100 XLM a cambio de 20 USD, especificarías el precio como 100, que equivaldría a 5 XLM por 1 USD (o $.20 por XLM).
Gestionar Oferta de Venta
Al crear una orden de venta en Stellar a través de la operación Gestionar Oferta de Venta, el precio se especifica como 1 unidad de la moneda base (el activo que se vende), en términos del activo de cotización (el activo que se está comprando). Por ejemplo, si estás vendiendo 100 XLM a cambio de 40 USD, especificarías el precio como 100, que equivaldría a 2,5 XLM por 1 USD (o $.40 por XLM).
Orden Pasiva
Las órdenes pasivas permiten que los mercados tengan un diferencial cero. Si deseas intercambiar USD del ancla A por USD del ancla B a un precio de 1:1, puedes crear dos órdenes pasivas para que las dos órdenes no se llenen entre sí.
Una orden pasiva es una orden que no se ejecuta contra una orden de contraparte comercial con el mismo precio. Solo se llenará si los precios no son iguales. Por ejemplo, si la mejor orden para comprar BTC por XLM tiene un precio de 100XLM/BTC, y haces una oferta pasiva para vender BTC a 100XLM/BTC, tu oferta pasiva no toma esa oferta existente. Si en su lugar haces una oferta pasiva para vender BTC a 99XLM/BTC, esta cruzaría la oferta existente y se llenaría a 100XLM/BTC.
Una cuenta puede realizar una orden de venta pasiva a través de la operación Crear Oferta de Venta Pasiva.
Comisiones
El precio de orden que establezcas es independiente de la tarifa que pagas por presentar esa orden en una transacción. Las tarifas siempre se pagan en XLM, y las especificas como un parámetro separado al presentar la orden a la red.
Para aprender más sobre las tarifas de transacción, consulta nuestra sección sobre Tarifas.
Fondos de liquidez
Los fondos de liquidez permiten la creación automática de mercados en la red Stellar. La liquidez se refiere a cuán fácilmente y de manera económica se puede convertir un activo en otro.
Creadores de Mercados Automatizados (AMMs)
En lugar de depender de las órdenes de compra y venta de exchanges descentralizados, los AMMs mantienen activos en un ecosistema líquido 24/7 utilizando fondos de liquidez.
Los creadores de mercados automatizados proporcionan liquidez utilizando una ecuación matemática. Los AMMs mantienen dos activos diferentes en un fondo de liquidez, y las cantidades de esos activos (o reservas) son entradas para esa ecuación (Activo A * Activo B = k). Si un AMM mantiene más de los activos de reserva, los precios de los activos se moverán menos en respuesta a un intercambio.
Precios de AMM
Los AMMs están dispuestos a realizar algunas operaciones y no están dispuestos a realizar otras. Por ejemplo, si 1 EUR = 1.17 USD, entonces el AMM podría estar dispuesto a vender 1 EUR por 1.18 USD y no estar dispuesto a vender 1 EUR por 1.16 USD. Para determinar qué operaciones son aceptables, el AMM hace cumplir un invariante. Hay muchos invariantes posibles, y Stellar hace cumplir un invariante de producto constante, por lo que se conoce como un creador de mercados de producto constante. Esto significa que los AMMs en Stellar nunca deben permitir que el producto de las reservas disminuya.
Por ejemplo, supongamos que las reservas actuales en el fondo de liquidez son 1000 EUR y 1170 USD, lo que implica un producto de 1.170.000. Vender 1 EUR por 1.18 USD sería aceptable porque eso dejaría reservas de 999 EUR y 1171.18 USD, lo que implica un producto de 1.170.008,82. Pero vender 1 EUR por 1.16 USD no sería aceptable porque eso dejaría reservas de 999 EUR y 1171.16 USD, lo que implica un producto de 1.169.988,84.
Los AMMs deciden tasas de cambio en base a la proporción de reservas en el fondo de liquidez. Si esta proporción es diferente a la verdadera tasa de cambio, los arbitrajistas intervendrán y comerciarán con el AMM a un precio favorable. Este comercio de arbitraje mueve la proporción de las reservas de regreso hacia la verdadera tasa de cambio.
Los AMMs cobran tarifas en cada operación, que son un porcentaje fijo del monto comprado por el AMM. Por ejemplo, si un creador de mercados automatizado vende 100 EUR por 118 USD, entonces la tarifa se cobra sobre los USD. La tarifa es de 30 bps, que equivale a 0,30%. Si realmente quisieras realizar este exchange, necesitarías pagar alrededor de 118,355 USD por 100 EUR. El creador de mercados automatizado tiene en cuenta las tarifas en el invariante de producto constante, por lo que en realidad el producto de las reservas crece después de cada operación.
Participación en el fondo de liquidez
Cualquier participante elegible puede depositar activos en un fondo de liquidez y, a cambio, recibirá acciones del fondo que representan su propiedad de ese activo. Si hay 150 acciones totales del fondo y un usuario posee 30, tiene derecho a retirar el 20% del activo del fondo de liquidez en cualquier momento.
Las acciones del fondo son similares a otros activos en Stellar, pero no pueden ser transferidas. Solo puedes aumentar el número de acciones del fondo que posees depositando en un fondo de liquidez con el LiquidityPoolDespositOp
y disminuir el número de acciones del fondo que posees retirándote de un fondo de liquidez con LiquidityPoolWithdrawOp
.
Una acción del fondo tiene dos representaciones. La representación completa se utiliza con ChangeTrustOp
y la representación hash se utiliza en todos los demás casos. Al construir la representación del activo de una acción del fondo, los activos deben estar en orden lexicográfico. Por ejemplo, A-B está en el orden correcto, pero B-A no. Esto resulta en una representación canónica de una acción del fondo.
Los AMMs cobran una tarifa en todas las operaciones y los participantes en el fondo de liquidez reciben una parte de la tarifa proporcional a su participación de los activos en el fondo de liquidez. Los participantes recogen estas tarifas cuando retiran sus activos del fondo. La tasa de tarifa en Stellar es 30 bps, que equivale a 0,30%. Estas tarifas son completamente independientes de las tarifas de la red.
Líneas de confianza
Los usuarios necesitan establecer líneas de confianza para tres activos diferentes para participar en un fondo de liquidez: ambos activos de reserva (a menos que uno de ellos sea XLM) y la acción del fondo.
Una cuenta necesita una línea de confianza para cada acción del fondo que desea poseer. No es posible depositar en un fondo de liquidez sin una línea de confianza para la acción correspondiente del fondo. Las líneas de confianza de acciones del fondo difieren de las líneas de confianza de otros activos en algunas maneras:
- Una línea de confianza para acciones del fondo no puede ser creada a menos que la cuenta ya tenga líneas de confianza que estén autorizadas o autorizadas para mantener obligaciones por los activos en el fondo de liquidez. Consulta a continuación para más información sobre cómo la autorización impacta las líneas de confianza de acciones del fondo.
- Una línea de confianza para acciones del fondo requiere 2 reservas base en vez de 1. Por ejemplo, una cuenta (2 reservas base) con una línea de confianza para el activo A (1 reserva base), una línea de confianza para el activo B (1 reserva base), y una línea de confianza para la acción A-B (2 reservas base) tendría un requerimiento de reserva de 6 reservas base.
Autorización
Las líneas de confianza de acciones del fondo no pueden ser autorizadas o desautorizadas de forma independiente. En cambio, la autorización de una línea de confianza de acciones del fondo se deriva de las líneas de confianza para los activos en el fondo de liquidez. Este diseño es necesario porque un fondo de liquidez puede contener activos de dos emisores diferentes, y ambos emisores deberían tener voz sobre si la línea de confianza de acciones del fondo es autorizada o no.
Hay algunas posibilidades con respecto a la autorización. El comportamiento de la línea de confianza de acciones A-B se determina de acuerdo con la siguiente tabla:
ESCENARIO | COMPORTAMIENTO |
---|---|
Las líneas de confianza para A y B están completamente autorizadas | Sin restricciones en el depósito y retirada |
La línea de confianza para A está completamente autorizada, pero la línea de confianza para B está autorizada para mantener obligaciones | Las líneas de confianza para A y B están autorizadas para mantener obligaciones |
La línea de confianza para B está completamente autorizada, pero la línea de confianza para A está autorizada para mantener obligaciones | Las líneas de confianza para A y B están autorizadas para mantener obligaciones |
Las líneas de confianza para A y B están autorizadas para mantener obligaciones | Las líneas de confianza para A y B están autorizadas para mantener obligaciones |
La línea de confianza para A no está autorizada o no existe | La línea de confianza de acciones del fondo no existe |
La línea de confianza para B no está autorizada o no existe | La línea de confianza de acciones del fondo no existe |
Si el emisor de A o B revoca la autorización, entonces la cuenta se retirará automáticamente de todos los fondos de liquidez que contengan ese activo y esas líneas de confianza de acciones del fondo se eliminarán. Decimos que estas acciones del fondo han sido canjeadas. Por ejemplo, si la cuenta participa en los fondos de liquidez A-B, A-C y B-C, y el emisor de A revoca la autorización, entonces la cuenta canjeará de A-B y A-C pero no de B-C. Por cada línea de confianza de acción del fondo canjeada, se creará un Balance Reclamable para cada activo contenido en el fondo si hay un saldo que se está retirando y el reclamante no es el emisor de ese activo. El reclamante del Balance Reclamable será el propietario de la línea de confianza de acción del fondo eliminada, y el patrocinador del Balance Reclamable será el patrocinador de la línea de confianza de acción del fondo eliminada. El BalanceID de cada Balance Reclamable es el hash SHA-256 del revokeID
.
Operaciones
Hay dos operaciones que facilitan la participación en un fondo de liquidez: LiquidityPoolDeposit
y LiquidityPoolWithdraw
. Utiliza LiquidityPoolDeposit
para comenzar a proporcionar liquidez al mercado. Utiliza LiquidityPoolWithdraw
para dejar de proporcionar liquidez al mercado.
Sin embargo, los usuarios no necesitan participar en el fondo para aprovechar lo que ofrece: una forma fácil de intercambiar dos activos. Para eso, solo usa PathPaymentStrictReceive
o PathPaymentStrictSend
. Si tu aplicación ya está utilizando pagos por rutas, entonces no necesitas cambiar nada para que los usuarios aprovechen los precios disponibles en los fondos de liquidez.
Ejemplos
Aquí cubriremos la participación básica en fondos de liquidez y la consulta.
Prólogo
Para todos los ejemplos siguientes, estaremos trabajando con tres cuentas financiadas de Testnet. Si deseas seguir, genera algunos keypairs y financíalos a través del friendbot.
El siguiente código configura las cuentas y define algunas funciones auxiliares. Estos deberían ser familiares si has experimentado con otros ejemplos como los clawbacks.
- JavaScript
- Python
const sdk = require("stellar-sdk");
const BigNumber = require("bignumber.js");
let server = new sdk.Server("https://horizon-testnet.stellar.org");
/// Helps simplify creating & signing a transaction.
function buildTx(source, signer, ...ops) {
let tx = new sdk.TransactionBuilder(source, {
fee: sdk.BASE_FEE,
networkPassphrase: sdk.Networks.TESTNET,
});
ops.forEach((op) => tx.addOperation(op));
tx = tx.setTimeout(30).build();
tx.sign(signer);
return tx;
}
/// Returns the given asset pair in "protocol order."
function orderAssets(A, B) {
return sdk.Asset.compare(A, B) <= 0 ? [A, B] : [B, A];
}
/// Returns all of the accounts we'll be using.
function getAccounts() {
return Promise.all(kps.map((kp) => server.loadAccount(kp.publicKey())));
}
const kps = [
"SBGCD73TK2PTW2DQNWUYZSTCTHHVJPL4GZF3GVZMCDL6GYETYNAYOADN",
"SAAQFHI2FMSIC6OFPWZ3PDIIX3OF64RS3EB52VLYYZBX6GYB54TW3Q4U",
"SCJWYFTBDMDPAABHVJZE3DRMBRTEH4AIC5YUM54QGW57NUBM2XX6433P",
].map((s) => sdk.Keypair.fromSecret(s));
// kp0 issues the assets
const kp0 = kps[0];
const [A, B] = orderAssets(
...[new sdk.Asset("A", kp0.publicKey()), new sdk.Asset("B", kp0.publicKey())],
);
/// Establishes trustlines and funds `recipientKps` for all `assets`.
function distributeAssets(issuerKp, recipientKps, ...assets) {
return server.loadAccount(issuerKp.publicKey()).then((issuer) => {
const ops = recipientKps
.map((recipientKp) =>
assets.map((asset) => [
sdk.Operation.changeTrust({
source: recipientKp.publicKey(),
limit: "100000",
asset: asset,
}),
sdk.Operation.payment({
source: issuerKp.publicKey(),
destination: recipientKp.publicKey(),
amount: "100000",
asset: asset,
}),
]),
)
.flat(2);
let tx = buildTx(issuer, issuerKp, ...ops);
tx.sign(...recipientKps);
return server.submitTransaction(tx);
});
}
function preamble() {
return distributeAssets(kp0, [kps[1], kps[2]], A, B);
}
from decimal import Decimal
from typing import List, Any, Dict
from stellar_sdk import *
server = Server("https://horizon-testnet.stellar.org")
# Preamble
def new_tx_builder(source: str) -> TransactionBuilder:
network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE
base_fee = 100
source_account = server.load_account(source)
builder = TransactionBuilder(
source_account=source_account, network_passphrase=network_passphrase, base_fee=base_fee
).set_timeout(30)
return builder
# Returns the given asset pair in "protocol order."
def order_asset(a: Asset, b: Asset) -> List[Asset]:
return [a, b] if LiquidityPoolAsset.is_valid_lexicographic_order(a, b) else [b, a]
secrets = [
"SBGCD73TK2PTW2DQNWUYZSTCTHHVJPL4GZF3GVZMCDL6GYETYNAYOADN",
"SAAQFHI2FMSIC6OFPWZ3PDIIX3OF64RS3EB52VLYYZBX6GYB54TW3Q4U",
"SCJWYFTBDMDPAABHVJZE3DRMBRTEH4AIC5YUM54QGW57NUBM2XX6433P",
]
kps = [Keypair.from_secret(secret=secret) for secret in secrets]
# kp0 issues the assets
kp0 = kps[0]
asset_a, asset_b = order_asset(Asset("A", kp0.public_key), Asset("B", kp0.public_key))
def distribute_assets(
issuer_kp: Keypair, recipient_kp: Keypair, assets: List[Asset]
) -> Dict[str, Any]:
builder = new_tx_builder(issuer_kp.public_key)
for asset in assets:
builder.append_change_trust_op(
asset=asset, limit="100000", source=recipient_kp.public_key
).append_payment_op(
destination=recipient_kp.public_key,
asset=asset,
amount="100000",
source=issuer_kp.public_key,
)
tx = builder.build()
tx.sign(issuer_kp)
tx.sign(recipient_kp)
resp = server.submit_transaction(tx)
return resp
def preamble() -> None:
resp1 = distribute_assets(kp0, kps[1], [asset_a, asset_b])
resp2 = distribute_assets(kp0, kps[2], [asset_a, asset_b])
# ...
Aquí, utilizamos distributeAssets()
para establecer líneas de confianza y configurar saldos iniciales de dos activos personalizados (A
y B
, emitidos por kp0
) para dos cuentas (kp2
y kp3
). Para que alguien participe en el fondo, debe establecer líneas de confianza con cada uno de los emisores de activos y con el activo de la acción del fondo (explicado a continuación).
Nota la función auxiliar orderAssets()
aquí. Las operaciones relacionadas con los fondos de liquidez se refieren arbitrariamente al par de activos como A
y B
; sin embargo, deben ser “ordenados” de modo que A
< B
. Este orden se define por el protocolo, pero sus detalles no deberían ser relevantes (si tienes curiosidad, está ordenado lexicográficamente por tipo de activo, código y luego emisor). Podemos utilizar los métodos de comparación integrados en los SDK (como Asset.compare
) para asegurarnos de pasarlos en el orden correcto y evitar errores.
Participación: Creación
Primero, vamos a crear un fondo de liquidez para el par de activos definido en el prólogo. Esto implica establecer una línea de confianza hacia el fondo en sí:
- JavaScript
- Python
const poolShareAsset = new sdk.LiquidityPoolAsset(
A,
B,
sdk.LiquidityPoolFeeV18,
);
function establishPoolTrustline(account, keypair, poolAsset) {
return server.submitTransaction(
buildTx(
account,
keypair,
sdk.Operation.changeTrust({
asset: poolAsset,
limit: "100000",
}),
),
);
}
pool_share_asset = LiquidityPoolAsset(asset_a=asset_a, asset_b=asset_b)
def establish_pool_trustline(source: Keypair, pool_asset: LiquidityPoolAsset) -> Dict[str, Any]:
tx = (
new_tx_builder(source.public_key)
.append_change_trust_op(asset=pool_asset, limit="100000")
.build()
)
tx.sign(source)
return server.submit_transaction(tx)
Esto permite a los participantes mantener acciones del fondo, lo que significa que ahora pueden realizar depósitos y retiradas.
Participación: Depósitos
Para trabajar con un fondo de liquidez, necesitas conocer su ID de antemano. Es un valor determinista, y solo puede existir un único fondo de liquidez para un par de activos particular, por lo que puedes calcularlo localmente a partir de los parámetros del fondo.
- JavaScript
- Python
const poolId = sdk
.getLiquidityPoolId(
"constant_product",
poolShareAsset.getLiquidityPoolParameters(),
)
.toString("hex");
function addLiquidity(source, signer, poolId, maxReserveA, maxReserveB) {
const exactPrice = maxReserveA / maxReserveB;
const minPrice = exactPrice - exactPrice * 0.1;
const maxPrice = exactPrice + exactPrice * 0.1;
return server.submitTransaction(
buildTx(
source,
signer,
sdk.Operation.liquidityPoolDeposit({
liquidityPoolId: poolId,
maxAmountA: maxReserveA,
maxAmountB: maxReserveB,
minPrice: minPrice.toFixed(7),
maxPrice: maxPrice.toFixed(7),
}),
),
);
}
pool_id = pool_share_asset.liquidity_pool_id
def add_liquidity(
source: Keypair,
pool_id: str,
max_reserve_a: Decimal,
max_reserve_b: Decimal,
) -> dict[str, Any]:
exact_price = max_reserve_a / max_reserve_b
min_price = exact_price - exact_price * Decimal("0.1")
max_price = exact_price + exact_price * Decimal("0.1")
tx = (
new_tx_builder(source.public_key)
.append_liquidity_pool_deposit_op(
liquidity_pool_id=pool_id,
max_amount_a=f"{max_reserve_a:.7f}",
max_amount_b=f"{max_reserve_b:.7f}",
min_price=min_price,
max_price=max_price,
)
.build()
)
tx.sign(source)
return server.submit_transaction(tx)
Al depositar activos en un fondo de liquidez, necesitas definir tus límites de precio aceptables. En la función anterior, permitimos un margen de error de +/-10% respecto al “precio de mercado”. Este margen no es en modo alguno una recomendación y se elige solo para demostración.
Ten en cuenta que también especificamos el monto máximo de cada reserva que estamos dispuestos a depositar. Esto, junto con los precios mínimos y máximos, ayuda a definir límites para el depósito, ya que siempre puede haber un cambio en la tasa de cambio entre el envío de la operación y su aceptación por la red.
Participación: Retiros
Si posees acciones de un fondo particular, puedes retirar reservas de él. La estructura de la operación refleja de cerca el depósito:
- JavaScript
- Python
function removeLiquidity(source, signer, poolId, sharesAmount) {
return server
.liquidityPools()
.liquidityPoolId(poolId)
.call()
.then((poolInfo) => {
let totalShares = poolInfo.total_shares;
let minReserveA =
(sharesAmount / totalShares) * poolInfo.reserves[0].amount * 0.95;
let minReserveB =
(sharesAmount / totalShares) * poolInfo.reserves[1].amount * 0.95;
return server.submitTransaction(
buildTx(
source,
signer,
sdk.Operation.liquidityPoolWithdraw({
liquidityPoolId: poolId,
amount: sharesAmount,
minAmountA: minReserveA.toFixed(7),
minAmountB: minReserveB.toFixed(7),
}),
),
);
});
}
def remove_liquidity(
source: Keypair, pool_id: str, shares_amount: Decimal
) -> dict[str, Any]:
pool_info = server.liquidity_pools().liquidity_pool(pool_id).call()
total_shares = Decimal(pool_info["total_shares"])
min_reserve_a = (
shares_amount
/ total_shares
* Decimal(pool_info["reserves"][0]["amount"])
* Decimal("0.95")
) #
min_reserve_b = (
shares_amount
/ total_shares
* Decimal(pool_info["reserves"][1]["amount"])
* Decimal("0.95")
)
tx = (
new_tx_builder(source.public_key)
.append_liquidity_pool_withdraw_op(
liquidity_pool_id=pool_id,
amount=f"{shares_amount:.7f}",
min_amount_a=f"{min_reserve_a:.7f}",
min_amount_b=f"{min_reserve_b:.7f}",
)
.build()
)
tx.sign(source)
return server.submit_transaction(tx)
Notar aquí que especificamos el monto mínimo. Al igual que con un pago de ruta de recepción estricta, especificamos que no estamos dispuestos a recibir menos de esta cantidad de cada activo del fondo. Esto define efectivamente un precio mínimo de retiro.
Uniendo todo
Finalmente, podemos combinar estas piezas para simular alguna participación en un fondo de liquidez. Haremos que todos depositen montos crecientes en el fondo, luego un participante retira sus acciones. Entre cada paso, recuperaremos el precio de mercado.
- JavaScript
- Python
function main() {
return getAccounts()
.then((accounts) => {
return Promise.all(
kps.map((kp, i) => {
const acc = accounts[i];
const depositA = ((i + 1) * 1000).toString();
const depositB = ((i + 1) * 3000).toString(); // maintain a 1:3 ratio
return establishPoolTrustline(acc, kp, poolShareAsset)
.then(() => addLiquidity(acc, kp, poolId, depositA, depositB))
.then(() => getSpotPrice());
}),
).then(() => accounts);
})
.then((accounts) => {
// kp1 takes all his/her shares out
return server
.accounts()
.accountId(kps[1].publicKey())
.call()
.then(({ balances }) => {
let balance = 0;
balances.every((bal) => {
if (
bal.asset_type === "liquidity_pool_shares" &&
bal.liquidity_pool_id === poolId
) {
balance = bal.balance;
return false;
}
return true;
});
return balance;
})
.then((balance) =>
removeLiquidity(accounts[1], kps[1], poolId, balance),
);
})
.then(() => getSpotPrice());
}
function getSpotPrice() {
return server
.liquidityPools()
.liquidityPoolId(poolId)
.call()
.then((pool) => {
const [a, b] = pool.reserves.map((r) => r.amount);
const spotPrice = new BigNumber(a).div(b);
console.log(`Price: ${a}/${b} = ${spotPrice.toFormat(2)}`);
});
}
preamble().then(main);
def main():
deposit_a = Decimal(1000)
deposit_b = Decimal(3000) # maintain a 1:3 ratio
establish_pool_trustline(kps[1], pool_share_asset)
add_liquidity(kps[1], pool_id, deposit_a, deposit_b)
get_spot_price()
deposit_a = Decimal(2000)
deposit_b = Decimal(6000) # maintain a 1:3 ratio
establish_pool_trustline(kps[2], pool_share_asset)
add_liquidity(kps[2], pool_id, deposit_a, deposit_b)
get_spot_price()
# kp1 takes all his/her shares out
balance = 0
for b in server.accounts().account_id(kps[1].public_key).call()["balances"]:
if (
b["asset_type"] == "liquidity_pool_shares"
and b["liquidity_pool_id"] == pool_id
):
balance = Decimal(b["balance"])
break
if not balance:
raise
remove_liquidity(kps[1], pool_id, balance)
get_spot_price()
def get_spot_price():
resp = server.liquidity_pools().liquidity_pool(pool_id).call()
amount_a = resp["reserves"][0]["amount"]
amount_b = resp["reserves"][1]["amount"]
spot_price = Decimal(amount_a) / Decimal(amount_b)
print(f"Price: {amount_a}/{amount_b} = {spot_price:.7f}")
if __name__ == '__main__':
preamble()
main()
Observando la actividad del fondo de liquidez
Puedes acceder a las transacciones, operaciones y efectos relacionados con un fondo de liquidez si deseas rastrear su actividad. Veamos cómo podemos rastrear los últimos depósitos en un fondo (supongamos que poolId
se define como antes):
- JavaScript
- Python
server
.operations()
.forLiquidityPool(poolId)
.call()
.then((ops) => {
ops.records
.filter((op) => op.type == "liquidity_pool_deposit")
.forEach((op) => {
console.log("Reserves deposited:");
op.reserves_deposited.forEach((r) =>
console.log(` ${r.amount} of ${r.asset}`),
);
console.log(" for pool shares: ", op.shares_received);
});
});
def watch_liquidity_pool_activity():
for op in (
server.operations()
.for_liquidity_pool(liquidity_pool_id=pool_id)
.cursor("now")
.stream()
):
if op["type"] == "liquidity_pool_deposit":
print("Reserves deposited:")
for r in op["reserves_deposited"]:
print(f" {r['amount']} of {r['asset']}")
print(f" for pool shares: {op['shares_received']}")
# ...