File size: 13,763 Bytes
9d4bd7c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
# schemas.py
from pydantic import BaseModel, Field, ConfigDict  # Use ConfigDict for Pydantic V2
from typing import Optional, List
from datetime import date, datetime
from decimal import Decimal


# --- Portfolio Schemas ---
class PortfolioBase(BaseModel):
    id: int
    name: str
    description: Optional[str] = None
    is_active: bool
    created_at: datetime
    updated_at: datetime

    model_config = ConfigDict(from_attributes=True)


class PortfolioCreate(BaseModel):
    name: str = Field(
        ..., min_length=1, max_length=100, description="Name of the portfolio"
    )
    description: Optional[str] = Field(
        None, description="Optional description for the portfolio"
    )


class PortfolioUpdate(BaseModel):
    name: Optional[str] = Field(
        None, min_length=1, max_length=100, description="New name for the portfolio"
    )
    description: Optional[str] = Field(
        None, description="New description for the portfolio"
    )
    is_active: Optional[bool] = Field(
        None, description="Set portfolio active or inactive status"
    )


class PortfolioListResponse(BaseModel):
    portfolios: List[PortfolioBase]
    total_count: int

    model_config = ConfigDict(from_attributes=True)


# --- Stock Holding Schemas ---
class StockHoldingBase(BaseModel):
    stock_id: int = Field(..., description="Internal ID of the stock master record")
    quantity: Decimal = Field(..., gt=0, description="Number of shares held")
    purchase_price: Decimal = Field(
        ...,
        gt=0,
        description="Average price per share at purchase for the aggregated holding",
    )
    purchase_date: date = Field(
        ..., description="Representative date of stock purchase (e.g., latest buy)"
    )
    notes: Optional[str] = Field(None, description="Additional notes for this holding")


class StockHoldingCreate(StockHoldingBase):
    # Used when adding a new lot of stocks. purchase_price is unit price for this lot.
    pass


class StockHoldingUpdate(BaseModel):
    # For updating notes or other specific fields on an aggregated holding.
    # Avoid direct updates to quantity/purchase_price here unless specific logic handles recalculation of average price.
    quantity: Optional[Decimal] = Field(
        None, gt=0, description="Updated total number of shares (use with caution)"
    )
    purchase_price: Optional[Decimal] = Field(
        None,
        gt=0,
        description="Updated average purchase price per share (use with caution)",
    )
    purchase_date: Optional[date] = Field(
        None, description="Updated representative purchase date"
    )
    notes: Optional[str] = Field(None, description="Updated notes")


class StockHoldingResponse(StockHoldingBase):
    id: int = Field(
        ..., description="Unique ID of the PortfolioStock (aggregated holding) record"
    )
    stock_symbol: str = Field(..., description="Ticker symbol of the stock")
    stock_name: str = Field(..., description="Name of the stock company")
    current_price: Optional[Decimal] = Field(
        None, description="Current market price per share"
    )
    market_value: Optional[Decimal] = Field(
        None, description="Total current market value of the holding"
    )
    gain_loss: Optional[Decimal] = Field(None, description="Absolute gain or loss")
    gain_loss_percentage: Optional[Decimal] = Field(
        None, description="Percentage gain or loss"
    )
    created_at: datetime

    model_config = ConfigDict(from_attributes=True)


class StockSellSchema(BaseModel):
    quantity: Decimal = Field(..., gt=0, description="Number of shares to sell")
    sell_price: Decimal = Field(
        ..., gt=0, description="Price per share at which stock was sold"
    )
    sell_date: date = Field(..., description="Date of the sale")
    notes: Optional[str] = Field(
        None, description="Additional notes for the sell transaction"
    )


# --- UTT (Unit Trust / Mutual Fund) Holding Schemas ---
class UTTHoldingBase(BaseModel):
    utt_fund_id: int = Field(
        ..., description="Internal ID of the UTT fund master record"
    )
    units_held: Decimal = Field(..., gt=0, description="Number of units held")
    purchase_price: Decimal = Field(
        ...,
        gt=0,
        description="Average price per unit at purchase (NAV) for the aggregated holding",
    )
    purchase_date: date = Field(
        ..., description="Representative date of UTT purchase (e.g., latest buy)"
    )
    notes: Optional[str] = Field(None, description="Additional notes for this holding")


class UTTHoldingCreate(UTTHoldingBase):
    # Used when adding a new lot of UTTs. purchase_price is unit price for this lot.
    pass


