@@ -45,7 +45,7 @@ class LoginRegisterModel(BaseModel):
4545 str ,
4646 StringConstraints (min_length = 1 , max_length = 19 , pattern = r"^[a-zA-Z0-9_-]+$" ),
4747 ]
48- password : str
48+ password : Annotated [ str , StringConstraints ( min_length = 4 )]
4949
5050
5151class AuthParams (BaseModel ):
@@ -111,6 +111,7 @@ class UserSettingsRequest(BaseModel):
111111 currency : str | None = Field (default = None , min_length = 1 , max_length = 10 , description = "Currency symbol, e.g. €, $, £" )
112112 apprise_url : str | None = Field (default = None , description = "Comma-separated Apprise notification URLs" )
113113 dark_mode : bool | None = Field (default = None )
114+ earnings_notify : bool | None = Field (default = None )
114115
115116
116117class UserSettingsOut (BaseModel ):
@@ -119,6 +120,7 @@ class UserSettingsOut(BaseModel):
119120 currency : str | None
120121 apprise_url : str | None
121122 dark_mode : bool | None
123+ earnings_notify : bool | None
122124
123125
124126# =============================================================================
@@ -130,21 +132,34 @@ class AlertRequest(BaseModel):
130132 ticker : str = Field (min_length = 1 , max_length = 20 )
131133 target_price : float = Field (gt = 0 )
132134 trigger_above : bool
135+ notes : str | None = Field (default = None , max_length = 500 )
136+ actionable : bool = False
133137
134138 @field_validator ("ticker" )
135139 @classmethod
136140 def normalize_ticker (cls , v : str ) -> str :
137141 return v .strip ().upper ()
138142
143+ @field_validator ("notes" )
144+ @classmethod
145+ def normalize_notes (cls , v : str | None ) -> str | None :
146+ if v is not None :
147+ stripped = v .strip ()
148+ return stripped if stripped else None
149+ return None
150+
139151
140152class AlertUpdateRequest (BaseModel ):
141153 target_price : float | None = Field (default = None , gt = 0 )
142154 trigger_above : bool | None = None
155+ notes : str | None = None # None = don't change; "" = clear; non-empty = set
156+ actionable : bool | None = None
143157
144158 @model_validator (mode = "after" )
145159 def at_least_one_field (self ) -> "AlertUpdateRequest" :
146- if self .target_price is None and self .trigger_above is None :
147- raise ValueError ("Provide at least one of: target_price, trigger_above" )
160+ if (self .target_price is None and self .trigger_above is None
161+ and self .notes is None and self .actionable is None ):
162+ raise ValueError ("Provide at least one field to update" )
148163 return self
149164
150165
@@ -157,6 +172,8 @@ class AlertOut(BaseModel):
157172 trigger_above : bool
158173 is_armed : bool
159174 last_triggered : date | None
175+ notes : str | None
176+ actionable : bool
160177
161178
162179# =============================================================================
@@ -512,6 +529,36 @@ class DashboardResponse(BaseModel):
512529 user_currency : str
513530
514531
532+ class RawPositionRow (BaseModel ):
533+ """Position computed from WAC only — no live price fields."""
534+ ticker : str
535+ envelope_name : str
536+ shares : float
537+ avg_cost : float
538+ cost_basis : float
539+
540+
541+ class StaticTotals (BaseModel ):
542+ """Totals computable from DB alone — no live prices required."""
543+ total_cash : float
544+ net_deposits : dict [str , float ]
545+ dividend_income_90d : float
546+
547+
548+ class DashboardLedgerResponse (BaseModel ):
549+ """
550+ Fast DB+CPU slice of the dashboard. Returned by GET /profile/dashboard/ledger
551+ in ~20 ms. Contains everything that does not require a yfinance call so the
552+ frontend can render envelopes, the transaction ledger, and position skeletons
553+ while the full /dashboard market-data call is still in flight.
554+ """
555+ envelopes : list [EnvelopeSummary ] # total_value = cash_available only (no equity yet)
556+ transactions : list [TransactionOut ]
557+ user_currency : str
558+ raw_positions : list [RawPositionRow ]
559+ static_totals : StaticTotals
560+
561+
515562# =============================================================================
516563# PORTFOLIO OVERVIEW
517564# =============================================================================
@@ -588,6 +635,7 @@ class ProjectionRequest(BaseModel):
588635 deposit : float = Field (ge = 0.0 , default = 0.0 )
589636 annual_rate_pct : float = Field (ge = 0.0 , le = 100.0 )
590637 deposit_frequency : DepositFrequency = DepositFrequency .MONTHLY
638+ initial_balance : float | None = Field (default = None , ge = 0.0 )
591639
592640
593641class ProjectionDataset (BaseModel ):
@@ -626,3 +674,60 @@ class ProjectionResponse(BaseModel):
626674 milestones : list [ProjectionMilestone ]
627675 summary : ProjectionSummary
628676 inputs : ProjectionInputs
677+
678+
679+ # =============================================================================
680+ # EARNINGS CALENDAR
681+ # =============================================================================
682+
683+
684+ class EarningsEntry (BaseModel ):
685+ ticker : str
686+ company_name : str | None
687+ earnings_date : str
688+ sector : str | None
689+ currency : str | None
690+ history : list [PricePoint ]
691+
692+
693+ class EarningsCalendarResponse (BaseModel ):
694+ entries : list [EarningsEntry ]
695+ generated_at : str
696+
697+
698+ # =============================================================================
699+ # SCREENER
700+ # =============================================================================
701+
702+
703+ class ScreenerRow (BaseModel ):
704+ ticker : str
705+ name : str
706+ sector : str | None
707+ currency : str
708+ current_price : float
709+ change_1d_pct : float
710+ change_5d_pct : float | None
711+ change_1m_pct : float | None
712+ rsi_14 : float | None
713+ bollinger_b : float | None
714+ bollinger_signal : str | None
715+ sma_50 : float | None
716+ sma_200 : float | None
717+ sma_signal : str | None
718+ macd_signal : str | None
719+ stochastic_k : float | None
720+ stochastic_signal : str | None
721+ volume_trend_ratio : float | None
722+ volume_trend_signal : str | None
723+ buy_pct : int
724+ hold_pct : int
725+ sell_pct : int
726+ is_held : bool
727+
728+
729+ class ScreenerSentiment (BaseModel ):
730+ ticker : str
731+ sentiment_score : float # -1.0 to 1.0 (winsorized)
732+ label : str # positive | neutral | negative
733+ article_count : int
0 commit comments