bullishpy 0.4.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 +114 -250
- bullish/analysis/filter.py +581 -79
- bullish/analysis/functions.py +344 -0
- bullish/analysis/indicators.py +450 -0
- bullish/analysis/predefined_filters.py +87 -0
- bullish/app/app.py +235 -83
- bullish/cli.py +3 -1
- bullish/database/alembic/versions/037dbd721317_.py +1 -0
- bullish/database/alembic/versions/08ac1116e055_.py +592 -0
- bullish/database/alembic/versions/11d35a452b40_.py +368 -0
- bullish/database/alembic/versions/49c83f9eb5ac_.py +103 -0
- bullish/database/alembic/versions/4b0a2f40b7d3_.py +1 -0
- bullish/database/alembic/versions/73564b60fe24_.py +1 -0
- bullish/database/alembic/versions/ee5baabb35f8_.py +51 -0
- bullish/database/crud.py +14 -2
- bullish/database/schemas.py +13 -0
- bullish/figures/figures.py +52 -12
- bullish/interface/interface.py +16 -27
- bullish/jobs/tasks.py +10 -0
- bullish/utils/__init__.py +0 -0
- bullish/utils/checks.py +64 -0
- {bullishpy-0.4.0.dist-info → bullishpy-0.6.0.dist-info}/METADATA +4 -5
- bullishpy-0.6.0.dist-info/RECORD +43 -0
- bullishpy-0.4.0.dist-info/RECORD +0 -34
- {bullishpy-0.4.0.dist-info → bullishpy-0.6.0.dist-info}/WHEEL +0 -0
- {bullishpy-0.4.0.dist-info → bullishpy-0.6.0.dist-info}/entry_points.txt +0 -0
bullish/app/app.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import shelve
|
|
2
2
|
import uuid
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Optional
|
|
4
|
+
from typing import Optional, List, Type, Dict, Any
|
|
5
5
|
|
|
6
6
|
import pandas as pd
|
|
7
7
|
import streamlit as st
|
|
@@ -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,9 +19,20 @@ from bullish.analysis.filter import (
|
|
|
18
19
|
FilterUpdate,
|
|
19
20
|
FilteredResults,
|
|
20
21
|
FilterQueryStored,
|
|
22
|
+
FundamentalAnalysisFilters,
|
|
23
|
+
GROUP_MAPPING,
|
|
24
|
+
GeneralFilter,
|
|
25
|
+
TechnicalAnalysisFilters,
|
|
21
26
|
)
|
|
22
27
|
from bullish.jobs.models import JobTracker
|
|
23
|
-
from bullish.jobs.tasks import update, news
|
|
28
|
+
from bullish.jobs.tasks import update, news, analysis
|
|
29
|
+
from pydantic import BaseModel
|
|
30
|
+
|
|
31
|
+
from bullish.utils.checks import (
|
|
32
|
+
compatible_bearish_database,
|
|
33
|
+
compatible_bullish_database,
|
|
34
|
+
empty_analysis_table,
|
|
35
|
+
)
|
|
24
36
|
|
|
25
37
|
CACHE_SHELVE = "user_cache"
|
|
26
38
|
DB_KEY = "db_path"
|
|
@@ -88,15 +100,151 @@ def dialog_pick_database() -> None:
|
|
|
88
100
|
if event:
|
|
89
101
|
db_path = Path(current_working_directory).joinpath(event["target"]["path"])
|
|
90
102
|
if not (db_path.exists() and db_path.is_file()):
|
|
91
|
-
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.")
|
|
92
106
|
st.stop()
|
|
93
107
|
st.session_state.database_path = db_path
|
|
94
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)
|
|
95
118
|
st.rerun()
|
|
96
119
|
if event is None:
|
|
97
120
|
st.stop()
|
|
98
121
|
|
|
99
122
|
|
|
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
|
+
|
|
136
|
+
for field, info in model.model_fields.items():
|
|
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]
|
|
141
|
+
if info.annotation == Optional[List[str]]: # type: ignore
|
|
142
|
+
data[field] = st.multiselect(
|
|
143
|
+
name,
|
|
144
|
+
groups_mapping()[field],
|
|
145
|
+
default=default,
|
|
146
|
+
key=hash((model.__name__, field)),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
else:
|
|
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
|
+
)
|
|
163
|
+
return data
|
|
164
|
+
|
|
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
|
+
|
|
206
|
+
@st.dialog("🔍 Filter", width="large")
|
|
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)
|
|
235
|
+
|
|
236
|
+
if st.button("🔍 Apply"):
|
|
237
|
+
query = FilterQuery.model_validate(st.session_state.filter_query)
|
|
238
|
+
if query.valid():
|
|
239
|
+
st.session_state.data = bearish_db(
|
|
240
|
+
st.session_state.database_path
|
|
241
|
+
).read_filter_query(query)
|
|
242
|
+
st.session_state.ticker_figure = None
|
|
243
|
+
st.session_state.filter_query = {}
|
|
244
|
+
st.session_state.query = query
|
|
245
|
+
st.rerun()
|
|
246
|
+
|
|
247
|
+
|
|
100
248
|
@st.dialog("📈 Price history and analysis", width="large")
|
|
101
249
|
def dialog_plot_figure() -> None:
|
|
102
250
|
st.markdown(
|
|
@@ -104,7 +252,7 @@ def dialog_plot_figure() -> None:
|
|
|
104
252
|
<style>
|
|
105
253
|
div[data-testid="stDialog"] div[role="dialog"]:has(.big-dialog) {
|
|
106
254
|
width: 90vw;
|
|
107
|
-
height:
|
|
255
|
+
height: 170vh;
|
|
108
256
|
}
|
|
109
257
|
</style>
|
|
110
258
|
""",
|
|
@@ -115,97 +263,101 @@ def dialog_plot_figure() -> None:
|
|
|
115
263
|
st.session_state.ticker_figure = None
|
|
116
264
|
|
|
117
265
|
|
|
118
|
-
|
|
266
|
+
@st.dialog("⭐ Save filtered results")
|
|
267
|
+
def save_filtered_results(bearish_db_: BullishDb) -> None:
|
|
268
|
+
user_input = st.text_input("Selection name").strip()
|
|
269
|
+
headless = st.checkbox("Headless mode", value=True)
|
|
270
|
+
apply = st.button("Apply")
|
|
271
|
+
if apply:
|
|
272
|
+
if not user_input:
|
|
273
|
+
st.error("This field is required.")
|
|
274
|
+
else:
|
|
275
|
+
symbols = st.session_state.data["symbol"].unique().tolist()
|
|
276
|
+
filtered_results = FilteredResults(
|
|
277
|
+
name=user_input,
|
|
278
|
+
filter_query=FilterQueryStored.model_validate(
|
|
279
|
+
st.session_state.query.model_dump(
|
|
280
|
+
exclude_unset=True, exclude_defaults=True
|
|
281
|
+
)
|
|
282
|
+
),
|
|
283
|
+
symbols=symbols,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
bearish_db_.write_filtered_results(filtered_results)
|
|
287
|
+
res = news(
|
|
288
|
+
database_path=st.session_state.database_path,
|
|
289
|
+
symbols=symbols,
|
|
290
|
+
headless=headless,
|
|
291
|
+
)
|
|
292
|
+
bearish_db_.write_job_tracker(
|
|
293
|
+
JobTracker(job_id=str(res.id), type="Fetching news")
|
|
294
|
+
)
|
|
295
|
+
st.session_state.filter_query = None
|
|
296
|
+
st.session_state.query = None
|
|
297
|
+
st.rerun()
|
|
298
|
+
|
|
299
|
+
|
|
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)
|
|
119
311
|
assign_db_state()
|
|
312
|
+
|
|
120
313
|
if st.session_state.database_path is None:
|
|
121
314
|
dialog_pick_database()
|
|
122
315
|
bearish_db_ = bearish_db(st.session_state.database_path)
|
|
123
316
|
charts_tab, jobs_tab = st.tabs(["Charts", "Jobs"])
|
|
124
317
|
if "data" not in st.session_state:
|
|
125
318
|
st.session_state.data = load_analysis_data(bearish_db_)
|
|
126
|
-
with st.sidebar:
|
|
127
|
-
with st.expander("Filter"):
|
|
128
|
-
view_query = sp.pydantic_form(key="my_form", model=FilterQuery)
|
|
129
|
-
if view_query:
|
|
130
|
-
st.session_state.data = bearish_db_.read_filter_query(view_query)
|
|
131
|
-
st.session_state.ticker_figure = None
|
|
132
|
-
st.session_state.filter_query = view_query
|
|
133
|
-
with st.container(border=True):
|
|
134
|
-
disabled = "filter_query" not in st.session_state
|
|
135
|
-
if "filter_query" in st.session_state:
|
|
136
|
-
disabled = st.session_state.filter_query is None
|
|
137
|
-
user_input = st.text_input("Enter your name:", disabled=disabled)
|
|
138
|
-
headless = st.checkbox("Headless mode", value=True, disabled=disabled)
|
|
139
|
-
if st.button("Save", disabled=disabled):
|
|
140
|
-
name = user_input.strip()
|
|
141
|
-
if not name:
|
|
142
|
-
st.error("This field is required.")
|
|
143
|
-
else:
|
|
144
|
-
symbols = st.session_state.data["symbol"].unique().tolist()
|
|
145
|
-
filtered_results = FilteredResults(
|
|
146
|
-
name=name,
|
|
147
|
-
filter_query=FilterQueryStored.model_validate(
|
|
148
|
-
st.session_state.filter_query.model_dump()
|
|
149
|
-
),
|
|
150
|
-
symbols=symbols,
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
bearish_db_.write_filtered_results(filtered_results)
|
|
154
|
-
res = news(
|
|
155
|
-
database_path=st.session_state.database_path,
|
|
156
|
-
symbols=symbols,
|
|
157
|
-
headless=headless,
|
|
158
|
-
)
|
|
159
|
-
bearish_db_.write_job_tracker(
|
|
160
|
-
JobTracker(job_id=str(res.id), type="Fetching news")
|
|
161
|
-
)
|
|
162
|
-
st.session_state.filter_query = None
|
|
163
|
-
st.success(f"Hello, {user_input}!")
|
|
164
|
-
with st.expander("Load"):
|
|
165
|
-
existing_filtered_results = bearish_db_.read_list_filtered_results()
|
|
166
|
-
option = st.selectbox("Saved results", ["", *existing_filtered_results])
|
|
167
|
-
if option:
|
|
168
|
-
filtered_results_ = bearish_db_.read_filtered_results(option)
|
|
169
|
-
if filtered_results_:
|
|
170
|
-
st.session_state.data = bearish_db_.read_analysis_data(
|
|
171
|
-
symbols=filtered_results_.symbols
|
|
172
|
-
)
|
|
173
319
|
|
|
174
|
-
|
|
175
|
-
|
|
320
|
+
with charts_tab:
|
|
321
|
+
with st.container():
|
|
322
|
+
columns = st.columns(12)
|
|
323
|
+
with columns[0]:
|
|
324
|
+
if st.button(" 🔍 ", use_container_width=True):
|
|
325
|
+
st.session_state.filter_query = {}
|
|
326
|
+
filter()
|
|
327
|
+
with columns[1]:
|
|
328
|
+
if (
|
|
329
|
+
"query" in st.session_state
|
|
330
|
+
and st.session_state.query is not None
|
|
331
|
+
and st.session_state.query.valid()
|
|
332
|
+
):
|
|
333
|
+
favorite = st.button(" ⭐ ", use_container_width=True)
|
|
334
|
+
if favorite:
|
|
335
|
+
save_filtered_results(bearish_db_)
|
|
336
|
+
with columns[-1]:
|
|
337
|
+
if st.button(" 📥 ", use_container_width=True):
|
|
338
|
+
load()
|
|
339
|
+
|
|
340
|
+
with st.container():
|
|
341
|
+
st.dataframe(
|
|
342
|
+
st.session_state.data,
|
|
343
|
+
on_select=on_table_select,
|
|
344
|
+
selection_mode="single-row",
|
|
345
|
+
key="selected_data",
|
|
346
|
+
use_container_width=True,
|
|
347
|
+
height=600,
|
|
348
|
+
)
|
|
176
349
|
if (
|
|
177
|
-
|
|
178
|
-
and st.session_state.
|
|
179
|
-
and not st.session_state.data.empty
|
|
350
|
+
"ticker_figure" in st.session_state
|
|
351
|
+
and st.session_state.ticker_figure is not None
|
|
180
352
|
):
|
|
181
|
-
|
|
182
|
-
res = update(
|
|
183
|
-
database_path=st.session_state.database_path,
|
|
184
|
-
symbols=symbols,
|
|
185
|
-
update_query=update_query,
|
|
186
|
-
) # enqueue & get result-handle
|
|
187
|
-
bearish_db_.write_job_tracker(
|
|
188
|
-
JobTracker(job_id=str(res.id), type="Update data")
|
|
189
|
-
)
|
|
190
|
-
st.success("Data update job has been enqueued.")
|
|
191
|
-
st.rerun()
|
|
192
|
-
with charts_tab:
|
|
193
|
-
st.header("✅ Data overview")
|
|
194
|
-
st.dataframe(
|
|
195
|
-
st.session_state.data,
|
|
196
|
-
on_select=on_table_select,
|
|
197
|
-
selection_mode="single-row",
|
|
198
|
-
key="selected_data",
|
|
199
|
-
use_container_width=True,
|
|
200
|
-
height=600,
|
|
201
|
-
)
|
|
202
|
-
if (
|
|
203
|
-
"ticker_figure" in st.session_state
|
|
204
|
-
and st.session_state.ticker_figure is not None
|
|
205
|
-
):
|
|
206
|
-
dialog_plot_figure()
|
|
353
|
+
dialog_plot_figure()
|
|
207
354
|
|
|
208
355
|
with jobs_tab:
|
|
356
|
+
columns = st.columns(12)
|
|
357
|
+
with columns[0]:
|
|
358
|
+
if st.button(" ⏳ ", use_container_width=True):
|
|
359
|
+
jobs()
|
|
360
|
+
|
|
209
361
|
job_trackers = bearish_db_.read_job_trackers()
|
|
210
362
|
st.dataframe(
|
|
211
363
|
job_trackers,
|
bullish/cli.py
CHANGED
|
@@ -64,7 +64,9 @@ def serve(
|
|
|
64
64
|
time.sleep(1)
|
|
65
65
|
|
|
66
66
|
except Exception as exc: # pragma: no cover
|
|
67
|
-
typer.secho(
|
|
67
|
+
typer.secho(
|
|
68
|
+
f"❌ Failed to start services: {exc}", fg=typer.colors.RED, err=True
|
|
69
|
+
)
|
|
68
70
|
_shutdown()
|
|
69
71
|
|
|
70
72
|
|