Virtual Markets, Part Ten: Interregional Trade Dynamics

Up-until this point, the trade strategies and reports we’ve developed for EvE Online all operate under the assumption that trade occurs within the economic vacuum of a single region. It is now time to break this barrier and begin investigating the possibilities of interregional trade.

In this article, we will:

  • Make quantitative comparisons between major trade regions using available market data.
  • Investigate the game mechanics and associated barriers, costs and risks associated with hauling commodities between regions.
  • Use all gathered information to make a final verdict on the viability of interregional trade and set the foundation for our next reporting strategy.

Regional Comparisons

Before delving into any game mechanics, we need to conduct some macroeconomic review in-order to better understand broader market conditions and determine whether or not it makes any sense to pursue interregional trade opportunities.

In this section, we’ll investigate the largest trade hubs in the game and measure trade volume, price indices, and capital inflows versus outflows.

Measuring Jita’s Dominance

Up-until now, we have been operating under a general assumption that the trade hub Jita 4-4 and its associated region ‘The Forge’ is the single most dominant market in the game. Anybody who has even casually played EvE Online knows this intuitively; Jita is easily the most heavily populated solar system in New Eden and is the subject of most discourse surrounding the game and its economy.

But how do we actually know that Jita is supreme? Fortunately, with some help from our database, we don’t have to guess. With a fairly rudimentary script, we can calculate and compare the total value of all cleared transactions per day for any region. This is roughly equivalent to calculating the daily “Dollar Volume” for all commodities traded on the market, using the in-game currency ISK instead of Dollars.

In the context of real securities, Dollar volume is typically used to gauge the liquidity of a specific security. By aggregating the Dollar volumes for all securities traded on a single exchange, we can gauge the liquidity of the exchange itself. For our purposes, we can apply this approach against the regions of EvE Online in-order to compare and rank their importance in handling transactions across the entirety of the game.

Let’s start our script by importing some modules, setting some initial variables, and introducing a function:


import sqlite3
import os
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker


### define regions to report against
regionlist = ['TheForge', 'Domain', 'Essence', 'Metropolis', 'SinqLaison', 'Heimatar', 'TashMurkon']

### define database location where source data is stored
databaselocation = 'marketdata.sqlite'

### change working directory to script location
pathtorunfile = os.path.dirname(__file__)
os.chdir(pathtorunfile)

# function used to format Y axis for our plot
def format_y_trillions(value, tick_number):
    return f'{int(value / 1000000000000)}T'

For this exercise, we’ll be using the matplotlib library to help with visualization. The ‘format_y_trillions’ function will help us with our graph scaling once we’re ready to put this library to work.

Next, we have a pretty basic runtime:

### runtime
if __name__ == '__main__':

	for region in regionlist:

		## create in-memory database to temporarily store tables used for report generation	
		tempdb = sqlite3.connect(":memory:")
		cur = tempdb.cursor()
		
		## attach primary database (READ-ONLY)
		cur.execute(f"ATTACH 'file:{databaselocation}' AS marketdatadb;")

		## calculate ISK exchanged per day
		cur.execute(f"CREATE TABLE HistorySpanDates AS SELECT DISTINCT date AS CurrentDates FROM marketdatadb.{region}History ORDER BY CurrentDates DESC LIMIT 30;")
		cur.execute(f"CREATE TABLE HistoryCurrentDates AS SELECT * FROM marketdatadb.{region}History, HistorySpanDates WHERE date = CurrentDates;")
		cur.execute("ALTER TABLE HistoryCurrentDates ADD COLUMN TradedISK;")
		cur.execute("UPDATE HistoryCurrentDates SET TradedISK = average * volume;")
		
		## extract and plot data
		ResultData = pd.read_sql("SELECT sum(TradedISK) AS TradedISK, date FROM HistoryCurrentDates GROUP BY date;", tempdb)
		ResultData['date']= pd.to_datetime(ResultData['date'])
		plt.plot(ResultData.date,ResultData.TradedISK, label = f"{region}")
		
	## generate plot graph
	plt.gca().yaxis.set_major_formatter(ticker.FuncFormatter(format_y_trillions))
	plt.legend()
	plt.title("ISK Exchanged in Regional Markets Per Day")
	plt.show()

