CounterpartyXCP / counterparty-core

Counterparty Protocol Reference Implementation
http://counterparty.io
MIT License
282 stars 206 forks source link

Market Price Logic #2152

Open droplister opened 1 month ago

droplister commented 1 month ago

I'm really excited about the inclusion of a market price in the order match api, but I wonder if the logic needs to be adjusted a bit. For the below order match I would expect the market price to be 0.0001135 and not 8810.57270000 for XCP/BTC.

Working with trade data from the dex, I find it's helpful to do something similar to what counterblock used to do, where you handle asset pairs as base and quote asset to make sure you're getting results priced in the quote asset.

Or if this is working consistently as intended, maybe update the API documents to indicate that asset1 is the quote asset.

https://api.counterparty.info/v2/orders/XCP/BTC/matches?status=completed&verbose=true

{
"id": "055950833596708871d59e62a5f84b9a85c7a8deff50e646194bf021f3f06143_ce3e3b758d010098d3010da7d7b4b92f1188278505f3819472906910e6879efd",
"tx0_index": 2742952,
"tx0_hash": "055950833596708871d59e62a5f84b9a85c7a8deff50e646194bf021f3f06143",
"tx0_address": "16mrwziWRBYhSPcWdJkaCvBuawXosLnTeG",
"tx1_index": 2743003,
"tx1_hash": "ce3e3b758d010098d3010da7d7b4b92f1188278505f3819472906910e6879efd",
"tx1_address": "bc1qvuqzfsaknpt2655kx7fe0hwwyqee6mushzpp87",
"forward_asset": "XCP",
"forward_quantity": 30000000000,
"backward_asset": "BTC",
"backward_quantity": 3405000,
"tx0_block_index": 854468,
"tx1_block_index": 854471,
"block_index": 854471,
"tx0_expiration": 8064,
"tx1_expiration": 8064,
"match_expire_index": 854491,
"fee_paid": 0,
"status": "completed",
"confirmed": true,
"market_pair": "XCP/BTC",
"market_dir": "SELL",
"market_price": "8810.57270000",
"block_time": 1722244895,
"forward_asset_info": {
"divisible": true,
"asset_longname": null,
"description": "The Counterparty protocol native currency",
"locked": true,
"issuer": null
},
"backward_asset_info": {
"divisible": true,
"asset_longname": null,
"description": "The Bitcoin cryptocurrency",
"locked": false,
"issuer": null
},
"forward_quantity_normalized": "300.00000000",
"backward_quantity_normalized": "0.03405000",
"fee_paid_normalized": "0.00000000"
},

https://api.counterparty.info/v2/orders/BTC/XCP/matches?status=completed&verbose=true

{
"id": "055950833596708871d59e62a5f84b9a85c7a8deff50e646194bf021f3f06143_ce3e3b758d010098d3010da7d7b4b92f1188278505f3819472906910e6879efd",
"tx0_index": 2742952,
"tx0_hash": "055950833596708871d59e62a5f84b9a85c7a8deff50e646194bf021f3f06143",
"tx0_address": "16mrwziWRBYhSPcWdJkaCvBuawXosLnTeG",
"tx1_index": 2743003,
"tx1_hash": "ce3e3b758d010098d3010da7d7b4b92f1188278505f3819472906910e6879efd",
"tx1_address": "bc1qvuqzfsaknpt2655kx7fe0hwwyqee6mushzpp87",
"forward_asset": "XCP",
"forward_quantity": 30000000000,
"backward_asset": "BTC",
"backward_quantity": 3405000,
"tx0_block_index": 854468,
"tx1_block_index": 854471,
"block_index": 854471,
"tx0_expiration": 8064,
"tx1_expiration": 8064,
"match_expire_index": 854491,
"fee_paid": 0,
"status": "completed",
"confirmed": true,
"market_pair": "BTC/XCP",
"market_dir": "BUY",
"market_price": "0.00011350",
"block_time": 1722244895,
"forward_asset_info": {
"divisible": true,
"asset_longname": null,
"description": "The Counterparty protocol native currency",
"locked": true,
"issuer": null
},
"backward_asset_info": {
"divisible": true,
"asset_longname": null,
"description": "The Bitcoin cryptocurrency",
"locked": false,
"issuer": null
},
"forward_quantity_normalized": "300.00000000",
"backward_quantity_normalized": "0.03405000",
"fee_paid_normalized": "0.00000000"
},

market_dir: https://github.com/CounterpartyXCP/counterparty-core/blob/4434a4dd334393c1af8c30c09d3cd92b9666d8f3/counterparty-core/counterpartycore/lib/api/queries.py#L2272-L2280

market_price: https://github.com/CounterpartyXCP/counterparty-core/blob/4434a4dd334393c1af8c30c09d3cd92b9666d8f3/counterparty-core/counterpartycore/lib/api/queries.py#L2272-L2280

