From Yield Curves to 3D Vol Surfaces: A Practical Guide to FX Options Visualization

6 min read Original article ↗

DolphinDB

Press enter or click to view image in full size

FX options, volatility smiles, term structures — how do you turn complex financial data into something you can actually read? In this article, we walk through DolphinDB’s built-in plot() function using an FX option volatility surface as a running example, showing you how to build professional 2D and 3D charts with mouse-driven rotation, zoom, and hover tooltips.

1. Core Parameters of the plot() Function

DolphinDB’s plot() is a lightweight yet powerful charting function. No third-party libraries required — it takes you straight from data to visualization entirely within the database.

plot(data, [labels], title, chartType, [stacking], [extras])

2. Setting Up the Data: USDCNY FX Option Volatility Surface

Before we can plot anything, we need a volatility surface to work with. We’ll build one using DolphinDB’s built-in fxVolatilitySurfaceBuilder. The inputs are option quotes across 13 tenors for five quote types — ATM, D25_RR, D25_BF, D10_RR, D10_BF — which gives us enough resolution for smooth charts.

refDate = 2025.08.18
ccyPair = "USDCNY"
spot = 7.1627
quoteTerms = ["1d","1w","2w","3w","1M","2M","3M","6M","9M","1y","18M","2y","3y"]
quoteNames = ["ATM","D25_RR","D25_BF","D10_RR","D10_BF"]
rawQuotes = [
0.030000,-0.007500, 0.003500,-0.010000, 0.005500,
...(13×5 = 65 values in total)
0.044750, 0.006250, 0.003400, 0.009000, 0.008550]
quotes = reshape(rawQuotes, 5:13).transpose() // → 13-row × 5-column matrix
// Build interest rate curves and volatility surface
domesticCurve = parseMktData(domesticCurveInfo) // CNY IrYieldCurve
foreignCurve = parseMktData(foreignCurveInfo) // USD IrYieldCurve
surf = fxVolatilitySurfaceBuilder(
refDate, ccyPair, quoteNames, quoteTerms, quotes, spot,
domesticCurve, foreignCurve, "SVI")

With the surface ready, we can now start plotting!

Example 1: 2D Line Chart (LINE) — CNY vs USD Interest Rate Curves

Many quant strategies require monitoring both CNY and USD interest rate levels simultaneously. The catch is that CNY and USD rates often sit at very different absolute levels — plot them on a shared Y-axis and the smaller series gets squashed into a flat line, making it unreadable.

plot() solves this with dual Y-axes, letting both curves display clearly on the same chart.

In this example, we use curvePredict to sample rate values at 100 evenly-spaced dates from the vol surface object we built earlier, then plot both curves as smooth lines.

// Prepare date array: 100 evenly-spaced points from 1M to 3Y
startDate = curveDates[4] // start at 1M, skipping short-end noise
endDate = curveDates[12] // 3Y
nPts = 100
span = endDate - startDate
dates100 = array(DATE, nPts)
for (i in 0:nPts) { dates100[i] = startDate + int(i * span / (nPts - 1.0)) }

cnyDense = curvePredict(domesticCurve, dates100) * 100.0
usdDense = curvePredict(foreignCurve, dates100) * 100.0

// Build matrix & assign labels
labels100 = array(STRING, nPts)
for (i in 0:nPts) { labels100[i] = string(dates100[i]) }\

mCurves = matrix(cnyDense, usdDense) // 100-row × 2-col
mCurves.rename!(labels100, ["CNY (%)", "USD (%)"])
// ↑ X-axis ↑ legend labels

// Plot
plot(mCurves, ,
["CNY vs USD Interest Rate Curves", "Date", "Rate (%)"],
LINE, false, { multiYAxes: true, autoScaleYAxes: true })

Press enter or click to view image in full size

Example 2: 3D Surface Chart (SURFACE) — Volatility Surface by Strike

Implied volatility in FX options isn’t flat — it varies across both expiry and strike, forming a complex surface. A 3D surface chart makes this easy to read at a glance: you can immediately see where vol is elevated and where it’s suppressed.

In this example, we use optionVolPredict to query 630 IV values across a 30-date × 21-strike grid, then render them as an interactive 3D surface.

// Data preparation
nK = 21; nT = 30
denseStrikes = globalKmin + ... * double(0..(nK - 1)) // 21 evenly-spaced strikes
denseDates = ... // 30 evenly-spaced dates
volMat = optionVolPredict(surf, denseDates, denseStrikes) * 100.0 // 30×21 matrix
volMat.rename!(dateLabels, strikeLabels)
// ↑ Y-axis (row labels) ↑ X-axis (col labels)
// Plot
plot(volMat, ,
["USDCNY Volatility Surface (Strike, 30×21)", "Strike", "Maturity Date", "Implied Vol (%)"],
SURFACE)