class UTTHoldingUpdate(BaseModel):
    units_held: Optional[Decimal] = Field(
        None, gt=0, description="Updated number of units held (use with caution)"
    )
    purchase_price: Optional[Decimal] = Field(
        None,
        gt=0,
        description="Updated average purchase price per unit (use with caution)",
    )
    purchase_date: Optional[date] = Field(
        None, description="Updated representative purchase date"
    )
    notes: Optional[str] = Field(None, description="Updated notes")


class UTTHoldingResponse(UTTHoldingBase):
    id: int = Field(
        ..., description="Unique ID of the PortfolioUTT (aggregated holding) record"
    )
    fund_symbol: str = Field(..., description="Symbol of the UTT fund")
    fund_name: str = Field(..., description="Name of the UTT fund")
    current_nav: Optional[Decimal] = Field(
        None, description="Current Net Asset Value (NAV) per unit"
    )
    market_value: Optional[Decimal] = Field(
        None, description="Total current market value of the holding"
    )
    gain_loss: Optional[Decimal] = Field(None, description="Absolute gain or loss")
    gain_loss_percentage: Optional[Decimal] = Field(
        None, description="Percentage gain or loss"
    )
    created_at: datetime

    model_config = ConfigDict(from_attributes=True)


class UTTSellSchema(BaseModel):
    units_to_sell: Decimal = Field(
        ..., gt=0, description="Number of UTT units to sell"
    )  # Changed from 'units'
    sell_price: Decimal = Field(
        ..., gt=0, description="Price per unit at which UTT was sold (NAV)"
    )
    sell_date: date = Field(..., description="Date of the sale")
    notes: Optional[str] = Field(
        None, description="Additional notes for the sell transaction"
    )


# --- Bond Holding Schemas ---
class BondHoldingBase(BaseModel):
    # bond_id: int = Field(..., description="Internal ID of the bond master record")
    face_value_held: Decimal = Field(
        ..., gt=0, description="Total face value of the bond held"
    )
    auction_number: Optional[int] = Field(
        None, description="Auction number if applicable (e.g., for government bonds)"
    )
    auction_date: Optional[date] = Field(
        None, description="Auction date if applicable (e.g., for government bonds)"
    )
    purchase_price: Decimal = Field(
        ...,
        gt=0,
        description="TOTAL purchase price paid for the entire face_value_held (aggregated holding).",
    )
    purchase_date: date = Field(
        ..., description="Representative date of bond purchase (e.g., latest buy)"
    )
    notes: Optional[str] = Field(None, description="Additional notes for this holding")


class BondHoldingCreate(BondHoldingBase):
    # Used when adding a new lot of bonds. purchase_price is TOTAL cost for this specific lot of face_value_held.
    pass


class BondHoldingUpdate(BaseModel):
    face_value_held: Optional[Decimal] = Field(
        None, gt=0, description="Updated total face value held"
    )
    purchase_price: Optional[Decimal] = Field(
        None,
        gt=0,
        description="Updated TOTAL purchase price for the new face_value_held (use with caution)",
    )
    purchase_date: Optional[date] = Field(
        None, description="Updated representative purchase date"
    )
    notes: Optional[str] = Field(None, description="Updated notes")


class BondHoldingResponse(BondHoldingBase):
    id: int = Field(
        ..., description="Unique ID of the PortfolioBond (aggregated holding) record"
    )
    instrument_type: str = Field(..., description="Type of bond instrument")
    auction_number: Optional[int] = Field(
        None, description="Auction number if applicable"
    )
    maturity_date: date = Field(..., description="Maturity date of the bond")
    current_price: Optional[Decimal] = Field(
        None,
        description="Current market price (e.g., percentage of face value like 99.5)",
    )
    market_value: Optional[Decimal] = Field(
        None, description="Total current market value of the holding"
    )
    accrued_interest: Optional[Decimal] = Field(
        None, description="Accrued interest on the bond"
    )
    yield_to_maturity: Optional[Decimal] = Field(
        None, description="Yield to maturity of the bond"
    )
    gain_loss: Optional[Decimal] = Field(
        None, description="Absolute gain or loss on principal"
    )
    created_at: datetime

    model_config = ConfigDict(from_attributes=True)


class BondSellSchema(BaseModel):
    face_value_to_sell: Decimal = Field(
        ..., gt=0, description="Face value of the bond portion being sold"
    )  # Changed from 'face_value_sold'
    sell_price: Decimal = Field(
        ..., gt=0, description="TOTAL selling proceeds for the face_value_to_sell."
    )
    sell_date: date = Field(..., description="Date of the sale")
    notes: Optional[str] = Field(
        None, description="Additional notes for the sell transaction"
    )


