bullishpy 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of bullishpy might be problematic. Click here for more details.
- bullish/analysis/analysis.py +112 -251
- bullish/analysis/filter.py +83 -41
- bullish/analysis/functions.py +344 -0
- bullish/analysis/indicators.py +450 -0
- bullish/analysis/predefined_filters.py +87 -0
- bullish/app/app.py +149 -67
- bullish/database/alembic/versions/08ac1116e055_.py +592 -0
- bullish/database/alembic/versions/49c83f9eb5ac_.py +103 -0
- bullish/database/alembic/versions/ee5baabb35f8_.py +51 -0
- bullish/database/crud.py +5 -0
- bullish/database/schemas.py +13 -0
- bullish/figures/figures.py +52 -12
- bullish/interface/interface.py +3 -0
- bullish/jobs/tasks.py +10 -0
- bullish/utils/__init__.py +0 -0
- bullish/utils/checks.py +64 -0
- {bullishpy-0.5.0.dist-info → bullishpy-0.6.0.dist-info}/METADATA +3 -4
- {bullishpy-0.5.0.dist-info → bullishpy-0.6.0.dist-info}/RECORD +20 -12
- {bullishpy-0.5.0.dist-info → bullishpy-0.6.0.dist-info}/WHEEL +0 -0
- {bullishpy-0.5.0.dist-info → bullishpy-0.6.0.dist-info}/entry_points.txt +0 -0
bullish/app/app.py
CHANGED
|
@@ -11,6 +11,7 @@ from bearish.models.price.prices import Prices # type: ignore
|
|
|
11
11
|
from bearish.models.query.query import AssetQuery, Symbols # type: ignore
|
|
12
12
|
from streamlit_file_browser import st_file_browser # type: ignore
|
|
13
13
|
|
|
14
|
+
from bullish.analysis.predefined_filters import PredefinedFilters
|
|
14
15
|
from bullish.database.crud import BullishDb
|
|
15
16
|
from bullish.figures.figures import plot
|
|
16
17
|
from bullish.analysis.filter import (
|
|
@@ -18,15 +19,21 @@ from bullish.analysis.filter import (
|
|
|
18
19
|
FilterUpdate,
|
|
19
20
|
FilteredResults,
|
|
20
21
|
FilterQueryStored,
|
|
21
|
-
|
|
22
|
-
FundamentalAnalysisFilter,
|
|
22
|
+
FundamentalAnalysisFilters,
|
|
23
23
|
GROUP_MAPPING,
|
|
24
24
|
GeneralFilter,
|
|
25
|
+
TechnicalAnalysisFilters,
|
|
25
26
|
)
|
|
26
27
|
from bullish.jobs.models import JobTracker
|
|
27
|
-
from bullish.jobs.tasks import update, news
|
|
28
|
+
from bullish.jobs.tasks import update, news, analysis
|
|
28
29
|
from pydantic import BaseModel
|
|
29
30
|
|
|
31
|
+
from bullish.utils.checks import (
|
|
32
|
+
compatible_bearish_database,
|
|
33
|
+
compatible_bullish_database,
|
|
34
|
+
empty_analysis_table,
|
|
35
|
+
)
|
|
36
|
+
|
|
30
37
|
CACHE_SHELVE = "user_cache"
|
|
31
38
|
DB_KEY = "db_path"
|
|
32
39
|
|
|
@@ -93,52 +100,139 @@ def dialog_pick_database() -> None:
|
|
|
93
100
|
if event:
|
|
94
101
|
db_path = Path(current_working_directory).joinpath(event["target"]["path"])
|
|
95
102
|
if not (db_path.exists() and db_path.is_file()):
|
|
96
|
-
st.
|
|
103
|
+
st.stop()
|
|
104
|
+
if not compatible_bearish_database(db_path):
|
|
105
|
+
st.error(f"The database {db_path} is not compatible with this application.")
|
|
97
106
|
st.stop()
|
|
98
107
|
st.session_state.database_path = db_path
|
|
99
108
|
store_db(db_path)
|
|
109
|
+
compatible_bullish_db = compatible_bullish_database(db_path)
|
|
110
|
+
if (not compatible_bullish_db) or (
|
|
111
|
+
compatible_bullish_db and empty_analysis_table(db_path)
|
|
112
|
+
):
|
|
113
|
+
st.warning(
|
|
114
|
+
f"The database {db_path} has not the necessary data to run this application. "
|
|
115
|
+
"A backround job will be started to update the data."
|
|
116
|
+
)
|
|
117
|
+
analysis(db_path)
|
|
100
118
|
st.rerun()
|
|
101
119
|
if event is None:
|
|
102
120
|
st.stop()
|
|
103
121
|
|
|
104
122
|
|
|
105
|
-
|
|
106
|
-
|
|
123
|
+
@st.cache_resource
|
|
124
|
+
def symbols() -> List[str]:
|
|
125
|
+
bearish_db_ = bearish_db(st.session_state.database_path)
|
|
126
|
+
return bearish_db_.read_symbols()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def groups_mapping() -> Dict[str, List[str]]:
|
|
130
|
+
GROUP_MAPPING["symbol"] = symbols()
|
|
131
|
+
return GROUP_MAPPING
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def build_filter(model: Type[BaseModel], data: Dict[str, Any]) -> Dict[str, Any]:
|
|
135
|
+
|
|
107
136
|
for field, info in model.model_fields.items():
|
|
108
|
-
name = info.alias or field
|
|
137
|
+
name = info.description or info.alias or field
|
|
138
|
+
default = info.default
|
|
139
|
+
if data.get(field) and data[field] != info.default:
|
|
140
|
+
default = data[field]
|
|
109
141
|
if info.annotation == Optional[List[str]]: # type: ignore
|
|
110
|
-
data[field] = st.multiselect(
|
|
142
|
+
data[field] = st.multiselect(
|
|
143
|
+
name,
|
|
144
|
+
groups_mapping()[field],
|
|
145
|
+
default=default,
|
|
146
|
+
key=hash((model.__name__, field)),
|
|
147
|
+
)
|
|
111
148
|
|
|
112
149
|
else:
|
|
113
|
-
ge = next(
|
|
114
|
-
|
|
115
|
-
|
|
150
|
+
ge = next(
|
|
151
|
+
(item.ge for item in info.metadata if hasattr(item, "ge")),
|
|
152
|
+
info.default[0] if info.default and len(info.default) == 2 else None,
|
|
153
|
+
)
|
|
154
|
+
le = next(
|
|
155
|
+
(item.le for item in info.metadata if hasattr(item, "le")),
|
|
156
|
+
info.default[1] if info.default and len(info.default) == 2 else None,
|
|
157
|
+
)
|
|
158
|
+
data[field] = list(
|
|
159
|
+
st.slider( # type: ignore
|
|
160
|
+
name, ge, le, tuple(default), key=hash((model.__name__, field))
|
|
161
|
+
)
|
|
162
|
+
)
|
|
116
163
|
return data
|
|
117
164
|
|
|
118
165
|
|
|
166
|
+
@st.dialog("⏳ Jobs", width="large")
|
|
167
|
+
def jobs() -> None:
|
|
168
|
+
with st.expander("Update data"):
|
|
169
|
+
bearish_db_ = bearish_db(st.session_state.database_path)
|
|
170
|
+
update_query = sp.pydantic_form(key="update", model=FilterUpdate)
|
|
171
|
+
if (
|
|
172
|
+
update_query
|
|
173
|
+
and st.session_state.data is not None
|
|
174
|
+
and not st.session_state.data.empty
|
|
175
|
+
):
|
|
176
|
+
symbols = st.session_state.data["symbol"].unique().tolist()
|
|
177
|
+
res = update(
|
|
178
|
+
database_path=st.session_state.database_path,
|
|
179
|
+
symbols=symbols,
|
|
180
|
+
update_query=update_query,
|
|
181
|
+
) # enqueue & get result-handle
|
|
182
|
+
bearish_db_.write_job_tracker(
|
|
183
|
+
JobTracker(job_id=str(res.id), type="Update data")
|
|
184
|
+
)
|
|
185
|
+
st.success("Data update job has been enqueued.")
|
|
186
|
+
st.rerun()
|
|
187
|
+
with st.expander("Update analysis"):
|
|
188
|
+
if st.button("Update analysis"):
|
|
189
|
+
analysis(st.session_state.database_path)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@st.dialog("📥 Load", width="large")
|
|
193
|
+
def load() -> None:
|
|
194
|
+
bearish_db_ = bearish_db(st.session_state.database_path)
|
|
195
|
+
existing_filtered_results = bearish_db_.read_list_filtered_results()
|
|
196
|
+
option = st.selectbox("Select portfolio", ["", *existing_filtered_results])
|
|
197
|
+
if option:
|
|
198
|
+
filtered_results_ = bearish_db_.read_filtered_results(option)
|
|
199
|
+
if filtered_results_:
|
|
200
|
+
st.session_state.data = bearish_db_.read_analysis_data(
|
|
201
|
+
symbols=filtered_results_.symbols
|
|
202
|
+
)
|
|
203
|
+
st.rerun()
|
|
204
|
+
|
|
205
|
+
|
|
119
206
|
@st.dialog("🔍 Filter", width="large")
|
|
120
207
|
def filter() -> None:
|
|
208
|
+
with st.container():
|
|
209
|
+
column_1, column_2 = st.columns(2)
|
|
210
|
+
with column_1:
|
|
211
|
+
# TODO: order here matters
|
|
212
|
+
with st.expander("Predefined filters"):
|
|
213
|
+
predefined_filter_names = (
|
|
214
|
+
PredefinedFilters().get_predefined_filter_names()
|
|
215
|
+
)
|
|
216
|
+
option = st.selectbox(
|
|
217
|
+
"Select a predefined filter",
|
|
218
|
+
["", *predefined_filter_names],
|
|
219
|
+
)
|
|
220
|
+
if option:
|
|
221
|
+
data_ = PredefinedFilters().get_predefined_filter(option)
|
|
222
|
+
st.session_state.filter_query.update(data_)
|
|
223
|
+
with st.expander("Technical Analysis"):
|
|
224
|
+
for filter in TechnicalAnalysisFilters:
|
|
225
|
+
with st.expander(filter._description): # type: ignore
|
|
226
|
+
build_filter(filter, st.session_state.filter_query)
|
|
227
|
+
|
|
228
|
+
with column_2:
|
|
229
|
+
with st.expander("Fundamental Analysis"):
|
|
230
|
+
for filter in FundamentalAnalysisFilters:
|
|
231
|
+
with st.expander(filter._description): # type: ignore
|
|
232
|
+
build_filter(filter, st.session_state.filter_query)
|
|
233
|
+
with st.expander("General filter"):
|
|
234
|
+
build_filter(GeneralFilter, st.session_state.filter_query)
|
|
121
235
|
|
|
122
|
-
st.markdown(
|
|
123
|
-
"""
|
|
124
|
-
<style>
|
|
125
|
-
div[data-testid="stDialog"] div[role="dialog"]:has(.big-dialog) {
|
|
126
|
-
width: 90vw;
|
|
127
|
-
height: 110vh;
|
|
128
|
-
}
|
|
129
|
-
</style>
|
|
130
|
-
""",
|
|
131
|
-
unsafe_allow_html=True,
|
|
132
|
-
)
|
|
133
|
-
with st.expander("Technical Analysis"):
|
|
134
|
-
data_ = build_filter(TechnicalAnalysisFilter)
|
|
135
|
-
st.session_state.filter_query.update(data_)
|
|
136
|
-
with st.expander("Fundamental Analysis"):
|
|
137
|
-
data_ = build_filter(FundamentalAnalysisFilter)
|
|
138
|
-
st.session_state.filter_query.update(data_)
|
|
139
|
-
with st.expander("General filter"):
|
|
140
|
-
data_ = build_filter(GeneralFilter)
|
|
141
|
-
st.session_state.filter_query.update(data_)
|
|
142
236
|
if st.button("🔍 Apply"):
|
|
143
237
|
query = FilterQuery.model_validate(st.session_state.filter_query)
|
|
144
238
|
if query.valid():
|
|
@@ -158,7 +252,7 @@ def dialog_plot_figure() -> None:
|
|
|
158
252
|
<style>
|
|
159
253
|
div[data-testid="stDialog"] div[role="dialog"]:has(.big-dialog) {
|
|
160
254
|
width: 90vw;
|
|
161
|
-
height:
|
|
255
|
+
height: 170vh;
|
|
162
256
|
}
|
|
163
257
|
</style>
|
|
164
258
|
""",
|
|
@@ -204,6 +298,16 @@ def save_filtered_results(bearish_db_: BullishDb) -> None:
|
|
|
204
298
|
|
|
205
299
|
|
|
206
300
|
def main() -> None:
|
|
301
|
+
hide_elements = """
|
|
302
|
+
<style>
|
|
303
|
+
div[data-testid="stSliderTickBarMin"],
|
|
304
|
+
div[data-testid="stSliderTickBarMax"] {
|
|
305
|
+
display: none;
|
|
306
|
+
}
|
|
307
|
+
</style>
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
st.markdown(hide_elements, unsafe_allow_html=True)
|
|
207
311
|
assign_db_state()
|
|
208
312
|
|
|
209
313
|
if st.session_state.database_path is None:
|
|
@@ -212,55 +316,28 @@ def main() -> None:
|
|
|
212
316
|
charts_tab, jobs_tab = st.tabs(["Charts", "Jobs"])
|
|
213
317
|
if "data" not in st.session_state:
|
|
214
318
|
st.session_state.data = load_analysis_data(bearish_db_)
|
|
215
|
-
with st.sidebar:
|
|
216
|
-
with st.expander("Load"):
|
|
217
|
-
existing_filtered_results = bearish_db_.read_list_filtered_results()
|
|
218
|
-
option = st.selectbox("Saved results", ["", *existing_filtered_results])
|
|
219
|
-
if option:
|
|
220
|
-
filtered_results_ = bearish_db_.read_filtered_results(option)
|
|
221
|
-
if filtered_results_:
|
|
222
|
-
st.session_state.data = bearish_db_.read_analysis_data(
|
|
223
|
-
symbols=filtered_results_.symbols
|
|
224
|
-
)
|
|
225
319
|
|
|
226
|
-
with st.expander("Update"):
|
|
227
|
-
update_query = sp.pydantic_form(key="update", model=FilterUpdate)
|
|
228
|
-
if (
|
|
229
|
-
update_query
|
|
230
|
-
and st.session_state.data is not None
|
|
231
|
-
and not st.session_state.data.empty
|
|
232
|
-
):
|
|
233
|
-
symbols = st.session_state.data["symbol"].unique().tolist()
|
|
234
|
-
res = update(
|
|
235
|
-
database_path=st.session_state.database_path,
|
|
236
|
-
symbols=symbols,
|
|
237
|
-
update_query=update_query,
|
|
238
|
-
) # enqueue & get result-handle
|
|
239
|
-
bearish_db_.write_job_tracker(
|
|
240
|
-
JobTracker(job_id=str(res.id), type="Update data")
|
|
241
|
-
)
|
|
242
|
-
st.success("Data update job has been enqueued.")
|
|
243
|
-
st.rerun()
|
|
244
320
|
with charts_tab:
|
|
245
321
|
with st.container():
|
|
246
|
-
|
|
247
|
-
with
|
|
248
|
-
if st.button("🔍
|
|
322
|
+
columns = st.columns(12)
|
|
323
|
+
with columns[0]:
|
|
324
|
+
if st.button(" 🔍 ", use_container_width=True):
|
|
249
325
|
st.session_state.filter_query = {}
|
|
250
326
|
filter()
|
|
251
|
-
with
|
|
327
|
+
with columns[1]:
|
|
252
328
|
if (
|
|
253
329
|
"query" in st.session_state
|
|
254
330
|
and st.session_state.query is not None
|
|
255
331
|
and st.session_state.query.valid()
|
|
256
332
|
):
|
|
257
|
-
favorite = st.button(" ⭐ ")
|
|
333
|
+
favorite = st.button(" ⭐ ", use_container_width=True)
|
|
258
334
|
if favorite:
|
|
259
335
|
save_filtered_results(bearish_db_)
|
|
336
|
+
with columns[-1]:
|
|
337
|
+
if st.button(" 📥 ", use_container_width=True):
|
|
338
|
+
load()
|
|
260
339
|
|
|
261
340
|
with st.container():
|
|
262
|
-
st.header("✅ Data overview")
|
|
263
|
-
|
|
264
341
|
st.dataframe(
|
|
265
342
|
st.session_state.data,
|
|
266
343
|
on_select=on_table_select,
|
|
@@ -276,6 +353,11 @@ def main() -> None:
|
|
|
276
353
|
dialog_plot_figure()
|
|
277
354
|
|
|
278
355
|
with jobs_tab:
|
|
356
|
+
columns = st.columns(12)
|
|
357
|
+
with columns[0]:
|
|
358
|
+
if st.button(" ⏳ ", use_container_width=True):
|
|
359
|
+
jobs()
|
|
360
|
+
|
|
279
361
|
job_trackers = bearish_db_.read_job_trackers()
|
|
280
362
|
st.dataframe(
|
|
281
363
|
job_trackers,
|