opcalc 0.2.1

An easy-to-use black-scholes option calculator. Made for JS, built in Rust.
Documentation
import React, { useEffect, useState } from "react";
import "./App.css";

const getSecondTimestamp = (dt: Date): number => {
  return Math.floor(dt.getTime() / 1000);
};

type OpCalcType = typeof import("opcalc");

interface OptionDefinition {
  assetPrice: number;
  strike: number;
  volatility: number;
  interest: number;
  currTime: Date;
  expiryTime: Date;
}

interface OptionOutputs {
  value: number;
  delta: number;
  gamma: number;
  vega: number;
  theta: number;
}

interface CallPutOutputs {
  call: OptionOutputs;
  put: OptionOutputs;
}

function computeOptionOutputs(
  input: OptionDefinition,
  opcalc: OpCalcType
): Promise<CallPutOutputs> {
  return new Promise((resolve, reject) => {
    try {
      const option = opcalc
        .create_option()
        .with_asset_price(input.assetPrice)
        .with_strike(input.strike)
        .with_volatility(input.volatility)
        .with_interest(input.interest)
        .with_current_time(getSecondTimestamp(input.currTime))
        .with_maturity_time(getSecondTimestamp(input.expiryTime))
        .finalize();

      resolve({
        call: {
          value: option.call_value(),
          delta: option.call_delta(),
          gamma: option.call_gamma(),
          vega: option.call_vega(),
          theta: option.call_theta(),
        },
        put: {
          value: option.put_value(),
          delta: option.put_delta(),
          gamma: option.put_gamma(),
          vega: option.put_vega(),
          theta: option.put_theta(),
        },
      });
    } catch (e) {
      reject(e);
    }
  });
}

enum OpCalcLoadStatus {
  NotLoaded,
  Loaded,
  LoadErred,
}

/** Load the `opcalc` module, or report loading error status. */
const useOpCalcModuleImport = () => {
  const [{ instance: opcalc, loadStatus }, setOpCalcModule] = useState<{
    instance: OpCalcType | undefined;
    loadStatus: OpCalcLoadStatus;
  }>({ instance: undefined, loadStatus: OpCalcLoadStatus.NotLoaded });

  // use the 'opcalc' string literal here to prevent a webpack bundle splitting
  // bailout. Imports with variable path names cannot be statically analyzed
  // and therefore cannot be used for webpack optimizations.
  useEffect(() => {
    import("opcalc")
      .then((instance) =>
        setOpCalcModule({ instance, loadStatus: OpCalcLoadStatus.Loaded })
      )
      .catch((e) => {
        console.error(e);
        setOpCalcModule({
          instance: undefined,
          loadStatus: OpCalcLoadStatus.LoadErred,
        });
      });
  }, []);

  return {
    opcalc,
    loadStatus: {
      loaded: loadStatus === OpCalcLoadStatus.Loaded,
      erred: loadStatus === OpCalcLoadStatus.LoadErred,
    },
  };
};

const useOpCalc = (initialOptionInput: OptionDefinition) => {
  const { opcalc, loadStatus } = useOpCalcModuleImport();

  const [optionInput, updateInput] = useState<OptionDefinition>(
    initialOptionInput
  );

  const [optionOutputs, setOptionOutputs] = useState<
    CallPutOutputs | undefined
  >(undefined);

  useEffect(() => {
    if (!opcalc) return;

    computeOptionOutputs(optionInput, opcalc)
      .then((res) => setOptionOutputs(res))
      .catch((e) => console.error(e));
  }, [opcalc, optionInput]);

  return {
    currentOptionInput: optionInput,
    updateInput,
    outputs: optionOutputs,
    loadStatus,
  };
};

const initialOptionInput: OptionDefinition = {
  assetPrice: 100,
  strike: 105,
  volatility: 0.23,
  interest: 0.005,
  currTime: new Date(2020, 11, 1),
  expiryTime: new Date(2021, 0, 15),
};

