Skip to content

Commit

Permalink
feat: support for fetching OTC stocks institutional investors' trades
Browse files Browse the repository at this point in the history
  • Loading branch information
chunkai1312 committed Dec 11, 2023
1 parent c931c7c commit 73ebb0a
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 8 deletions.
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,53 @@ console.log(stockHistorical);
// }
```

### `stocks.instTrades(params)`

Get institutional investors' trades for a specific stock on a given date.

- `params`:
- `date`: Date in the format 'YYYY-MM-DD'
- `market` (optional): Filter stocks by market ('TSE' or 'OTC')
- `symbol` (optional): Stock symbol
- Returns: Promise that resolves to the institutional investors' trades.

```js
const stockInstTrades = await twstock.stocks.instTrades({ date: '2023-01-30', symbol: '2330' });
console.log(stockInstTrades);
// Prints:
// {
// date: '2023-01-30',
// exchange: 'TWSE',
// market: 'TSE',
// symbol: '2330',
// name: '台積電',
// finiWithoutDealersBuy: 133236588,
// finiWithoutDealersSell: 52595539,
// finiWithoutDealersNetBuySell: 80641049,
// finiDealersBuy: 0,
// finiDealersSell: 0,
// finiDealersNetBuySell: 0,
// finiBuy: 133236588,
// finiSell: 52595539,
// finiNetBuySell: 80641049,
// sitcBuy: 1032000,
// sitcSell: 94327,
// sitcNetBuySell: 937673,
// dealersForProprietaryBuy: 978000,
// dealersForProprietarySell: 537000,
// dealersForProprietaryNetBuySell: 441000,
// dealersForHedgingBuy: 1227511,
// dealersForHedgingSell: 788103,
// dealersForHedgingNetBuySell: 439408,
// dealersBuy: 2205511,
// dealersSell: 1325103,
// dealersNetBuySell: 880408,
// totalInstInvestorsBuy: 136474099,
// totalInstInvestorsSell: 54014969,
// totalInstInvestorsNetBuySell: 82459130
// }
```

## Indices

### `indices.list(params)`
Expand Down
51 changes: 51 additions & 0 deletions src/scrapers/tpex-scraper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,57 @@ export class TpexScraper extends Scraper {
});
}

async fetchStocksInstTrades(options: { date: string }) {
const { date } = options;
const [year, month, day] = date.split('-');
const query = new URLSearchParams({
d: `${+year - 1911}/${month}/${day}`,
se: 'EW',
t: 'D',
o: 'json',
});
const url = `https://www.tpex.org.tw/web/stock/3insti/daily_trade/3itrade_hedge_result.php?${query}`;

const response = await this.httpService.get(url);
const json = response.data.iTotalRecords > 0 && response.data;
if (!json) return null;

return json.aaData.map((row: any) => {
const [symbol, name, ...values] = row;
const data: Record<string, any> = {};
data.date = date;
data.exchange = Exchange.TPEx;
data.market = Market.OTC;
data.symbol = symbol;
data.name = name.trim();
data.finiWithoutDealersBuy = numeral(values[0]).value();
data.finiWithoutDealersSell = numeral(values[1]).value();
data.finiWithoutDealersNetBuySell = numeral(values[2]).value();
data.finiDealersBuy = numeral(values[3]).value();
data.finiDealersSell = numeral(values[4]).value();
data.finiDealersNetBuySell = numeral(values[5]).value();
data.finiBuy = numeral(values[6]).value();
data.finiSell = numeral(values[7]).value();
data.finiNetBuySell = numeral(values[8]).value();
data.sitcBuy = numeral(values[9]).value();
data.sitcSell = numeral(values[10]).value();
data.sitcNetBuySell = numeral(values[11]).value();
data.dealersForProprietaryBuy = numeral(values[12]).value();
data.dealersForProprietarySell = numeral(values[13]).value();
data.dealersForProprietaryNetBuySell = numeral(values[14]).value();
data.dealersForHedgingBuy = numeral(values[15]).value();
data.dealersForHedgingSell = numeral(values[16]).value();
data.dealersForHedgingNetBuySell = numeral(values[17]).value();
data.dealersBuy = numeral(values[18]).value();
data.dealersSell = numeral(values[19]).value();
data.dealersNetBuySell = numeral(values[20]).value();
data.totalInstInvestorsBuy = data.finiBuy + data.sitcBuy + data.dealersBuy;
data.totalInstInvestorsSell = data.finiSell + data.sitcSell + data.dealersSell;
data.totalInstInvestorsNetBuySell = numeral(values[21]).value();
return data;
});
}

