Table of Contents
Abstract
This concept paper proposes that the area of a marketplace, modeled as a non-equilateral triangle, is related to the profit distribution between unequal-sized firms in arbitrage situations. Arbitrage, the practice of exploiting price differences across markets, is a fundamental concept in finance and economics, and most treatments focus on the price discrepancy itself. We introduce a geometric framework, using integral calculus, in which the partition of a triangular market space determines how a profit density is divided among market segments. Under a constant profit density, each segment’s share of profit equals its share of the area, which gives the hypothesis a testable form. As an exploratory illustration, we compare price-volume curves and ARIMA forecasts for NYMEX WTI and ICE Brent crude oil futures from January 2022 to January 2024. The comparison recovers the familiar Brent-WTI price differential, which reflects differences in crude grade and delivery location and therefore carries basis risk. The paper does not yet test the hypothesis formally; we outline the data on firm size and segment-level profits that such a test would require.
Introduction
Arbitrage, the practice of exploiting price differences in different markets, is central to financial theory. This paper explores the hypothesis that the area of the marketplace, modeled as a non-equilateral triangle, is related to the profit distribution between unequal-sized firms in arbitrage situations. We propose a geometric framework, using integral calculus, to describe how market structure could shape economic interactions and arbitrage opportunities.
Crude oil illustrates the setting well. The same commodity trades on several exchanges (for example, WTI on NYMEX and Brent on ICE), and the persistent price differential between the two benchmarks has a large literature of its own. We use these two contracts as an exploratory case.
Hypothesis and Theoretical Framework
\(H_0\): The area of the marketplace, represented as a non-equilateral triangle, has no significant effect on the profit distribution between unequal-sized firms in an arbitrage situation.
\(H_1\): The area of the marketplace, represented as a non-equilateral triangle, is significantly related to the profit distribution between unequal-sized firms in an arbitrage situation.
The theoretical framework draws on geometric modeling and integral calculus, and is motivated by geometric arbitrage theory. The marketplace is modeled as a non-equilateral triangle in a Cartesian coordinate system, where the area of the triangle represents the market space. This representation lets us state how the partition of that space among market segments translates into a partition of profits.
Mathematical Framework
Let the marketplace be represented by a non-equilateral triangle \(\triangle ABC\) in a Cartesian plane. Points \(A\), \(B\), and \(C\) denote the vertices of the market space, with each side representing a different market boundary.
Figure 1: \(\triangle ACD\) represents Market A, and \(\triangle ADB\) represents Market B
A line segment \(AD\), where \(D\) lies on \(BC\), divides the triangle into two segments: Market A (\(\triangle ACD\)) and Market B (\(\triangle ADB\)). The triangle is positioned on a Cartesian coordinate plane to facilitate the analysis of vertical integration and spatial relationships.
To quantify the total trade potential within the marketplace, we use the geometric area of \(\triangle ABC\), which represents the market space bounded by firms or segments. A larger area reflects a broader scope for arbitrage activity and profit realization. For a triangle with vertices \(A = (x_A, y_A)\), \(B = (x_B, y_B)\), and \(C = (x_C, y_C)\), the area follows from the determinant (shoelace) formula:
\[\begin{equation} \text{Area}(\triangle ABC) = \frac{1}{2}\left|x_A(y_B-y_C) + x_B(y_C-y_A) + x_C(y_A-y_B)\right| \end{equation}\]
Assume the profit in each segment is proportional to its area. Let \(P_A\) and \(P_B\) denote the profits in Market A and Market B:
\[\begin{equation} P_A = k \cdot \text{Area}(\triangle ACD) \end{equation}\]
\[\begin{equation} P_B = k \cdot \text{Area}(\triangle ADB) \end{equation}\]
where \(k\) is a constant reflecting market conditions and efficiency. It follows that each market’s profit share equals its area share, \(P_A / (P_A + P_B) = \text{Area}(\triangle ACD) / \text{Area}(\triangle ABC)\), independent of \(k\).
The dividing segment \(AD\), with \(A = (x_A, y_A)\) and \(D = (x_D, y_D)\), lies on the line
\[\begin{equation} y - y_A = m(x - x_A), \qquad m = \frac{y_D - y_A}{x_D - x_A} \end{equation}\]
provided \(x_D \neq x_A\) (if \(AD\) is vertical, the line is simply \(x = x_A\)). The segment areas follow from the same determinant formula, for example
\[\begin{equation} \text{Area}(\triangle ADB) = \frac{1}{2}\left|x_A(y_D-y_B) + x_D(y_B-y_A) + x_B(y_A-y_D)\right| \end{equation}\]
and, because \(D\) lies on \(BC\), the two segments exhaust the market:
\[\begin{equation} \text{Area}(\triangle ABC) = \text{Area}(\triangle ACD) + \text{Area}(\triangle ADB) \end{equation}\]
The area can equivalently be written as an iterated integral. Orient the triangle so that \(BC\) is the base; then
\[\begin{equation} \text{Area}(\triangle ABC) = \int_{x_C}^{x_B}\left(\int_{y_{\text{bottom}}(x)}^{y_{\text{top}}(x)}dy\right)dx \end{equation}\]
where \(y_{\text{bottom}}(x)\) is the line through \(B\) and \(C\), and \(y_{\text{top}}(x)\) is the line through \(C\) and \(A\) for \(x \le x_A\) and the line through \(A\) and \(B\) for \(x \ge x_A\).
Now let a profit density \(p(x,y)\) be defined over the market space. With the dividing point \(D\) directly below \(A\) (\(x_D = x_A\)), the profits in each market are
\[\begin{equation} P_A = \iint_{\triangle ACD} p(x,y)\,dA = \int_{x_C}^{x_D}\int_{y_{\text{bottom}}(x)}^{y_{\text{top}}(x)} p(x,y)\,dy\,dx \end{equation}\]
\[\begin{equation} P_B = \iint_{\triangle ADB} p(x,y)\,dA = \int_{x_D}^{x_B}\int_{y_{\text{bottom}}(x)}^{y_{\text{top}}(x)} p(x,y)\,dy\,dx \end{equation}\]
When \(p(x,y) = k\) is constant, these reduce to equations (2) and (3). A non-constant density lets profit concentrate in part of the market space, so area share and profit share can diverge; measuring that divergence is one way to test \(H_1\) against data.
The boundary between the two markets can be parameterized as
\[\begin{equation} \mathbf{r}(t) = (1-t)\mathbf{A} + t\mathbf{D}, \qquad 0 \le t \le 1 \end{equation}\]
and the profit density accumulated along that boundary is the line integral
\[\begin{equation} \int_{AD} p(x,y)\,ds = \int_{0}^{1} p(\mathbf{r}(t))\,\lVert\mathbf{r}'(t)\rVert\,dt, \qquad \lVert\mathbf{r}'(t)\rVert = \lVert \mathbf{D} - \mathbf{A} \rVert \end{equation}\]
When does profit net to zero?
Consider a linear profit potential \(\pi(x,y) = -x + y\), in which price (along \(x\)) reduces profit and access or demand (along \(y\)) increases it. For a linear function over any region, the integral equals the area times the function’s value at the centroid \((\bar{x}, \bar{y})\):
\[\begin{equation} \iint_{\triangle ABC} (-x + y)\,dA = \text{Area}(\triangle ABC)\,(\bar{y} - \bar{x}), \qquad (\bar{x}, \bar{y}) = \left(\tfrac{x_A + x_B + x_C}{3}, \tfrac{y_A + y_B + y_C}{3}\right) \end{equation}\]
The total is zero only when the centroid lies on the line \(y = x\). For the triangle in Figure 2 below, with \(A=(0,3)\), \(B=(6,0)\), \(C=(-2,0)\), the area is 12 and the centroid is \((4/3, 1)\), so the integral equals \(12 \times (1 - 4/3) = -4\). Geometry alone therefore does not force profits to balance. A system-wide balance has to come from an economic assumption, such as no-arbitrage, which constrains the profit density itself. This distinction matters for the empirical section: if a market is efficient in the no-arbitrage sense, persistent price differences between segments should reflect costs and risks (transport, quality, financing) and should not yield riskless profit.
Market Space Dynamics in Commodity Trading
Consider a simplified commodity market represented as a non-equilateral triangle, where the x-axis represents price and the y-axis represents quantity traded. The triangular market space is bounded by the price limits and the maximum quantity that can be traded.
Figure 2: \(\triangle ACD\) represents Market A, and \(\triangle ADB\) represents Market B
The area of \(\triangle ADB\), with \(A=(0,3)\), \(D=(0,0)\), and \(B=(6,0)\), is
\[\begin{equation} \text{Area}(\triangle ADB) = \frac{1}{2}\times \text{base}\times \text{height} = \frac{1}{2}\times6\times3=9 \end{equation}\]
The same value follows from integration. The upper edge \(AB\) runs from \((0,3)\) to \((6,0)\), so its slope is \(-\tfrac{1}{2}\) and \(y_{\text{top}}(x) = -\tfrac{1}{2}x + 3\):
\[\begin{equation} \int_{0}^{6} \left(-\frac{1}{2}x+3\right) dx = \left[-\frac{x^2}{4} + 3x\right]_{0}^{6} = -9 + 18 = 9 \end{equation}\]
For Market A, the edge \(CA\) runs from \((-2,0)\) to \((0,3)\), so \(y_{\text{top}}(x) = \tfrac{3}{2}x + 3\):
\[\begin{equation} \text{Area}(\triangle ACD) = \int_{-2}^{0} \left(\frac{3}{2}x+3\right) dx = \left[\frac{3x^2}{4} + 3x\right]_{-2}^{0} = 0 - (3 - 6) = 3 \end{equation}\]
The total market space is \(3 + 9 = 12\), matching \(\tfrac{1}{2} \times 8 \times 3\) for base \(CB\). Under a constant profit density, Market A earns \(3/12 = 25\%\) of total profit and Market B earns \(75\%\). This is the quantity \(H_1\) concerns: if Market B corresponds to the larger firm, the model predicts a 3:1 profit split driven entirely by the geometry of market access.
Practical Implications
Profit Distribution
In the illustration above, profit shares follow directly from area shares. Moving the dividing point \(D\) along \(BC\) changes the split continuously, which gives a simple way to describe how changes in market access (a new pipeline, an exchange listing, a regulatory change) would redistribute profit between segments. With a non-constant density \(p(x,y)\), the same calculation shows where in price-quantity space the profit is concentrated.
Arbitrage Opportunities
Within this framework, an arbitrage opportunity is a region where realized prices depart from the equilibrium relationship implied by the market space. If observed prices at a given quantity sit persistently above or below that relationship in one segment relative to another, the gap is a candidate opportunity. Whether it is a true arbitrage depends on whether the gap exceeds the cost of moving the commodity or position between segments.
Market Efficiency
Under efficiency, those gaps should be no larger than transaction, transport, and risk costs, so the expected excess profit across segments is zero. Section 4 shows this balance is an economic restriction on \(p(x,y)\) and does not follow from the triangle’s geometry. The empirical question is therefore whether observed differences between segments exceed what costs and risks can explain.
Literature Review
The relationship between market structure and arbitrage opportunities has a long history in financial and agricultural economics. Much of the arbitrage literature focuses on price discrepancies; geometric and spatial approaches add structure to where those discrepancies arise and who captures them.
Geometric Arbitrage Theory
Farinelli and Takada (2021) develop Geometric Arbitrage Theory (GAT), which restates classical stochastic finance in the language of differential geometry. Markets are modeled as principal fibre bundles, and the curvature of the bundle’s connection measures instantaneous arbitrage capability. Arbitrage conditions such as No-Free-Lunch-with-Vanishing-Risk (NFLVR) and No-Unbounded-Profit-with-Bounded-Risk (NUPBR) are characterized in geometric terms, and the zero eigenspace of the connection Laplacian parameterizes the risk-neutral measures equivalent to the statistical one. This extends asset bubble theory to markets that do not satisfy NFLVR. The present paper borrows GAT’s motivation, that market geometry carries information about arbitrage, while working with far simpler planar geometry.
Spatial Price Equilibrium and Limits of Arbitrage
Samuelson (1952) showed that equilibrium prices across spatially separated markets can be found by solving a linear programming problem, with price differences between connected markets equal to transport costs. This spatial price equilibrium idea is the natural benchmark for the triangle model: segments that are geometrically separated should sustain price gaps up to the cost of crossing between them. Shleifer and Vishny (1997) explain why gaps can persist even beyond those costs, since real arbitrage requires capital and carries risk, and specialized arbitrageurs with limited capital may be unable to close mispricings fully. Both ideas bear on the hypothesis: the size and access of a firm determine how much of a cross-segment gap it can actually capture.
The Brent-WTI Spread
The crude oil case connects to a substantial literature on benchmark price differentials. Fattouh (2010) studies the dynamics of crude oil price differentials and finds that differentials between crudes of different quality are generally stationary, consistent with linked markets. Büyükşahin et al. (2013) examine the widening of the WTI-Brent spread after 2010 and relate it to physical market conditions, including inventory build-ups at Cushing, alongside the role of financial traders. Both studies indicate that a nonzero Brent-WTI spread is a normal feature of the market, which shapes how the results below should be read.
Methods
Daily closing prices and volumes for the front-month NYMEX WTI (CL=F) and ICE Brent (BZ=F) continuous futures were downloaded from Yahoo Finance for January 2022 through December 2023. Series were set to a business-day frequency, missing values were forward filled, and zero-volume days were dropped.
WTI and Brent volumes differ in scale, and in the raw data their volume ranges overlap on only a handful of days. Each contract’s daily volume was therefore converted to its own percentile rank \(v \in (0, 1]\). For each contract, a price-volume curve was built by sorting observations by volume percentile (averaging prices for tied ranks) and interpolating linearly between them. The area under each curve was computed exactly over the volume-percentile range \([v_{\min}, v_{\max}]\) shared by both contracts, then divided by the width of that range:
\[\begin{equation} \bar{P} = \frac{1}{v_{\max} - v_{\min}} \int_{v_{\min}}^{v_{\max}} f(v)\,dv \end{equation}\]
The normalized area \(\bar{P}\) is the average height of the price-volume curve, expressed in dollars per barrel. Because volume percentiles are evenly spaced, each trading day carries equal weight, and \(\bar{P}\) is close to the contract’s mean price over the sample. The area representation adds a view of how price varies across low- and high-activity days (Figure 3), while the comparison of \(\bar{P}\) across contracts is in effect a comparison of average price levels.
To extend the comparison forward, a non-seasonal ARIMA model was fit to each closing-price series using a stepwise search over orders up to \((3, d, 3)\), selected by AIC. A seasonal component was not used, since business-day data give no basis for a fixed seasonal period. Each model produced a 60-business-day forecast. Volume is not forecast, so the forecasted comparison uses the mean forecast price for each contract.
Results
Table 1 summarizes the comparison. WTI (CL=F) sits below Brent (BZ=F) on both measures. Across all 518 trading days, Brent averaged $4.68 per barrel above WTI, and the gap stood at $5.39 at the end of 2023.
Table 1: Normalized price-volume areas and mean 60-day forecasts (USD per barrel)
| Measure | WTI (CL=F) | Brent (BZ=F) | Brent minus WTI |
|---|---|---|---|
| Normalized area, Jan 2022 to Dec 2023 | 85.95 | 90.63 | 4.68 |
| Mean 60-day ARIMA forecast | 71.65 | 77.04 | 5.39 |
Figure 3 shows mean price within 20 equal-width volume-percentile bins for each contract, with dashed lines at the normalized areas reported in Table 1. Brent sits above WTI in all but a few bins, and neither contract shows a consistent relationship between price and relative trading activity, so the area comparison mainly reflects the level difference between the two benchmarks. Figure 4 shows closing prices from January 2022 to January 2024.
Figure 3: Mean Price by Volume Percentile for NYMEX WTI and ICE Brent (January 2022 to January 2024)
Figure 4: NYMEX WTI and ICE Brent Crude Oil Closing Prices Over Time
The stepwise search selected ARIMA(0,1,0) for both WTI (AIC = 2,373.36) and Brent (AIC = 2,382.71). This is a random walk, so the best forecast of each price is its last observed close, and the 60-day forecasts in Figure 5 are flat at $71.65 for WTI and $77.04 for Brent. The forecast spread of $5.39 is therefore the Brent-WTI spread at the end of 2023. The random-walk result is consistent with weak-form efficiency in both futures markets: past prices alone carried no usable information about the direction of the next move.
Figure 5: NYMEX WTI and ICE Brent Crude Oil 60-Day ARIMA Forecast
The difference between the two normalized areas is the average Brent-WTI differential across comparable levels of relative trading activity. A long WTI, short Brent position based on this ordering is a spread trade. WTI is a lighter, sweeter crude priced for delivery at Cushing, Oklahoma, while Brent is a waterborne North Sea benchmark, so the spread compensates for quality, location, and transport differences and moves with conditions at both delivery points. Profiting from it requires the spread to narrow, and the position carries basis risk. In the language of the framework, the two contracts occupy different segments of the market space, and the differential is the price of crossing between them. A test of \(H_1\) would ask whether firms with greater access to both segments (for example, integrated producers or traders with shipping capacity) capture a larger share of that differential than smaller firms.
Limitations
Several limitations apply. The triangle model simplifies market dynamics, and the assumption that market space is well represented by a non-equilateral triangle may not hold. The hypothesis concerns unequal-sized firms, but this paper uses only exchange-level price and volume data and does not measure firm size or firm-level profits, so \(H_0\) and \(H_1\) are not formally tested here.
The empirical illustration has its own constraints. Continuous front-month futures series contain roll effects in both price and volume. Converting volume to percentiles makes the two contracts comparable but discards information about absolute trading activity, so the analysis cannot speak to how the size of a market affects pricing. The price-volume curve treats volume as an ordering variable and ignores time, so it does not capture how the price-volume relationship shifts over the sample. The ARIMA forecasts reduce to random walks and were not evaluated out of sample, so they serve only as a forward extension of the end-of-sample spread. Finally, the comparison ignores transaction, financing, and transport costs, which determine whether any observed differential is exploitable.
Conclusion
This paper represents market space as a non-equilateral triangle and shows that, under a constant profit density, each segment’s profit share equals its area share. The worked example yields a 3:1 split from geometry alone, and the framework makes clear that system-wide profit balance must come from an economic assumption such as no-arbitrage, which constrains the profit density beyond what the geometry provides.
The crude oil illustration recovers the Brent-WTI differential, which the literature attributes to quality, location, and physical market conditions. The natural next step is a formal test of \(H_1\) using firm-level data, for example comparing how integrated and non-integrated firms capture cross-market differentials. Further extensions include estimating non-constant profit densities from transaction data, accounting for transport and financing costs, and evaluating forecasts out of sample.
References
Büyükşahin, B., Lee, T. K., Moser, J. T., & Robe, M. A. (2013). Physical markets, paper markets and the WTI-Brent spread. The Energy Journal, 34(3), 129–151.
Farinelli, S., & Takada, H. (2021). Can you hear the shape of a market? Geometric arbitrage and spectral theory. Axioms, 10(4), 242. https://doi.org/10.3390/axioms10040242
Fattouh, B. (2010). The dynamics of crude oil price differentials. Energy Economics, 32(2), 334–342.
Samuelson, P. A. (1952). Spatial price equilibrium and linear programming. The American Economic Review, 42(3), 283–303.
Shleifer, A., & Vishny, R. W. (1997). The limits of arbitrage. The Journal of Finance, 52(1), 35–55.
Appendix: Market Space Dynamics Algorithm
1: Input: Historical price and volume data for each security 2: Output: Normalized areas, forecasts, and price-differential summary 3: Step 1: Data Collection and Cleaning 4: Download daily close and volume; set business-day frequency 5: Forward fill missing values; drop zero-volume days 5a: Convert each security's volume to its own percentile rank 6: Step 2: Build Price-Volume Curves 7: for each security do 8: Sort observations by volume percentile; average prices at tied ranks 9: fsecurity(v) = linear interpolation of price on volume percentile 10: end for 11: Step 3: Normalized Areas 12: [vmin, vmax] = volume-percentile range shared by every security 13: for each security do 14: area = ∫vminvmax fsecurity(v) dv (exact trapezoid over knots) 15: P̄security = area / (vmax − vmin) 16: end for 17: Step 4: Forecast 18: for each security do 19: Fit non-seasonal ARIMA by stepwise AIC search 20: Forecast h business days; record mean forecast price 21: end for 22: Step 5: Compare 23: Identify lowest and highest P̄ and the spread between them 24: Repeat using mean forecast prices 25: Step 6: Plot binned price by volume percentile, closing prices, and forecasts 26: End Algorithm
Appendix: MarketSpaceDynamics Class Implementation in Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 |
"""MarketSpaceDynamics: price-volume area comparison and ARIMA forecast. Run as a script to regenerate the figures and Table 1 values for the paper: python market_space_dynamics.py """ import numpy as np import pandas as pd import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import yfinance as yf from pmdarima import auto_arima class MarketSpaceDynamics: def __init__(self, tickers, start_date, end_date, forecast_steps=60, labels=None): self.tickers = tickers self.start_date = start_date self.end_date = end_date self.forecast_steps = forecast_steps self.labels = labels if labels is not None else {t: t for t in tickers} self.data = {} self.fetch_data() def fetch_data(self): for ticker in self.tickers: raw = yf.download( ticker, start=self.start_date, end=self.end_date, auto_adjust=False, progress=False, ) # Recent yfinance versions return MultiIndex columns even for one ticker if isinstance(raw.columns, pd.MultiIndex): raw.columns = raw.columns.get_level_values(0) df = raw[["Close", "Volume"]].asfreq("B").ffill() # Drop zero-volume rows (holidays, roll artifacts) df = df[df["Volume"] > 0].rename(columns={"Close": "Price"}) # Volume scales differ across exchanges, so each contract's volume # is expressed as its own percentile rank (0 to 1). Both curves then # share the same axis and every trading day contributes. df["VolPct"] = df["Volume"].rank(pct=True) self.data[ticker] = df def volume_range(self): """Full span of volume percentile across all tickers (used for plotting).""" lo = min(self.data[t]["VolPct"].min() for t in self.tickers) hi = max(self.data[t]["VolPct"].max() for t in self.tickers) return float(lo), float(hi) def overlap_range(self): """Volume range observed for every ticker (used for areas). Outside a ticker's own observed range, np.interp holds the end value flat, so integrating over the full span would let one boundary price dominate the area. Restricting to the overlap avoids that. """ lo = max(self.data[t]["VolPct"].min() for t in self.tickers) hi = min(self.data[t]["VolPct"].max() for t in self.tickers) if hi <= lo: raise ValueError("Volume ranges do not overlap; areas are not comparable.") return float(lo), float(hi) @staticmethod def price_volume_curve(df): """Mean price at each volume percentile, sorted by percentile. np.interp requires increasing x values, so the series is sorted by volume percentile and tied percentiles are averaged. """ curve = df.groupby("VolPct")["Price"].mean().sort_index() return curve.index.to_numpy(dtype=float), curve.to_numpy(dtype=float) def price_volume_function(self, df): xp, fp = self.price_volume_curve(df) return lambda x: np.interp(x, xp, fp) def calculate_area(self, df, lower_limit, upper_limit): """Exact integral of the piecewise-linear price-volume curve. The trapezoid rule is exact for a piecewise-linear function when it is evaluated at every knot, so no numerical quadrature is needed. """ xp, fp = self.price_volume_curve(df) inside = xp[(xp > lower_limit) & (xp < upper_limit)] x = np.concatenate([[lower_limit], inside, [upper_limit]]) y = np.interp(x, xp, fp) return float(np.sum((x[1:] - x[:-1]) * (y[1:] + y[:-1]) / 2)) def normalized_areas(self): """Area under each price-volume curve divided by the volume range. This equals the average height of the curve over the overlapping volume range, so the result is in price units. """ lo, hi = self.overlap_range() return { t: self.calculate_area(self.data[t], lo, hi) / (hi - lo) for t in self.tickers } def forecast_prices(self, df, steps): # Pass plain values: dropping zero-volume days leaves gaps in the # date index, which statsmodels cannot forecast from. model = auto_arima( df["Price"].to_numpy(dtype=float), start_p=1, start_q=1, max_p=3, max_q=3, seasonal=False, # no justified 12-period season in business-day data stepwise=True, suppress_warnings=True, trace=False, ) forecast = np.asarray(model.predict(n_periods=steps)) index = pd.bdate_range(df.index[-1] + pd.Timedelta(days=1), periods=steps) return pd.DataFrame({"Forecast": forecast}, index=index), model @staticmethod def compare(values): low = min(values, key=values.get) high = max(values, key=values.get) return { "low": (low, values[low]), "high": (high, values[high]), "spread": values[high] - values[low], } # ---------- plots ---------- def plot_price_volume_curves(self, path=None, n_bins=20): """Binned price-volume curves with each contract's normalized area. Daily points are too noisy to read, so prices are averaged within equal-width volume-percentile bins. Dashed lines mark the normalized areas used in Table 1. """ areas = self.normalized_areas() edges = np.linspace(0, 1, n_bins + 1) mids = (edges[:-1] + edges[1:]) / 2 plt.figure(figsize=(10, 6)) for i, t in enumerate(self.tickers): df = self.data[t] binned = df.groupby(pd.cut(df["VolPct"], edges, include_lowest=True), observed=False)["Price"].mean().to_numpy() color = f"C{i}" plt.plot(mids, binned, marker="o", color=color, label=self.labels[t]) plt.axhline(areas[t], color=color, linestyle="--", linewidth=1, label=f"{self.labels[t]} normalized area ({areas[t]:.2f})") plt.xlabel("Volume percentile") plt.ylabel("Mean price (USD)") plt.title("Price by Volume Percentile") plt.legend() self._finish(path) def plot_closing_prices(self, path=None): plt.figure(figsize=(10, 6)) for t in self.tickers: plt.plot(self.data[t].index, self.data[t]["Price"], label=self.labels[t]) plt.xlabel("Date") plt.ylabel("Price (USD)") plt.title("Closing Prices Over Time") plt.legend() self._finish(path) def plot_forecasts(self, forecasts, path=None): plt.figure(figsize=(10, 6)) for i, t in enumerate(self.tickers): color = f"C{i}" # same color per contract as Figure 4 plt.plot(self.data[t].index, self.data[t]["Price"], color=color, label=f"{self.labels[t]} Historical") plt.plot(forecasts[t].index, forecasts[t]["Forecast"], color=color, linestyle="--", label=f"{self.labels[t]} Forecast") plt.xlabel("Date") plt.ylabel("Price (USD)") plt.title(f"{self.forecast_steps}-Day ARIMA Forecast") plt.legend() self._finish(path) @staticmethod def _finish(path): plt.tight_layout() if path: plt.savefig(path) plt.close() else: plt.show() # ---------- driver ---------- def run_analysis(self, save_figures=False): current = self.compare(self.normalized_areas()) forecasts, models = {}, {} for t in self.tickers: forecasts[t], models[t] = self.forecast_prices(self.data[t], self.forecast_steps) forecasted = self.compare({t: float(forecasts[t]["Forecast"].mean()) for t in self.tickers}) if save_figures: self.plot_price_volume_curves("price_volume_curves.svg") self.plot_closing_prices("closing_prices.svg") self.plot_forecasts(forecasts, "market_analysis_forecast.svg") for name, res in [("Current (normalized area)", current), ("Forecasted (mean forecast price)", forecasted)]: print(f"{name}:") print(f" Lower: {res['low'][0]} = {res['low'][1]:.2f}") print(f" Higher: {res['high'][0]} = {res['high'][1]:.2f}") print(f" Spread: {res['spread']:.2f}") for t, m in models.items(): print(f"{t}: ARIMA{m.order}, AIC = {m.aic():.2f}") lo, hi = self.overlap_range() print(f"Overlapping volume-percentile range: {lo:.3f} to {hi:.3f}") for t in self.tickers: v = self.data[t]["VolPct"] n_in = int(((v >= lo) & (v <= hi)).sum()) print(f" {t}: {n_in} of {len(v)} days inside the overlap") return current, forecasted, forecasts, models if __name__ == "__main__": labels = {"CL=F": "NYMEX WTI Crude Oil", "BZ=F": "ICE Brent Crude Oil"} analysis = MarketSpaceDynamics( ["CL=F", "BZ=F"], "2022-01-01", "2024-01-01", forecast_steps=60, labels=labels, ) analysis.run_analysis(save_figures=True) |