This iterates through each indicated region drawing from the last 30 days of historical market data, calculating the ISK value of all exchanged goods for each day. The average for each region is printed, and a plot graph is generated providing a means of visually comparing each region.

Executing this script returns the following output:

 For TheForge (in trillions of ISK)
(17.680519302411856,)
 For Domain (in trillions of ISK)
(1.465574183075155,)
 For Essence (in trillions of ISK)
(0.10618288093079865,)
 For Metropolis (in trillions of ISK)
(0.32709224471667897,)
 For SinqLaison (in trillions of ISK)
(0.4585916036212447,)
 For Heimatar (in trillions of ISK)
(0.2251469285752347,)
 For TashMurkon (in trillions of ISK)
(0.07257280523938567,)

Which is followed by a graph:

The result pretty clearly answers our question: Between seven of the largest trade regions in EvE Online, ‘The Forge’ facilitates the vast majority of transactions, comprising 85% of all ISK volume. While there are many other miscellaneous regions not included in this report, they comprise a tiny fraction of remaining in-game transactions. Even if we eliminate niche items traded in high volumes which aren’t subject to interregional hauling mechanics such as PLEX and skill extractors, the complete dominance of ‘The Forge’ continues to hold true.

Our result begs another important question: If Jita (or more specifically, The Forge) is so dominant, why should we care about other regions and interregional trade at all?

Price Indices

With an average daily ISK volume of 17.6T ISK, market liquidity in ‘The Forge’ is over an order of magnitude greater than the next largest region, ‘Domain’. This means that transactions are cleared at least ten times faster in ‘The Forge’ than anywhere else. In the context of our intraregional trading strategies involving station trading and reprocessing, this results in a greater quantity of trade opportunities, shorter order deltas, and overall greater earning potential when compared with less-active regions.

However, large and popular markets also tend to be more efficient, leaving little room for profit. As we learned when developing our station trading report, competition plays a huge part in forcing profit margins to shrink. At the time of this writing, the top 30 results of our station trading report for ‘The Forge’ reveal a cumulative potential daily profit amounting to only 7-8B ISK per day, or just 0.05% of ISK volume. The sheer amount of competition drawn to large markets like ‘The Forge’ results in razor-thin margins, with no single item type remaining profitable to trade for very long.

So, what opportunity does interregional trade offer? Since ‘The Forge’ and ‘Domain’ represent mutually distinct markets, they feature mutually distinct pricing. In much the same way that Bid-Ask spreads present profit opportunities within a single region, price differentials present opportunities between different regions. So, if prices between ‘The Forge’ and ‘Domain’ are substantially different, we have an opportunity to move goods between them and extract the differential, sans costs, as profit.

Since regional markets in EvE Online feature a large basket of different commodities, measuring price differentials will require calculating a price index. In real-world contexts, price indexes such as CPI and PPI are typically used to help measure changes in price as a function of time, usually in-order to track inflation. For our use-case, we will be measuring differences in prices between regions in New Eden in a manner similar to how the Bureau of Economic Analysis uses Regional Price Parities (RPPs) against metropolitan regions across the United States to measure price differentials as a function of both geography and time.

Before we get started writing our script, we should figure out how we want to calculate our price index. There are a litany of different price index formulas to choose from, all featuring their own strengths and weaknesses. Since our data involves many different types of commodities which all feature variable pricing and trade quantities, we can immediately exclude all unweighted or “elementary” indices; weighing is necessary in our case since different commodities share different proportions of a market’s total ISK volume.

After spending some time in SQLite manually adopting the Törnqvist and Marshall-Edgeworth formulas, I discovered the R library pricelevels which supports dozens of different price indices, including the more contemporary Geary-Khamis index, which to my knowledge is what is used by the BEA for their analysis. So, instead of writing pure Python, we’ll be writing a short section of our script in R to take advantage of this resource. Our end-goal will be to generate 90-day plot graphs for the Laspeyres, Marshall-Edgeworth, Törnqvist, and Geary-Khamis price indices respectively for all regions we are collecting data for.

So, let’s jump into our script, starting with our first Python snippet. Our initial modules and starting values haven’t changed all that much from earlier examples:

import sqlite3
import os
import pandas as pd

