From 39930cea19e6ee7f8f3016e3099082fbf9178c0f Mon Sep 17 00:00:00 2001
From: Eskil Gjerde Sviggum <Eskils@users.noreply.github.com>
Date: Tue, 3 Sep 2024 13:20:34 +0200
Subject: [PATCH] [OHLCV Converter] Add option to use Close for Open, High and
 Low when Volume is zero (#28)

* Add converter option `replaceZeroWithCloseValue` to OHLCVSeriesOptions
* Add `replaceZeroWithCloseValue` to demo
* Add unit test
---
 demos/stock-ohlcv/demo.js                     |  1 +
 .../Converters/OHLCVSeriesConverter.ts        | 14 +++-
 src/TimeSeries/TimeSeriesConnector.ts         |  6 +-
 src/TimeSeries/TimeSeriesOptions.ts           | 11 +++
 test/unit-tests/TimeSeries/OHLCV.test.ts      | 68 +++++++++++++++++++
 5 files changed, 96 insertions(+), 4 deletions(-)

diff --git a/demos/stock-ohlcv/demo.js b/demos/stock-ohlcv/demo.js
index 09ace0a..005bff1 100644
--- a/demos/stock-ohlcv/demo.js
+++ b/demos/stock-ohlcv/demo.js
@@ -7,6 +7,7 @@ async function displayOHLCV (postmanJSON) {
         postman: {
             environmentJSON: postmanJSON
         },
+        replaceZeroWithCloseValue: true,
         series: {
             type: 'OHLCV'
         },
diff --git a/src/TimeSeries/Converters/OHLCVSeriesConverter.ts b/src/TimeSeries/Converters/OHLCVSeriesConverter.ts
index 5e7db84..a9d09e2 100644
--- a/src/TimeSeries/Converters/OHLCVSeriesConverter.ts
+++ b/src/TimeSeries/Converters/OHLCVSeriesConverter.ts
@@ -108,10 +108,20 @@ export class OHLCVSeriesConverter extends TimeSeriesConverter {
         const sortedOHLCVItems: Array<OHLCV> = [];
 
         for (const ohlcvItem of json) {
+            const [date, /* Open */ , /* High */, /* Low */, close, volume] = ohlcvItem;
+            let [/* Date */, open, high, low] = ohlcvItem;
+
+
+            if (volume === 0 && userOptions.replaceZeroWithCloseValue) {
+                open = close;
+                high = close;
+                low  = close;
+            }
+
             sortedOHLCVItems.push({
                 Id: securityIds[0],
-                Date: ohlcvItem[0],
-                Value: [ohlcvItem[1], ohlcvItem[2], ohlcvItem[3], ohlcvItem[4], ohlcvItem[5]]
+                Date: date,
+                Value: [open, high, low, close, volume]
             });
         }
 
diff --git a/src/TimeSeries/TimeSeriesConnector.ts b/src/TimeSeries/TimeSeriesConnector.ts
index 2da6fbe..0c49f7b 100644
--- a/src/TimeSeries/TimeSeriesConnector.ts
+++ b/src/TimeSeries/TimeSeriesConnector.ts
@@ -29,7 +29,7 @@ import MorningstarConnector from '../Shared/MorningstarConnector';
 import MorningstarURL from '../Shared/MorningstarURL';
 import CumulativeReturnSeriesConverter from './Converters/CumulativeReturnSeriesConverter';
 import DividendSeriesConverter from './Converters/DividendSeriesConverter';
-import TimeSeriesOptions from './TimeSeriesOptions';
+import TimeSeriesOptions, { OHLCVSeriesOptions } from './TimeSeriesOptions';
 import TimeSeriesRatingConverter from './Converters/RatingSeriesConverter';
 import PriceSeriesConverter from './Converters/PriceSeriesConverter';
 import TimeSeriesConverter from './TimeSeriesConverter';
@@ -99,7 +99,9 @@ export class TimeSeriesConnector extends MorningstarConnector {
                 this.converter = new OHLCVSeriesConverter({
                     ...options.converter,
                     ...options.series,
-                    securities: options.securities
+                    securities: options.securities,
+                    replaceZeroWithCloseValue: (options as OHLCVSeriesOptions)
+                        .replaceZeroWithCloseValue
                 });
                 break;
 
diff --git a/src/TimeSeries/TimeSeriesOptions.ts b/src/TimeSeries/TimeSeriesOptions.ts
index 1bf4544..380aedf 100644
--- a/src/TimeSeries/TimeSeriesOptions.ts
+++ b/src/TimeSeries/TimeSeriesOptions.ts
@@ -119,6 +119,17 @@ export interface OHLCVSeriesOptions extends TimeSeriesConverterOptions {
      */
     type: 'OHLCV';
 
+    /**
+     * When this property is `true`, open, high and low are replaced with 
+     * the close value if the volume is zero. 
+     * 
+     * If volume is zero, open high low are zero too. If you do not prefer this
+     * behavior, you can enable this property.
+     * 
+     * @default false
+     */
+    replaceZeroWithCloseValue?: boolean;
+
     /**
      * Security to retrieve.
      */
diff --git a/test/unit-tests/TimeSeries/OHLCV.test.ts b/test/unit-tests/TimeSeries/OHLCV.test.ts
index 1d416f4..57e61af 100644
--- a/test/unit-tests/TimeSeries/OHLCV.test.ts
+++ b/test/unit-tests/TimeSeries/OHLCV.test.ts
@@ -51,3 +51,71 @@ export async function ohlcvLoad (
     );
 
 }
+
+export function ohlcvReplaceZeroWithClose () {
+    const data = [
+        [1722816000000, 0, 0, 0, 14754.871, 0],
+        [1722816000001, 0, 0, 0, 14754.871, 1]
+    ];
+
+    const expectedFirstRow = [1722816000000, 14754.871, 14754.871, 14754.871, 14754.871, 0];
+    const expectedSecondRow = [1722816000001, 0, 0, 0, 14754.871, 1];
+
+    const converter = new MC.TimeSeriesConverters.OHLCVSeriesConverter({
+        type: 'OHLCV',
+        replaceZeroWithCloseValue: true,
+        securities: [
+            {
+                id: 'XIUSA000O2',
+                idType: 'SECID'
+            }
+        ],
+        json: data
+    });
+
+    converter.parse({ type: 'OHLCV' });
+
+    const dataTable = converter.getTable();
+
+    Assert.deepEqual(
+        dataTable.getRow(0),
+        expectedFirstRow,
+        'Should replace o, h, l with c when v is 0'
+    );
+
+    Assert.deepEqual(
+        dataTable.getRow(1),
+        expectedSecondRow,
+        'Should not replace o, h, l with c when v is not 0'
+    );
+}
+
+export function ohlcvDoNotReplaceZeroWithClose () {
+    const data = [
+        [1722816000000, 0, 0, 0, 14754.871, 0]
+    ];
+
+    const expectedRow = [1722816000000, 0, 0, 0, 14754.871, 0];
+
+    const converter = new MC.TimeSeriesConverters.OHLCVSeriesConverter({
+        type: 'OHLCV',
+        replaceZeroWithCloseValue: false,
+        securities: [
+            {
+                id: 'XIUSA000O2',
+                idType: 'SECID'
+            }
+        ],
+        json: data
+    });
+
+    converter.parse({ type: 'OHLCV' });
+
+    const dataTable = converter.getTable();
+
+    Assert.deepEqual(
+        dataTable.getRow(0),
+        expectedRow,
+        'Should not replace o, h, l with c when v is 0'
+    );
+}