diff --git a/src/service/index.d.ts b/src/service/index.d.ts index 108084e..fc49bdc 100644 --- a/src/service/index.d.ts +++ b/src/service/index.d.ts @@ -152,3 +152,33 @@ export interface AmericanRawParams { steps: string; optionType: string; } + +export interface KikoParams { + spot: number; + strike: number; + timeToMaturity: number; + riskFreeRate: number; + volatility: number; + lowerBarrier: number; + upperBarrier: number; + observeTime: number; + rebate: number; +} + +export interface KikoRawParams { + spot: string; + strike: string; + timeToMaturity: string; + riskFreeRate: string; + volatility: string; + lowerBarrier: string; + upperBarrier: string; + observeTime: string; + rebate: string; +} + +export interface KikoResult { + value: number; + std: number; + confInt: number[]; +} diff --git a/src/service/kiko.ts b/src/service/kiko.ts index e69de29..92b9e78 100644 --- a/src/service/kiko.ts +++ b/src/service/kiko.ts @@ -0,0 +1,246 @@ +import { Gaussian } from "ts-gaussian"; +import { KikoParams, KikoResult } from "service"; +import { create, all, Matrix } from "mathjs"; +import * as lobos from "lobos"; + +/** + * Instance of mathjs + */ +const math = create(all, { + number: "number", +}); + +/** + * Instance of Gaussian Distribution + */ +const std = new Gaussian(0, 1); + +/** + * @constant NUM_OF_PATHS + * @description number of paths in Monte Carlo + */ +const NUM_OF_PATHS = 100; + +/** + * Class for KIKO Put option + * @class Kiko + * @implements KikoParams + */ +export default class Kiko implements KikoParams { + spot: number; + + strike: number; + + timeToMaturity: number; + + riskFreeRate: number; + + volatility: number; + + /** + * down-and-in at lowerBarrier + * up-and-out at upperBarrier with rebate + */ + lowerBarrier: number; + + upperBarrier: number; + + observeTime: number; + + rebate: number; + + private interval: number; // Delta t + + private samples: number[][]; + + private samplesCumSum: number[][]; + + private spots: Matrix; + + private values: number[]; + + constructor(args: KikoParams) { + this.spot = args.spot; + this.strike = args.strike; + this.timeToMaturity = args.timeToMaturity; + this.riskFreeRate = args.riskFreeRate; + this.volatility = args.volatility; + this.lowerBarrier = args.lowerBarrier; + this.upperBarrier = args.upperBarrier; + this.observeTime = args.observeTime; + this.rebate = args.rebate; + this.interval = math.divide(this.timeToMaturity, this.observeTime); + this.samples = []; + this.samplesCumSum = []; + this.values = []; + this.setSamples(this.samples); + this.setCumSum(this.samplesCumSum); + this.spots = this.setSpots(); + this.setValues(this.values); + } + + /** + * Set the samples 2D array with std normal samples + * @param samples [M][N] array + * @returns Promise + */ + private async setSamples(samples: number[][]): Promise { + return new Promise((resolve) => { + const sequence = new lobos.Sobol(this.observeTime, { + params: "new-joe-kuo-6.21201", + resolution: 32, + }); + /** + * The uniformSamples slice the first row, take care + */ + const uniformSamples: number[][] = sequence + .take(NUM_OF_PATHS + 1) + .slice(1); + for (let m = 0; m < NUM_OF_PATHS; m += 1) { + samples[m] = []; + for (let n = 0; n < this.observeTime; n += 1) { + samples[m].push( + math.add( + ( + math.multiply( + this.interval, + math.subtract( + this.riskFreeRate, + math.multiply( + 0.5, + math.pow(this.volatility, 2) + ) + ) + ) + ), + ( + math.multiply( + math.multiply( + this.volatility, + std.ppf(uniformSamples[m][n]) + ), + math.sqrt(this.interval) + ) + ) + ) + ); + } + } + resolve(); + }); + } + + private async setCumSum(samplesCumSum: number[][]): Promise { + return new Promise((resolve) => { + for (let n = 0; n < this.observeTime; n += 1) { + samplesCumSum[n] = []; + let colSum = 0; + for (let m = 0; m < NUM_OF_PATHS; m += 1) { + colSum += this.samples[m][n]; + samplesCumSum[n].push(colSum); + } + } + this.samplesCumSum = math.transpose(samplesCumSum); + resolve(); + }); + } + + private setSpots(): Matrix { + const result = math.multiply( + this.spot, + this.samplesCumSum.map((row: number[]) => { + return row.map((x: number) => math.exp(x)); + }) + ); + return result; + } + + private setValues(values: number[]): Promise { + const getRow = (m: Matrix, r: number) => + math.flatten(math.row(m, r).valueOf()); + + // eslint-disable-next-line no-unused-vars + const getCol = (m: Matrix, c: number) => + math.flatten(math.column(m, c).valueOf()); + + return new Promise((resolve) => { + const { spots } = this; + const size = math.size(spots); + for (let m = 0; m < size[0]; m += 1) { + const pathMax = math.max(...getRow(spots, m)); + const pathMin = math.min(...getRow(spots, m)); + if (pathMax >= this.upperBarrier) { + // Up-and-out + const outTime = getRow(spots, m).indexOf(pathMax); + const payoff = math.multiply( + this.rebate, + math.exp( + 0 - + math.multiply( + math.multiply(outTime, this.riskFreeRate), + this.interval + ) + ) + ); + values.push(payoff); + } else if (pathMin <= this.lowerBarrier) { + // Down-and-in + const finalValue = getRow(spots, m).at(-1) as number; + const payoff = math.multiply( + ( + math.exp( + 0 - + math.multiply( + this.riskFreeRate, + this.timeToMaturity + ) + ) + ), + math.max(this.strike - finalValue, 0) + ); + values.push(payoff); + } else { + // No knock out or knock in + values.push(0); + } + } + resolve(); + }); + } + + public getResults(): KikoResult { + const value = math.mean(...this.values); + const stdDev = math.std(...this.values); + const upper = math.add( + value, + ( + math.divide( + math.multiply(1.96, stdDev), + math.sqrt(NUM_OF_PATHS) + ) + ) + ); + const lower = math.subtract( + value, + ( + math.divide( + math.multiply(1.96, stdDev), + math.sqrt(NUM_OF_PATHS) + ) + ) + ); + return { + value, + std: stdDev, + confInt: [lower, upper], + }; + } + + public debug(): void { + console.log("Samples:", this.samples); + console.log("CumSum:", this.samplesCumSum); + console.log("Spots:", this.spots); + console.log("Values:", this.values); + console.log(this.getResults()); + } +} diff --git a/src/view/KikoView.tsx b/src/view/KikoView.tsx index 9e0153e..6034aff 100644 --- a/src/view/KikoView.tsx +++ b/src/view/KikoView.tsx @@ -1,10 +1,10 @@ import React, { useEffect, useState } from "react"; -import { Button, Col, Form, Input, Row, Select, Table } from "antd"; +import { Button, Col, Form, Input, Row, Table } from "antd"; import { ColumnsType } from "antd/es/table"; -import { ExtendedBSParams, ExtendedBSRawParams } from "service"; -import ExtendedBS from "service/extendedBs"; +import { KikoParams, KikoRawParams, KikoResult } from "service"; import "../style/view/global.css"; import { parseRawParams } from "utils/utils"; +import Kiko from "service/kiko"; const layout = { labelCol: { span: 8 }, @@ -40,35 +40,53 @@ const ResultColumns: ColumnsType = [ }, ]; -const parseData = (ebs: ExtendedBSParams | undefined): ResultDataType[] => { - if (ebs) { +const parseData = ( + kikoParams: KikoParams | undefined, + kiko: KikoResult | undefined +): ResultDataType[] => { + if (kikoParams && kiko) { return [ { key: "1", key1: "Spot Price", - value1: ebs.spot, + value1: kikoParams.spot, key2: "Strike", - value2: ebs.strike, + value2: kikoParams.strike, }, { key: "2", - key1: "Term to Maturity", - value1: ebs.termToMaturity, + key1: "Time to Maturity", + value1: kikoParams.timeToMaturity, key2: "Risk-Free Rate", - value2: ebs.riskFreeRate, + value2: kikoParams.riskFreeRate, }, { key: "3", - key1: "Repo Rate", - value1: ebs.repoRate, - key2: "Volatility", - value2: ebs.volatility, + key1: "Volatility", + value1: kikoParams.volatility, + key2: "Rebate", + value2: kikoParams.rebate, }, { key: "4", key1: "Option Type", - value1: - ebs.optionType === "C" ? "European Call" : "European Put", + value1: "KIKO Put", + key2: "Observe Time", + value2: kikoParams.observeTime, + }, + { + key: "5", + key1: "Lower Barrier", + value1: kikoParams.lowerBarrier, + key2: "Upper Barrier", + value2: kikoParams.upperBarrier, + }, + { + key: "6", + key1: "Std Dev", + value1: kiko.std, + key2: "Conf Int", + value2: `[${kiko.confInt[0]}, ${kiko.confInt[1]}]`, }, ]; } @@ -76,23 +94,28 @@ const parseData = (ebs: ExtendedBSParams | undefined): ResultDataType[] => { }; const ResultTable: React.FunctionComponent<{ - value: number; - ebsParams: ExtendedBSParams | undefined; -}> = (props: { value: number; ebsParams: ExtendedBSParams | undefined }) => { + kikoParams: KikoParams | undefined; + kiko: KikoResult | undefined; +}> = (props: { + kikoParams: KikoParams | undefined; + kiko: KikoResult | undefined; +}) => { const tableTitle = () => { - return

Option value by Extened Black Scholes: {props.value}

; + if (props.kiko) + return

KIKO Put value by Quasi-MC: {props.kiko.value}

; + return undefined; }; return ( <> {" "} - {props.ebsParams ? ( + {props.kikoParams ? ( = () => { - const [ebsParams, setEbsParams] = useState( + const [kikoParams, setKikoParams] = useState( undefined ); - const [ebs, setEbs] = useState(undefined); + const [kiko, setKiko] = useState(undefined); const [form] = Form.useForm(); useEffect(() => { - if (typeof ebsParams !== "undefined") setEbs(new ExtendedBS(ebsParams)); - }, [ebsParams]); + if (kikoParams) setKiko(new Kiko(kikoParams)); + }, [kikoParams]); + + /* + useEffect(() => { + if (kiko) kiko.debug(); + }, [kiko]); + */ - const onFinish = (value: ExtendedBSRawParams) => { - setEbsParams( - parseRawParams(value) - ); + const onFinish = (value: KikoRawParams) => { + setKikoParams(parseRawParams(value)); }; const onReset = () => { form.resetFields(); - setEbsParams(undefined); - setEbs(undefined); + setKikoParams(undefined); + setKiko(undefined); }; return ( @@ -144,12 +171,19 @@ const KikoView: React.FunctionComponent<{}> = () => { + + + = () => { + + + - + @@ -210,12 +244,12 @@ const KikoView: React.FunctionComponent<{}> = () => { - {typeof ebs !== "undefined" ? ( + {kiko ? (