<jupyter_start><jupyter_text>Bokeh taught me JavaScript[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/jburgy/blog/blob/master/notebooks/Black-Scholes.ipynb) A story of trade-offs in 4 actsTL;DR [Bokeh](https://bokeh.pydata.org/en/latest) is an amazing framework which lets data scientists define interactive web apps entirely in python.  This requires complex synchronization between a client written in JavaScript and a server written in python.  The resulting applications, though easy to develop, are trick to [deploy](https://bokeh.pydata.org/en/latest/docs/user_guide/server.htmldeployment-scenarios).  We show through a sample app how Bokeh users can trade off some of that development ease to produce an app that's easier to deploy. Act IPretend you're trying to understand the [Black-Scholes formula](https://en.wikipedia.org/wiki/Black%E2%80%93Scholes_modelBlack%E2%80%93Scholes_formula) for pricing [European](https://en.wikipedia.org/wiki/Option_styleAmerican_and_European_options) [Call options](https://en.wikipedia.org/wiki/Call_option).  You can easily create an interactive visualization using [Bokeh Widgets](https://bokeh.pydata.org/en/latest/docs/user_guide/interaction/widgets.html).  Beacuse you're more comfortable with [python](https://www.python.org/) than [JavaScript](https://www.javascript.com/), you decide to [run a Bokeh Server](https://bokeh.pydata.org/en/latest/docs/user_guide/server.html).  After all, the "capability to synchronize between python and the browser is the main purpose of the Bokeh server."The first version of our app is a pretty straightforward adaptation of the [embed server in notebook](https://github.com/bokeh/bokeh/blob/master/examples/hotwo/server_embed/notebook_embed.ipynb) example.  As in the example, [show](https://bokeh.pydata.org/en/latest/docs/reference/io.htmlbokeh.io.show) creates a [Bokeh Server](https://bokeh.pydata.org/en/latest/docs/reference/server/server.htmlbokeh.server.server.Server) which then wraps the ```modify_doc``` [callable](https://docs.python.org/3/library/functions.htmlcallable) in a [FunctionHandler](https://bokeh.pydata.org/en/latest/docs/reference/application/handlers/function.htmlbokeh.application.handlers.function.FunctionHandler) inside an [Application](https://bokeh.pydata.org/en/latest/docs/reference/application/application.htmlbokeh.application.application.Application).  You should already begin to see the number of layers.  What's even more impressive is the lifecycle of a typical request to that server.  First, a graph of python models is created on the server.  These python models are serialized to [JSON](https://en.wikipedia.org/wiki/JSON), with some very clever handling of model types and references, then sent to the client.  Once they reach the client, they are rehydrated to equivalent JavaScript models.  Those models are then rendered to views and the corresponding views are [embedded](https://bokeh.pydata.org/en/latest/docs/user_guide/embed.html) in the web page. User interactions are communicated back to the server over [websockets](https://en.wikipedia.org/wiki/WebSocket) where they are applied to the original python models.  The effects they have on server-side python models are sent back to the client and applied to their JavaScript counterparts.  If you add that the whole process can be initiated in a [Jupyter Notebook](https://jupyter.org) and your head doesn't already spin, then you're not paying attention! What's even better is that it just works. Simply amazing!<jupyter_code>from bokeh.io import output_notebook, show
output_notebook()
from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource, Slider
from bokeh.plotting import figure
from math import erf, exp, log, sqrt

SQRT_2 = sqrt(2)

def phi(x: float) -> float:
    return (1 + erf(x / SQRT_2)) / 2

def black_scholes(s_over_k: float, r: float, sigma: float, t: float) -> float:
    sigma_sqrt_t = max(sigma * sqrt(t), 1e-8)
    d1 = (log(s_over_k) + r * t) / sigma_sqrt_t + sigma_sqrt_t / 2
    d2 = d1 - sigma_sqrt_t
    return phi(d1) * s_over_k - phi(d2) * exp(-r * t)

def modify_doc(doc) -> None:
    source = ColumnDataSource(data=dict(
        s_over_k=[x * .02 for x in range(25, 75)],
        c_over_k=[max(x * .02, 0) for x in range(-25, 25)],
    ))
    
    ρ = Slider(value=.01, start=0, end=.2, step=.005, title='ρ')
    σ = Slider(value=.2,  start=0, end=.5, step=.01,  title='σ')
    τ = Slider(value=0,   start=0, end=2,  step=.25,  title='τ')
    
    def callback(attr: str, old: float, new: float) -> None:
        s_over_k, r, sigma, t = source.data['s_over_k'], ρ.value, σ.value, τ.value
        source.data = dict(
            s_over_k=s_over_k,
            c_over_k=[black_scholes(x, r, sigma, t) for x in s_over_k],
        )
        
    ρ.on_change('value', callback)
    σ.on_change('value', callback)
    τ.on_change('value', callback)
    
    plot = figure(title='Call Option', x_axis_label='S/K', y_axis_label='C/K')
    plot.line('s_over_k', 'c_over_k', source=source)
    
    doc.add_root(row(column(ρ, σ, τ), plot))
    
show(modify_doc)<jupyter_output><empty_output><jupyter_text>Act IIThis is the point where we encounter our first challenge.  So far, you've done all of this on your development machine.  You've installed python and the required dependencies.  Everything works just great.  Now your colleague wants to use your tool as well.  You have a number of options, none of them particularly attractive in a corporate environment if your department code doesn't say 'IT'.  Fear not, Bokeh is extremely flexible and lets you make certain tradeoffs.  Remember that the documentation on [Adding Widgets](https://bokeh.pydata.org/en/latests/docs/interactions/widgets.html) mentioned two ways to attach behavior to widget interactions.  We only explored the [Bokeh Server](https://bokeh.pydata.org/en/latest/docs/user_guide/server.html) approach because we didn't want to write any JavaScript.  This is where the story gets a little wild: Bokeh lets you write [JavaScript Callbacks](https://bokeh.pydata.org/en/latest/docs/user_guide/interaction/callbacks.htmljavascript-callbacks) without actually writing [any JavaScript](https://bokeh.pydata.org/en/latest/docs/user_guide/interaction/callbacks.htmlcustomjs-with-a-python-function)!  Bokeh leverages [PScript](https://pscript.readthedocs.io/en/latest/index.html) to _translate_ a strict subset of python to JavaScript.  In this setup, interactions no longer need to be sent to the server.  A server is in fact no longer necessary.  The original python models are discared after they've been serialized to the client.  It can't be just this easy, surely there must be a catch.The catch revolves are the "script subset of python" that [PScript supports](https://pscript.readthedocs.io/en/latest/guide.html).  The thing to remember is that although [PScript is valid Python](https://pscript.readthedocs.io/en/latest/intro.htmlpscript-is-valid-python), at the end of the day [PScript is just JavaScript](https://pscript.readthedocs.io/en/latest/intro.hmltpscript-is-just-javascript).  The biggest problem you'll face with the original app is that ```pythonfrom math import erf, exp, log, sqrt```is not supported. What's even worse is that JavaScript doesn't have a built-in [error function](https://en.wikipedia.org/wiki/Error_function).  Grr.  Look, I said there were going to be trade-offs.  Fortunately, <NAME> suggested the following formula in his article on [Evaluating the Normal Distribution](https://www.jstatsoft.org/article/view/v011i04/v11i04.pdf):$$\Phi(x)=\frac{1}{2}+\phi(x)\sum_{k=1}^{\infty}\frac{x^{2k-1}}{(2k-1)!!}$$The series converges faster than the client-server round-trip.  As a consequence, the user experience improves because the application feels more responsive.Notice a number of other differences between this version and the original:* ```Math.exp``` and ```Math.sqrt``` now replace ```exp```, respectively ```sqrt```* callbacks are registered by [```js_on_change```](https://bokeh.pydata.org/en/latest/docs/reference/model.htmlbokeh.model.Model.js_on_change) instead of [```on_change```](https://bokeh.pydata.org/en/latest/docs/reference/model.htmlbokeh.model.Model.on_change)* we show the model graph directly instead of adding it as a root to the provided documentNote that if you ran that code in a python script and used [output_file](https://bokeh.pydata.org/en/latest/docs/reference/io.htmlbokeh.io.output_file) instead of [output_notebook](https://bokeh.pydata.org/en/latest/docs/reference/io.htmlbokeh.io.output_notebook), it would generate a self-contained HTML file that users can open directly of that you can serve directly from your favorite static content web server.<jupyter_code>from bokeh.models import CustomJS
from pscript import py2js

source = ColumnDataSource(data=dict(
    s_over_k=[x * .02 for x in range(25, 75)],
    c_over_k=[max(x * .02, 0) for x in range(-25, 25)],
))

ρ = Slider(value=.01, start=0, end=.2, step=.005, title='ρ')
σ = Slider(value=.2,  start=0, end=.5, step=.01,  title='σ')
τ = Slider(value=0,   start=0, end=2,  step=.25,  title='τ')

def callback(source, ρ, σ, τ):
    PSCRIPT_OVERLOAD = False

    def phi(x):
        s, t, u, j, q = x, 0, x, 1, x * x
        while s != t:
            t = s
            j += 2
            u *= q / j
            s += u
        if s == -Infinity:
            return 0
        if s == Infinity:
            return 1
        return 0.5 + s * Math.exp(-0.5 * q - 0.9189385332046728)

    def black_scholes(s_over_k, r, sigma, t):
        sigma_sqrt_t = Math.max(sigma * Math.sqrt(t), 1e-8)
        d1 = (Math.log(s_over_k) + r * t) / sigma_sqrt_t + sigma_sqrt_t / 2
        d2 = d1 - sigma_sqrt_t
        return phi(d1) * s_over_k - phi(d2) * Math.exp(-r * t)
    
    s_over_k, r, sigma, t = source.data['s_over_k'], ρ.value, σ.value, τ.value
    source.data = dict(
        s_over_k=s_over_k,
        c_over_k=[black_scholes(x, r, sigma, t) for x in s_over_k],
    )

js_callback = CustomJS(
    code=py2js(callback, "cb") + "cb(source, ρ, σ, τ);\n",
    args=dict(source=source, ρ=ρ, σ=σ, τ=τ),
)
    
ρ.js_on_change('value', js_callback)
σ.js_on_change('value', js_callback)
τ.js_on_change('value', js_callback)

plot = figure(title='Call Option', x_axis_label='S/K', y_axis_label='C/K')
plot.line('s_over_k', 'c_over_k', source=source)

show(row(column(ρ, σ, τ), plot))<jupyter_output><empty_output><jupyter_text>Act IIINow that you crossed the [Pons asinorum](https://en.wikipedia.org/wiki/Pons_asinorum) and overcame your reservations towards JavaScript, you notice that debugging transpiled code is no picnic.  Releasing software into the wild is a great way to be reminded of [Rick Cook's quote](https://en.wikipedia.org/wiki/Rick_Cook) that "the Universe is winning."  Pretty soon, your users complain that the charts go blank or freeze for particular choices of inputs (true story, the fact that ```NaN !== NaN``` in JavaScript interacts in a fun and unexpected way with the termination condition in ```phi```).  At this point, it's not a big leap to grab the translated JavaScript, prettify it by hand a little, and call it your own.  That would give something like the following version.<jupyter_code>from bokeh.models import CustomJS

source = ColumnDataSource(data=dict(
    s_over_k=[x * .02 for x in range(25, 75)],
    c_over_k=[max(x * .02, 0) for x in range(-25, 25)],
))

ρ = Slider(value=.01, start=0, end=.2, step=.005, title='ρ')
σ = Slider(value=.2,  start=0, end=.5, step=.01,  title='σ')
τ = Slider(value=0,   start=0, end=2,  step=.25,  title='τ')

js_callback = CustomJS(args=dict(source=source, ρ=ρ, σ=σ, τ=τ), code='''
function phi(x) {
    if (Number.isNaN(x)) {
        return x;
    }
    let s = x;
    let t = 0;
    let u = x;
    let j = 1;
    let q = x * x;
    while (s !== t) {
        t = s;
        j += 2;
        u *= q / j;
        s += u;
    }
    if (s === -Infinity) {
        return 0;
    }
    if (s === Infinity) {
        return 1;
    }
    return 0.5 + s * Math.exp(-0.5 * q - 0.9189385332046728);
}

function black_scholes(s_over_k, r, sigma, t) {
    const sigma_sqrt_t = Math.max(sigma * Math.sqrt(t), 1e-8);
    const d1 = (Math.log(s_over_k) + r * t) / sigma_sqrt_t + sigma_sqrt_t / 2;
    const d2 = d1 - sigma_sqrt_t;
    return phi(d1) * s_over_k - phi(d2) * Math.exp(-r * t);
}

const { data: { s_over_k } } = source;
const { value: r } = ρ;
const { value: sigma } = σ;
const { value: t } = τ;
source.data = {
    s_over_k,
    c_over_k: s_over_k.map(x => black_scholes(x, r, sigma, t)),
};''')
    
ρ.js_on_change('value', js_callback)
σ.js_on_change('value', js_callback)
τ.js_on_change('value', js_callback)

plot = figure(title='Call Option', x_axis_label='S/K', y_axis_label='C/K')
plot.line('s_over_k', 'c_over_k', source=source)

show(row(column(ρ, σ, τ), plot))<jupyter_output><empty_output><jupyter_text>Act IV (dénouement)At this point, you might ask yourself: hang on a minute, I'm no longer relying on PScript to translate my callback from python to JavaScript, couldn't I do the same with my models?  If Bokeh is clever enough to rehydrate a model graph from a JSON blob, then surely it must provide APIs to declare it directly in JavaScript.  Well, what do you know, [it does](https://bokeh.pydata.org/en/latest/docs/user_guide/bokehjs.html)!  Now don't get too excited.  At this point, you've already strayed rather far from "the main goal of Bokeh \[...\] to provide a path to create rich interactive visualizations in the browser, **purely from python**" \[emphasis added\]. You should therefore expect commensurate support.  For instance, Bokeh plots are complicated beasts, so much so that Bokeh provides a convenient [factory function](https://bokeh.pydata.org/en/latest/docs/reference/plotting.htmlbokeh.plotting.figure.figure) to simplify their construction.  The JavaScript equivalent is not part of the scripts that are linked by default.  This explains why the fourth and last implementation is significantly longer than the previous three.  The flipside is that now you can write the whole thing in an HTML file yourself and dispense with python altogether (``````).<jupyter_code>%%javascript

const models = new Proxy(Bokeh.Models, { get(target, prop) { return target(prop) } });
const source = new models.ColumnDataSource({ data: {
    s_over_k: Array.from({ length: 100 }, (_, i) => i * .02 - .5),
    c_over_k: Array.from({ length: 100 }, (_, i) => Math.max(i * .02 - 1, 0)),
}});

const ρ = new Bokeh.Widgets.Slider({ value: .01, start: 0, end: .2, step: .005, title: 'ρ' });
const σ = new Bokeh.Widgets.Slider({ value: .2,  start: 0, end: .5, step: .01,  title: 'σ' });
const τ = new Bokeh.Widgets.Slider({ value: 0,   start: 0, end: 2,  step: .25,  title: 'τ' });

function phi(x) {
    if (Number.isNaN(x)) {
        return x;
    }
    let s = x;
    let t = 0;
    let u = x;
    let j = 1;
    let q = x * x;
    while (s !== t) {
        t = s;
        j += 2;
        u *= q / j;
        s += u;
    }
    if (s === -Infinity) {
        return 0;
    }
    if (s === Infinity) {
        return 1;
    }
    return 0.5 + s * Math.exp(-0.5 * q - 0.9189385332046728);
}

function black_scholes(s_over_k, r, sigma, t) {
    const sigma_sqrt_t = Math.max(sigma * Math.sqrt(t), 1e-8);
    const d1 = (Math.log(s_over_k) + r * t) / sigma_sqrt_t + sigma_sqrt_t / 2;
    const d2 = d1 - sigma_sqrt_t;
    return phi(d1) * s_over_k - phi(d2) * Math.exp(-r * t);
}

function callback() {
    const { data: { s_over_k } } = source;
    const { value: r } = ρ;
    const { value: sigma } = σ;
    const { value: t } = τ;
    source.data = {
        s_over_k,
        c_over_k: s_over_k.map(x => black_scholes(x, r, sigma, t)),
    };
}

ρ.properties.value.change.connect(callback);
σ.properties.value.change.connect(callback);
τ.properties.value.change.connect(callback);

const plot = new models.Plot({
    title: 'Call Option',
    x_range: new models.DataRange1d(),
    x_scale: new models.LinearScale(),
    y_range: new models.DataRange1d(),
    y_scale: new models.LinearScale(),
});
plot.add_tools(
    new models.PanTool({ dimensions: 'both' }),
    new models.WheelZoomTool({ dimensions: 'both' }),
    new models.BoxZoomTool({ dimensions: 'both' }),
    new models.SaveTool(),
    new models.ResetTool(),
    new models.HelpTool(),
);

const xAxis = new models.LinearAxis({ axis_label: 'S/K' });
plot.add_layout(xAxis, 'below');
plot.add_layout(new models.Grid({ dimension: 0, ticker: xAxis.ticker }));

const yAxis = new models.LinearAxis({ axis_label: 'C/K' });
plot.add_layout(yAxis, 'left');
plot.add_layout(new models.Grid({ dimension: 1, ticker: yAxis.ticker }));

plot.add_glyph(new models.Line({
    x: { field: 's_over_k' },
    y: { field: 'c_over_k' },
    line_color: '#1f77b4',
    line_alpha: 1,
}), source);

const root = new models.Row({
    children: [
        new models.WidgetBox({
            children: [ρ, σ, τ],
            width: 300,
            sizing_mode: 'fixed',
        }),
        plot,
    ]
});

const { Document } = Bokeh.require('document');
const doc = new Document();
doc.add_root(root);

Bokeh.embed.add_document_standalone(doc, element[0]);<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.1308265465257321, 'fraction_numerical': 0.01605729798417374, 'mean_word_length': 3.7723263506063947, 'pattern_counts': {'":': 0, '<': 18, '<?xml version=': 0, '>': 26, 'https://': 39, 'lorem ipsum': 0, 'www.': 3, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '4', 'dir': 'jupyter-structured-clean-dedup', 'id': '214058', 'license': 'Apache-2.0', 'n_tokens_mistral': 6070, 'n_tokens_neox': 5747, 'n_words': 1773, 'path': 'notebooks/Black-Scholes.ipynb', 'repo_name': 'jburgy/blog', 'size': '17323'}