ouziel-slama commented 1 month ago

Or if this is working consistently as intended, maybe update the API documents to indicate that asset1 is the quote asset.

ye that the idea, the quote asset is the asset1 !

droplister commented 1 month ago

I think this part needs to be normalized to work as intended (quantity_normalized):

https://github.com/CounterpartyXCP/counterparty-core/blob/929d8aa68073da52d81f1ab1549a396fcb54a07e/counterparty-core/counterpartycore/lib/api/queries.py#L2140-L2147

https://api.counterparty.io:4000/v2/orders/XCP/NBAPEPE/matches?verbose=true

"market_price": "15000000000.00000000",
"forward_quantity_normalized": "1",
"backward_quantity_normalized": "150.00000000",

https://api.counterparty.io:4000/v2/orders/XCP/NBAPEPE/matches?verbose=true

"market_price": "0.00000000",
"forward_quantity_normalized": "1",
"backward_quantity_normalized": "150.00000000",

In my apps, I've found it useful to have precision of 18 for price to handle itsy bitsy prices. I'll probably continue using my own price calculations for now.

droplister commented 1 month ago

If the design is that asset1 is always the quote asset, for the following I think the logic is backwards:

     order["market_pair"] = f"{asset1}/{asset2}" 
     if order["give_asset"] == asset1: 
         order["market_dir"] = "SELL" 
         order["market_price"] = divide(order["give_quantity"], order["get_quantity"]) 
     else: 
         order["market_dir"] = "BUY" 
         order["market_price"] = divide(order["get_quantity"], order["give_quantity"]) 

Because if you're giving the quote asset, you're buying the base asset. If you're getting the quote asset you're selling the base asset.

In my apps, I'm enforcing a really opinionated logic for trading pairs. Including some of the util functions I have for working with this API for reference/context also.

import BigNumber from 'bignumber.js';

export interface AssetInfo {
  asset_longname: string | null;
  description: string;
  issuer: string | null;
  divisible: boolean;
  locked: boolean;
}

export interface Order {
  tx_index: number;
  tx_hash: string;
  block_index: number;
  source: string;
  give_asset: string;
  give_quantity: number;
  give_remaining: number;
  get_asset: string;
  get_quantity: number;
  get_remaining: number;
  expiration: number;
  expire_index: number;
  fee_required: number;
  fee_required_remaining: number;
  fee_provided: number;
  fee_provided_remaining: number;
  status: string;
  confirmed: boolean;
  block_time: number;
  give_asset_info: AssetInfo;
  get_asset_info: AssetInfo;
  give_quantity_normalized: string;
  get_quantity_normalized: string;
  get_remaining_normalized: string;
  give_remaining_normalized: string;
  fee_provided_normalized: string;
  fee_required_normalized: string;
  fee_required_remaining_normalized: string;
  fee_provided_remaining_normalized: string;
}

const quoteAssets: string[] = [
  'BTC', 'XCP', 'XBTC', 'FLDC', 'SJCX', 'BITCRYSTALS', 'LTBCOIN', 'SCOTCOIN',
  'PEPECASH', 'BITCORN', 'CORNFUTURES', 'NEWBITCORN', 'DATABITS', 'MAFIACASH',
  'PENISIUM', 'RUSTBITS', 'WILLCOIN', 'XFCCOIN', 'SOVEREIGNC', 'OLINCOIN',
  'BITROCK', 'DANKMEMECASH', 'COMMONFROG.PURCHASE', 'PEPSTEIN.HUSHMONEY',
  'SCUDOCOIN', 'GREEEEEECOIN', 'MOULACOIN', 'LICKOIN', 'IAMCOIN', 'NEOCASH',
  'RELICASH', 'SHADILAYCASH', 'BLUEBEARCASH', 'FAKEAPECASH', 'DANKROSECASH',
  'DESANTISCASH', 'DOLLARCASH', 'BOBOCASH', 'SHARPS', 'CRONOS', 'BOBOXX', 'SWARM',
  'DABC', 'KEKO', 'NVST', 'POWC', 'NOJAK', 'NOMNI', 'BASSMINT', 'RAIZER.BTC', 
  'RAIZER', 'FUUUUUH.BTC', 'FUUUUUH', 'WOOOOK', 'VACUS', 'MUUI', 'FUTURECREDIT'
];

const quoteKeywords: string[] = ['CASH', 'COIN', 'MONEY', 'BTC'];

const getQuoteRank = (symbol: string): number => {
  const index = quoteAssets.indexOf(symbol);
  return index !== -1 ? index : quoteAssets.length;
};

const isQuoteAssetDirect = (symbol: string): boolean => {
  return quoteAssets.includes(symbol);
};

const isQuoteAssetFallback = (symbol: string): boolean => {
  return quoteKeywords.some(keyword => symbol.toUpperCase().includes(keyword));
};

