nanopy-bank 1.0.8__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.
- nanopy_bank/__init__.py +20 -0
- nanopy_bank/api/__init__.py +10 -0
- nanopy_bank/api/server.py +242 -0
- nanopy_bank/app.py +282 -0
- nanopy_bank/cli.py +152 -0
- nanopy_bank/core/__init__.py +85 -0
- nanopy_bank/core/audit.py +404 -0
- nanopy_bank/core/auth.py +306 -0
- nanopy_bank/core/bank.py +407 -0
- nanopy_bank/core/beneficiary.py +258 -0
- nanopy_bank/core/branch.py +319 -0
- nanopy_bank/core/fees.py +243 -0
- nanopy_bank/core/holding.py +416 -0
- nanopy_bank/core/models.py +308 -0
- nanopy_bank/core/products.py +300 -0
- nanopy_bank/data/__init__.py +31 -0
- nanopy_bank/data/demo.py +846 -0
- nanopy_bank/documents/__init__.py +11 -0
- nanopy_bank/documents/statement.py +304 -0
- nanopy_bank/sepa/__init__.py +10 -0
- nanopy_bank/sepa/sepa.py +452 -0
- nanopy_bank/storage/__init__.py +11 -0
- nanopy_bank/storage/json_storage.py +127 -0
- nanopy_bank/storage/sqlite_storage.py +326 -0
- nanopy_bank/ui/__init__.py +14 -0
- nanopy_bank/ui/pages/__init__.py +33 -0
- nanopy_bank/ui/pages/accounts.py +85 -0
- nanopy_bank/ui/pages/advisor.py +140 -0
- nanopy_bank/ui/pages/audit.py +73 -0
- nanopy_bank/ui/pages/beneficiaries.py +115 -0
- nanopy_bank/ui/pages/branches.py +64 -0
- nanopy_bank/ui/pages/cards.py +36 -0
- nanopy_bank/ui/pages/common.py +18 -0
- nanopy_bank/ui/pages/dashboard.py +100 -0
- nanopy_bank/ui/pages/fees.py +60 -0
- nanopy_bank/ui/pages/holding.py +943 -0
- nanopy_bank/ui/pages/loans.py +105 -0
- nanopy_bank/ui/pages/login.py +174 -0
- nanopy_bank/ui/pages/sepa.py +118 -0
- nanopy_bank/ui/pages/settings.py +48 -0
- nanopy_bank/ui/pages/transfers.py +94 -0
- nanopy_bank/ui/pages.py +16 -0
- nanopy_bank-1.0.8.dist-info/METADATA +72 -0
- nanopy_bank-1.0.8.dist-info/RECORD +47 -0
- nanopy_bank-1.0.8.dist-info/WHEEL +5 -0
- nanopy_bank-1.0.8.dist-info/entry_points.txt +2 -0
- nanopy_bank-1.0.8.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,943 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Holding page - Group management view for Nova x Genesis SASU
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import streamlit as st
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
from datetime import datetime, date
|
|
8
|
+
|
|
9
|
+
from .common import page_header
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from ...data.demo import get_demo_holding_data
|
|
13
|
+
except ImportError:
|
|
14
|
+
from nanopy_bank.data.demo import get_demo_holding_data
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_holding_data():
|
|
18
|
+
"""Get or create holding data in session state"""
|
|
19
|
+
if "holding_data" not in st.session_state:
|
|
20
|
+
st.session_state.holding_data = get_demo_holding_data()
|
|
21
|
+
return st.session_state.holding_data
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def format_amount(amount: Decimal, currency: str = "EUR") -> str:
|
|
25
|
+
"""Format amount with thousands separator"""
|
|
26
|
+
return f"{amount:,.2f} {currency}".replace(",", " ").replace(".", ",").replace(" ", " ")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def render_holding(tab: str = "dashboard"):
|
|
30
|
+
"""Render holding/group management dashboard"""
|
|
31
|
+
data = get_holding_data()
|
|
32
|
+
holding = data["holding"]
|
|
33
|
+
|
|
34
|
+
page_header(holding["name"])
|
|
35
|
+
|
|
36
|
+
st.markdown(f"""
|
|
37
|
+
<div style="background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); border: 1px solid #333; border-radius: 12px; padding: 20px; margin-bottom: 20px;">
|
|
38
|
+
<div style="color: #00ff88; font-size: 12px; text-transform: uppercase;">{holding['legal_name']}</div>
|
|
39
|
+
<div style="color: white; font-size: 24px; font-weight: bold; margin: 8px 0;">SIREN: {holding['siren']} | LEI: {holding['lei']}</div>
|
|
40
|
+
</div>
|
|
41
|
+
""", unsafe_allow_html=True)
|
|
42
|
+
|
|
43
|
+
if tab == "dashboard":
|
|
44
|
+
render_dashboard_tab(data)
|
|
45
|
+
elif tab == "tresorerie":
|
|
46
|
+
render_tresorerie_tab(data)
|
|
47
|
+
elif tab == "investissements":
|
|
48
|
+
render_investissements_tab(data)
|
|
49
|
+
elif tab == "assurances":
|
|
50
|
+
render_assurances_tab(data)
|
|
51
|
+
elif tab == "filiales":
|
|
52
|
+
render_filiales_tab(data)
|
|
53
|
+
elif tab == "consolidation":
|
|
54
|
+
render_consolidation_tab(data)
|
|
55
|
+
elif tab == "risques":
|
|
56
|
+
render_risques_tab(data)
|
|
57
|
+
elif tab == "gouvernance":
|
|
58
|
+
render_gouvernance_tab(data)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def render_dashboard_tab(data):
|
|
62
|
+
"""Dashboard with KPIs"""
|
|
63
|
+
st.markdown("### Vue d'ensemble")
|
|
64
|
+
|
|
65
|
+
# Calculate totals
|
|
66
|
+
total_treasury = data["accounts"]["principal"]["balance"] + data["accounts"]["tresorerie"]["balance"]
|
|
67
|
+
total_assets = sum(s["assets"] for s in data["subsidiaries"])
|
|
68
|
+
total_employees = sum(s["employees"] for s in data["subsidiaries"])
|
|
69
|
+
pool_net = sum(s["pool_balance"] for s in data["subsidiaries"])
|
|
70
|
+
|
|
71
|
+
col1, col2, col3, col4 = st.columns(4)
|
|
72
|
+
with col1:
|
|
73
|
+
st.metric("Tresorerie Holding", format_amount(total_treasury))
|
|
74
|
+
with col2:
|
|
75
|
+
st.metric("Actifs Consolides", f"{total_assets/1000000000:.1f}B EUR")
|
|
76
|
+
with col3:
|
|
77
|
+
st.metric("Effectif Groupe", f"{total_employees:,}".replace(",", " "))
|
|
78
|
+
with col4:
|
|
79
|
+
st.metric("Position Cash Pool", format_amount(pool_net))
|
|
80
|
+
|
|
81
|
+
st.divider()
|
|
82
|
+
|
|
83
|
+
# Recent activity
|
|
84
|
+
col1, col2 = st.columns(2)
|
|
85
|
+
|
|
86
|
+
with col1:
|
|
87
|
+
st.markdown("### Filiales")
|
|
88
|
+
for sub in data["subsidiaries"][:3]:
|
|
89
|
+
status_color = "#00ff88" if sub["status"] == "active" else "#ffd93d"
|
|
90
|
+
st.markdown(f"""
|
|
91
|
+
<div style="background: #1e1e2f; padding: 12px; border-radius: 8px; margin-bottom: 8px;">
|
|
92
|
+
<div style="display: flex; justify-content: space-between;">
|
|
93
|
+
<span style="color: white; font-weight: bold;">{sub['name']}</span>
|
|
94
|
+
<span style="color: {status_color};">{sub['ownership']}%</span>
|
|
95
|
+
</div>
|
|
96
|
+
<div style="color: #888; font-size: 12px;">{sub['type']}</div>
|
|
97
|
+
</div>
|
|
98
|
+
""", unsafe_allow_html=True)
|
|
99
|
+
|
|
100
|
+
with col2:
|
|
101
|
+
st.markdown("### Prets Intra-Groupe")
|
|
102
|
+
for loan in data["intra_group_loans"][:3]:
|
|
103
|
+
st.markdown(f"""
|
|
104
|
+
<div style="background: #1e1e2f; padding: 12px; border-radius: 8px; margin-bottom: 8px;">
|
|
105
|
+
<div style="display: flex; justify-content: space-between;">
|
|
106
|
+
<span style="color: white; font-weight: bold;">{loan['borrower']}</span>
|
|
107
|
+
<span style="color: #00d4ff;">{format_amount(loan['outstanding'])}</span>
|
|
108
|
+
</div>
|
|
109
|
+
<div style="color: #888; font-size: 12px;">Taux: {loan['rate']}% | Echeance: {loan['maturity'].strftime('%d/%m/%Y')}</div>
|
|
110
|
+
</div>
|
|
111
|
+
""", unsafe_allow_html=True)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def render_tresorerie_tab(data):
|
|
115
|
+
"""Treasury management"""
|
|
116
|
+
st.markdown("### Comptes Tresorerie")
|
|
117
|
+
|
|
118
|
+
col1, col2 = st.columns(2)
|
|
119
|
+
|
|
120
|
+
for key, account in data["accounts"].items():
|
|
121
|
+
with col1 if key == "principal" else col2:
|
|
122
|
+
color = "#00ff88" if key == "principal" else "#00d4ff"
|
|
123
|
+
st.markdown(f"""
|
|
124
|
+
<div style="background: linear-gradient(135deg, #1b4e2d, #0d2818); border: 1px solid {color}; border-radius: 12px; padding: 20px;">
|
|
125
|
+
<div style="color: {color}; font-size: 12px; text-transform: uppercase;">{account['name']}</div>
|
|
126
|
+
<div style="color: white; font-size: 11px; margin: 4px 0;">{account['iban']}</div>
|
|
127
|
+
<div style="color: {color}; font-size: 28px; font-weight: bold; margin-top: 12px;">{format_amount(account['balance'])}</div>
|
|
128
|
+
</div>
|
|
129
|
+
""", unsafe_allow_html=True)
|
|
130
|
+
|
|
131
|
+
st.divider()
|
|
132
|
+
|
|
133
|
+
# Cash pooling
|
|
134
|
+
st.markdown("### Cash Pooling - Position Nette")
|
|
135
|
+
|
|
136
|
+
for sub in data["subsidiaries"]:
|
|
137
|
+
col1, col2, col3, col4 = st.columns([2, 2, 2, 1])
|
|
138
|
+
with col1:
|
|
139
|
+
st.markdown(f"**{sub['name']}**")
|
|
140
|
+
st.caption(f"FR76 NANP {sub['id'][-4:]} xxxx")
|
|
141
|
+
with col2:
|
|
142
|
+
balance = sub["pool_balance"]
|
|
143
|
+
if balance >= 0:
|
|
144
|
+
st.markdown(f":green[+{format_amount(balance)}]")
|
|
145
|
+
else:
|
|
146
|
+
st.markdown(f":red[{format_amount(balance)}]")
|
|
147
|
+
with col3:
|
|
148
|
+
st.caption(f"Limite: {format_amount(sub['pool_limit'])}")
|
|
149
|
+
with col4:
|
|
150
|
+
if st.button("Virement", key=f"pool_transfer_{sub['id']}"):
|
|
151
|
+
st.session_state.show_transfer_modal = sub['id']
|
|
152
|
+
|
|
153
|
+
# Transfer modal
|
|
154
|
+
if "show_transfer_modal" in st.session_state and st.session_state.show_transfer_modal:
|
|
155
|
+
sub_id = st.session_state.show_transfer_modal
|
|
156
|
+
sub = next((s for s in data["subsidiaries"] if s["id"] == sub_id), None)
|
|
157
|
+
if sub:
|
|
158
|
+
with st.expander(f"Virement vers/depuis {sub['name']}", expanded=True):
|
|
159
|
+
direction = st.radio("Direction", ["Holding -> Filiale", "Filiale -> Holding"], key=f"dir_{sub_id}")
|
|
160
|
+
amount = st.number_input("Montant (EUR)", min_value=0.0, max_value=10000000.0, step=1000.0, key=f"amt_{sub_id}")
|
|
161
|
+
motif = st.text_input("Motif", key=f"motif_{sub_id}")
|
|
162
|
+
|
|
163
|
+
col1, col2 = st.columns(2)
|
|
164
|
+
with col1:
|
|
165
|
+
if st.button("Annuler", key=f"cancel_{sub_id}"):
|
|
166
|
+
del st.session_state.show_transfer_modal
|
|
167
|
+
st.rerun()
|
|
168
|
+
with col2:
|
|
169
|
+
if st.button("Valider", type="primary", key=f"validate_{sub_id}"):
|
|
170
|
+
if amount > 0 and motif:
|
|
171
|
+
# Process transfer
|
|
172
|
+
if "Holding -> Filiale" in direction:
|
|
173
|
+
data["accounts"]["principal"]["balance"] -= Decimal(str(amount))
|
|
174
|
+
sub["pool_balance"] += Decimal(str(amount))
|
|
175
|
+
else:
|
|
176
|
+
data["accounts"]["principal"]["balance"] += Decimal(str(amount))
|
|
177
|
+
sub["pool_balance"] -= Decimal(str(amount))
|
|
178
|
+
|
|
179
|
+
data["transactions"].append({
|
|
180
|
+
"date": datetime.now(),
|
|
181
|
+
"type": "pool_transfer",
|
|
182
|
+
"direction": direction,
|
|
183
|
+
"subsidiary": sub["name"],
|
|
184
|
+
"amount": Decimal(str(amount)),
|
|
185
|
+
"motif": motif,
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
del st.session_state.show_transfer_modal
|
|
189
|
+
st.success(f"Virement de {format_amount(Decimal(str(amount)))} effectue")
|
|
190
|
+
st.rerun()
|
|
191
|
+
else:
|
|
192
|
+
st.error("Veuillez remplir tous les champs")
|
|
193
|
+
|
|
194
|
+
st.divider()
|
|
195
|
+
|
|
196
|
+
pool_net = sum(s["pool_balance"] for s in data["subsidiaries"])
|
|
197
|
+
col1, col2, col3 = st.columns(3)
|
|
198
|
+
with col1:
|
|
199
|
+
st.metric("Position Nette Pool", format_amount(pool_net))
|
|
200
|
+
with col2:
|
|
201
|
+
st.metric("Taux Crediteur", f"{data['pool_rates']['credit']}%")
|
|
202
|
+
with col3:
|
|
203
|
+
st.metric("Taux Debiteur", f"{data['pool_rates']['debit']}%")
|
|
204
|
+
|
|
205
|
+
st.divider()
|
|
206
|
+
|
|
207
|
+
# Intra-group loans
|
|
208
|
+
st.markdown("### Prets Intra-Groupe")
|
|
209
|
+
|
|
210
|
+
for loan in data["intra_group_loans"]:
|
|
211
|
+
col1, col2, col3, col4, col5 = st.columns([2, 2, 1, 2, 1])
|
|
212
|
+
with col1:
|
|
213
|
+
st.markdown(f"**{loan['borrower']}**")
|
|
214
|
+
st.caption(f"Ref: {loan['id']}")
|
|
215
|
+
with col2:
|
|
216
|
+
st.markdown(f":blue[{format_amount(loan['outstanding'])}]")
|
|
217
|
+
st.caption(f"Principal: {format_amount(loan['principal'])}")
|
|
218
|
+
with col3:
|
|
219
|
+
st.caption(f"Taux: {loan['rate']}%")
|
|
220
|
+
with col4:
|
|
221
|
+
st.caption(f"Echeance: {loan['maturity'].strftime('%d/%m/%Y')}")
|
|
222
|
+
with col5:
|
|
223
|
+
if st.button("Details", key=f"loan_{loan['id']}"):
|
|
224
|
+
st.info(f"Debut: {loan['start_date'].strftime('%d/%m/%Y')}\nRembourse: {format_amount(loan['principal'] - loan['outstanding'])}")
|
|
225
|
+
st.divider()
|
|
226
|
+
|
|
227
|
+
total_loans = sum(l["outstanding"] for l in data["intra_group_loans"])
|
|
228
|
+
st.metric("Total Encours Prets", format_amount(total_loans))
|
|
229
|
+
|
|
230
|
+
# New loan button
|
|
231
|
+
if st.button("Nouveau Pret Intra-Groupe", type="primary"):
|
|
232
|
+
st.session_state.show_new_loan = True
|
|
233
|
+
|
|
234
|
+
if st.session_state.get("show_new_loan"):
|
|
235
|
+
with st.expander("Nouveau Pret", expanded=True):
|
|
236
|
+
borrower = st.selectbox("Emprunteur", [s["name"] for s in data["subsidiaries"]])
|
|
237
|
+
principal = st.number_input("Montant (EUR)", min_value=100000.0, max_value=50000000.0, step=100000.0)
|
|
238
|
+
rate = st.number_input("Taux annuel (%)", min_value=0.5, max_value=5.0, step=0.25, value=1.25)
|
|
239
|
+
maturity = st.date_input("Date d'echeance", min_value=date.today())
|
|
240
|
+
|
|
241
|
+
if st.button("Creer le pret", type="primary"):
|
|
242
|
+
sub = next((s for s in data["subsidiaries"] if s["name"] == borrower), None)
|
|
243
|
+
if sub:
|
|
244
|
+
new_loan = {
|
|
245
|
+
"id": f"IGL{len(data['intra_group_loans'])+1:03d}",
|
|
246
|
+
"borrower": borrower,
|
|
247
|
+
"borrower_id": sub["id"],
|
|
248
|
+
"principal": Decimal(str(principal)),
|
|
249
|
+
"outstanding": Decimal(str(principal)),
|
|
250
|
+
"rate": Decimal(str(rate)),
|
|
251
|
+
"start_date": date.today(),
|
|
252
|
+
"maturity": maturity,
|
|
253
|
+
"status": "active",
|
|
254
|
+
}
|
|
255
|
+
data["intra_group_loans"].append(new_loan)
|
|
256
|
+
data["accounts"]["principal"]["balance"] -= Decimal(str(principal))
|
|
257
|
+
del st.session_state.show_new_loan
|
|
258
|
+
st.success(f"Pret de {format_amount(Decimal(str(principal)))} accorde a {borrower}")
|
|
259
|
+
st.rerun()
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def render_filiales_tab(data):
|
|
263
|
+
"""Subsidiaries management"""
|
|
264
|
+
st.markdown("### Filiales du Groupe")
|
|
265
|
+
|
|
266
|
+
for sub in data["subsidiaries"]:
|
|
267
|
+
with st.expander(f"{sub['name']} ({sub['ownership']}%)", expanded=False):
|
|
268
|
+
col1, col2, col3 = st.columns(3)
|
|
269
|
+
with col1:
|
|
270
|
+
st.markdown(f"**Type:** {sub['type']}")
|
|
271
|
+
st.markdown(f"**Statut:** {'Actif' if sub['status'] == 'active' else 'Startup'}")
|
|
272
|
+
with col2:
|
|
273
|
+
st.markdown(f"**Actifs:** {sub['assets']/1000000:.0f}M EUR")
|
|
274
|
+
st.markdown(f"**Effectif:** {sub['employees']}")
|
|
275
|
+
with col3:
|
|
276
|
+
st.markdown(f"**Participation:** {sub['ownership']}%")
|
|
277
|
+
pool_color = "green" if sub["pool_balance"] >= 0 else "red"
|
|
278
|
+
st.markdown(f"**Position Pool:** :{pool_color}[{format_amount(sub['pool_balance'])}]")
|
|
279
|
+
|
|
280
|
+
# Actions
|
|
281
|
+
col1, col2, col3 = st.columns(3)
|
|
282
|
+
with col1:
|
|
283
|
+
if st.button("Voir details", key=f"details_{sub['id']}"):
|
|
284
|
+
st.info(f"ID: {sub['id']}\nLimite pool: {format_amount(sub['pool_limit'])}")
|
|
285
|
+
with col2:
|
|
286
|
+
if st.button("Historique", key=f"history_{sub['id']}"):
|
|
287
|
+
txs = [t for t in data["transactions"] if t.get("subsidiary") == sub["name"]]
|
|
288
|
+
if txs:
|
|
289
|
+
for tx in txs[-5:]:
|
|
290
|
+
st.write(f"{tx['date'].strftime('%d/%m/%Y')} - {tx['type']} - {format_amount(tx['amount'])}")
|
|
291
|
+
else:
|
|
292
|
+
st.info("Aucune transaction")
|
|
293
|
+
|
|
294
|
+
st.divider()
|
|
295
|
+
|
|
296
|
+
# Dividends
|
|
297
|
+
st.markdown("### Dividendes")
|
|
298
|
+
|
|
299
|
+
for div in data["dividends"]:
|
|
300
|
+
col1, col2, col3, col4, col5 = st.columns([2, 2, 2, 2, 1])
|
|
301
|
+
with col1:
|
|
302
|
+
st.markdown(f"**{div['subsidiary']}**")
|
|
303
|
+
st.caption(f"Exercice {div['year']}")
|
|
304
|
+
with col2:
|
|
305
|
+
st.caption(f"Brut: {format_amount(div['gross'])}")
|
|
306
|
+
with col3:
|
|
307
|
+
st.caption(f"Retenue: {format_amount(div['tax'])}")
|
|
308
|
+
with col4:
|
|
309
|
+
st.markdown(f":green[Net: {format_amount(div['net'])}]")
|
|
310
|
+
with col5:
|
|
311
|
+
status_map = {"paid": ("Paye", "success"), "approved": ("Approuve", "info"), "declared": ("Declare", "warning")}
|
|
312
|
+
label, style = status_map.get(div["status"], ("?", "info"))
|
|
313
|
+
if style == "success":
|
|
314
|
+
st.success(label)
|
|
315
|
+
elif style == "warning":
|
|
316
|
+
st.warning(label)
|
|
317
|
+
else:
|
|
318
|
+
st.info(label)
|
|
319
|
+
|
|
320
|
+
total_div = sum(d["net"] for d in data["dividends"])
|
|
321
|
+
st.metric("Total Dividendes 2025", format_amount(total_div))
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def render_investissements_tab(data):
|
|
325
|
+
"""Sovereign bonds portfolio management"""
|
|
326
|
+
st.markdown("### Portefeuille Obligataire Souverain")
|
|
327
|
+
|
|
328
|
+
bonds = data.get("sovereign_bonds", [])
|
|
329
|
+
available = data.get("available_bonds", [])
|
|
330
|
+
|
|
331
|
+
# Separate high-yield emerging bonds from core bonds
|
|
332
|
+
emerging_countries = ["UA", "PL", "RO"] # Ukraine, Poland, Romania
|
|
333
|
+
emerging_bonds = [b for b in bonds if b["country"] in emerging_countries]
|
|
334
|
+
core_bonds = [b for b in bonds if b["country"] not in emerging_countries]
|
|
335
|
+
|
|
336
|
+
# Calculate portfolio value
|
|
337
|
+
total_nominal = sum(b["nominal"] for b in bonds) if bonds else Decimal("0")
|
|
338
|
+
total_market_value = sum(b["nominal"] * b["current_price"] / 100 for b in bonds) if bonds else Decimal("0")
|
|
339
|
+
total_cost = sum(b["nominal"] * b["purchase_price"] / 100 for b in bonds) if bonds else Decimal("0")
|
|
340
|
+
pnl = total_market_value - total_cost
|
|
341
|
+
annual_coupons = sum(b["nominal"] * b["coupon"] / 100 for b in bonds) if bonds else Decimal("0")
|
|
342
|
+
|
|
343
|
+
# Emerging bonds specific
|
|
344
|
+
emerging_nominal = sum(b["nominal"] for b in emerging_bonds) if emerging_bonds else Decimal("0")
|
|
345
|
+
emerging_coupons = sum(b["nominal"] * b["coupon"] / 100 for b in emerging_bonds) if emerging_bonds else Decimal("0")
|
|
346
|
+
emerging_pnl = sum(b["nominal"] * (b["current_price"] - b["purchase_price"]) / 100 for b in emerging_bonds) if emerging_bonds else Decimal("0")
|
|
347
|
+
|
|
348
|
+
# Portfolio KPIs
|
|
349
|
+
col1, col2, col3, col4 = st.columns(4)
|
|
350
|
+
with col1:
|
|
351
|
+
st.metric("Valeur Nominale", format_amount(total_nominal))
|
|
352
|
+
with col2:
|
|
353
|
+
st.metric("Valeur de Marche", format_amount(total_market_value))
|
|
354
|
+
with col3:
|
|
355
|
+
pnl_delta = f"{'+' if pnl >= 0 else ''}{float(pnl/total_cost)*100:.2f}%" if total_cost > 0 else "0%"
|
|
356
|
+
st.metric("P&L Latent", format_amount(pnl), pnl_delta)
|
|
357
|
+
with col4:
|
|
358
|
+
st.metric("Coupons Annuels", format_amount(annual_coupons))
|
|
359
|
+
|
|
360
|
+
st.divider()
|
|
361
|
+
|
|
362
|
+
# ==================== SECTION EMERGENTS ====================
|
|
363
|
+
if emerging_bonds:
|
|
364
|
+
st.markdown("### Obligations Emergentes (Haut Rendement)")
|
|
365
|
+
st.markdown("""
|
|
366
|
+
<div style="background: linear-gradient(135deg, #4a1a1a, #1a1a2e); border: 1px solid #ff6b6b; border-radius: 12px; padding: 15px; margin-bottom: 20px;">
|
|
367
|
+
<div style="color: #ff6b6b; font-size: 12px; text-transform: uppercase;">Ukraine (War Bonds), Pologne, Roumanie</div>
|
|
368
|
+
</div>
|
|
369
|
+
""", unsafe_allow_html=True)
|
|
370
|
+
|
|
371
|
+
col1, col2, col3, col4 = st.columns(4)
|
|
372
|
+
with col1:
|
|
373
|
+
emerging_pct = float(emerging_nominal / total_nominal * 100) if total_nominal > 0 else 0
|
|
374
|
+
st.metric("Exposition Emergents", format_amount(emerging_nominal), f"{emerging_pct:.1f}% du portefeuille")
|
|
375
|
+
with col2:
|
|
376
|
+
st.metric("Coupons Emergents", format_amount(emerging_coupons), "annuels")
|
|
377
|
+
with col3:
|
|
378
|
+
emerging_pnl_pct = f"{'+' if emerging_pnl >= 0 else ''}{float(emerging_pnl / (emerging_nominal * Decimal('0.5')) * 100):.1f}%" if emerging_nominal > 0 else "0%"
|
|
379
|
+
st.metric("P&L Emergents", format_amount(emerging_pnl), emerging_pnl_pct)
|
|
380
|
+
with col4:
|
|
381
|
+
avg_yield = sum(b["coupon"] for b in emerging_bonds) / len(emerging_bonds) if emerging_bonds else Decimal("0")
|
|
382
|
+
st.metric("Rendement Moyen", f"{avg_yield:.2f}%", "brut")
|
|
383
|
+
|
|
384
|
+
# Emerging bonds details
|
|
385
|
+
for bond in emerging_bonds:
|
|
386
|
+
market_val = bond["nominal"] * bond["current_price"] / 100
|
|
387
|
+
cost_val = bond["nominal"] * bond["purchase_price"] / 100
|
|
388
|
+
bond_pnl = market_val - cost_val
|
|
389
|
+
pnl_pct = float(bond_pnl / cost_val * 100) if cost_val > 0 else 0
|
|
390
|
+
|
|
391
|
+
# Risk color and label based on country
|
|
392
|
+
if bond["country"] == "UA":
|
|
393
|
+
risk_color = "#ff6b6b"
|
|
394
|
+
risk_label = "WAR BOND"
|
|
395
|
+
elif bond["country"] == "RO":
|
|
396
|
+
risk_color = "#ffd93d"
|
|
397
|
+
risk_label = "HIGH YIELD"
|
|
398
|
+
else:
|
|
399
|
+
risk_color = "#00d4ff"
|
|
400
|
+
risk_label = "INVESTMENT GRADE"
|
|
401
|
+
|
|
402
|
+
st.markdown(f"""
|
|
403
|
+
<div style="background: #1e1e2f; border-left: 4px solid {risk_color}; padding: 12px; border-radius: 0 8px 8px 0; margin-bottom: 8px;">
|
|
404
|
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
405
|
+
<div>
|
|
406
|
+
<span style="color: white; font-weight: bold;">{bond['country']} | {bond['name']}</span>
|
|
407
|
+
<span style="background: {risk_color}; color: black; font-size: 10px; padding: 2px 6px; border-radius: 4px; margin-left: 10px;">{risk_label}</span>
|
|
408
|
+
</div>
|
|
409
|
+
<div style="text-align: right;">
|
|
410
|
+
<div style="color: #00d4ff;">{format_amount(bond['nominal'])} nominal</div>
|
|
411
|
+
<div style="color: {'#00ff88' if bond_pnl >= 0 else '#ff6b6b'};">P&L: {format_amount(bond_pnl)} ({pnl_pct:+.1f}%)</div>
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
<div style="color: #888; font-size: 12px; margin-top: 8px;">
|
|
415
|
+
Coupon: {bond['coupon']}% | Prix achat: {bond['purchase_price']}% | Prix actuel: {bond['current_price']}% | Echeance: {bond['maturity'].strftime('%d/%m/%Y')}
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
""", unsafe_allow_html=True)
|
|
419
|
+
|
|
420
|
+
st.divider()
|
|
421
|
+
|
|
422
|
+
# ==================== ALIMENTATION COMPTE TITRES (en premier) ====================
|
|
423
|
+
st.markdown("### Alimentation Compte Titres")
|
|
424
|
+
st.caption(f"Solde compte principal: {format_amount(data['accounts']['principal']['balance'])} | Solde compte titres: {format_amount(data['accounts']['titres']['balance'])}")
|
|
425
|
+
|
|
426
|
+
col1, col2, col3 = st.columns([2, 1, 1])
|
|
427
|
+
with col1:
|
|
428
|
+
transfer_amount = st.number_input(
|
|
429
|
+
"Montant a transferer (EUR)",
|
|
430
|
+
min_value=100000.0,
|
|
431
|
+
max_value=float(data["accounts"]["principal"]["balance"]),
|
|
432
|
+
step=100000.0,
|
|
433
|
+
value=1000000.0,
|
|
434
|
+
key="fund_titres"
|
|
435
|
+
)
|
|
436
|
+
with col2:
|
|
437
|
+
if st.button("Alimenter Compte Titres", type="primary", key="btn_fund"):
|
|
438
|
+
amount = Decimal(str(transfer_amount))
|
|
439
|
+
data["accounts"]["principal"]["balance"] -= amount
|
|
440
|
+
data["accounts"]["titres"]["balance"] += amount
|
|
441
|
+
data.get("transactions", []).append({
|
|
442
|
+
"date": datetime.now(),
|
|
443
|
+
"type": "internal_transfer",
|
|
444
|
+
"from": "principal",
|
|
445
|
+
"to": "titres",
|
|
446
|
+
"amount": amount,
|
|
447
|
+
"motif": "Alimentation compte titres",
|
|
448
|
+
})
|
|
449
|
+
st.success(f"Transfert de {format_amount(amount)} effectue")
|
|
450
|
+
st.rerun()
|
|
451
|
+
with col3:
|
|
452
|
+
if st.button("Retirer vers Principal", key="btn_withdraw"):
|
|
453
|
+
if data["accounts"]["titres"]["balance"] >= Decimal(str(transfer_amount)):
|
|
454
|
+
amount = Decimal(str(transfer_amount))
|
|
455
|
+
data["accounts"]["titres"]["balance"] -= amount
|
|
456
|
+
data["accounts"]["principal"]["balance"] += amount
|
|
457
|
+
st.success(f"Retrait de {format_amount(amount)} effectue")
|
|
458
|
+
st.rerun()
|
|
459
|
+
else:
|
|
460
|
+
st.error("Solde insuffisant")
|
|
461
|
+
|
|
462
|
+
st.divider()
|
|
463
|
+
|
|
464
|
+
# ==================== FORMULAIRE D'ACHAT ====================
|
|
465
|
+
st.markdown("### Acheter des Obligations")
|
|
466
|
+
|
|
467
|
+
# Filter by region
|
|
468
|
+
region = st.radio("Region", ["Toutes", "Zone Euro", "Hors Zone Euro"], horizontal=True, key="region_filter")
|
|
469
|
+
|
|
470
|
+
eurozone = ["FR", "DE", "IT", "ES", "NL", "BE", "AT", "PT", "IE", "FI"]
|
|
471
|
+
filtered = available
|
|
472
|
+
if region == "Zone Euro":
|
|
473
|
+
filtered = [b for b in available if b["country"] in eurozone]
|
|
474
|
+
elif region == "Hors Zone Euro":
|
|
475
|
+
filtered = [b for b in available if b["country"] not in eurozone]
|
|
476
|
+
|
|
477
|
+
# Bond selection
|
|
478
|
+
bond_options = {f"{b['country']} - {b['name']} (Rdt: {b['yield']}%)": b['isin'] for b in filtered}
|
|
479
|
+
|
|
480
|
+
if bond_options:
|
|
481
|
+
selected_label = st.selectbox("Selectionnez une obligation", list(bond_options.keys()), key="select_bond")
|
|
482
|
+
selected_isin = bond_options[selected_label]
|
|
483
|
+
selected_bond = next((b for b in filtered if b["isin"] == selected_isin), None)
|
|
484
|
+
|
|
485
|
+
if selected_bond:
|
|
486
|
+
col1, col2, col3 = st.columns(3)
|
|
487
|
+
with col1:
|
|
488
|
+
st.info(f"**{selected_bond['name']}**\nISIN: {selected_bond['isin']}")
|
|
489
|
+
with col2:
|
|
490
|
+
st.info(f"**Coupon:** {selected_bond['coupon']}%\n**Echeance:** {selected_bond['maturity'].strftime('%d/%m/%Y')}")
|
|
491
|
+
with col3:
|
|
492
|
+
st.info(f"**Prix:** {selected_bond['current_price']}%\n**Rendement:** {selected_bond['yield']}%")
|
|
493
|
+
|
|
494
|
+
col1, col2 = st.columns(2)
|
|
495
|
+
with col1:
|
|
496
|
+
buy_nominal = st.number_input(
|
|
497
|
+
"Nominal a acheter (EUR)",
|
|
498
|
+
min_value=100000.0,
|
|
499
|
+
max_value=10000000.0,
|
|
500
|
+
step=100000.0,
|
|
501
|
+
value=1000000.0,
|
|
502
|
+
key="buy_nominal_input"
|
|
503
|
+
)
|
|
504
|
+
buy_cost = Decimal(str(buy_nominal)) * selected_bond["current_price"] / 100
|
|
505
|
+
st.caption(f"Cout total: **{format_amount(buy_cost)}**")
|
|
506
|
+
|
|
507
|
+
with col2:
|
|
508
|
+
cash_available = data["accounts"]["titres"]["balance"]
|
|
509
|
+
st.caption(f"Cash disponible: {format_amount(cash_available)}")
|
|
510
|
+
|
|
511
|
+
if st.button("Acheter", type="primary", key="btn_buy_bond", disabled=buy_cost > cash_available):
|
|
512
|
+
if buy_cost > cash_available:
|
|
513
|
+
st.error("Fonds insuffisants! Alimentez d'abord le compte titres.")
|
|
514
|
+
else:
|
|
515
|
+
new_bond = {
|
|
516
|
+
"isin": selected_bond["isin"],
|
|
517
|
+
"name": selected_bond["name"],
|
|
518
|
+
"country": selected_bond["country"],
|
|
519
|
+
"country_name": selected_bond["country_name"],
|
|
520
|
+
"coupon": selected_bond["coupon"],
|
|
521
|
+
"maturity": selected_bond["maturity"],
|
|
522
|
+
"nominal": Decimal(str(buy_nominal)),
|
|
523
|
+
"purchase_price": selected_bond["current_price"],
|
|
524
|
+
"current_price": selected_bond["current_price"],
|
|
525
|
+
"purchase_date": date.today(),
|
|
526
|
+
"quantity": int(buy_nominal / 100000),
|
|
527
|
+
}
|
|
528
|
+
data["sovereign_bonds"].append(new_bond)
|
|
529
|
+
data["accounts"]["titres"]["balance"] -= buy_cost
|
|
530
|
+
if "bond_transactions" not in data:
|
|
531
|
+
data["bond_transactions"] = []
|
|
532
|
+
data["bond_transactions"].append({
|
|
533
|
+
"date": datetime.now(),
|
|
534
|
+
"type": "buy",
|
|
535
|
+
"isin": selected_bond["isin"],
|
|
536
|
+
"name": selected_bond["name"],
|
|
537
|
+
"nominal": Decimal(str(buy_nominal)),
|
|
538
|
+
"price": selected_bond["current_price"],
|
|
539
|
+
"amount": buy_cost,
|
|
540
|
+
})
|
|
541
|
+
st.success(f"Achat de {format_amount(Decimal(str(buy_nominal)))} nominal execute!")
|
|
542
|
+
st.rerun()
|
|
543
|
+
|
|
544
|
+
if buy_cost > cash_available:
|
|
545
|
+
st.warning(f"Il manque {format_amount(buy_cost - cash_available)} sur le compte titres")
|
|
546
|
+
|
|
547
|
+
st.divider()
|
|
548
|
+
|
|
549
|
+
# ==================== PORTEFEUILLE ACTUEL ====================
|
|
550
|
+
st.markdown("### Repartition Geographique")
|
|
551
|
+
|
|
552
|
+
if bonds:
|
|
553
|
+
countries = {}
|
|
554
|
+
for bond in bonds:
|
|
555
|
+
country = bond["country_name"]
|
|
556
|
+
if country not in countries:
|
|
557
|
+
countries[country] = Decimal("0")
|
|
558
|
+
countries[country] += bond["nominal"]
|
|
559
|
+
|
|
560
|
+
for country, nominal in sorted(countries.items(), key=lambda x: x[1], reverse=True):
|
|
561
|
+
pct = float(nominal / total_nominal * 100) if total_nominal > 0 else 0
|
|
562
|
+
col1, col2 = st.columns([1, 3])
|
|
563
|
+
with col1:
|
|
564
|
+
flag_map = {"France": "FR", "Allemagne": "DE", "Italie": "IT", "Espagne": "ES", "Etats-Unis": "US", "Royaume-Uni": "GB", "Japon": "JP", "Suisse": "CH", "Pays-Bas": "NL", "Belgique": "BE"}
|
|
565
|
+
flag = flag_map.get(country, "EU")
|
|
566
|
+
st.markdown(f"**{flag} {country}**")
|
|
567
|
+
st.caption(format_amount(nominal))
|
|
568
|
+
with col2:
|
|
569
|
+
st.progress(pct / 100)
|
|
570
|
+
st.caption(f"{pct:.1f}%")
|
|
571
|
+
else:
|
|
572
|
+
st.info("Aucune obligation en portefeuille")
|
|
573
|
+
|
|
574
|
+
st.divider()
|
|
575
|
+
|
|
576
|
+
# ==================== POSITIONS ACTUELLES ====================
|
|
577
|
+
st.markdown("### Positions Actuelles")
|
|
578
|
+
|
|
579
|
+
if bonds:
|
|
580
|
+
for idx, bond in enumerate(bonds):
|
|
581
|
+
market_val = bond["nominal"] * bond["current_price"] / 100
|
|
582
|
+
cost_val = bond["nominal"] * bond["purchase_price"] / 100
|
|
583
|
+
bond_pnl = market_val - cost_val
|
|
584
|
+
pnl_pct = float(bond_pnl / cost_val * 100) if cost_val > 0 else 0
|
|
585
|
+
|
|
586
|
+
with st.expander(f"{bond['country']} | {bond['name']} | {format_amount(bond['nominal'])} nominal", expanded=False):
|
|
587
|
+
col1, col2, col3, col4 = st.columns(4)
|
|
588
|
+
with col1:
|
|
589
|
+
st.markdown(f"**ISIN:** {bond['isin']}")
|
|
590
|
+
st.markdown(f"**Nominal:** {format_amount(bond['nominal'])}")
|
|
591
|
+
with col2:
|
|
592
|
+
st.markdown(f"**Coupon:** {bond['coupon']}%")
|
|
593
|
+
st.markdown(f"**Echeance:** {bond['maturity'].strftime('%d/%m/%Y')}")
|
|
594
|
+
with col3:
|
|
595
|
+
st.markdown(f"**Prix achat:** {bond['purchase_price']}%")
|
|
596
|
+
st.markdown(f"**Prix actuel:** {bond['current_price']}%")
|
|
597
|
+
with col4:
|
|
598
|
+
pnl_color = "green" if bond_pnl >= 0 else "red"
|
|
599
|
+
st.markdown(f"**Valeur marche:** {format_amount(market_val)}")
|
|
600
|
+
st.markdown(f"**P&L:** :{pnl_color}[{format_amount(bond_pnl)} ({pnl_pct:+.2f}%)]")
|
|
601
|
+
|
|
602
|
+
# Vente
|
|
603
|
+
st.markdown("---")
|
|
604
|
+
col1, col2, col3 = st.columns([2, 1, 1])
|
|
605
|
+
with col1:
|
|
606
|
+
sell_pct = st.slider(f"% a vendre", 10, 100, 100, 10, key=f"sell_pct_{idx}")
|
|
607
|
+
sell_nominal = bond["nominal"] * sell_pct / 100
|
|
608
|
+
sell_value = sell_nominal * bond["current_price"] / 100
|
|
609
|
+
st.caption(f"Vente: {format_amount(sell_nominal)} nominal = {format_amount(sell_value)}")
|
|
610
|
+
with col2:
|
|
611
|
+
if st.button("Vendre", key=f"sell_btn_{idx}", type="secondary"):
|
|
612
|
+
if sell_pct == 100:
|
|
613
|
+
data["sovereign_bonds"].remove(bond)
|
|
614
|
+
else:
|
|
615
|
+
bond["nominal"] -= sell_nominal
|
|
616
|
+
data["accounts"]["titres"]["balance"] += sell_value
|
|
617
|
+
if "bond_transactions" not in data:
|
|
618
|
+
data["bond_transactions"] = []
|
|
619
|
+
data["bond_transactions"].append({
|
|
620
|
+
"date": datetime.now(),
|
|
621
|
+
"type": "sell",
|
|
622
|
+
"isin": bond["isin"],
|
|
623
|
+
"name": bond["name"],
|
|
624
|
+
"nominal": sell_nominal,
|
|
625
|
+
"price": bond["current_price"],
|
|
626
|
+
"amount": sell_value,
|
|
627
|
+
})
|
|
628
|
+
st.success(f"Vente executee: {format_amount(sell_value)}")
|
|
629
|
+
st.rerun()
|
|
630
|
+
with col3:
|
|
631
|
+
txs = [t for t in data.get("bond_transactions", []) if t.get("isin") == bond["isin"]]
|
|
632
|
+
if txs:
|
|
633
|
+
st.caption(f"{len(txs)} transaction(s)")
|
|
634
|
+
else:
|
|
635
|
+
st.info("Aucune position. Achetez des obligations ci-dessus.")
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def render_assurances_tab(data):
|
|
639
|
+
"""Group insurance management"""
|
|
640
|
+
st.markdown("### Assurances Groupe")
|
|
641
|
+
|
|
642
|
+
insurances = data.get("insurances", [])
|
|
643
|
+
claims = data.get("claims", [])
|
|
644
|
+
|
|
645
|
+
# Calculate totals
|
|
646
|
+
total_coverage = sum(ins["coverage"] for ins in insurances) if insurances else Decimal("0")
|
|
647
|
+
total_premiums = sum(ins["premium"] for ins in insurances) if insurances else Decimal("0")
|
|
648
|
+
active_policies = len([ins for ins in insurances if ins["status"] == "active"])
|
|
649
|
+
|
|
650
|
+
# KPIs
|
|
651
|
+
col1, col2, col3, col4 = st.columns(4)
|
|
652
|
+
with col1:
|
|
653
|
+
st.metric("Couverture Totale", format_amount(total_coverage))
|
|
654
|
+
with col2:
|
|
655
|
+
st.metric("Primes Annuelles", format_amount(total_premiums))
|
|
656
|
+
with col3:
|
|
657
|
+
st.metric("Polices Actives", str(active_policies))
|
|
658
|
+
with col4:
|
|
659
|
+
pending_claims = len([c for c in claims if c["status"] == "pending"])
|
|
660
|
+
st.metric("Sinistres en cours", str(pending_claims))
|
|
661
|
+
|
|
662
|
+
st.divider()
|
|
663
|
+
|
|
664
|
+
# Insurance by type
|
|
665
|
+
st.markdown("### Polices d'Assurance")
|
|
666
|
+
|
|
667
|
+
type_labels = {
|
|
668
|
+
"D&O": ("RC Dirigeants", "#7b2cbf"),
|
|
669
|
+
"RC_PRO": ("RC Professionnelle", "#00d4ff"),
|
|
670
|
+
"CYBER": ("Cyber", "#ff6b6b"),
|
|
671
|
+
"PROPERTY": ("Immobilier", "#00ff88"),
|
|
672
|
+
"KEY_MAN": ("Homme-Cle", "#ffd93d"),
|
|
673
|
+
"CDS": ("Credit Default Swap", "#ff6b6b"),
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
for ins in insurances:
|
|
677
|
+
type_info = type_labels.get(ins["type"], (ins["type"], "#888"))
|
|
678
|
+
type_label, type_color = type_info
|
|
679
|
+
|
|
680
|
+
# Check if expiring soon (within 90 days)
|
|
681
|
+
days_to_expiry = (ins["end_date"] - date.today()).days
|
|
682
|
+
expiry_warning = days_to_expiry <= 90
|
|
683
|
+
|
|
684
|
+
st.markdown(f"""
|
|
685
|
+
<div style="background: #1e1e2f; border-left: 4px solid {type_color}; padding: 15px; border-radius: 0 8px 8px 0; margin-bottom: 12px;">
|
|
686
|
+
<div style="display: flex; justify-content: space-between; align-items: start;">
|
|
687
|
+
<div>
|
|
688
|
+
<span style="background: {type_color}; color: black; font-size: 10px; padding: 2px 8px; border-radius: 4px;">{type_label}</span>
|
|
689
|
+
<div style="color: white; font-weight: bold; font-size: 16px; margin-top: 8px;">{ins['name']}</div>
|
|
690
|
+
<div style="color: #888; font-size: 12px; margin-top: 4px;">Assureur: {ins['insurer']} | Police: {ins['policy_number']}</div>
|
|
691
|
+
</div>
|
|
692
|
+
<div style="text-align: right;">
|
|
693
|
+
<div style="color: #00d4ff; font-size: 18px; font-weight: bold;">{format_amount(ins['coverage'])}</div>
|
|
694
|
+
<div style="color: #888; font-size: 12px;">couverture</div>
|
|
695
|
+
<div style="color: {'#ff6b6b' if expiry_warning else '#00ff88'}; font-size: 12px; margin-top: 4px;">
|
|
696
|
+
{'Expire dans ' + str(days_to_expiry) + 'j' if expiry_warning else 'Valide'}
|
|
697
|
+
</div>
|
|
698
|
+
</div>
|
|
699
|
+
</div>
|
|
700
|
+
<div style="display: flex; gap: 30px; margin-top: 12px; color: #aaa; font-size: 13px;">
|
|
701
|
+
<div><span style="color: #888;">Prime:</span> {format_amount(ins['premium'])}/an</div>
|
|
702
|
+
<div><span style="color: #888;">Debut:</span> {ins['start_date'].strftime('%d/%m/%Y')}</div>
|
|
703
|
+
<div><span style="color: #888;">Fin:</span> {ins['end_date'].strftime('%d/%m/%Y')}</div>
|
|
704
|
+
</div>
|
|
705
|
+
</div>
|
|
706
|
+
""", unsafe_allow_html=True)
|
|
707
|
+
|
|
708
|
+
st.divider()
|
|
709
|
+
|
|
710
|
+
# CDS Section (special for sovereign risk)
|
|
711
|
+
cds_policies = [ins for ins in insurances if ins["type"] == "CDS"]
|
|
712
|
+
if cds_policies:
|
|
713
|
+
st.markdown("### Couverture Risque Souverain (CDS)")
|
|
714
|
+
st.markdown("""
|
|
715
|
+
<div style="background: linear-gradient(135deg, #4a1a1a, #1a1a2e); border: 1px solid #ff6b6b; border-radius: 12px; padding: 15px; margin-bottom: 20px;">
|
|
716
|
+
<div style="color: #ff6b6b; font-size: 12px; text-transform: uppercase;">Credit Default Swaps - Protection contre defaut souverain</div>
|
|
717
|
+
</div>
|
|
718
|
+
""", unsafe_allow_html=True)
|
|
719
|
+
|
|
720
|
+
for cds in cds_policies:
|
|
721
|
+
spread = cds.get("spread_bps", 0)
|
|
722
|
+
st.markdown(f"""
|
|
723
|
+
<div style="background: #1e1e2f; padding: 15px; border-radius: 8px; margin-bottom: 10px;">
|
|
724
|
+
<div style="display: flex; justify-content: space-between;">
|
|
725
|
+
<div>
|
|
726
|
+
<div style="color: white; font-weight: bold;">{cds['name']}</div>
|
|
727
|
+
<div style="color: #888; font-size: 12px;">Contrepartie: {cds['insurer']}</div>
|
|
728
|
+
</div>
|
|
729
|
+
<div style="text-align: right;">
|
|
730
|
+
<div style="color: #ff6b6b; font-size: 18px; font-weight: bold;">{spread} bps</div>
|
|
731
|
+
<div style="color: #888; font-size: 12px;">spread annuel</div>
|
|
732
|
+
</div>
|
|
733
|
+
</div>
|
|
734
|
+
<div style="margin-top: 10px; display: flex; gap: 20px; color: #aaa; font-size: 13px;">
|
|
735
|
+
<div>Notionnel: {format_amount(cds['coverage'])}</div>
|
|
736
|
+
<div>Prime annuelle: {format_amount(cds['premium'])}</div>
|
|
737
|
+
<div>Maturite: {cds['end_date'].strftime('%d/%m/%Y')}</div>
|
|
738
|
+
</div>
|
|
739
|
+
</div>
|
|
740
|
+
""", unsafe_allow_html=True)
|
|
741
|
+
|
|
742
|
+
st.divider()
|
|
743
|
+
|
|
744
|
+
# Claims section
|
|
745
|
+
st.markdown("### Historique des Sinistres")
|
|
746
|
+
|
|
747
|
+
if claims:
|
|
748
|
+
for claim in claims:
|
|
749
|
+
# Find insurance
|
|
750
|
+
ins = next((i for i in insurances if i["id"] == claim["insurance_id"]), None)
|
|
751
|
+
ins_name = ins["name"] if ins else "N/A"
|
|
752
|
+
|
|
753
|
+
status_colors = {"paid": "#00ff88", "pending": "#ffd93d", "rejected": "#ff6b6b"}
|
|
754
|
+
status_labels = {"paid": "Rembourse", "pending": "En cours", "rejected": "Rejete"}
|
|
755
|
+
|
|
756
|
+
col1, col2, col3, col4, col5 = st.columns([2, 2, 2, 2, 1])
|
|
757
|
+
with col1:
|
|
758
|
+
st.markdown(f"**{claim['type']}**")
|
|
759
|
+
st.caption(f"{claim['date'].strftime('%d/%m/%Y')}")
|
|
760
|
+
with col2:
|
|
761
|
+
st.caption(ins_name)
|
|
762
|
+
with col3:
|
|
763
|
+
st.markdown(f"Demande: {format_amount(claim['amount_claimed'])}")
|
|
764
|
+
with col4:
|
|
765
|
+
if claim["amount_paid"] > 0:
|
|
766
|
+
st.markdown(f":green[Paye: {format_amount(claim['amount_paid'])}]")
|
|
767
|
+
else:
|
|
768
|
+
st.caption("En attente")
|
|
769
|
+
with col5:
|
|
770
|
+
color = status_colors.get(claim["status"], "#888")
|
|
771
|
+
label = status_labels.get(claim["status"], claim["status"])
|
|
772
|
+
st.markdown(f"<span style='background: {color}; color: black; padding: 2px 8px; border-radius: 4px; font-size: 11px;'>{label}</span>", unsafe_allow_html=True)
|
|
773
|
+
st.divider()
|
|
774
|
+
else:
|
|
775
|
+
st.info("Aucun sinistre enregistre")
|
|
776
|
+
|
|
777
|
+
# Summary
|
|
778
|
+
st.markdown("### Synthese Annuelle")
|
|
779
|
+
col1, col2, col3 = st.columns(3)
|
|
780
|
+
with col1:
|
|
781
|
+
claims_paid = sum(c["amount_paid"] for c in claims if c["status"] == "paid")
|
|
782
|
+
st.metric("Sinistres Rembourses", format_amount(claims_paid))
|
|
783
|
+
with col2:
|
|
784
|
+
ratio = float(claims_paid / total_premiums * 100) if total_premiums > 0 else 0
|
|
785
|
+
st.metric("Ratio S/P", f"{ratio:.1f}%")
|
|
786
|
+
with col3:
|
|
787
|
+
st.metric("Cout Net Assurance", format_amount(total_premiums - claims_paid))
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def render_consolidation_tab(data):
|
|
791
|
+
"""Consolidated financials"""
|
|
792
|
+
st.markdown("### Bilan Consolide")
|
|
793
|
+
|
|
794
|
+
total_assets = sum(s["assets"] for s in data["subsidiaries"])
|
|
795
|
+
treasury = data["accounts"]["principal"]["balance"] + data["accounts"]["tresorerie"]["balance"]
|
|
796
|
+
|
|
797
|
+
col1, col2, col3, col4 = st.columns(4)
|
|
798
|
+
with col1:
|
|
799
|
+
st.metric("Total Actifs", f"{(total_assets + treasury)/1000000000:.2f}B EUR", "+5.2%")
|
|
800
|
+
with col2:
|
|
801
|
+
st.metric("Fonds Propres", "420M EUR", "+3.1%")
|
|
802
|
+
with col3:
|
|
803
|
+
st.metric("PNB Groupe", "285M EUR", "+8.4%")
|
|
804
|
+
with col4:
|
|
805
|
+
st.metric("Resultat Net", "62M EUR", "+12.3%")
|
|
806
|
+
|
|
807
|
+
st.divider()
|
|
808
|
+
|
|
809
|
+
st.markdown("### Repartition par Activite")
|
|
810
|
+
|
|
811
|
+
activities = [
|
|
812
|
+
{"name": "Banque de detail", "revenue": 180, "percent": 63},
|
|
813
|
+
{"name": "Gestion d'actifs", "revenue": 52, "percent": 18},
|
|
814
|
+
{"name": "Assurance", "revenue": 38, "percent": 13},
|
|
815
|
+
{"name": "Autres", "revenue": 15, "percent": 6},
|
|
816
|
+
]
|
|
817
|
+
|
|
818
|
+
for act in activities:
|
|
819
|
+
col1, col2 = st.columns([1, 3])
|
|
820
|
+
with col1:
|
|
821
|
+
st.markdown(f"**{act['name']}**")
|
|
822
|
+
st.caption(f"{act['revenue']}M EUR")
|
|
823
|
+
with col2:
|
|
824
|
+
st.progress(act['percent'] / 100)
|
|
825
|
+
|
|
826
|
+
st.divider()
|
|
827
|
+
|
|
828
|
+
st.markdown("### Contribution des Filiales")
|
|
829
|
+
|
|
830
|
+
for sub in data["subsidiaries"]:
|
|
831
|
+
contribution = float(sub["assets"] / total_assets) * 100
|
|
832
|
+
st.markdown(f"**{sub['name']}** - {contribution:.1f}%")
|
|
833
|
+
st.progress(contribution / 100)
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def render_risques_tab(data):
|
|
837
|
+
"""Risk management"""
|
|
838
|
+
st.markdown("### Ratios Reglementaires")
|
|
839
|
+
|
|
840
|
+
col1, col2, col3 = st.columns(3)
|
|
841
|
+
with col1:
|
|
842
|
+
st.metric("Ratio CET1", "14.2%", "+0.3%")
|
|
843
|
+
st.caption("Minimum requis: 10.5%")
|
|
844
|
+
st.progress(0.142 / 0.20)
|
|
845
|
+
with col2:
|
|
846
|
+
st.metric("Ratio LCR", "142%", "+5%")
|
|
847
|
+
st.caption("Minimum requis: 100%")
|
|
848
|
+
st.progress(1.42 / 2.0)
|
|
849
|
+
with col3:
|
|
850
|
+
st.metric("Ratio NSFR", "118%", "+2%")
|
|
851
|
+
st.caption("Minimum requis: 100%")
|
|
852
|
+
st.progress(1.18 / 2.0)
|
|
853
|
+
|
|
854
|
+
st.divider()
|
|
855
|
+
|
|
856
|
+
st.markdown("### Exposition par Type de Risque")
|
|
857
|
+
|
|
858
|
+
risks = [
|
|
859
|
+
{"type": "Risque de credit", "exposure": "2.1B EUR", "provision": "45M EUR", "level": "medium"},
|
|
860
|
+
{"type": "Risque de marche", "exposure": "320M EUR", "provision": "8M EUR", "level": "low"},
|
|
861
|
+
{"type": "Risque operationnel", "exposure": "-", "provision": "12M EUR", "level": "low"},
|
|
862
|
+
{"type": "Risque de liquidite", "exposure": "450M EUR", "provision": "-", "level": "low"},
|
|
863
|
+
]
|
|
864
|
+
|
|
865
|
+
for risk in risks:
|
|
866
|
+
col1, col2, col3, col4 = st.columns([2, 2, 2, 1])
|
|
867
|
+
with col1:
|
|
868
|
+
st.markdown(f"**{risk['type']}**")
|
|
869
|
+
with col2:
|
|
870
|
+
st.caption(f"Exposition: {risk['exposure']}")
|
|
871
|
+
with col3:
|
|
872
|
+
st.caption(f"Provision: {risk['provision']}")
|
|
873
|
+
with col4:
|
|
874
|
+
if risk['level'] == 'high':
|
|
875
|
+
st.error("Eleve")
|
|
876
|
+
elif risk['level'] == 'medium':
|
|
877
|
+
st.warning("Moyen")
|
|
878
|
+
else:
|
|
879
|
+
st.success("Faible")
|
|
880
|
+
|
|
881
|
+
st.divider()
|
|
882
|
+
|
|
883
|
+
st.markdown("### Concentration")
|
|
884
|
+
|
|
885
|
+
pool_total = sum(abs(s["pool_balance"]) for s in data["subsidiaries"])
|
|
886
|
+
for sub in data["subsidiaries"]:
|
|
887
|
+
concentration = float(abs(sub["pool_balance"]) / pool_total * 100) if pool_total > 0 else 0.0
|
|
888
|
+
st.markdown(f"{sub['name']}: {concentration:.1f}%")
|
|
889
|
+
st.progress(concentration / 100)
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
def render_gouvernance_tab(data):
|
|
893
|
+
"""Governance"""
|
|
894
|
+
st.markdown("### Conseil d'Administration")
|
|
895
|
+
|
|
896
|
+
board = [
|
|
897
|
+
{"name": "Laurent Dubois", "role": "President-Directeur General", "since": "2018"},
|
|
898
|
+
{"name": "Marie-Claire Fontaine", "role": "Directrice Generale Deleguee", "since": "2019"},
|
|
899
|
+
{"name": "Philippe Martin", "role": "Directeur Financier", "since": "2020"},
|
|
900
|
+
{"name": "Isabelle Leroy", "role": "Directrice des Risques", "since": "2021"},
|
|
901
|
+
{"name": "Jean-Pierre Moreau", "role": "Administrateur Independant", "since": "2017"},
|
|
902
|
+
]
|
|
903
|
+
|
|
904
|
+
for member in board:
|
|
905
|
+
col1, col2, col3 = st.columns([2, 2, 1])
|
|
906
|
+
with col1:
|
|
907
|
+
st.markdown(f"**{member['name']}**")
|
|
908
|
+
with col2:
|
|
909
|
+
st.caption(member['role'])
|
|
910
|
+
with col3:
|
|
911
|
+
st.caption(f"Depuis {member['since']}")
|
|
912
|
+
|
|
913
|
+
st.divider()
|
|
914
|
+
|
|
915
|
+
st.markdown("### Comites")
|
|
916
|
+
|
|
917
|
+
committees = [
|
|
918
|
+
{"name": "Comite d'Audit", "members": 4, "meetings": 12},
|
|
919
|
+
{"name": "Comite des Risques", "members": 3, "meetings": 24},
|
|
920
|
+
{"name": "Comite des Remunerations", "members": 3, "meetings": 4},
|
|
921
|
+
{"name": "Comite de Nomination", "members": 2, "meetings": 2},
|
|
922
|
+
]
|
|
923
|
+
|
|
924
|
+
for committee in committees:
|
|
925
|
+
col1, col2, col3 = st.columns([2, 1, 1])
|
|
926
|
+
with col1:
|
|
927
|
+
st.markdown(f"**{committee['name']}**")
|
|
928
|
+
with col2:
|
|
929
|
+
st.caption(f"{committee['members']} membres")
|
|
930
|
+
with col3:
|
|
931
|
+
st.caption(f"{committee['meetings']} reunions/an")
|
|
932
|
+
|
|
933
|
+
st.divider()
|
|
934
|
+
|
|
935
|
+
st.markdown("### Documents Reglementaires")
|
|
936
|
+
|
|
937
|
+
col1, col2 = st.columns(2)
|
|
938
|
+
with col1:
|
|
939
|
+
st.download_button("Rapport Annuel 2025", "Contenu du rapport annuel...", "rapport_annuel_2025.pdf", mime="application/pdf")
|
|
940
|
+
st.download_button("Rapport Pilier 3", "Contenu du rapport Pilier 3...", "pilier3_2025.pdf", mime="application/pdf")
|
|
941
|
+
with col2:
|
|
942
|
+
st.download_button("Rapport RSE", "Contenu du rapport RSE...", "rse_2025.pdf", mime="application/pdf")
|
|
943
|
+
st.download_button("Charte Ethique", "Contenu de la charte...", "charte_ethique.pdf", mime="application/pdf")
|