async fetchIndicesHistorical(options: { date: string }) {
const { date } = options;
const [ year, month, day ] = date.split('-');
Expand Down
22 changes: 22 additions & 0 deletions src/twstock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export class TwStock {
list: this.getStocksList.bind(this),
quote: this.getStocksQuote.bind(this),
historical: this.getStocksHistorical.bind(this),
instTrades: this.getStocksInstTrades.bind(this),
};
}

Expand Down Expand Up @@ -101,6 +102,27 @@ export class TwStock {
return data && symbol ? data.find((row: any) => row.symbol === symbol) : data;
}

private async getStocksInstTrades(params: { date: string, market?: 'TSE' | 'OTC', symbol?: string }) {
const { date, symbol } = params;

if (!this._stocks.size) {
await this.loadStocks();
}
if (symbol && !this._stocks.has(symbol)) {
throw new Error('symbol not found');
}

const ticker = this._stocks.get(symbol as string) as Ticker;
const market = symbol ? ticker.market : params.market;

const data = (market === Market.OTC)
? await TpexScraper.fetchStocksInstTrades({ date })
: await TwseScraper.fetchStocksInstTrades({ date });

/* istanbul ignore next */
return data && symbol ? data.find((row: any) => row.symbol === symbol) : data;
}

private async getIndicesList(params?: { market: 'TSE' | 'OTC' }) {
const { market } = params ?? {};

Expand Down
1 change: 1 addition & 0 deletions test/fixtures/fetched-otc-stocks-inst-trades.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions test/fixtures/otc-stocks-inst-trades.json

Large diffs are not rendered by default.

55 changes: 55 additions & 0 deletions test/scrapers/tpex-scraper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,61 @@ describe('TpexScraper', () => {
});
});

describe('.fetchStocksInstTrades()', () => {
it('should fetch stocks institutional investors\' trades for the given date', async () => {
const data = require('../fixtures/otc-stocks-inst-trades.json');
mockAxios.get.mockResolvedValueOnce({ data });

const stocks = await scraper.fetchStocksInstTrades({ date: '2023-01-30' });
expect(mockAxios.get).toHaveBeenCalledWith(
'https://www.tpex.org.tw/web/stock/3insti/daily_trade/3itrade_hedge_result.php?d=112%2F01%2F30&se=EW&t=D&o=json',
);
expect(stocks).toBeDefined();
expect(stocks.length).toBeGreaterThan(0);
expect(stocks[0]).toEqual({
date: '2023-01-30',
exchange: 'TPEx',
market: 'OTC',
symbol: '00679B',
name: '元大美債20年',
finiWithoutDealersBuy: 425061,
finiWithoutDealersSell: 282000,
finiWithoutDealersNetBuySell: 143061,
finiDealersBuy: 0,
finiDealersSell: 0,
finiDealersNetBuySell: 0,
finiBuy: 425061,
finiSell: 282000,
finiNetBuySell: 143061,
sitcBuy: 0,
sitcSell: 0,
sitcNetBuySell: 0,
dealersForProprietaryBuy: 250000,
dealersForProprietarySell: 0,
dealersForProprietaryNetBuySell: 250000,
dealersForHedgingBuy: 1874000,
dealersForHedgingSell: 9422229,
dealersForHedgingNetBuySell: -7548229,
dealersBuy: 2124000,
dealersSell: 9422229,
dealersNetBuySell: -7298229,
totalInstInvestorsBuy: 2549061,
totalInstInvestorsSell: 9704229,
totalInstInvestorsNetBuySell: -7155168,
});
});

it('should return null when no data is available', async () => {
mockAxios.get.mockResolvedValueOnce({ data: {} });

const stocks = await scraper.fetchStocksInstTrades({ date: '2023-01-01' });
expect(mockAxios.get).toHaveBeenCalledWith(
'https://www.tpex.org.tw/web/stock/3insti/daily_trade/3itrade_hedge_result.php?d=112%2F01%2F01&se=EW&t=D&o=json',
);
expect(stocks).toBe(null);
});
});