### define regions to report against
regionlist = ['Domain', 'Essence', 'Metropolis', 'SinqLaison', 'Heimatar', 'TashMurkon']

### define database location where source data is stored
databaselocation = 'marketdata.sqlite'

### change working directory to script location
pathtorunfile = os.path.dirname(__file__)
os.chdir(pathtorunfile)

Next, we’ll move into our runtime. Our goal here is to complete some data normalization so that our data is ready to be fed into R for the pricelevels library to consume. Descriptive comments are outlined in bold:

### runtime
if __name__ == '__main__':

	## create in-memory database to temporarily store tables used for report generation	
	tempdb = sqlite3.connect(":memory:")
	cur = tempdb.cursor()
			
	## attach primary database (READ-ONLY)
	cur.execute(f"ATTACH 'file:{databaselocation}' AS marketdatadb;")

	## Generate a table containing the desired quantity of dates to report against
	cur.execute("CREATE TABLE ReportDates AS SELECT DISTINCT date AS ReportDates FROM marketdatadb.TheForgeHistory ORDER BY ReportDates DESC LIMIT 90;")
	
	## Extract oldest date
	cur.execute("CREATE TABLE FirstDate AS SELECT ReportDates AS FirstDate FROM ReportDates ORDER BY FirstDate ASC LIMIT 1;")
	
	## Generate a reference table of type_ids belonging to the earliest date for 'The Forge' which we will use as our reference region/date for calculating price indices
	cur.execute(f"CREATE TABLE type_ids AS SELECT DISTINCT type_id FROM marketdatadb.TheForgeHistory, Firstdate WHERE date = FirstDate;")
	
	## Grab relevant data belonging to Jita, including type_ids featured on the oldest date. Save as "MasterData" table
	cur.execute("CREATE TABLE MasterData AS SELECT average AS price, 'TheForge' || '-' || date AS region, volume, TheForgeHistory.type_id AS type_id FROM marketdatadb.TheForgeHistory, type_ids, ReportDates WHERE TheForgeHistory.type_id = type_ids.type_id AND date = ReportDates;")

	## Repeat this step for all other regions and insert the results into MasterData
	for region in regionlist:
		cur.execute(f"CREATE TABLE {region}Data AS SELECT average AS price, '{region}' || '-' || date AS region, volume, {region}History.type_id AS type_id FROM marketdatadb.{region}History, type_ids, ReportDates WHERE {region}History.type_id = type_ids.type_id AND date = ReportDates;")
		cur.execute(f"INSERT INTO MasterData SELECT * FROM {region}Data;")

	## Attach oldest date to new column before we send the table off to R
	cur.execute("ALTER TABLE MasterData ADD COLUMN BaseDateRegion;")
	cur.execute("UPDATE MasterData SET BaseDateRegion = 'TheForge-' || FirstDate FROM FirstDate LIMIT 1;")

	## Export for subsequent analysis using R
	ResultData = pd.read_sql("SELECT * FROM MasterData;", tempdb)
	ResultData.to_csv('out.csv', index=False)

The first important thing to note about this block is that we are defining a ‘base’ region and date, which we’ve defined in our case as the earliest date available for ‘The Forge’ within the reporting window. This is analogous to how CPI is calculated as 100 for its base year of 1982, from which all other years use as a reference for their CPI calculations. Not all indexing methods require having a base year, however setting a base provides a convenient reference from which to compare all other differences in time and space.

The next important thing to note is that we are filtering-out any commodities not featured at our base region and date. This serves to prevent any apples-to-oranges comparisons from being made in the event that any distinct commodities appear elsewhere. The documentation for pricelevels describes data ‘gaps’ in more detail, which I’ve chosen to handle in SQLite before passing the data off.

Finally, data for all regions is inserted into a table comprised of five columns which match each of the values needed by pricelevels. Executing the script dumps this all into to a .csv file.

Moving-on, our next block is written in R. After installing the pricelevels and data.table packages from CRAN, we can execute the following to obtain 4 .csv files containing all of our computed price indices:

library('pricelevels')
library('data.table')
marketdata <- read.csv("#path/out.csv")
dt <- data.table(marketdata)
dt2 <- dt[, type_id:=as.character(type_id)]