Press enter or click to view image in full size

Example 3: 3D Surface Chart (SURFACE) — Volatility Surface by Delta

FX options traders often prefer to look at volatility through the lens of Delta — the option’s sensitivity to the spot rate — rather than strike. The reason is practical: ATM strikes differ across tenors, so using Delta provides a standardized basis for comparison.

Before plotting, we need to build a 9-point Delta grid: 10P, 20P, 30P, 40P, ATM, 40C, 30C, 20C, 10C.

The X-axis maps to Delta — running from 10P through ATM to 10C across 9 points. The Y-axis covers 9 tenors, and the Z-axis shows implied volatility.

//Plot
plot(deltaIvMat, ,
["USDCNY Volatility Surface (Delta, 9×9)", "Delta", "Tenor", "Implied Vol (%)"],
SURFACE)

Press enter or click to view image in full size

Pro tip

Since deltaIvMat already has labels set via rename!, there's no need to pass a labels argument explicitly:

  • Row labels = ["1M","2M",...,"3y"] → Y-axis
  • Column labels = ["10P","20P",...,"10C"] → X-axis
  • Matrix values = IV (%) → Z-axis

To switch between the strike view and the delta view, all you need to change is data and title — the SURFACE call structure stays identical.

Example 4: Line Chart (LINE) — Vol Smile by Strike, Multiple Tenors

Sometimes a full 3D surface is more than you need. If the goal is to compare vol smile shapes across a few representative tenors — how implied volatility curves as you move away from ATM — a simple multi-line chart gets the point across more directly.

In this example, we select 5 tenors and plot each one as a line showing IV against strike.

// Data preparation
pickNames = ["1M","3M","6M","1Y","3Y"]
pickDates = [usedDates[0], usedDates[2], usedDates[3], usedDates[5], usedDates[8]]
strikeMat = optionVolPredict(surf, pickDates, denseStrikes) * 100.0 // 5 rows × 21 cols
mVolStrike = strikeMat.transpose() // 21 rows × 5 cols: one line per column
mVolStrike.rename!(strikeLabels, pickNames)
// ↑ X-axis ticks ↑ legend labels
// Plot
plot(mVolStrike, ,
["Vol vs Strike Smile", "Strike", "Implied Vol (%)"],
LINE, false, { autoScaleYAxes: true })

Press enter or click to view image in full size

Example 5: Line Chart (LINE) — Vol Smile by Delta

In the previous chart, using strike as the X-axis means each tenor’s ATM sits at a different horizontal position — the curves shift left and right, making it harder to directly compare the shape of the smile.

Switching to Delta as the X-axis eliminates that problem. ATM always corresponds to Delta = 50 regardless of tenor, so all curves are anchored at the same center point. This makes it much easier to compare smile asymmetry (skew) and curvature across tenors.

// Data preparation
pickIdx = [0, 2, 3, 5, 8] // indices into usedTerms
mVolDelta = matrix(
flatten(deltaIvMat[pickIdx[0], ]), // 9 delta points for row 0 (1M)
flatten(deltaIvMat[pickIdx[1], ]), // row 2 (3M)
flatten(deltaIvMat[pickIdx[2], ]),
flatten(deltaIvMat[pickIdx[3], ]),
flatten(deltaIvMat[pickIdx[4], ]))
mVolDelta.rename!(deltaLabels, pickNames)
// ↑ X-axis: 10P...10C ↑ legend: 1M/3M/6M/1Y/3Y
// Plot
plot(mVolDelta, ,
["Vol vs Delta Smile", "Delta", "Implied Vol (%)"],
LINE, false, { autoScaleYAxes: true })

Press enter or click to view image in full size

Summary: What Makes DolphinDB’s plot() Stand Out

Requires DolphinDB 3.00.5 or later.

plot() is a small function with a big idea behind it: keeping computation and visualization in the same place. Whether you're running quant backtests, monitoring risk, or putting together an investment research report, you no longer need to export a CSV and switch to Python just to draw a chart. The entire journey from raw data to insight happens inside one DolphinDB script.

If you’d like to try it yourself, download the community edition or visit our website to explore more financial use cases.

To get the full demo script used in this article, reach out to us on LinkedIn or drop us an email at info@dolphindb.com.