How We Built Our TCGX Index

ByDimaDima

We recently released TCGX, a perpetual that tracks a basket of trading cards. Building an index around these kinds of assets comes with many challenges, from deciding which cards to include to making sure one outlier doesn't swing the entire index.

This article covers how we decided which cards to include in our index, how much each card should affect the index, and how we prevent outlier prices from moving the index too much.

The Problem with Indexing Cards

As you can imagine, when building an index from a basket of cards, we have far less data to work with than an index like the S&P 500. There is no easy data on the supply of cards, and we don't have a continuously executable price for the whole basket. Cards can be valued very highly while hardly trading at all. Using supply to determine each card's weight won't work because we don't know the supply. Using price to determine weight would simply give higher-value cards more influence on the index, which isn't ideal for a tradeable index either.

Selecting Cards Based on Data

If we tracked one big card, our oracle would rely on a single price for that card. Including more cards helps with this problem, but only if each card provides a good input for our price. The more bad inputs we include, the more susceptible we become to manipulation.

So we decided to pick cards based on how good their data was. Each card in our index is a specific variation of that card, defined by set, printing, condition and language. We wanted cards whose data updated frequently and fluctuated often. We also looked at price spikes, both up and down. To make sure we don't overweight certain cards, we limited how many cards can come from the same set or series.

We ended up with 160 cards that fit our criteria: 80 from Magic: The Gathering, 48 from Pokémon, 16 from One Piece and 16 from Lorcana. This list will stay constant unless we change our method for choosing cards. If a card's price begins to fall, or it stops updating as frequently, we won't replace it.

When initially selecting cards, we filtered them by price range. However, a minimum price doesn't tell us how expensive it would be to move that card's price. Someone buying one copy of a card may not move the price we are looking at. Frequent price fluctuations show repricing activity, but don't establish how often a card trades. Again, that doesn't mean its price is executable. If a card drops below our initial price range, it will stay in our index.

How Did We Decide Magic Shouldn't Make Up 50% of the Index?

The number of cards we chose from each game shouldn't dictate how much weight that game has. If we weighted each card equally, Magic would take up 50% of the index simply because we chose the most cards from that game.

Using the recorded price to weight each card also presents problems. Let g represent a game and pᵢ(t) be the price we accept for each card in that game. We could then find the share of the total that each game's recorded prices make up:

Rejected alternative: a game's recorded-price share is the sum of its card prices divided by the sum over all games.

This is that game's share of the total recorded prices. As stated before, we don't know the supply of these cards or how much they trade, so this would skew our index toward more expensive cards.

By allocating a set percentage to each game, we can control how much exposure our index has to each game. Pokémon makes up 45% of our index, Magic 42%, One Piece 11% and Lorcana 2%. These are the exposures we decided our index should have. Once we allocated each game its weight Wg, we divided it by ng, the number of cards in that game, to get the weight of each card:

The weight of card i equals its game allocation W sub g divided by the number of cards n sub g in that game. Example: 45 percent divided by 48 Pokémon cards is 0.9375 percent per card.

So each Pokémon card has a weight of 0.9375%, each Magic card 0.525%, each One Piece card 0.6875% and each Lorcana card 0.125%. Adding more cards to a game would lower each card's weight within that game. Dividing evenly doesn't make the cards independent, though. Similar cards can still move together, and many bad inputs at once can skew our index.

Two bars of 160 cards. By count, Magic has 80 cards, Pokémon 48, One Piece 16 and Lorcana 16. By index weight, Pokémon is 45 percent, Magic 42, One Piece 11 and Lorcana 2. Per card: Pokémon 0.9375 percent, Magic 0.525, One Piece 0.6875, Lorcana 0.125.

Scaling All Prices Equally

By taking the log of each card's price relative to an anchor price, we put all of our cards on the same playing field:

z sub i at time t equals the natural log of accepted price p sub i over fixed anchor a sub i.

aᵢ is the anchor price we choose for each card. This keeps our index from being skewed by cards with higher nominal values. If a card goes up 10% today, its log ratio changes by the same amount whether it costs $10 or $1,000. Its effect on the index also depends on its weight and distance from the center.

Finding the Center of Our Prices

We now need to find one value from our 160 cards, all scaled the same way. There are many ways to do this. If we take a simple average of all the values, one bad input could affect our index drastically. Taking the median helps with bad inputs, but it may not capture every real price movement. Smoothing the prices dampens bad prices, but it also slows down the index's movement.