laspeyres <- dt[, laspeyres(p=price, r=region, n=type_id, q=volume, base=max(BaseDateRegion))]
write.csv(laspeyres,"#path/laspeyres.csv", row.names = TRUE)

medgeworth <- dt[, medgeworth(p=price, r=region, n=type_id, q=volume, base=max(BaseDateRegion))]
write.csv(medgeworth,"#path/medgeworth.csv", row.names = TRUE)

toernqvist <- dt[, toernqvist(p=price, r=region, n=type_id, q=volume, base=max(BaseDateRegion))]
write.csv(toernqvist,"#path/toernqvist.csv", row.names = TRUE)

gkhamis <- dt[, gkhamis(p=price, r=region, n=type_id, q=volume, base=max(BaseDateRegion))]
write.csv(gkhamis,"#path/gkhamis.csv", row.names = TRUE)

I can attest that this is much shorter (and probably faster) than trying to accomplish the same with Python/SQLite!

With that done, we can pull our data back into Python/SQLite in-order to split our data by region and spit-out some neat graphs:

import sqlite3
import csv
import os
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker

### define regions to report against
regionlist = ['Domain', 'Essence', 'Metropolis', 'SinqLaison', 'Heimatar', 'TashMurkon']
indices = ['laspeyres', 'medgeworth', 'toernqvist', 'gkhamis']

### change working directory to script location
pathtorunfile = os.path.dirname(__file__)
os.chdir(pathtorunfile)

### runtime
if __name__ == '__main__':

	for index in indices:
		## create in-memory database to temporarily store tables used for report generation	
		tempdb = sqlite3.connect(":memory:")
		cur = tempdb.cursor()
		
		## import index data from our R script
		df = pd.read_csv(f'{index}.csv')
		df.columns = ['x', 'y']
		df.to_sql('IndexData', tempdb, if_exists='append', index=False)
		
		# grab and plot data for TheForge. Since we are using this as our 'base', it will have a thicker line width than other regions
		ResultData = pd.read_sql(f"SELECT SUBSTR (x, -10)  AS date, y AS TargetRegion FROM IndexData WHERE x LIKE '%TheForge%';", tempdb)
		ResultData['date']= pd.to_datetime(ResultData['date'])
		plt.figure(figsize=(10,6))
		plt.plot(ResultData.date,ResultData.TargetRegion,label = 'TheForge',linewidth=3)
		
		# grab and plot data for all other regions
		for region in regionlist:
			ResultData = pd.read_sql(f"SELECT SUBSTR (x, -10)  AS date, y AS TargetRegion FROM IndexData WHERE x LIKE '%{region}%';", tempdb)
			ResultData['date']= pd.to_datetime(ResultData['date'])
			plt.plot(ResultData.date,ResultData.TargetRegion,label = region)

		# generate plot graph
		ax = plt.gca()
		ax.set_ylim([0.95, 1.15])
		plt.legend(loc='upper left')
		plt.savefig(f"{index}.png")
		plt.close()

Executing this script reveals four graphs which we can now analyze, starting with Marshall-Edgeworth and Laspeyres:

While the formulas which define these indices are very similar, they both characterize volatility in ‘The Forge’ quite differently due to their different weighing methods. While Marshall-Edgeworth uses symmetric weighing, Laspeyres is heavily weighted in the base region. These indices otherwise both seem to agree that prices in ‘The Forge’ consistently trend about 3% lower than other regions over a 90-day span.

Next, we have Törnqvist and Geary-Khamis:

These indices differ from the prior two in a few ways. In-addition to presenting some previously unseen sharp price dips for the region ‘Tash-Murkon’, it also appears that prices in ‘The Forge’ are trending about 2-2.5% lower than other regions. Besides these differences they’re otherwise very similar in composition.

We can deduce two key market facts presented through this exercise:

  • Prices of cleared orders are consistently higher in regions outside of ‘The Forge’, with an average price differential of about 2-3%.
  • Prices are generally more volatile outside of ‘The Forge’. The Marshall-Edgeworth index disagrees, however out of all four indexing methods it is the sole outlier.

Higher prices outside ‘The Forge’ present a strong case for interregional trade; all else being equal, it makes sense to move goods to locations where higher prices are commanded.

