Mbonea commited on
Commit
bb10fde
·
1 Parent(s): dfa95e6

Seed DSE issued shares and transaction fees

Browse files
App/routers/stocks/crud.py CHANGED
@@ -378,6 +378,7 @@ async def get_market_highlights(limit: int = 5) -> Dict:
378
  "turnover": profile.turnover,
379
  "deals": profile.deals,
380
  "market_cap": profile.market_cap,
 
381
  "logo_url": profile.logo_url,
382
  "market_segment": profile.market_segment,
383
  }
 
378
  "turnover": profile.turnover,
379
  "deals": profile.deals,
380
  "market_cap": profile.market_cap,
381
+ "total_shares_issued": profile.total_shares_issued,
382
  "logo_url": profile.logo_url,
383
  "market_segment": profile.market_segment,
384
  }
App/routers/stocks/dse_transaction_fees.json ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schema_version": 1,
3
+ "currency": "TZS",
4
+ "unit": "percent_of_consideration",
5
+ "source_note": "DSE equity transaction cost table supplied for seed data on 2026-05-13.",
6
+ "bands": [
7
+ {
8
+ "label": "Up to 10 mln",
9
+ "min_consideration": 0,
10
+ "max_consideration": 10000000,
11
+ "brokerage_commission": 1.70,
12
+ "transaction_fee_cmsa": 0.14,
13
+ "transaction_fee_dse": 0.14,
14
+ "fidelity_fee": 0.02,
15
+ "csd_fee": 0.06,
16
+ "total_cost_to_investor": 2.06
17
+ },
18
+ {
19
+ "label": "Next 40 mln",
20
+ "min_consideration": 10000000,
21
+ "max_consideration": 50000000,
22
+ "brokerage_commission": 1.50,
23
+ "transaction_fee_cmsa": 0.14,
24
+ "transaction_fee_dse": 0.14,
25
+ "fidelity_fee": 0.02,
26
+ "csd_fee": 0.06,
27
+ "total_cost_to_investor": 1.86
28
+ },
29
+ {
30
+ "label": "Above 50 mln",
31
+ "min_consideration": 50000000,
32
+ "max_consideration": null,
33
+ "brokerage_commission": 0.80,
34
+ "transaction_fee_cmsa": 0.14,
35
+ "transaction_fee_dse": 0.14,
36
+ "fidelity_fee": 0.02,
37
+ "csd_fee": 0.06,
38
+ "total_cost_to_investor": 1.16
39
+ }
40
+ ]
41
+ }
App/routers/stocks/routes.py CHANGED
@@ -26,7 +26,7 @@ from .crud import (
26
  )
27
  from .service import fetch_dse_stock_data
28
  from .metrics import calculate_metrics
29
- from .models import Stock, StockPriceData, Dividend
30
  from .news_service import (
31
  build_news_queries,
32
  extract_article_content,
@@ -42,6 +42,7 @@ from typing import Dict, List, Optional
42
  from datetime import datetime, timedelta, date
43
  from .utils import AsyncCurlCffiDividendScraper, run_stock_import_task
44
  from .dse import DseScraper
 
45
  from App.routers.users.utils import get_current_user
46
 
47
  router = APIRouter(prefix="/stocks", tags=["stocks"])
@@ -100,6 +101,7 @@ async def list_stocks_orm():
100
  "low": float(latest.low) if latest else (float(profile.day_low) if profile and profile.day_low is not None else None),
101
  "volume": latest.volume if latest else (profile.volume if profile else None),
102
  "market_cap": latest.market_cap if latest else (profile.market_cap if profile else None),
 
103
  "latest_date": latest.date.isoformat() if latest else None,
104
  "logo_url": profile.logo_url if profile else None,
105
  "security_type": profile.security_type if profile else None,
@@ -117,6 +119,38 @@ async def list_stocks_orm():
117
  raise AppException(status_code=500, message=f"Error retrieving stocks: {str(e)}")
118
 
119
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  @router.get("/{symbol}/price/{price_date}", response_model=ResponseModel)
121
  async def get_stock_price_by_date(symbol: str, price_date: str):
122
  """Return the closing price for a stock on a given date (or nearest prior trading day)."""
 
26
  )
27
  from .service import fetch_dse_stock_data
28
  from .metrics import calculate_metrics
