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/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.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.")
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: 110vh;
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
- def main() -> None: # noqa: PLR0915, C901
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
- with st.expander("Update"):
175
- update_query = sp.pydantic_form(key="update", model=FilterUpdate)
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
- update_query
178
- and st.session_state.data is not None
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
- symbols = st.session_state.data["symbol"].unique().tolist()
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(f"❌ Failed to start services: {exc}", fg=typer.colors.RED, err=True)
67
+ typer.secho(
68
+ f"❌ Failed to start services: {exc}", fg=typer.colors.RED, err=True
69
+ )
68
70
  _shutdown()
69
71
 
70
72
 
@@ -5,6 +5,7 @@ Revises:
5
5
  Create Date: 2025-06-14 16:50:56.919222
6
6
 
7
7
  """
8
+
8
9
  from typing import Sequence, Union
9
10
 
10
11
  import sqlalchemy as sa