The case for interregional trade is further bolstered by the apparent regional differences in volatility; over 90 days, prices in ‘The Forge’ fluctuated by about 2.5%, while over the same period other regions saw fluctuations closer to 4-5%, punctuated by occasional spikes and dips. Lower volatility combined with lower prices presents a relatively safe purchasing market in ‘The Forge’, while higher volatility paired with higher prices in other regions present lucrative sellers’ markets, as volatility can amplify margins whenever there is a surge in demand.

While interregional trade is starting to sound like a pretty good idea at this point, there is one other macroeconomic factor we need to check.

Capital Inflows and Outflows

Price indices and dollar volumes only matter if capital flows in the right directions for our intended trade strategy. For instance, it might not be worth attempting to sell goods in a region which primarily services the clearing of Buy orders; this scenario can occur if players treat a region as a place to liquidate excess inventory rather than as a place to restock. It is thus important to understand the composition of capital inflows and outflows so that we can apply better context to price indices and dollar volumes and garner a better general understanding of trade flows within a region before committing to a specific trade strategy.

As we discussed when developing our station trading strategy, the historical endpoint data for ESI unfortunately does not differentiate between cleared Buy or Sell orders. Fortunately, as part of that exercise we devised a data normalization routine which through some extrapolation can provide an approximate ratio of trade balance.

We’ll start our exercise by borrowing the entire data normalization routine from our station trading script. From there, we can follow-up with a handful of lines to calculate the aggregate ratio of Buy to Sell ISK volume:

...
		## run calculations to obtain aggregate Buy/Sell ratio
		cur.execute("CREATE TABLE HistorySpanNrmlSum AS SELECT type_id, BuySellRatio, sum(volume) AS VolumeSum, sum(average * volume) AS TradedISK, sum(average * volume) / sum(volume) AS AverageUnitPrice FROM HistorySpanNrml GROUP BY type_id;")
		cur.execute("ALTER TABLE HistorySpanNrmlSum ADD COLUMN TradedISKSum;")
		cur.execute("CREATE TABLE TotalTradedISK AS SELECT sum(TradedISK) AS TotalTradedISK FROM HistorySpanNrmlSum;")
		cur.execute("UPDATE HistorySpanNrmlSum SET TradedISKSum = TotalTradedISK FROM TotalTradedISK;")
		cur.execute("ALTER TABLE HistorySpanNrmlSum ADD COLUMN MarketShare;")
		cur.execute("UPDATE HistorySpanNrmlSum SET MarketShare = TradedISK / TradedISKSum;")

		## print result
		print(region)
		print(ResultData)

Executing the full script returns the following output for each region:

TheForge
   AggregateBuySellRatio
0               0.491444
Domain
   AggregateBuySellRatio
0               0.509242
Essence
   AggregateBuySellRatio
0               0.531525
Metropolis
   AggregateBuySellRatio
0               0.506646
SinqLaison
   AggregateBuySellRatio
0               0.510876
Heimatar
   AggregateBuySellRatio
0               0.544789
TashMurkon
   AggregateBuySellRatio
0               0.541982

Based off of these results, trade in each region appears to be pretty balanced, with ‘The Forge’ having a marginally greater proportion of ISK Volume attributed to the clearing of Sell orders while all other regions inversely feature more clearing of Buy orders.

This makes sense given the context of the price differentials; with lower prices than anywhere else, a greater proportion of players are willing to buy directly from prevailing Sell orders in ‘The Forge’ since they likely represent the lowest price for that specific commodity in all of New Eden. Likewise, since remote regions feature higher prices, it is more likely for the prevailing Buy order to represent a price higher than what is offered in ‘The Forge’, encouraging more direct liquidation of goods into currency.

While interesting and something to keep in-mind, trade is still well-balanced across all of the reported regions, with these ratios not being extreme enough to diminish the feasibility of interregional trade.

Space Logistics

With our initial macroeconomic review complete, we’ve established a strong basis for the theoretical feasibility of interregional trade. Before we can come-up with an actionable strategy, we need to now consider all of the game mechanics involving the movement of goods and characters between regions in space and how these mechanics might impact our calculus.

Space Trucking

