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/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
- TechnicalAnalysisFilter,
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.error("Please choose a valid file.")
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
- def build_filter(model: Type[BaseModel]) -> Dict[str, Any]:
106
- data: Dict[str, Any] = {}
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(name, GROUP_MAPPING[field], key=name)
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((item.ge for item in info.metadata if hasattr(item, "ge")), None)
114
- le = next((item.le for item in info.metadata if hasattr(item, "le")), None)
115
- data[field] = list(st.slider(name, le, ge, tuple(info.default))) # type: ignore
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: 110vh;
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
- col1, col2, _ = st.columns([0.5, 0.5, 5])
247
- with col1:
248
- if st.button("🔍 Filter"):
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 col2:
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,