function App() {
  const { outputs, loadStatus, currentOptionInput, updateInput } = useOpCalc(
    initialOptionInput
  );

  return (
    <div className="App">
      <main>
        <section className="demo-body">
          <h1>
            <code className="header">OpCalc</code>
          </h1>

          <OptionInput input={currentOptionInput} onChange={updateInput} />

          {!loadStatus.loaded ? (
            <Loading />
          ) : loadStatus.erred ? (
            <LoadErred />
          ) : (
            <OutputTable data={outputs} />
          )}
        </section>
      </main>
    </div>
  );
}

const Loading: React.FC = () => {
  return <span className="user-message">Loading option calculator...</span>;
};

const LoadErred: React.FC = () => {
  return (
    <span className="user-message">
      An error has occurred during calculation.
    </span>
  );
};

const OptionInput: React.FC<{
  input: OptionDefinition;
  onChange: (input: OptionDefinition) => void;
}> = ({ input, onChange: onInputChange }) => {
  const daysBetweenDates = (start: Date, end: Date) => {
    return Math.floor(
      (end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)
    );
  };

  return (
    <div className="option-input-container">
      <NumericInput
        name="Asset Price"
        input={input.assetPrice}
        min={0}
        onChange={(assetPrice) => onInputChange({ ...input, assetPrice })}
      />

      <NumericInput
        name="Strike Price"
        input={input.strike}
        min={0}
        onChange={(strike) => onInputChange({ ...input, strike })}
      />

      <NumericInput
        name="Interest Rate"
        input={input.interest}
        step={0.001}
        onChange={(interest) => onInputChange({ ...input, interest })}
      />

      <NumericInput
        name="Volatility"
        input={input.volatility}
        step={0.01}
        onChange={(volatility) => onInputChange({ ...input, volatility })}
      />

      <NumericInput
        name="Days to maturity"
        input={daysBetweenDates(input.currTime, input.expiryTime)}
        step={1}
        min={0}
        onChange={(newDayDiff) => {
          const newExpiryTime = new Date(
            input.currTime.getTime() + newDayDiff * 24 * 60 * 60 * 1000
          );
          onInputChange({ ...input, expiryTime: newExpiryTime });
        }}
      />
    </div>
  );
};

const NumericInput: React.FC<{
  name: string;
  input: number;
  min?: number;
  max?: number;
  step?: number;
  onChange: (newValue: number) => unknown;
}> = ({
  name,
  input,
  min = Number.NEGATIVE_INFINITY,
  max = Number.POSITIVE_INFINITY,
  step = 1,
  onChange,
}) => {
  const formatInputId = (inputName: string) => {
    return "option-input-form__" + inputName.toLowerCase().split(" ").join("-");
  };

  return (
    <div className="form-entry">
      <label htmlFor={formatInputId(name)}>{name}</label>
      <input
        type="number"
        id={formatInputId(name)}
        min={min}
        max={max}
        step={step}
        value={input}
        onChange={(e) => onChange(Number.parseFloat(e.target.value))}
      ></input>
    </div>
  );
};

const OutputTable: React.FC<{ data: CallPutOutputs | undefined }> = ({
  data,
}) => {
  return (
    <table className="output">
      <tbody>
        <tr>
          <th></th>
          <th>Value</th>
          <th>Delta</th>
          <th>Gamma</th>
          <th>Vega</th>
          <th>Theta</th>
        </tr>

        <tr>
          <th>Call</th>
          <DecimalCell value={data?.call.value} />
          <DecimalCell value={data?.call.delta} />
          <DecimalCell value={data?.call.gamma} />
          <DecimalCell value={data?.call.vega} />
          <DecimalCell value={data?.call.theta} />
        </tr>

        <tr>
          <th>Put</th>
          <DecimalCell value={data?.put.value} />
          <DecimalCell value={data?.put.delta} />
          <DecimalCell value={data?.put.gamma} />
          <DecimalCell value={data?.put.vega} />
          <DecimalCell value={data?.put.theta} />
        </tr>
      </tbody>
    </table>
  );
};

const DecimalCell: React.FC<{
  value: number | undefined;
  decimalCount?: number;
}> = ({ value, decimalCount = 5 }) => {
  return <td>{value?.toFixed(decimalCount)}</td>;
};

export default App;