In Old School Runescape, there is no friction presented by the movement of goods; the mechanics of banking, bank notes, and the centralized nature of the Grand Exchange effectively eliminate all logistical barriers to trade.

While station trading and reprocessing within a single region in EvE Online feature similar logistical complexity to our OSRS trade strategies, interregional trading in EvE necessitates the physical movement of material between regions, referred to in-game as hauling.

The general act of hauling can be very easily summarized by a few steps:

  1. Goods are loaded from a station-based inventory onto a ship.
  2. The ship, piloted by a player, leaves the station and enters open space
  3. The pilot uses a variety of means to move the ship through space (gates, wormholes, jump drives)
  4. The pilot docks the ship at a destination station, where goods can be unloaded into a station inventory.

This procedure faces a variety of frictions which can also all be easily summarized:

  • The physical volume of goods is measured in cubic meters (m3). Ships are limited in the volume of goods they can haul, with ships featuring different capacities depending on model and degree of customization.
  • Players must have requisite skills to pilot ships. Freighters offer the largest cargo holds, requiring months of real-time training before they can be piloted.
  • Space is dangerous. Players can and will attack (and possibly destroy) your ship while en-route.

The first two complications can be regarded as types of capital expenditure. Once a player has performed the necessary training and has all of the ships they need, these costs do not reoccur. Since ships do not suffer from depreciation, the only truly fixed or “sunk” costs associated with hauling involve the consumption of skill books as well as the opportunity cost of spent skill points (which can be reallocated, given some marginal expense). The end-result is that the true capital expenditures associated with hauling very quickly reduce to zero as players generate revenue from hauling activities.

However, the third complication introduces risk, and thus a major operational expense. EvE Online features very permissible “player-versus-player” combat mechanics in much the same way that personal and property crime can occur at any time in real-life. While hauling through high-security space offers some degree of protection via CONCORD (police) mechanics, haulers are still subject to a very specific type of conduct by malevolent players motivated by profit, politics, revenge, or simply their own morbid amusement.

As a result, the operational costs of hauling become a function of risk which must be factored into our decision to engage in interregional trade opportunities. Fortunately, we can spare the esoteric details of hauling risk management in EvE Online, as the market has already calculated this for us by proxy.

Courier Contracts

In much the same way that real-life corporations leverage third-party contractors for logistics, players in EvE Online can issue courier contracts to the same effect. This mechanism allows players to specify a reward, collateral, deadline, list of items, and destination when drafting a courier contract. If a hauling contractor then sees and agrees to the terms of the contract, they can accept it and haul the items on the original player’s behalf.

Thanks to this mechanic, players can move goods throughout space without any consideration of risk or the need to haul goods themselves. Instead, all of the capital and operational expenditures associated with hauling are effectively translated into a flat fee per job.

While hauling costs can be quite variable, there is some emergent standardization we can leverage for our reporting purposes through the prices advertised by in-game hauling corporations. Red Frog Freight is one of the more well-known and reputable hauling corporations operating in high-security space, offering the following rates for jobs between Jita and each of the trade hubs we have been reporting against:

  • Domain (Amarr): 59M ISK,
  • Essence (Oursulaert): 19.4M ISK
  • Metropolis (Hek): 27.8M ISK
  • Sinq Laison (Dodixie): 23M ISK
  • Heimatar (Rens): 35M ISK
  • Tash-Murkon (Tash-Murkon Prime): 61.4M ISK

The above prices assume a maximum volume of 845,000m3 and collateral of 1.5B ISK, revealing some important truths concerning hauling in high-security space:

  • 845k m3 represents the maximum volumetric capacity of the (arguably) most optimal freighter configuration available in the game. (Fenir)
  • 1.5B ISK represents an approximate break-even point for would-be attackers. Below this cargo value, it typically isn’t worth the material cost and/or time spent coordinating a fleet of Catalysts to simultaneously attack and destroy a freighter in high-security space.
  • Hauling costs are otherwise a function of the number of high-security “jumps” between the origin station and the destination, which roughly correspond with travel time. For instance, while there are 12 jumps between Jita and Oursulaert, there are 47 jumps between Jita and Tash-Murkon, resulting in their corresponding differences in-price. There is therefore an approximate “price per jump” of about 1.5M ISK.

