Skip to content

Commit 4145960

Browse files
author
Alexey Hurko
committed
Initial commit
0 parents  commit 4145960

17 files changed

+26259
-0
lines changed

.gitignore

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
.idea
2+
.test
3+
4+
# Byte-compiled / optimized / DLL files
5+
__pycache__/
6+
*.py[cod]
7+
*$py.class
8+
9+
# C extensions
10+
*.so
11+
12+
# Distribution / packaging
13+
.Python
14+
env/
15+
build/
16+
develop-eggs/
17+
dist/
18+
downloads/
19+
eggs/
20+
.eggs/
21+
parts/
22+
sdist/
23+
*.egg-info/
24+
.installed.cfg
25+
*.egg
26+
27+
# PyInstaller
28+
# Usually these files are written by a python script from a template
29+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
30+
*.manifest
31+
*.spec
32+
33+
# Installer logs
34+
pip-log.txt
35+
pip-delete-this-directory.txt
36+
37+
# Unit test / coverage reports
38+
htmlcov/
39+
.tox/
40+
.coverage
41+
.coverage.*
42+
.cache
43+
nosetests.xml
44+
coverage.xml
45+
*,cover
46+
.hypothesis/
47+
48+
# Translations
49+
*.mo
50+
*.pot
51+
52+
# Django stuff:
53+
*.log
54+
local_settings.py
55+
56+
# Flask stuff:
57+
instance/
58+
.webassets-cache
59+
60+
# Scrapy stuff:
61+
.scrapy
62+
63+
# Sphinx documentation
64+
docs/_build/
65+
66+
# PyBuilder
67+
target/
68+
69+
# IPython Notebook
70+
.ipynb_checkpoints
71+
72+
# pyenv
73+
.python-version
74+
75+
# celery beat schedule file
76+
celerybeat-schedule
77+
78+
# dotenv
79+
.env
80+
81+
# virtualenv
82+
venv/
83+
ENV/
84+
85+
# Spyder project settings
86+
.spyderproject
87+
88+
# Rope project settings
89+
.ropeproject
90+
91+
*.deb
92+
93+
.idea/
94+
95+
.mypy_cache/

README.md

Whitespace-only changes.

pricelevels/__init__.py

Whitespace-only changes.

pricelevels/cluster.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import numpy as np
2+
import pandas as pd
3+
from sklearn.cluster import AgglomerativeClustering
4+
from zigzag import peak_valley_pivots
5+
6+
from .exceptions import InvalidParameterException, InvalidArgumentException
7+
8+
9+
class PriceLevelFromClusters:
10+
11+
def __init__(self, peak_percent_delta, merge_distance, merge_percent=None, peaks='All',
12+
level_selector='median', verbose=False):
13+
self._peak_percent_delta = peak_percent_delta
14+
self._merge_distance = merge_distance
15+
self._merge_percent = merge_percent
16+
17+
self._level_selector = level_selector
18+
self._verbose = verbose
19+
self._peaks = peaks
20+
self._levels = None
21+
self._validate_init_args()
22+
23+
@property
24+
def levels(self):
25+
return self._levels
26+
27+
def _validate_init_args(self):
28+
pass
29+
30+
def fit(self, data):
31+
if isinstance(data, pd.DataFrame):
32+
X = data['Close'].values
33+
elif isinstance(data, np.array):
34+
X = data
35+
else:
36+
raise InvalidArgumentException(
37+
'Only np.array and pd.DataFrame are supported in `fit` method'
38+
)
39+
40+
pivot_prices = self._find_pivot_prices(X)
41+
levels = self._aggregate_prices_to_levels(pivot_prices, self._get_distance(X))
42+
43+
self._levels = levels
44+
45+
def _get_distance(self, X):
46+
if self._merge_distance:
47+
return self._merge_distance
48+
49+
mean_price = np.mean(X)
50+
return self._merge_percent * mean_price / 100
51+
52+
def _find_pivot_prices(self, X):
53+
pivots = peak_valley_pivots(X, self._peak_percent_delta, -self._peak_percent_delta)
54+
if self._peaks == 'All':
55+
indexes = np.where(np.abs(pivots) == 1)
56+
elif self._peaks == 'High':
57+
indexes = np.where(pivots == 1)
58+
elif self._peaks == 'Low':
59+
indexes = np.where(pivots == -1)
60+
else:
61+
raise InvalidParameterException(
62+
'Peaks argument should be one of: `All`, `High`, `Low`'
63+
)
64+
65+
pivot_prices = X[indexes]
66+
67+
return pivot_prices
68+
69+
def _aggregate_prices_to_levels(self, pivot_prices, distance):
70+
clustering = AgglomerativeClustering(distance_threshold=distance, n_clusters=None)
71+
clustering.fit(pivot_prices.reshape(-1, 1))
72+
73+
df = pd.DataFrame(data=pivot_prices, columns=('price',))
74+
df['cluster'] = clustering.labels_
75+
df['peak_count'] = 1
76+
77+
grouped = df.groupby('cluster').agg(
78+
{
79+
'price': self._level_selector,
80+
'peak_count': 'sum'
81+
}
82+
).reset_index()
83+
84+
return grouped.to_dict('records')