In previous versions of our formula, we capped how much each card's accepted price could move. This made our index path dependent: if a card hit its cap a couple of weeks ago, it would still affect our index today. So we decided not to cap how much the accepted price can move. Because we use only our accepted prices and anchor prices, our index will always be the same given the same accepted prices, anchors and weights.

What we came up with is a Huber estimator. It finds the center θ of our inputs, but once an input is a certain distance away from θ, it clips that input's distance from the center before applying its weight:

The Huber score psi with threshold 0.8 equals max of negative 0.8 and min of x and 0.8, where x is a card's log ratio minus the center.

Where x = zᵢ − θ. We can then solve for θ:

Solve for theta such that the sum over cards of weight times the clipped score of z sub i minus theta equals zero.

All cards that fall within the threshold count with their full weighted distance. Those beyond the threshold count at most 0.8 × their weight, in either direction. The 0.8 threshold is measured in log distance from the center. If no cards are clipped, θ is simply the weighted mean of the logs.

If no card is clipped, theta equals the sum of each card weight times its log ratio, the weighted mean.

Centering on the Index

Our index value becomes:

The index at time t equals the fixed reference level I zero times e to the theta.

Where I₀ is a fixed number we choose to index from. It is not equal to the price of a basket of cards or the combined market cap of those cards.

By clipping these contributions, we limit the influence of any one big price move. But if all cards went up 5%, all of the log ratios would go up by ln(1.05), and so would our center. All of the residuals would stay the same, so our index would also go up 5%, regardless of how far each card is from its anchor price. The downside is that if only a few cards have a big price move, they may be clipped, even if the movement is valid.

Four steps from card prices to the index. 01: the latest accepted price for each of 160 cards and a fixed anchor per card. 02: the log ratio of price to anchor. 03: solve for the weighted Huber center theta, where outlier contributions are capped at plus or minus 0.8 times their weight. 04: the index equals a fixed reference level times e to the theta. Weights are each game's allocation split equally across its cards. RFQ quotes and the exchange mark are not inputs.

A 10x Price Increase

Let's look at an example. Assume we have 160 cards, all with equal weight and all at their anchor prices. Now we move one of those prices to 10x its anchor. A simple log average would increase our index by about 1.45%. But since we are using a Huber estimator, that card will be clipped, and we end up with:

In the illustrative equal-weight basket, one card is repriced to ten times its anchor. The equation 0.8 minus 159 theta equals zero gives theta equals 0.8 over 159.

Which gives I/I₀ = e^(0.8/159), an increase of about 0.50%.

The new index over the initial level is e to the 0.8 over 159, about 1.0050: a 0.50 percent rise, against 1.45 percent for a plain log mean.

Using our current weights and prices from September 4, the largest single-card 10x price move we tested increased our index by 1.12%. This result applies to that snapshot; it isn't a universal bound or an estimate of manipulation cost.

Huber score chart: the unweighted score grows with a card's log distance from the center until plus or minus 0.8, then stays flat; the unclipped score has no limit. In a toy basket of 160 equally weighted cards, repricing one card to 10 times its anchor raises a plain log mean by 1.45 percent and the Huber index by 0.50 percent. Raising every card 5 percent raises the Huber index 5 percent.

Of course, if multiple cards moved at once, that would be difficult to distinguish from normal buying pressure. Someone could try to attack multiple prices at once, or many prices could be invalid at the same time. We have no way of telling these apart from the prices alone.

Low Price Action

Cards that are repriced less frequently will affect the index less frequently, but not necessarily by smaller amounts. The median Magic card in our basket had 76 price changes across 89 day-to-day movements. The median Pokémon card had 24 price changes over the same period. These counts measure how often recorded prices changed. Our basket was also chosen with hindsight, so our historical data carries hindsight-selection bias.

Even though our candles are set to 1 second, there can be long periods where no price updates. We will not artificially generate price movement.

We will monitor freshness using the weights of the constituents, as well as coverage by game. For example, if 10 Pokémon variants became stale, they would account for 9.375% of the index weight, while 10 Lorcana variants would account for 1.25%. One metric may not cover every case.

If we do not have fresh data, we will not reweight our basket. Our last accepted prices will stay in place and continue to age. If prices are invalid, we will stop accepting them. Failed freshness checks may immediately affect the ability to trade, and we may pause or limit trading if prices become unreliable. We will communicate with our traders if we make major changes to our basket or index.

What's Next

This is our first Discovery Market, and we're excited for people to start trading it. We'll continue to manage risk as the market grows and may update the index over time. As we see more activity, we plan to increase the OI cap and tighten spreads. Discovery Markets is only getting started, and we have a lot more exciting listings ahead. Stay tuned!