Skip to content

Commit

Permalink
[GH-122] Update calculation of middle (#129)
Browse files Browse the repository at this point in the history
The typical price or middle price is calculated by:
```
(high + low + close) / 3
```

But it would be accurate if the data source has the `amount` that
represents the total amount of capital traded in that period.
Use `middle = amount / volume` when amout is available.

Also fix some typos in the read me file.
Add some utilities for dropping columns, head or tail.

Add python 3.11 in CI and drop 3.9.

Update version to 0.5.0
  • Loading branch information
jealous authored Nov 17, 2022
1 parent 5b12bec commit f4e841b
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 9 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ "2.7", "3.9", "3.10" ]
python-version: [ "2.7", "3.10", "3.11" ]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
Expand All @@ -32,7 +32,7 @@ jobs:
run: |
py.test --cov=stockstats test.py
- name: Publish Test Results
if: ${{ matrix.python-version == '3.10' }}
if: ${{ matrix.python-version == '3.11' }}
run: codecov
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![codecov](https://codecov.io/gh/jealous/stockstats/branch/master/graph/badge.svg?token=IFMD1pVJ7T)](https://codecov.io/gh/jealous/stockstats)
[![pypi](https://img.shields.io/pypi/v/stockstats.svg)](https://pypi.python.org/pypi/stockstats)

VERSION: 0.4.1
VERSION: 0.5.0

## Introduction

Expand Down Expand Up @@ -263,7 +263,7 @@ In [22]: tp = df['middle']

In [23]: df['res'] = df['middle'] > df['close']

In [24]: df[['middle', 'close', 'res', 'res_-10_c']]
In [24]: df[['middle', 'close', 'res', 'res_10_c']]
Out[24]:
middle close res res_10_c
date
Expand Down Expand Up @@ -502,7 +502,7 @@ resistance levels.
It includes three lines:
* `df['kdjk']` - K series
* `df['kdjd']` - D series
* `df['kdfj']` - J series
* `df['kdjj']` - J series

The default window is 9. Use `StockDataFrame.KDJ_WINDOW` to change it.
Use `df['kdjk_6']` to retrieve the K series of 6 periods.
Expand All @@ -528,6 +528,9 @@ It contains 4 lines:
It's the average of `high`, `low` and `close`.
Use `df['middle']` to access this value.

When `amount` is available, `middle = amount / volume`
This should be more accurate because amount represents the total cash flow.

#### [Bollinger Bands](https://en.wikipedia.org/wiki/Bollinger_Bands)

The Bollinger bands includes three lines
Expand Down
50 changes: 47 additions & 3 deletions stockstats.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,10 @@ def _get_cci(self, window=None):
""" Commodity Channel Index
CCI = (Typical Price - 20-period SMA of TP) / (.015 x Mean Deviation)
Typical Price (TP) = (High + Low + Close)/3
* when amount is not available:
Typical Price (TP) = (High + Low + Close)/3
* when amount is available:
Typical Price (TP) = Amount / Volume
TP is also implemented as 'middle'.
:param window: number of periods
Expand Down Expand Up @@ -786,6 +789,8 @@ def _shifted_cr_sma(self, cr, window):
return self._shift(cr_sma, -int(window / 2.5 + 1))

def _tp(self):
if 'amount' in self:
return self['amount'] / self['volume']
return (self['close'] + self['high'] + self['low']).divide(3.0)

def _get_tp(self):
Expand Down Expand Up @@ -1175,6 +1180,41 @@ def init_all(self):
for handler in self.handler.values():
handler()

def drop_column(self, names=None, inplace=False):
""" drop column by the name
multiple names can be supplied in a list
:return: StockDataFrame
"""
if self.empty:
return self
ret = self.drop(names, axis=1, inplace=inplace)
if inplace is True:
return self
return wrap(ret)

def drop_tail(self, n, inplace=False):
""" drop n rows from the tail
:return: StockDataFrame
"""
tail = self.tail(n).index
ret = self.drop(tail, inplace=inplace)
if inplace is True:
return self
return wrap(ret)

def drop_head(self, n, inplace=False):
""" drop n rows from the beginning
:return: StockDataFrame
"""
head = self.head(n).index
ret = self.drop(head, inplace=inplace)
if inplace is True:
return self
return wrap(ret)

@property
def handler(self):
return {
Expand Down Expand Up @@ -1272,7 +1312,8 @@ def within(self, start_date, end_date):
def copy(self, deep=True):
return wrap(super(StockDataFrame, self).copy(deep))

def _ensure_type(self, obj):
@staticmethod
def _ensure_type(obj):
""" override the method in pandas, omit the check
This patch is not the perfect way but could make the lib work.
Expand All @@ -1293,10 +1334,13 @@ def retype(value, index_column=None):
if isinstance(value, StockDataFrame):
return value
elif isinstance(value, pd.DataFrame):
name = value.columns.name
# use all lower case for column name
value.columns = map(lambda c: c.lower(), value.columns)

if index_column in value.columns:
value.set_index(index_column, inplace=True)
return StockDataFrame(value)
ret = StockDataFrame(value)
ret.columns.name = name
return ret
return value
72 changes: 71 additions & 1 deletion test.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@

import pandas as pd
from hamcrest import greater_than, assert_that, equal_to, close_to, \
contains_exactly, none, is_not, raises, has_items, instance_of
contains_exactly, none, is_not, raises, has_items, instance_of, \
not_, has_item, has_length
from numpy import isnan

from stockstats import StockDataFrame as Sdf
Expand All @@ -48,6 +49,10 @@ def near_to(value):
return close_to(value, 1e-3)


def not_has(item):
return not_(has_item(item))


class StockDataFrameTest(TestCase):
_stock = wrap(pd.read_csv(get_file('987654.csv')))
_supor = Sdf.retype(pd.read_csv(get_file('002032.csv')))
Expand Down Expand Up @@ -140,6 +145,14 @@ def test_middle(self):
assert_that(middle.loc[idx], near_to(12.53))
assert_that(tp.loc[idx], equal_to(middle.loc[idx]))

def test_typical_price_with_amount(self):
stock = self._supor[:20]
tp = stock['tp']
assert_that(tp[20040817], near_to(11.541))

middle = stock['middle']
assert_that(middle[20040817], near_to(11.541))

def test_cr(self):
stock = self.get_stock_90day()
stock.get('cr')
Expand Down Expand Up @@ -465,6 +478,7 @@ def test_get_wr(self):

def test_get_cci(self):
stock = self._supor.within(20160701, 20160831)
stock.drop('amount', axis=1, inplace=True)
stock.get('cci_14')
stock.get('cci')
assert_that(stock.loc[20160817, 'cci'], near_to(50))
Expand Down Expand Up @@ -640,3 +654,59 @@ def test_supertrend(self):
assert_that(st[idx], near_to(12.9021))
assert_that(st_ub[idx], near_to(14.6457))
assert_that(st_lb[idx], near_to(12.9021))

def test_drop_column_inplace(self):
stock = self._supor[:20]
stock.columns.name = 'Luke'
ret = stock.drop_column(['open', 'close'], inplace=True)

assert_that(ret.columns.name, equal_to('Luke'))
assert_that(ret.keys(), has_items('high', 'low'))
assert_that(ret.keys(), not_has('open'))
assert_that(ret.keys(), not_has('close'))
assert_that(stock.keys(), has_items('high', 'low'))
assert_that(stock.keys(), not_has('open'))
assert_that(stock.keys(), not_has('close'))

def test_drop_column(self):
stock = self._supor[:20]
stock.columns.name = 'Luke'
ret = stock.drop_column(['open', 'close'])

assert_that(ret.columns.name, equal_to('Luke'))
assert_that(ret.keys(), has_items('high', 'low'))
assert_that(ret.keys(), not_has('open'))
assert_that(ret.keys(), not_has('close'))
assert_that(stock.keys(), has_items('high', 'low', 'open', 'close'))

def test_drop_head_inplace(self):
stock = self._supor[:20]
ret = stock.drop_head(9, inplace=True)
assert_that(ret, has_length(11))
assert_that(ret.iloc[0].name, equal_to(20040830))
assert_that(stock, has_length(11))
assert_that(stock.iloc[0].name, equal_to(20040830))

def test_drop_head(self):
stock = self._supor[:20]
ret = stock.drop_head(9)
assert_that(ret, has_length(11))
assert_that(ret.iloc[0].name, equal_to(20040830))
assert_that(stock, has_length(20))
assert_that(stock.iloc[0].name, equal_to(20040817))

def test_drop_tail_inplace(self):
stock = self._supor[:20]
ret = stock.drop_tail(9, inplace=True)
assert_that(ret, has_length(11))
assert_that(ret.iloc[-1].name, equal_to(20040831))
assert_that(stock, has_length(11))
assert_that(stock.iloc[-1].name, equal_to(20040831))

def test_drop_tail(self):
stock = self._supor[:20]
ret = stock.drop_tail(9)
assert_that(ret, has_length(11))
assert_that(ret.iloc[-1].name, equal_to(20040831))
assert_that(stock, has_length(20))
assert_that(stock.iloc[-1].name, equal_to(20040913))

0 comments on commit f4e841b

Please sign in to comment.