pricelevels/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class InvalidParameterException(Exception):
2+
pass
3+
4+
5+
class InvalidArgumentException(Exception):
6+
pass

pricelevels/scoring/__init__.py

Whitespace-only changes.

pricelevels/scoring/abstract.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
class BaseScorer:
2+
3+
def fit(self, levels, ohlc_df):
4+
raise NotImplementedError()

pricelevels/scoring/touch_scorer.py

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
from enum import Enum, auto
2+
3+
import numpy as np
4+
5+
from .abstract import BaseScorer
6+
7+
8+
class PointEventType(Enum):
9+
CUT_BODY = auto()
10+
CUT_WICK = auto()
11+
TOUCH_DOWN_HIGHLOW = auto()
12+
TOUCH_DOWN = auto()
13+
TOUCH_UP_HIGHLOW = auto()
14+
TOUCH_UP = auto()
15+
16+
17+
class PointEvent:
18+
def __init__(self, event_type, timestamp, score_change):
19+
self.type = event_type
20+
self.timestamp = timestamp
21+
self.score_change = score_change
22+
23+
24+
class PointScore:
25+
26+
def __init__(self, point, score, point_event_list):
27+
self.point = point
28+
self.score = score
29+
self.point_event_list = point_event_list
30+
31+
32+
class TouchScorer(BaseScorer):
33+
34+
def __init__(self, min_candles_between_body_cuts=5, diff_perc_from_extreme=0.05,
35+
min_distance_between_levels=0.1, min_trend_percent=0.5, diff_perc_for_candle_close=0.05,
36+
score_for_cut_body=-2, score_for_cut_wick=-1, score_for_touch_high_low=1, score_for_touch_normal=2):
37+
38+
self.DIFF_PERC_FOR_CANDLE_CLOSE = diff_perc_for_candle_close
39+
self.MIN_DIFF_FOR_CONSECUTIVE_CUT = min_candles_between_body_cuts
40+
self.DIFF_PERC_FROM_EXTREME = diff_perc_from_extreme
41+
self.DIFF_PERC_FOR_INTRASR_DISTANCE = min_distance_between_levels
42+
self.MIN_PERC_FOR_TREND = min_trend_percent
43+
44+
self.score_for_cut_body = score_for_cut_body
45+
self.score_for_cut_wick = score_for_cut_wick
46+
self.score_for_touch_high_low = score_for_touch_high_low
47+
self.score_for_touch_normal = score_for_touch_normal
48+
49+
self._scores = None
50+
51+
def closeFromExtreme(self, key, min_value, max_value):
52+
return abs(key - min_value) < (min_value * self.DIFF_PERC_FROM_EXTREME / 100.0) or \
53+
abs(key - max_value) < (max_value * self.DIFF_PERC_FROM_EXTREME / 100)
54+
55+
@staticmethod
56+
def getMin(candles):
57+
return candles['Low'].min()
58+
59+
@staticmethod
60+
def getMax(candles):
61+
return candles['High'].max()
62+
63+
def similar(self, key, used):
64+
for value in used:
65+
if abs(key - value) <= (self.DIFF_PERC_FOR_INTRASR_DISTANCE * value / 100):
66+
return True
67+
return False
68+
69+
def fit(self, levels, ohlc_df):
70+
scores = []
71+
high_low_list = self._get_high_low_list(ohlc_df)
72+
73+
for i, obj in enumerate(levels):
74+
if isinstance(obj, float):
75+
price = obj
76+
elif isinstance(obj, dict):
77+
try:
78+
price = obj['price']
79+
except KeyError:
80+
raise Exception('`levels` supposed to be a list of floats or list of dicts with `price` key')
81+
else:
82+
raise Exception('`levels` supposed to be a list of floats or list of dicts with `price` key')
83+
84+
score = self._get_level_score(ohlc_df, high_low_list, price)
85+
scores.append((i, price, score))
86+
87+
self._scores = scores
88+
89+
@property
90+
def scores(self):
91+
return self._scores
92+
93+
@staticmethod
94+
def _get_high_low_list(ohlc_df):
95+
rolling_lows = ohlc_df['Low'].rolling(window=3).min().shift(-1)
96+
rolling_highs = ohlc_df['High'].rolling(window=3).min().shift(-1)
97+
high_low_list = np.where(ohlc_df['Low'] == rolling_lows, True, False)
98+
high_low_list = np.where(ohlc_df['High'] == rolling_highs, True, high_low_list)
99+
100+
return high_low_list.tolist()
101+
102+
def _get_level_score(self, candles, high_low_marks, price):
103+
events = []
104+
score = 0.0
105+
pos = 0
106+
last_cut_pos = -10
107+
for i in range(len(candles)):
108+
candle = candles.iloc[i]
109+
# If the body of the candle cuts through the price, then deduct some score
110+
if self.cut_body(price, candle) and pos - last_cut_pos > self.MIN_DIFF_FOR_CONSECUTIVE_CUT:
111+
score += self.score_for_cut_body
112+
last_cut_pos = pos
113+
events.append(PointEvent(PointEventType.CUT_BODY, candle['Datetime'], self.score_for_cut_body))
114+
# If the wick of the candle cuts through the price, then deduct some score
115+
elif self.cut_wick(price, candle) and (pos - last_cut_pos > self.MIN_DIFF_FOR_CONSECUTIVE_CUT):
116+
score += self.score_for_cut_wick
117+
last_cut_pos = pos
118+
events.append(PointEvent(PointEventType.CUT_WICK, candle['Datetime'], self.score_for_cut_body))
119+
# If the if is close the high of some candle and it was in an uptrend, then add some score to this
120+
elif self.touch_high(price, candle) and self.in_up_trend(candles, price, pos):
121+
high_low_value = high_low_marks[pos]
122+
# If it is a high, then add some score S1
123+
if high_low_value:
124+
score += self.score_for_touch_high_low
125+
events.append(
126+
PointEvent(PointEventType.TOUCH_UP_HIGHLOW, candle['Datetime'], self.score_for_touch_high_low))
127+
# Else add S2. S2 > S1
128+
else:
129+
score += self.score_for_touch_normal
130+
events.append(PointEvent(PointEventType.TOUCH_UP, candle['Datetime'], self.score_for_touch_normal))
131+
132+
# If the if is close the low of some candle and it was in an downtrend, then add some score to this
133+
elif self.touch_low(price, candle) and self.in_down_trend(candles, price, pos):
134+
high_low_value = high_low_marks[pos]
135+
# If it is a high, then add some score S1
136+
if high_low_value is not None and not high_low_value:
137+
score += self.score_for_touch_high_low
138+
events.append(
139+
PointEvent(PointEventType.TOUCH_DOWN, candle['Datetime'], self.score_for_touch_high_low))
140+
# Else add S2. S2 > S1
141+
else:
142+
score += self.score_for_touch_normal
143+
events.append(
144+
PointEvent(PointEventType.TOUCH_DOWN_HIGHLOW, candle['Datetime'], self.score_for_touch_normal))
145+
146+
pos += 1
147+
148+
return PointScore(price, score, events)
149+
150+
def in_down_trend(self, candles, price, start_pos):
151+
# Either move #MIN_PERC_FOR_TREND in direction of trend, or cut through the price
152+
pos = start_pos
153+
while pos >= 0:
154+
if candles['Low'].iat[pos] < price:
155+
return False
156+
if candles['Low'].iat[pos] - price > (price * self.MIN_PERC_FOR_TREND / 100):
157+
return True
158+
pos -= 1
159+
160+
return False
161+
162+
def in_up_trend(self, candles, price, start_pos):
163+
pos = start_pos
164+
while pos >= 0:
165+
if candles['High'].iat[pos] > price:
166+
return False
167+
if price - candles['Low'].iat[pos] > (price * self.MIN_PERC_FOR_TREND / 100):
168+
return True
169+
pos -= 1
170+
171+
return False
172+
173+
def touch_high(self, price, candle):
174+
high = candle['High']
175+
ltp = candle['Close']
176+
return high <= price and abs(high - price) < ltp * self.DIFF_PERC_FOR_CANDLE_CLOSE / 100
177+
178+
def touch_low(self, price, candle):
179+
low = candle['Low']
180+
ltp = candle['Close']
181+
return low >= price and abs(low - price) < ltp * self.DIFF_PERC_FOR_CANDLE_CLOSE / 100
182+
183+
def cut_body(self, point, candle):
184+
return max(candle['Open'], candle['Close']) > point and min(candle['Open'], candle['Close']) < point
185+
186+
def cut_wick(self, price, candle):
187+
return not self.cut_body(price, candle) and candle['High'] > price and candle['Low'] < price

pricelevels/version.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__version__ = "REPLACE_ME_FROM_TAG"

setup.cfg

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[aliases]
2+
test = pytest
3+
4+
[tool:pytest]
5+
addopts = --verbose --pyargs .

0 commit comments

Comments
 (0)