These are by no means hard universal rules, but they are quite close to approximating the parameters for economical shipping between regions.

The limit on volume is going to be mostly irrelevant for the purposes of interregional trade; with some niche exceptions, 845k m3 represents an exceedingly huge amount of cargo which most traders are unlikely going to need to fully-leverage. Even in circumstances where this much space is needed, such loads can typically be split-up and shipped separately.

The ISK limit on the other hand effectively precludes interregional trade of anything worth more than 1.5B ISK per unit, which encompasses a surprisingly large basket of goods. While there are other couriers who might be willing to pay a higher collateral, they will demand substantially higher rewards to offset the increased risk, resulting in reduced trade margins.

It is also important to note that while Red Frog Freight charges fixed costs for these routes, traders looking for more economical shipping options are free to place public courier contracts featuring a lower “price per jump”. This is especially important for traders who may need to haul substantially less-valuable and/or smaller cargo than indicated by Red Frog Freight’s limits. Independent haulers based in Jita compete for access to courier contracts in much the same way that traders compete for access to the market spread. While there are no truly fixed rules concerning contract pricing, higher rewards and/or lower volumes typically result in contracts being taken-up and shipped faster, which will correspond with improved trade deltas.

In any case, we will keep Red Frog Freight’s rates in-mind to use as a baseline when determining the potential profitability of interregional trade opportunities.

Personal Transit and Clones

As important as it is to get our goods where they need to be sold, it is equally important to get a character to the target market as well. Shipped goods are not magically placed on the market; players must be physically present within the region where their goods are located in-order to place and update market orders.

The most comprehensive approach to this problem involves stationing separate characters at each trade hub. A single account with access to three characters can cover Jita plus two hubs in this manner. The main limitation of this approach is that expanding into more regions necessitates concurrent Omega (membership) status, representing additional operating costs.

At the expense of some convenience, a more economical approach would be to simply have one character travel around high-security space, managing trade at each hub as they move along. Fortunately, personal transit is a pretty trivial affair in EvE Online:

  • Jump clones represent the most expedient mode of character transportation, allowing what is effectively teleportation between stations. Clone jumping mechanics suffer some limitations however, most importantly a long cooldown period between uses.
  • In-contrast to large hauling vessels which are relatively slow and optimized for moving goods, travel fitted ships are optimized for character transit, allowing relatively fast transit between systems and regions. Once a character has arrived in their target region, they can either dock at the trade hub station, or, with adequate trade skills, remotely place market orders behind the cockpit of their ship. The auto-pilot function provides a hands-off traveling option at the expense of greater attack risk, however this risk can be almost entirely mitigated by using inexpensive ships and clones without implants.

While personal transit within high-security space is a pretty low-effort, low-risk activity, it represents a cost not shared with our aforementioned trade strategies which require no transit whatsoever. Quantifying these associated opportunity costs is difficult, but they’re important to keep in-mind.

Conclusion and Closing Notes

Now that we have a solid foundation of macroeconomic data to work with as well a better understanding of the costs associated with interregional trade, we can now combine these elements in an attempt to determine whether or not this strategy is worth pursuing.

Transit-Adjusted Price Indices

Using the prices charged by Red Frog Freight, we can calculate freight-adjusted price indices by modifying the scripts we developed earlier.

First, we need to exclude any item featuring an average price in ‘The Forge’ exceeding 1.5B ISK. This can be done by tweaking one line of SQLite from the first Python script:

	## Grab relevant data belonging to Jita, including type_ids featured on the oldest date. Save as "MasterData" table
	cur.execute("CREATE TABLE MasterData AS SELECT average AS price, 'TheForge' || '-' || date AS region, volume, TheForgeHistory.type_id AS type_id FROM marketdatadb.TheForgeHistory, type_ids, ReportDates WHERE TheForgeHistory.type_id = type_ids.type_id AND date = ReportDates AND average < 1500000000;")

While this only results in a marginal change from 82.3MB to 82.2MB in the size of the output .csv file, it is still generally a good practice to identify and eliminate any potential source of bias in our reporting.