# --- Calendar Event Schemas ---
class CalendarEventBase(BaseModel):
    event_date: date
    event_type: str = Field(..., max_length=50)
    title: str = Field(..., max_length=200)
    description: Optional[str] = None
    asset_type: Optional[str] = Field(None, max_length=10)
    asset_id: Optional[int] = None
    estimated_amount: Optional[Decimal] = None


class CalendarEventCreate(CalendarEventBase):
    pass


class CalendarEventResponse(CalendarEventBase):
    id: int
    is_completed: bool = Field(False)
    created_at: datetime

    model_config = ConfigDict(from_attributes=True)


# --- Transaction Schemas ---
class TransactionBase(BaseModel):
    transaction_type: str = Field(..., max_length=20)
    asset_type: str = Field(..., max_length=10)
    asset_id: Optional[int] = None
    asset_name: Optional[str] = Field(None, max_length=100)
    quantity: Optional[Decimal] = None
    price: Optional[Decimal] = Field(None, ge=0)
    transaction_date: date
    notes: Optional[str] = None


class TransactionCreate(TransactionBase):
    total_amount: Decimal  # Service layer calculates and provides this.


class TransactionResponse(TransactionBase):
    id: int
    total_amount: Decimal
    created_at: datetime

    model_config = ConfigDict(from_attributes=True)


# --- Portfolio Analytics & Summary Schemas ---
class AssetAllocation(BaseModel):
    stocks_percentage: Decimal = Field(Decimal("0.0"), ge=0, le=100)
    bonds_percentage: Decimal = Field(Decimal("0.0"), ge=0, le=100)
    utts_percentage: Decimal = Field(Decimal("0.0"), ge=0, le=100)
    cash_percentage: Decimal = Field(Decimal("0.0"), ge=0, le=100)
    total_value: Decimal

    model_config = ConfigDict(from_attributes=True)


class CalendarEventResponse(BaseModel):
    event_date: date
    event_type: str  # e.g., "Dividend Payment", "Bond Coupon"
    asset_symbol: str
    asset_name: str
    estimated_amount: Decimal
    notes: Optional[str] = None

    model_config = ConfigDict(
        from_attributes=True,
    )


class TransactionDetailResponse(BaseModel):
    id: int
    transaction_type: str
    asset_type: str
    asset_id: int
    quantity: Decimal
    price: Decimal
    total_amount: Decimal
    transaction_date: date
    notes: Optional[str] = None
    created_at: datetime

    # New fields to be added
    asset_name: Optional[str] = None
    asset_symbol: Optional[str] = None

    model_config = ConfigDict(
        from_attributes=True,
    )


class PortfolioSummary(BaseModel):
    portfolio: PortfolioBase
    total_market_value: Decimal
    total_cost_basis: Decimal
    overall_unrealized_gain_loss: Decimal
    overall_unrealized_gain_loss_percentage: Decimal
    stock_holdings: List[StockHoldingResponse] = Field(default_factory=list)
    utt_holdings: List[UTTHoldingResponse] = Field(default_factory=list)
    bond_holdings: List[BondHoldingResponse] = Field(default_factory=list)
    asset_allocation: AssetAllocation
    recent_transactions: List[TransactionResponse] = Field(default_factory=list)
    upcoming_events: List[CalendarEventResponse] = Field(default_factory=list)

    model_config = ConfigDict(from_attributes=True)


class AssetPerformanceDetail(BaseModel):
    asset_id: Optional[int] = None
    name: str
    return_value: Decimal
    asset_type: Optional[str] = None

    model_config = ConfigDict(from_attributes=True)


class PortfolioPerformance(BaseModel):
    portfolio_id: int
    period: str
    start_value: Decimal
    end_value: Decimal
    absolute_return: Decimal
    percentage_return: Decimal
    best_performer: Optional[AssetPerformanceDetail] = None
    worst_performer: Optional[AssetPerformanceDetail] = None

    model_config = ConfigDict(from_attributes=True)


class PositionResponse(BaseModel):
    asset_id: int
    asset_type: str
    asset_name: str
    asset_symbol: str
    quantity: Decimal
    avg_buy_price: Decimal
    total_invested: Decimal
    current_price: Decimal
    current_value: Decimal
    profit_loss: Decimal
    profit_loss_percent: float

    model_config = ConfigDict(
        from_attributes=True,
    )