29
+ from .models import Stock, StockPriceData, Dividend, StockProfile
30
  from .news_service import (
31
  build_news_queries,
32
  extract_article_content,
 
42
  from datetime import datetime, timedelta, date
43
  from .utils import AsyncCurlCffiDividendScraper, run_stock_import_task
44
  from .dse import DseScraper
45
+ from .seed import load_dse_transaction_fee_seed_data
46
  from App.routers.users.utils import get_current_user
47
 
48
  router = APIRouter(prefix="/stocks", tags=["stocks"])
 
101
  "low": float(latest.low) if latest else (float(profile.day_low) if profile and profile.day_low is not None else None),
102
  "volume": latest.volume if latest else (profile.volume if profile else None),
103
  "market_cap": latest.market_cap if latest else (profile.market_cap if profile else None),
104
+ "total_shares_issued": profile.total_shares_issued if profile else None,
105
  "latest_date": latest.date.isoformat() if latest else None,
106
  "logo_url": profile.logo_url if profile else None,
107
  "security_type": profile.security_type if profile else None,
 
119
  raise AppException(status_code=500, message=f"Error retrieving stocks: {str(e)}")
120
 
121
 
122
+ @router.get("/reference/issued-shares", response_model=ResponseModel)
123
+ async def list_issued_shares():
124
+ profiles = await StockProfile.all().select_related("stock").order_by("stock__symbol")
125
+ data = [
126
+ {
127
+ "symbol": profile.stock.symbol,
128
+ "name": profile.stock.name,
129
+ "isin": profile.security_id,
130
+ "security_type": profile.security_type,
131
+ "issued_shares": profile.total_shares_issued,
132
+ "source": profile.source,
133
+ }
134
+ for profile in profiles
135
+ if profile.total_shares_issued is not None
136
+ ]
137
+ return ResponseModel(
138
+ success=True,
139
+ message="Issued share reference data retrieved",
140
+ data={"stocks": data, "count": len(data)},
141
+ )
142
+
143
+
144
+ @router.get("/reference/transaction-fees", response_model=ResponseModel)
145
+ async def get_dse_transaction_fees():
146
+ payload = load_dse_transaction_fee_seed_data()
147
+ return ResponseModel(
148
+ success=True,
149
+ message="DSE transaction fee seed data retrieved",
150
+ data=payload,
151
+ )
152
+
153
+
154
  @router.get("/{symbol}/price/{price_date}", response_model=ResponseModel)
155
  async def get_stock_price_by_date(symbol: str, price_date: str):
156
  """Return the closing price for a stock on a given date (or nearest prior trading day)."""
App/routers/stocks/seed.py CHANGED
@@ -3,10 +3,12 @@ from datetime import date
3
  from pathlib import Path
4
  from decimal import Decimal, InvalidOperation
5
 
6
- from .models import Stock, StockCompanyInfo, Dividend
7
 
8
  STOCK_COMPANY_DATA_PATH = Path(__file__).parent / "stock_company_data.json"
9
  STOCK_DIVIDEND_DATA_PATH = Path(__file__).parent / "stock_dividends_data.json"
 
 
10
 
11
 
12
  def load_stock_company_seed_data() -> dict:
@@ -23,6 +25,20 @@ def load_stock_dividend_seed_data() -> dict:
23
  return {"dividends": []}
24
 
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  def _clip(value: str | None, max_length: int) -> str | None:
27
  if value is None:
28
  return None
@@ -125,3 +141,31 @@ async def sync_stock_dividends_from_json() -> int:
125
  synced += 1
126
 
127
  return synced
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  from pathlib import Path
4
  from decimal import Decimal, InvalidOperation
5
 
6
+ from .models import Stock, StockCompanyInfo, StockProfile, Dividend
7
 
8
  STOCK_COMPANY_DATA_PATH = Path(__file__).parent / "stock_company_data.json"
9
  STOCK_DIVIDEND_DATA_PATH = Path(__file__).parent / "stock_dividends_data.json"
10
+ STOCK_REFERENCE_DATA_PATH = Path(__file__).parent / "stock_reference_data.json"
11
+ DSE_TRANSACTION_FEES_PATH = Path(__file__).parent / "dse_transaction_fees.json"
12
 
13
 
14
  def load_stock_company_seed_data() -> dict:
 
25
  return {"dividends": []}
26
 
27
 
28
+ def load_stock_reference_seed_data() -> dict:
29
+ try:
30
+ return json.loads(STOCK_REFERENCE_DATA_PATH.read_text(encoding="utf-8"))
31
+ except Exception:
32
+ return {"stocks": []}
33
+
34
+
35
+ def load_dse_transaction_fee_seed_data() -> dict:
36
+ try:
37
+ return json.loads(DSE_TRANSACTION_FEES_PATH.read_text(encoding="utf-8"))
38
+ except Exception:
39
+ return {"bands": []}
40
+
41
+
42
  def _clip(value: str | None, max_length: int) -> str | None:
43
  if value is None:
44
  return None
 
141
  synced += 1
142
 
143
  return synced
144
+
145
+
146
+ async def sync_stock_reference_data_from_json() -> int:
147
+ payload = load_stock_reference_seed_data()
148
+ synced = 0
149
+
150
+ for entry in payload.get("stocks", []):
151
+ symbol = str(entry.get("symbol") or "").strip().upper()
152
+ issued_shares = entry.get("issued_shares")
153
+ if not symbol or issued_shares is None:
154
+ continue
155
+
156
+ stock = await Stock.get_or_none(symbol=symbol)
157
+ if stock is None:
158
+ stock = await Stock.create(symbol=symbol, name=entry.get("legal_name") or symbol)
159
+ elif entry.get("legal_name") and stock.name == stock.symbol:
160
+ stock.name = entry["legal_name"]
161
+ await stock.save(update_fields=["name", "updated_at"])
162
+
163
+ profile, _ = await StockProfile.get_or_create(stock=stock)
164
+ profile.security_id = _clip(entry.get("isin"), 50)
165
+ profile.security_type = _clip(entry.get("security_type"), 30)
166
+ profile.total_shares_issued = int(issued_shares)
167
+ profile.source = _clip(payload.get("source_name"), 50)
168
+ await profile.save()
169
+ synced += 1
170
+
171
+ return synced
App/routers/stocks/stock_reference_data.json ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schema_version": 1,
3
+ "last_verified_at": "2026-05-13",
4
+ "source_name": "CSD & Registry Company Limited Issuer Companies List",
5
+ "source_url": "https://csdr.co.tz/content/issuer-companies-list",
6
+ "stocks": [
7
+ {
8
+ "symbol": "CRDB",
9
+ "legal_name": "CRDB Bank PLC",
10
+ "isin": "TZ1996100305",
11
+ "issued_shares": 2611838584,
12
+ "security_type": "Equity",
13
+ "listing_date": "2009-06-17",
14
+ "listing_class": "domestic"
15
+ },
16
+ {
17
+ "symbol": "DSE",
18
+ "legal_name": "Dar es Salaam Stock Exchange PLC",
19
+ "isin": "TZ1996102434",
20
+ "issued_shares": 23824020,
21
+ "security_type": "Equity",
22
+ "listing_date": "2016-07-12",
23
+ "listing_class": "domestic"
24
+ },
25
+ {
26
+ "symbol": "DCB",
27
+ "legal_name": "DCB Commercial Bank PLC",
28
+ "isin": "TZ1996100214",
29
+ "issued_shares": 195293826,
30
+ "security_type": "Equity",
31
+ "listing_date": "2008-09-16",
32
+ "listing_class": "domestic"
33
+ },
34
+ {
35
+ "symbol": "JATU",
36
+ "legal_name": "JATU Public Limited Company",
37
+ "isin": "TZ1996103804",
38
+ "issued_shares": 19938837,
39
+ "security_type": "Equity",
40
+ "listing_date": "2020-11-23",
41
+ "listing_class": "domestic"
42
+ },
43
+ {
44
+ "symbol": "MBP",
45
+ "legal_name": "Maendeleo Bank PLC",
46
+ "isin": "TZ1996101683",
47
+ "issued_shares": 26431550,
48
+ "security_type": "Equity",
49
+ "listing_date": "2013-11-04",
50
+ "listing_class": "domestic"
51
+ },
52
+ {
53
+ "symbol": "MKCB",
54
+ "legal_name": "Mkombozi Commercial Bank PLC",
55
+ "isin": "TZ1996101972",
56
+ "issued_shares": 23555002,
57
+ "security_type": "Equity",
58
+ "listing_date": "2014-12-29",
59
+ "listing_class": "domestic"
60
+ },
61
+ {
62
+ "symbol": "MUCOBA",
63
+ "legal_name": "MUCOBA Bank PLC",
64
+ "isin": "TZ1996102418",
65
+ "issued_shares": 32666227,
66
+ "security_type": "Equity",
67
+ "listing_date": "2016-06-08",
68
+ "listing_class": "domestic"
69
+ },
70
+ {
71
+ "symbol": "MCB",
72
+ "legal_name": "Mwalimu Commercial Bank PLC",
73
+ "isin": "TZ1996102129",
74
+ "issued_shares": 61824920,
75
+ "security_type": "Equity",
76
+ "listing_date": "2015-11-27",
77
+ "listing_class": "domestic"
78
+ },
79
+ {
80
+ "symbol": "NICO",
81
+ "legal_name": "National Investment Company Limited",
82
+ "isin": "TZ1996103077",
83
+ "issued_shares": 61644834,
84
+ "security_type": "Equity",
85
+ "listing_date": "2018-06-06",
86
+ "listing_class": "domestic"
87
+ },
88
+ {
89
+ "symbol": "NMB",
90
+ "legal_name": "National Microfinance Bank PLC",
91
+ "isin": "TZ1996100222",
92
+ "issued_shares": 500000000,
93
+ "security_type": "Equity",
94
+ "listing_date": "2008-11-06",
95
+ "listing_class": "domestic"
96
+ },
97
+ {
98
+ "symbol": "PAL",
99
+ "legal_name": "Precision Air Services PLC",
100
+ "isin": "TZ1996101048",
101
+ "issued_shares": 160469800,
102
+ "security_type": "Equity",
103
+ "listing_date": "2011-12-21",
104
+ "listing_class": "domestic"
105
+ },
106
+ {
107
+ "symbol": "SWALA",
108
+ "legal_name": "Swala Oil and Gas Tanzania PLC",
109
+ "isin": "TZ1996101865",
110
+ "issued_shares": 106201621,
111
+ "security_type": "Equity",
112
+ "listing_date": "2014-08-11",
113
+ "listing_class": "domestic"
114
+ },
115
+ {
116
+ "symbol": "SWIS",
117
+ "legal_name": "Swissport Tanzania PLC",
118
+ "isin": "TZ1996100040",
119
+ "issued_shares": 36000000,
120
+ "security_type": "Equity",
121
+ "listing_date": "2003-06-03",
122
+ "listing_class": "domestic"
123
+ },
124
+ {
125
+ "symbol": "TCCL",
126
+ "legal_name": "Tanga Cement PLC",
127
+ "isin": "TZ1996100057",
128
+ "issued_shares": 63671045,
129
+ "security_type": "Equity",
130
+ "listing_date": "2002-09-26",
131
+ "listing_class": "domestic"
132
+ },
133
+ {
134
+ "symbol": "TBL",
135
+ "legal_name": "Tanzania Breweries Limited PLC",
136
+ "isin": "TZ1996100016",
137
+ "issued_shares": 295056063,
138
+ "security_type": "Equity",
139
+ "listing_date": "1998-09-19",
140
+ "listing_class": "domestic"
141
+ },
142
+ {
143
+ "symbol": "TCC",
144
+ "legal_name": "Tanzania Cigarette Company Limited",
145
+ "isin": "TZ1996100032",
146
+ "issued_shares": 100000000,
147
+ "security_type": "Equity",
148
+ "listing_date": "2000-11-16",
149
+ "listing_class": "domestic"
150
+ },
151
+ {
152
+ "symbol": "TPCC",
153
+ "legal_name": "Tanzania Portland Cement Company Limited",
154
+ "isin": "TZ1996100024",
155
+ "issued_shares": 179923100,
156
+ "security_type": "Equity",
157
+ "listing_date": "2006-09-29",
158
+ "listing_class": "domestic"
159
+ },
160
+ {
161
+ "symbol": "TTP",
162
+ "legal_name": "TATEPA Limited",
163
+ "isin": "TZ1996100065",
164
+ "issued_shares": 95057184,
165
+ "security_type": "Equity",
166
+ "listing_date": "1999-12-07",
167
+ "listing_class": "domestic"
168
+ },
169
+ {
170
+ "symbol": "AFRIPRISE",
171
+ "aliases": ["TICL"],
172
+ "legal_name": "Afriprise Investment PLC",
173
+ "isin": "TZ1996103010",
174
+ "issued_shares": 146034913,
175
+ "security_type": "Equity",
176
+ "listing_date": "2018-03-16",
177
+ "listing_class": "domestic"
178
+ },
179
+ {
180
+ "symbol": "TOL",
181
+ "legal_name": "TOL Gases PLC",
182
+ "isin": "TZ1996100008",
183
+ "issued_shares": 57505963,
184
+ "security_type": "Equity",
185
+ "listing_date": "1998-04-15",
186
+ "listing_class": "domestic"
187
+ },
188
+ {
189
+ "symbol": "VODA",
190
+ "legal_name": "Vodacom Tanzania PLC",
191
+ "isin": "TZ1996102715",
192
+ "issued_shares": 2240000300,
193
+ "security_type": "Equity",
194
+ "listing_date": "2017-08-15",
195
+ "listing_class": "domestic"
196
+ },
197
+ {
198
+ "symbol": "YETU",
199
+ "legal_name": "Yetu Microfinance PLC",
200
+ "isin": "TZ1996102343",
201
+ "issued_shares": 12112894,
202
+ "security_type": "Equity",
203
+ "listing_date": "2016-03-10",
204
+ "listing_class": "domestic"
205
+ },
206
+ {
207
+ "symbol": "EABL",
208
+ "legal_name": "East African Breweries Limited",
209
+ "isin": "KE0000000216",
210
+ "issued_shares": 790774356,
211
+ "security_type": "Equity",
212
+ "listing_date": "2005-06-29",
213
+ "listing_class": "foreign_cross_listed"
214
+ },
215
+ {
216
+ "symbol": "JHL",
217
+ "legal_name": "Jubilee Holdings Limited",
218
+ "isin": "KE0000000273",
219
+ "issued_shares": 72472950,
220
+ "security_type": "Equity",
221
+ "listing_date": "2006-12-20",
222
+ "listing_class": "foreign_cross_listed"
223
+ },
224
+ {
225
+ "symbol": "KA",
226
+ "legal_name": "Kenya Airways Limited",
227
+ "isin": "KE0000000307",
228
+ "issued_shares": 5681423711,
229
+ "security_type": "Equity",
230
+ "listing_date": "2004-10-01",
231
+ "listing_class": "foreign_cross_listed"
232
+ },
233
+ {
234
+ "symbol": "KCB",
235
+ "legal_name": "KCB Group Limited",
236
+ "isin": "KE0000000315",
237
+ "issued_shares": 2970340000,
238
+ "security_type": "Equity",
239
+ "listing_date": "2008-12-23",
240
+ "listing_class": "foreign_cross_listed"
241
+ },
242
+ {
243
+ "symbol": "NMG",
244
+ "legal_name": "Nation Media Group",
245
+ "isin": "KE0000000380",
246
+ "issued_shares": 188542286,
247
+ "security_type": "Equity",
248
+ "listing_date": "2014-06-12",
249
+ "listing_class": "foreign_cross_listed"
250
+ },
251
+ {
252
+ "symbol": "USL",
253
+ "legal_name": "Uchumi Supermarket Limited",
254
+ "isin": "KE0000000489",
255
+ "issued_shares": 364965594,
256
+ "security_type": "Equity",
257
+ "listing_date": "2014-08-15",
258
+ "listing_class": "foreign_cross_listed"
259
+ }
260
+ ]
261
+ }
262
+
db.py CHANGED
@@ -69,6 +69,7 @@ async def init_db():
69
  await ensure_user_auth_profile_schema()
