Skip to content

Commit f4e841b

Browse files
authored
[jealousGH-122] Update calculation of middle (jealous#129)
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
1 parent 5b12bec commit f4e841b

4 files changed

Lines changed: 126 additions & 9 deletions

File tree

.github/workflows/build-test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
runs-on: ubuntu-latest
1212
strategy:
1313
matrix:
14-
python-version: [ "2.7", "3.9", "3.10" ]
14+
python-version: [ "2.7", "3.10", "3.11" ]
1515
steps:
1616
- uses: actions/checkout@v2
1717
- name: Set up Python ${{ matrix.python-version }}
@@ -32,7 +32,7 @@ jobs:
3232
run: |
3333
py.test --cov=stockstats test.py
3434
- name: Publish Test Results
35-
if: ${{ matrix.python-version == '3.10' }}
35+
if: ${{ matrix.python-version == '3.11' }}
3636
run: codecov
3737
env:
3838
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[![codecov](https://codecov.io/gh/jealous/stockstats/branch/master/graph/badge.svg?token=IFMD1pVJ7T)](https://codecov.io/gh/jealous/stockstats)
55
[![pypi](https://img.shields.io/pypi/v/stockstats.svg)](https://pypi.python.org/pypi/stockstats)
66

7-
VERSION: 0.4.1
7+
VERSION: 0.5.0
88

99
## Introduction
1010

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

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

266-
In [24]: df[['middle', 'close', 'res', 'res_-10_c']]
266+
In [24]: df[['middle', 'close', 'res', 'res_10_c']]
267267
Out[24]:
268268
middle close res res_10_c
269269
date
@@ -502,7 +502,7 @@ resistance levels.
502502
It includes three lines:
503503
* `df['kdjk']` - K series
504504
* `df['kdjd']` - D series
505-
* `df['kdfj']` - J series
505+
* `df['kdjj']` - J series
506506

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

531+
When `amount` is available, `middle = amount / volume`
532+
This should be more accurate because amount represents the total cash flow.
533+
531534
#### [Bollinger Bands](https://en.wikipedia.org/wiki/Bollinger_Bands)
532535

533536
The Bollinger bands includes three lines

stockstats.py

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,10 @@ def _get_cci(self, window=None):
485485
""" Commodity Channel Index
486486
487487
CCI = (Typical Price - 20-period SMA of TP) / (.015 x Mean Deviation)
488-
Typical Price (TP) = (High + Low + Close)/3
488+
* when amount is not available:
489+
Typical Price (TP) = (High + Low + Close)/3
490+
* when amount is available:
491+
Typical Price (TP) = Amount / Volume
489492
TP is also implemented as 'middle'.
490493
491494
:param window: number of periods
@@ -786,6 +789,8 @@ def _shifted_cr_sma(self, cr, window):
786789
return self._shift(cr_sma, -int(window / 2.5 + 1))
787790

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

791796
def _get_tp(self):
@@ -1175,6 +1180,41 @@ def init_all(self):
11751180
for handler in self.handler.values():
11761181
handler()
11771182

1183+
def drop_column(self, names=None, inplace=False):
1184+
""" drop column by the name
1185+
1186+
multiple names can be supplied in a list
1187+
:return: StockDataFrame
1188+
"""
1189+
if self.empty:
1190+
return self
1191+
ret = self.drop(names, axis=1, inplace=inplace)
1192+
if inplace is True:
1193+
return self
1194+
return wrap(ret)
1195+
1196+
def drop_tail(self, n, inplace=False):
1197+
""" drop n rows from the tail
1198+
1199+
:return: StockDataFrame
1200+
"""
1201+
tail = self.tail(n).index
1202+
ret = self.drop(tail, inplace=inplace)
1203+
if inplace is True:
1204+
return self
1205+
return wrap(ret)
1206+
1207+
def drop_head(self, n, inplace=False):
1208+
""" drop n rows from the beginning
1209+
1210+
:return: StockDataFrame
1211+
"""
1212+
head = self.head(n).index
1213+
ret = self.drop(head, inplace=inplace)
1214+
if inplace is True:
1215+
return self
1216+
return wrap(ret)
1217+
11781218
@property
11791219
def handler(self):
11801220
return {
@@ -1272,7 +1312,8 @@ def within(self, start_date, end_date):
12721312
def copy(self, deep=True):
12731313
return wrap(super(StockDataFrame, self).copy(deep))
12741314

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

12991341
if index_column in value.columns:
13001342
value.set_index(index_column, inplace=True)
1301-
return StockDataFrame(value)
1343+
ret = StockDataFrame(value)
1344+
ret.columns.name = name
1345+
return ret
13021346
return value

test.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030

3131
import pandas as pd
3232
from hamcrest import greater_than, assert_that, equal_to, close_to, \
33-
contains_exactly, none, is_not, raises, has_items, instance_of
33+
contains_exactly, none, is_not, raises, has_items, instance_of, \
34+
not_, has_item, has_length
3435
from numpy import isnan
3536

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

5051

52+
def not_has(item):
53+
return not_(has_item(item))
54+
55+
5156
class StockDataFrameTest(TestCase):
5257
_stock = wrap(pd.read_csv(get_file('987654.csv')))
5358
_supor = Sdf.retype(pd.read_csv(get_file('002032.csv')))
@@ -140,6 +145,14 @@ def test_middle(self):
140145
assert_that(middle.loc[idx], near_to(12.53))
141146
assert_that(tp.loc[idx], equal_to(middle.loc[idx]))
142147

148+
def test_typical_price_with_amount(self):
149+
stock = self._supor[:20]
150+
tp = stock['tp']
151+
assert_that(tp[20040817], near_to(11.541))
152+
153+
middle = stock['middle']
154+
assert_that(middle[20040817], near_to(11.541))
155+
143156
def test_cr(self):
144157
stock = self.get_stock_90day()
145158
stock.get('cr')
@@ -465,6 +478,7 @@ def test_get_wr(self):
465478

466479
def test_get_cci(self):
467480
stock = self._supor.within(20160701, 20160831)
481+
stock.drop('amount', axis=1, inplace=True)
468482
stock.get('cci_14')
469483
stock.get('cci')
470484
assert_that(stock.loc[20160817, 'cci'], near_to(50))
@@ -640,3 +654,59 @@ def test_supertrend(self):
640654
assert_that(st[idx], near_to(12.9021))
641655
assert_that(st_ub[idx], near_to(14.6457))
642656
assert_that(st_lb[idx], near_to(12.9021))
657+
658+
def test_drop_column_inplace(self):
659+
stock = self._supor[:20]
660+
stock.columns.name = 'Luke'
661+
ret = stock.drop_column(['open', 'close'], inplace=True)
662+
663+
assert_that(ret.columns.name, equal_to('Luke'))
664+
assert_that(ret.keys(), has_items('high', 'low'))
665+
assert_that(ret.keys(), not_has('open'))
666+
assert_that(ret.keys(), not_has('close'))
667+
assert_that(stock.keys(), has_items('high', 'low'))
668+
assert_that(stock.keys(), not_has('open'))
669+
assert_that(stock.keys(), not_has('close'))
670+
671+
def test_drop_column(self):
672+
stock = self._supor[:20]
673+
stock.columns.name = 'Luke'
674+
ret = stock.drop_column(['open', 'close'])
675+
676+
assert_that(ret.columns.name, equal_to('Luke'))
677+
assert_that(ret.keys(), has_items('high', 'low'))
678+
assert_that(ret.keys(), not_has('open'))
679+
assert_that(ret.keys(), not_has('close'))
680+
assert_that(stock.keys(), has_items('high', 'low', 'open', 'close'))
681+
682+
def test_drop_head_inplace(self):
683+
stock = self._supor[:20]
684+
ret = stock.drop_head(9, inplace=True)
685+
assert_that(ret, has_length(11))
686+
assert_that(ret.iloc[0].name, equal_to(20040830))
687+
assert_that(stock, has_length(11))
688+
assert_that(stock.iloc[0].name, equal_to(20040830))
689+
690+
def test_drop_head(self):
691+
stock = self._supor[:20]
692+
ret = stock.drop_head(9)
693+
assert_that(ret, has_length(11))
694+
assert_that(ret.iloc[0].name, equal_to(20040830))
695+
assert_that(stock, has_length(20))
696+
assert_that(stock.iloc[0].name, equal_to(20040817))
697+
698+
def test_drop_tail_inplace(self):
699+
stock = self._supor[:20]
700+
ret = stock.drop_tail(9, inplace=True)
701+
assert_that(ret, has_length(11))
702+
assert_that(ret.iloc[-1].name, equal_to(20040831))
703+
assert_that(stock, has_length(11))
704+
assert_that(stock.iloc[-1].name, equal_to(20040831))
705+
706+
def test_drop_tail(self):
707+
stock = self._supor[:20]
708+
ret = stock.drop_tail(9)
709+
assert_that(ret, has_length(11))
710+
assert_that(ret.iloc[-1].name, equal_to(20040831))
711+
assert_that(stock, has_length(20))
712+
assert_that(stock.iloc[-1].name, equal_to(20040913))

0 commit comments

Comments
 (0)