const getAssetSymbol = (assetInfo: AssetInfo, fallback: string): string => {
  return assetInfo.asset_longname ? assetInfo.asset_longname : fallback;
};

export function assetsToTradingPair(order: Order, useRawAssets: boolean = false): [string, string] {
  const giveSymbol = getAssetSymbol(order.give_asset_info, order.give_asset);
  const getSymbol = getAssetSymbol(order.get_asset_info, order.get_asset);

  let baseSymbol: string, quoteSymbol: string;

  if (isQuoteAssetDirect(giveSymbol) && isQuoteAssetDirect(getSymbol)) {
    [baseSymbol, quoteSymbol] = getQuoteRank(giveSymbol) < getQuoteRank(getSymbol) ? [getSymbol, giveSymbol] : [giveSymbol, getSymbol];
  } else if (isQuoteAssetDirect(giveSymbol)) {
    [baseSymbol, quoteSymbol] = [getSymbol, giveSymbol];
  } else if (isQuoteAssetDirect(getSymbol)) {
    [baseSymbol, quoteSymbol] = [giveSymbol, getSymbol];
  } else if (isQuoteAssetFallback(giveSymbol) && isQuoteAssetFallback(getSymbol)) {
    [baseSymbol, quoteSymbol] = getQuoteRank(giveSymbol) < getQuoteRank(getSymbol) ? [getSymbol, giveSymbol] : [giveSymbol, getSymbol];
  } else if (isQuoteAssetFallback(giveSymbol)) {
    [baseSymbol, quoteSymbol] = [getSymbol, giveSymbol];
  } else if (isQuoteAssetFallback(getSymbol)) {
    [baseSymbol, quoteSymbol] = [giveSymbol, getSymbol];
  } else {
    [baseSymbol, quoteSymbol] = giveSymbol < getSymbol ? [giveSymbol, getSymbol] : [getSymbol, giveSymbol];
  }

  if (useRawAssets) {
    return (baseSymbol === giveSymbol) ? [order.give_asset, order.get_asset] : [order.get_asset, order.give_asset];
  }

  return [baseSymbol, quoteSymbol];
}

export function getTradingPairString(order: Order): string {
  const [base, quote] = assetsToTradingPair(order);
  return `${base}/${quote}`;
}

export function getBaseAssetString(order: Order): string {
  const [base] = assetsToTradingPair(order);
  return base;
}

export function getQuoteAssetString(order: Order): string {
  const [quote] = assetsToTradingPair(order);
  return quote;
}

export function getTradingDirection(order: Order): 'buy' | 'sell' {
  const [, quote] = assetsToTradingPair(order, true);
  return order.give_asset === quote ? 'buy' : 'sell';
}

export function calculatePrice(order: Order): string {
  const [baseSymbol, quoteSymbol] = assetsToTradingPair(order);
  const baseQuantity = new BigNumber(order.give_asset === baseSymbol ? order.give_quantity_normalized : order.get_quantity_normalized);
  const quoteQuantity = new BigNumber(order.give_asset === quoteSymbol ? order.give_quantity_normalized : order.get_quantity_normalized);
  const price = quoteQuantity.dividedBy(baseQuantity);
  return price.toFixed(18);
}

export function calculateAmount(order: Order): string {
  const [baseSymbol] = assetsToTradingPair(order);
  const baseQuantity = new BigNumber(order.give_asset === baseSymbol ? order.give_quantity_normalized : order.get_quantity_normalized);
  return baseQuantity.toFixed(8);
}

export function calculateTotal(order: Order): string {
  const [, quoteSymbol] = assetsToTradingPair(order);
  const quoteQuantity = new BigNumber(order.give_asset === quoteSymbol ? order.give_quantity_normalized : order.get_quantity_normalized);
  return quoteQuantity.toFixed(8);
}

It's based on Counterblock's: https://github.com/CounterpartyXCP/counterblock/blob/0bf2a190811672d0d4b3785e0da35168635c3461/counterblock/lib/util.py#L70-L80

def assets_to_asset_pair(asset1, asset2):
    base, quote = None, None

    for quote_asset in config.QUOTE_ASSETS:
        if asset1 == quote_asset or asset2 == quote_asset:
            base, quote = (asset2, asset1) if asset1 == quote_asset else (asset1, asset2)
            break
    else:
        base, quote = (asset1, asset2) if asset1 < asset2 else (asset2, asset1)

    return (base, quote)

Because the quote asset can be either asset1 or asset2, depending on the market. So something like the above is a helpful normalization.

ouziel-slama commented 2 weeks ago

1) I see what you mean but imo all this logic has no place in counterparty-core.. it is up to the API client (e.g. Counterblock) to implement and use the assets_to_asset_pair(asset1, asset2) function to determine which url to use 2) yes this field should be called market_price_normalized.