describe('.fetchIndicesHistorical()', () => {
it('should fetch indices historical data for the given date', async () => {
const data = fs.readFileSync('./test/fixtures/otc-indices-historical.html').toString();
Expand Down
51 changes: 43 additions & 8 deletions test/twstock.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ jest.mock('../src/scrapers', () => ({
if (date === '2023-01-30') return require('./fixtures/fetched-tse-stocks-historical.json');
return null;
}),
fetchStocksInstTrades: jest.fn(({ date }) => {
if (date === '2023-01-30') return require('./fixtures/fetched-tse-stocks-inst-trades.json');
return null;
}),
fetchIndicesHistorical: jest.fn(({ date }) => {
if (date === '2023-01-30') return require('./fixtures/fetched-tse-indices-historical.json');
return null;
Expand All @@ -21,6 +25,10 @@ jest.mock('../src/scrapers', () => ({
if (date === '2023-01-30') return require('./fixtures/fetched-otc-stocks-historical.json');
return null;
}),
fetchStocksInstTrades: jest.fn(({ date }) => {
if (date === '2023-01-30') return require('./fixtures/fetched-otc-stocks-inst-trades.json');
return null;
}),
fetchIndicesHistorical: jest.fn(({ date }) => {
if (date === '2023-01-30') return require('./fixtures/fetched-otc-indices-historical.json');
return null;
Expand Down Expand Up @@ -56,8 +64,8 @@ describe('TwStock', () => {
twstock = new TwStock();
});

describe('stocks', () => {
describe('list()', () => {
describe('.stocks', () => {
describe('.list()', () => {
it('should load stocks and return the list', async () => {
const stocks = await twstock.stocks.list();
expect(stocks).toBeDefined();
Expand All @@ -79,7 +87,7 @@ describe('TwStock', () => {
});
});

describe('quote()', () => {
describe('.quote()', () => {
it('should fetch stocks realtime quote', async () => {
const stock = await twstock.stocks.quote({ symbol: '2330' });
expect(stock).toBeDefined();
Expand All @@ -91,7 +99,7 @@ describe('TwStock', () => {
});
});

describe('historical()', () => {
describe('.historical()', () => {
it('should fetch stocks historical data for the symbol', async () => {
const stock = await twstock.stocks.historical({ date: '2023-01-30', symbol: '2330' });
expect(stock).toBeDefined();
Expand All @@ -116,10 +124,37 @@ describe('TwStock', () => {
expect(stocks.every((stock: any) => stock.market === 'OTC')).toBe(true);
});
});

describe('.instTrades()', () => {
it('should fetch stocks institutional investors\' trades for the symbol', async () => {
const stock = await twstock.stocks.instTrades({ date: '2023-01-30', symbol: '2330' });
expect(stock).toBeDefined();
expect(stock.symbol).toBe('2330');
console.log(stock)
});

it('should throw an error if symbol is not found', async () => {
await expect(() => twstock.stocks.instTrades({ date: '2023-01-30', symbol: 'foobar' })).rejects.toThrow('symbol not found');
});

it('should fetch stocks institutional investors\' trades for the TSE market', async () => {
const stocks = await twstock.stocks.instTrades({ date: '2023-01-30', market: 'TSE' });
expect(stocks).toBeDefined();
expect(stocks.length).toBeGreaterThan(0);
expect(stocks.every((stock: any) => stock.market === 'TSE')).toBe(true);
});

it('should fetch stocks institutional investors\' trades for the OTC market', async () => {
const stocks = await twstock.stocks.instTrades({ date: '2023-01-30', market: 'OTC' });
expect(stocks).toBeDefined();
expect(stocks.length).toBeGreaterThan(0);
expect(stocks.every((stock: any) => stock.market === 'OTC')).toBe(true);
});
});
});

describe('indices', () => {
describe('list()', () => {
describe('.indices', () => {
describe('.list()', () => {
it('should load indices and return the list', async () => {
const indices = await twstock.indices.list();
expect(indices).toBeDefined();
Expand All @@ -141,7 +176,7 @@ describe('TwStock', () => {
});
});

describe('quote()', () => {
describe('.quote()', () => {
it('should fetch indices realtime quote', async () => {
const index = await twstock.indices.quote({ symbol: 'IX0001' });
expect(index).toBeDefined();
Expand All @@ -153,7 +188,7 @@ describe('TwStock', () => {
});
});

describe('historical()', () => {
describe('.historical()', () => {
it('should fetch indices historical data for the symbol', async () => {
const index = await twstock.indices.historical({ date: '2023-01-30', symbol: 'IX0001' });
expect(index).toBeDefined();
Expand Down

0 comments on commit 73ebb0a

Please sign in to comment.