Next, after our R script has finished calculating indices per region/date, we now need to tweak our second Python script to weigh these indices against the freight fees associated with each region. We can divide each reward cost offered by Red Frog Freight by the maximum collateral value (1.5B ISK) to obtain freight cost as a percentage of cargo value per region:

  • Domain (Amarr): 3.93%
  • Essence (Oursulaert): 1.29%
  • Metropolis (Hek): 1.85%
  • Sinq Laison (Dodixie): 1.53%
  • Heimatar (Rens): 2.33%
  • Tash-Murkon (Tash-Murkon Prime): 4.09%

Since collateral is usually calculated based off of ‘The Forge’ market value, we can multiply these percentages by our price indices for ‘The Forge’ to obtain a weighted freight cost per day, which can then be subtracted by the price indices for every other region per day to generate a freight-adjusted index. Our modified script which incorporates these values now looks like this, with changes highlighted in bold:

...
### define regions to report against
regionlist = [['Domain', .0393], ['Essence', .0129], ['Metropolis', .0185], ['SinqLaison', .0153], ['Heimatar', .0233], ['TashMurkon', .0409]]

indices = ['laspeyres', 'medgeworth', 'toernqvist', 'gkhamis']

### change working directory to script location
pathtorunfile = os.path.dirname(__file__)
os.chdir(pathtorunfile)

### runtime
if __name__ == '__main__':

	for index in indices:
		## create in-memory database to temporarily store tables used for report generation	
		tempdb = sqlite3.connect(":memory:")
		cur = tempdb.cursor()
		
		## import index data from our R script
		df = pd.read_csv(f'{index}.csv')
		df.columns = ['x', 'y']
		df.to_sql('IndexData', tempdb, if_exists='append', index=False)
		
		# grab and plot data for TheForge. Since we are using this as our 'base', it will have a thicker line width than other regions
		cur.execute(f"CREATE TABLE ForgeData AS SELECT SUBSTR (x, -10)  AS date, y AS TargetRegion FROM IndexData WHERE x LIKE '%TheForge%';")
		ResultData = pd.read_sql(f"SELECT * FROM ForgeData", tempdb)
		ResultData['date']= pd.to_datetime(ResultData['date'])
		plt.figure(figsize=(10,6))
		plt.plot(ResultData.date,ResultData.TargetRegion,label = 'TheForge',linewidth=3)
		
		# grab and plot data for all other regions
		for region in regionlist:
			cur.execute(f"CREATE TABLE FeeTemp AS SELECT date, TargetRegion * {region[1]} AS RegionFee FROM ForgeData;")
			cur.execute(f"UPDATE IndexData SET y = y - RegionFee FROM FeeTemp WHERE x LIKE '%{region[0]}%'")
			cur.execute("DROP TABLE FeeTemp;")
			ResultData = pd.read_sql(f"SELECT SUBSTR (x, -10)  AS date, y AS TargetRegion FROM IndexData WHERE x LIKE '%{region[0]}%';", tempdb)
			ResultData['date']= pd.to_datetime(ResultData['date'])
			plt.plot(ResultData.date,ResultData.TargetRegion,label = region[0])

		...

Plotting our results reveal our freight-adjusted Marshall-Edgeworth, Laspeyres, Törnqvist and Geary-Khamis plot graphs, in that order:

The difference between these and our prior graphs is quite stark; by incorporating freight costs into our price indices, we have almost entirely eliminated the nominal price differentials seen earlier, with price indices for ‘The Forge’ for the most part resting comfortably in-between those of other regions.

Conclusion

Nominal price differentials between minor trade hubs and ‘The Forge’ can be largely attributed to freight costs. Despite this, prices outside ‘The Forge’ are still substantially more volatile, leaving frequent windows of opportunity open for profitable interregional trade for players who are willing to deal with the overhead associated with managing contracts and traveling across space to place and update orders. Furthermore, players who are willing to haul their own goods and invest in a vertically-integrated trade network have the capacity to extract substantially more profit by taking-on the calculated risks associated with potentially being attacked by other players.

In our next article we will use the lessons learned here and from prior exercises to develop a comprehensive reporting strategy for interregional trade. Stay tuned, and thank you for reading!

*Update: “Virtual Markets, Part Eleven: Interregional Trade” is now live!

Sept 30 2024 Update: Up-to-date ISK Volume and Price Index charts are now published here under the Projects page. These charts are automatically updated once per day.