70
  await ensure_admin_schema()
71
  await ensure_stock_news_schema()
 
72
  await sync_fund_info_seed_data()
73
  await sync_stock_company_seed_data()
74
  await sync_stock_dividend_seed_data()
@@ -154,6 +155,16 @@ async def sync_stock_company_seed_data():
154
  return
155
 
156
 
 
 
 
 
 
 
 
 
 
 
157
  async def sync_stock_dividend_seed_data():
158
  """Populate curated stock dividend history from bundled research JSON."""
159
  try:
 
69
  await ensure_user_auth_profile_schema()
70
  await ensure_admin_schema()
71
  await ensure_stock_news_schema()
72
+ await sync_stock_reference_seed_data()
73
  await sync_fund_info_seed_data()
74
  await sync_stock_company_seed_data()
75
  await sync_stock_dividend_seed_data()
 
155
  return
156
 
157
 
158
+ async def sync_stock_reference_seed_data():
159
+ """Populate stock issued-share reference data from the bundled seed JSON."""
160
+ try:
161
+ from App.routers.stocks.seed import sync_stock_reference_data_from_json
162
+
163
+ await sync_stock_reference_data_from_json()
164
+ except Exception:
165
+ return
166
+
167
+
168
  async def sync_stock_dividend_seed_data():
169
  """Populate curated stock dividend history from bundled research JSON."""
170
  try: