added portfolio view

This commit is contained in:
2023-01-26 16:22:58 +01:00
parent 060b796636
commit 4a55f94551
16 changed files with 750 additions and 87 deletions

6
allocation.sql Normal file
View File

@@ -0,0 +1,6 @@
INSERT INTO "allocation" VALUES(5,'Obligations',20,28.899999999999998578,74481.0);
INSERT INTO "allocation" VALUES(10,'Actions World',60,54.0,139404.14999999999418);
INSERT INTO "allocation" VALUES(12,'Cash',2,2.2000000000000001776,5591.0000000000000001);
INSERT INTO "allocation" VALUES(16,'Actions REITS',2,6.0999999999999996447,15607.200000000000727);
INSERT INTO "allocation" VALUES(17,'Actions Moment',15,8.1999999999999992894,21223.43999999999869);
INSERT INTO "allocation" VALUES(19,'Crypto',1,0.59999999999999997779,1623.349999999999909);

Binary file not shown.

View File

@@ -0,0 +1,30 @@
"""added relationship allocation-classe
Revision ID: 42a297861f20
Revises: 19d939dbc6d0
Create Date: 2023-01-26 14:23:42.771763
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '42a297861f20'
down_revision = '19d939dbc6d0'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('allocation', sa.Column('classe_id', sa.Unicode(length=45), nullable=True))
op.create_foreign_key(op.f('fk_allocation_classe_id_classes'), 'allocation', 'classes', ['classe_id'], ['classe'])
op.drop_column('allocation', 'classe')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('allocation', sa.Column('classe', sa.VARCHAR(length=45), nullable=False))
op.drop_constraint(op.f('fk_allocation_classe_id_classes'), 'allocation', type_='foreignkey')
op.drop_column('allocation', 'classe_id')
# ### end Alembic commands ###

View File

@@ -0,0 +1,34 @@
"""added relationship allocation-classe
Revision ID: bbfb79cb9dad
Revises: 42a297861f20
Create Date: 2023-01-26 14:41:29.955558
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'bbfb79cb9dad'
down_revision = '42a297861f20'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('allocation',
sa.Column('no_cat', sa.Integer(), nullable=False),
sa.Column('classe_id', sa.Unicode(length=45), nullable=True),
sa.Column('pc_cible', sa.Integer(), nullable=True),
sa.Column('pc_atteint', sa.Float(), nullable=True),
sa.Column('valeur', sa.Float(), nullable=True),
sa.ForeignKeyConstraint(['classe_id'], ['classes.classe'], name=op.f('fk_allocation_classe_id_classes')),
sa.PrimaryKeyConstraint('no_cat', name=op.f('pk_allocation'))
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('allocation')
# ### end Alembic commands ###

View File

@@ -1,4 +1,4 @@
from wtforms import Form, StringField, TextAreaField, SelectField, validators
from wtforms import Form, StringField, TextAreaField, SelectField, DecimalField
from wtforms import IntegerField, PasswordField
from wtforms.validators import InputRequired, Length
from wtforms.widgets import HiddenInput
@@ -22,7 +22,6 @@ class BlogSearchForm(Form):
class TagForm(Form):
id = IntegerField(widget=HiddenInput())
tag = StringField('Tag', validators=[InputRequired(), Length(min=1, max=25)],
filters=[strip_filter])
@@ -32,3 +31,11 @@ class UserCreateForm(Form):
filters=[strip_filter])
password = PasswordField('Mot de passe', validators=[InputRequired(), Length(min=6)])
class HistoForm(Form):
no_id = IntegerField(widget=HiddenInput())
mvt_cash = DecimalField(places=2, validators=[InputRequired()])
class AllocationForm(Form):
no_cat = IntegerField(widget=HiddenInput())
classe = SelectField('Classe')
pc_cible = IntegerField(validators=[InputRequired()])

View File

@@ -1,5 +1,6 @@
import datetime #<- will be used to set default dates on models
from cao_blogr.models.meta import Base #<- we need to import our sqlalchemy metadata from which model classes will inherit
from sqlalchemy.orm import relationship
from sqlalchemy import (
Column,
Integer,
@@ -38,14 +39,17 @@ class Actifs(Base):
class Allocation(Base):
__tablename__ = 'allocation'
no_cat = Column(Integer, primary_key=True)
classe = Column(Unicode(45), nullable=False)
classe_id = Column(Unicode(45), ForeignKey('classes.classe'))
pc_cible = Column(Integer)
pc_atteint = Column(Float)
valeur = Column(Float)
# relationship
classe = relationship('Classes', backref="allocation")
class Classes(Base):
__tablename__ = 'classes'
classe = Column(Unicode, primary_key=True)
classe = Column(Unicode(45), primary_key=True)
type = Column(Unicode(45), default='ACTION')
ordre = Column(Integer)
bg_color = Column(Unicode(45))

View File

@@ -1,15 +1,90 @@
import sqlalchemy as sa
from ..models.portfolio import Histo
from sqlalchemy import func
from ..models.portfolio import Actifs, Allocation, Classes, Histo
class PFService(object):
@classmethod
def get_actifs(cls, request, no_id):
if no_id == '0':
items = request.dbsession.query(Actifs).order_by(Actifs.classe, Actifs.libelle).all()
else:
# lire une allocation par le no_id
items = request.dbsession.query(Actifs).filter(Actifs.no_id == no_id).first()
return items
@classmethod
def get_allocation(cls, request, no_cat):
if no_cat == '0':
query = request.dbsession.query(Allocation).join(Classes).filter(Classes.classe == Allocation.classe_id)
query = query.order_by(sa.asc(Allocation.classe_id)).all()
else:
# lire une allocation par le no_id
query = request.dbsession.query(Allocation).filter(Allocation.no_cat == no_cat).first()
return query
@classmethod
def get_classes(cls, request, classe):
if classe == '0':
items = request.dbsession.query(Classes).order_by(sa.asc(Classes.ordre)).all()
else:
# lire une allocation par le no_id
items = request.dbsession.query(Classes).filter(Classes.classe == classe).first()
return items
@classmethod
def get_histo(cls, request, no_id):
if no_id == '0':
items = request.dbsession.query(Histo).order_by(sa.asc(Histo.date)).all()
items = request.dbsession.query(Histo).order_by(sa.desc(Histo.date)).all()
else:
# lire le histo par le no_id
items = request.dbsession.query(Histo).filter(Histo.id == id).first()
items = request.dbsession.query(Histo).filter(Histo.no_id == no_id).first()
return items
@classmethod
def delete_allocation(cls, request, no_id):
request.dbsession.query(Allocation).filter(Histo.no_ == no_id).delete(synchronize_session=False)
return
@classmethod
def delete_histo(cls, request, no_id):
request.dbsession.query(Histo).filter(Histo.no_id == no_id).delete(synchronize_session=False)
return
@classmethod
def update_actif_devise(request, devise, taux):
request.dbsession.query(Actifs).filter(Actifs.devise == devise).update({'parite': taux})
request.dbsession.commit()
return
def update_actif_valeur(request, symbole, cours, dividends):
request.dbsession.query(Actifs).filter(Actifs.symbole == symbole).update({'symbole': symbole, 'cours': cours, 'dividends': dividends})
request.dbsession.commit()
return
def update_portefeuille(request):
TotalValue = request.dbsession.query(Actifs, func.sum(Actifs.valeur).label("TotalValue")).first()
# maj du pourcentage d'allocation des lignes du portefeuille
request.dbsession.query(Actifs).update({'pc_allocation': Actifs.valeur / TotalValue * 100})
request.dbsession.commit()
# maj des allocations
items = PFService.get_allocation(request, '0')
for item in items:
TotalClasse = request.dbsession.query(Actifs, func.sum(Actifs.valeur).label("TotalValue")
).filter(Actifs.classe == item.classe).first()
item.valeur = TotalClasse,
item.pc_atteint = item.valeur / TotalValue * 100;
request.dbsession.commit()
return
def delete_actif(request, no_id):
request.dbsession.query(Actifs).filter(Actifs.no_id == no_id).delete(synchronize_session=False)
request.dbsession.commit()
return

View File

@@ -2,8 +2,8 @@
/* https://www.w3schools.com/bootstrap/bootstrap_theme_band.asp */
body {
font: 400 19px/1.8 Georgia, serif;
color: #777;
font: 200 16px/1.8 Georgia, serif;
color: rgb(90, 89, 89);
}
h3, h4 {
margin: 10px 0 30px 0;

View File

@@ -3,7 +3,7 @@
{% block content %}
{% if request.authenticated_userid %}
<p>
<a href="{{ request.route_url('home') }}">[ Retour ]</a>
<a href="{{ return_url }}">[ Retour ]</a>
<a href="{{ request.route_url('blog_edit', id=entry.id) }}">[ Modifier ]</a>
</p>

View File

@@ -0,0 +1,60 @@
{% extends "cao_blogr:templates/layout.jinja2" %}
{% block content %}
<form action="{{ url }}" method="post" class="form">
<div class="form-group">
<label class="required-field">{{ form.classe.label }}</label>
{{ form.classe(class_='form-control') }}
</div>
{% for error in form.pc_cible.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
<div class="form-group">
<label class="required-field" for="tag">{{form.pc_cible.label}}</label>
{{form.pc_cible(class_='form-control')}}
</div>
<br>
<div class="form-group">
<a class="btn btn-default" href="{{ request.route_url('portfolio') }}">
<span class="glyphicon glyphicon-chevron-left"></span> Retour</a>
<button class="btn btn-primary" type="submit" name="form.submitted">
<span class="glyphicon glyphicon-ok"></span> Enregistrer</button>
{% if form.no_cat.data %}
<button class="btn btn-danger" type="button" data-toggle="modal" data-target="#confirmDelete">
<span class="glyphicon glyphicon-remove"></span> Supprimer</button>
{% endif %}
</div>
</form>
<!-- Modal : Confirmation SUPRESSION -->
<div id="confirmDelete" class="modal" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title">Supprimer cette allocation</h4>
</div>
<div class="modal-body">
<!-- The form is placed inside the body of modal -->
<p>Etes-vous certain(e) de vouloir supprimer la ligne <b>{{ form.classe.data }}</b> ?</p>
</div>
<div class="modal-footer">
<div class="form-group">
<div class="text-center">
<form id="confirmForm" method="post">
<button type="submit" class="btn btn-danger" name="form.deleted">Supprimer</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,67 +1,59 @@
<div metal:use-macro="load: ../global_layout.pt">
<div metal:fill-slot="content">
{% extends "cao_blogr:templates/layout.jinja2" %}
<div tal:condition="message" tal:content="message" class="alert alert-danger" />
<br />
<div class="row">
<form id="histo_edit-form" class="form-horizontal" action="${url}" method="post" tal:condition="item"
data-fv-framework="bootstrap"
data-fv-icon-valid="glyphicon glyphicon-ok"
data-fv-icon-invalid="glyphicon glyphicon-remove"
data-fv-icon-validating="glyphicon glyphicon-refresh">
{% block content %}
<div class="form-group">
<label class="col-xs-2 control-label">Date</label>
<div class="col-xs-2">
<p class="form-control-static"><b>${item.date.strftime('%d-%m-%Y')}</b></p>
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-2" for="mvt_cash">Montant cash</label>
<div class="col-xs-2">
<div class="input-group">
<div class="input-group-addon">€</div>
<input class="form-control" type="text" id="mvt_cash" name="mvt_cash" value="${item.mvt_cash}"
data-fv-numeric="true"
data-fv-numeric-message="Le montant doit être composé de chiffres ou de ., +, -" />
</div>
</div>
</div>
<div class="form-group">
<label class="col-xs-2 control-label">Nombre part</label>
<div class="col-xs-2">
<p class="form-control-static"><b>${item.nb_part}</b></p>
</div>
</div>
<form action="{{ url }}" method="post" class="form">
<br />
<div class="form-group">
<div class="col-xs-offset-2 col-xs-10">
<div class="form-group">
<a class="btn btn-default" href="${request.application_url}/histo_list">
<span class="glyphicon glyphicon-chevron-left"></span> Retour</a>
<button class="btn btn-primary" type="submit" name="form.submitted">
<span class="glyphicon glyphicon-ok"></span> Enregistrer</button>
<button class="btn btn-warning" type="submit" name="form.deleted"
tal:condition="item.no_id != 0">
<span class="glyphicon glyphicon-remove"></span> Supprimer</button>
</div>
</div>
</div>
</form>
<div class="form-group">
<p class="form-control-static">Date : <b>{{ item.date.strftime('%d-%m-%Y') }}</b></p>
</div>
<br />
<br />
</div> <!-- row -->
{% for error in form.mvt_cash.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
<div class="form-group">
<label class="required-field" for="tag">{{form.mvt_cash.label}}</label>
{{form.mvt_cash(class_='form-control')}}
</div>
<script>
$(document).ready(function() {
$('#histo_edit-form').formValidation();
$('form input').on('keypress', function(e) {
return e.which !== 13;
});
});
</script>
<br>
<div class="form-group">
<a class="btn btn-default" href="{{ request.route_url('histo_list') }}">
<span class="glyphicon glyphicon-chevron-left"></span> Retour</a>
<button class="btn btn-primary" type="submit" name="form.submitted">
<span class="glyphicon glyphicon-ok"></span> Enregistrer</button>
{% if form.no_id.data %}
<button class="btn btn-danger" type="button" data-toggle="modal" data-target="#confirmDelete">
<span class="glyphicon glyphicon-remove"></span> Supprimer</button>
{% endif %}
</div>
</div>
</div>
</form>
<!-- Modal : Confirmation SUPRESSION -->
<div id="confirmDelete" class="modal" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title">Supprimer cet historique</h4>
</div>
<div class="modal-body">
<!-- The form is placed inside the body of modal -->
<p>Etes-vous certain(e) de vouloir supprimer la ligne <b>{{ form.no_id.data }}</b> ?</p>
</div>
<div class="modal-footer">
<div class="form-group">
<div class="text-center">
<form id="confirmForm" method="post">
<button type="submit" class="btn btn-danger" name="form.deleted">Supprimer</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -15,19 +15,19 @@
<th align='right'>Nb Part</th>
<th align='right'>Valeur Part</th>
<th align='right'>Cours ref</th>
<th align='right'>Valeur Part ref</th>
<th align='right'>Nb Part ref</th>
<th align='center'>No Id</th>
</tr>
</thead>
{% for item in items %}
<tr>
<td>{{ item.date.strftime('%d/%m/%Y') }}</td>
<td align='right'>{{ item.mvt_cash }}</td>
<td align='right'>{{ item.valeur_pf }}</td>
<td align='right'>{{ item.nb_part }}</td>
<td align='right'>{{ item.val_part }}</td>
<td align='right'>{{ item.nb_part_ref }}</td>
<td align='right'>{{ item.val_part_ref }}</td>
<td align='right'>{{ '{0:0.2f} €'.format(item.mvt_cash) }}</td>
<td align='right'>{{ '{0:0.2f} €'.format(item.valeur_pf) }}</td>
<td align='right'>{{ '{0:0.2f}'.format(item.nb_part) }}</td>
<td align='right'>{{ '{0:0.2f} €'.format(item.val_part) }}</td>
<td align='right'>{{ '{0:0.2f} €'.format(item.cours_ref) }}</td>
<td align='right'>{{ '{0:0.2f}'.format(item.val_part_ref) }}</td>
<td align='center'>
<a href="{{ request.route_url('histo_edit', no_id=item.no_id) }}">{{ item.no_id }}</a>
</td>
@@ -37,6 +37,7 @@
<br />
<br />
{% endblock %}

View File

@@ -0,0 +1,249 @@
{% extends "cao_blogr:templates/layout.jinja2" %}
{% block content %}
<p>
<a href="allocation_edit/0" class="btn btn-success" role="button">
<span class="glyphicon glyphicon-plus"></span> Nouvelle classe</a>
</p>
<div class="row">
<div class="col-md-6">
<table class="table table-condensed table-bordered">
<thead>
<tr>
<th>Classe</th>
<th class="text-right">% cible</th>
<th class="text-right">% actuel</th>
<th class="text-right">Ecart</th>
<th class="text-right">Valeur</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td class="{{ item.bg_color }}">{{ item.classe }}</td>
<td class="text-right"><a href="allocation_edit/{{ item.no_cat }}">{{ item.pc_cible }} %</a></td>
<td class="text-right">{{ item.pc_atteint }}</td>
{% if (item.pc_atteint - item.pc_cible) >= 0 %}
<td class="text-right" style="color: green;">{{ item.pc_atteint }}</td>
{% else %}
<td class="text-right" style="color: red;">{{ item.pc_atteint }}</td>
{% endif %}
<td class="text-right">{{ '{0:0.2f} €'.format(item.valeur) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="col-md-6">
<table id="portfolio" class="table table-condensed table-bordered">
<thead>
<tr>
<th>Portefeuille</th>
<th class="text-right">Montant</th>
<th class="text-right">%</th>
</tr>
</thead>
<tbody>
<tr>
<td>Valorisation</td>
<td class="text-right">{{ '{0:0.2f} €'.format(total_valeur) }}</td>
<td></td>
</tr>
<tr>
<td>Plus value</td>
<td class="text-right">{{ '{0:0.2f} €'.format(total_pv) }}</td>
<td class="text-right">{{ '{0:0.1f} %'.format(total_pc_value) }}</td>
</tr>
<tr>
<td>Safe Withdrawal Rate</td>
<td class="text-right text-success"><b>{{ '{0:0.1f} %'.format(swr_amount) }}</b></td>
<td class="text-right text-success"><b>{{ '{0:0.1f} %'.format(swr_rate) }}</b></td>
</tr>
</tbody>
</table>
<p>
<b>Allocation globale</b> : 70% actions + 30% obligations<br />
[Inspirée du
<a href="https://www.nbim.no/en/the-fund/how-we-invest/benchmark-index/" target="_blank">Fond souverain Norvégien</a>]<br />
<b>Allocation actions</b> : 80% Monde (56%) + 20% Croissance (14%)<br />
</p>
</div>
</div>
<div class="row">
<div class="col-md-6">
<!-- graphique donut cible -->
<div id="donutchart_cible" style="width: 100%; height: 500px;"></div>
</div>
<div class="col-md-6">
<!-- graphique donut actuel -->
<div id="donutchart_actuel" style="width: 100%; height: 500px;"></div>
</div>
</div>
<div class="row">
<h3>Mes actifs</h3>
<p>"<i>Diversification is not determined by the number of securities held.</i>"
<a href="http://www.etf.com/sections/index-investor-corner" target="_blank">Larry Swedroe</a></p>
<form id="actif_list-form" action="{{ url }}" method="post">
<div class="form-group">
<button id="updateButton" class="btn btn-primary" type="submit" name="form.submitted">
<i class="glyphicon glyphicon-refresh"></i> MAJ du portefeuille</button>
<a href="#" class="btn btn-success" role="button"
data-toggle="modal" data-target="#choixTypeActif"><span class="glyphicon glyphicon-plus"></span> Nouvel actif</a>
<a href="/histo_list" class="btn btn-default" role="button">
<span class="glyphicon glyphicon-stats"></span> Historique</a>
<a href="/blog/2/mouvements-du-portefeuille" class="btn btn-default" role="button">
<span class="glyphicon glyphicon-folder-close"></span> Mouvements</a>
</div>
</form>
<table id="actifs_list" class="table table-condensed table-bordered">
<thead>
<tr>
<th>Classe</th>
<th>Libellé</th>
<th class="text-right">Cours</th>
<th class="text-right">Nb</th>
<th class="text-right">Valeur</th>
<th class="text-right">+/- Valeur</th>
<th class="text-right">% de +/-</th>
<th class="text-right">% TER</th>
<th class="text-right">% PF</th>
</tr>
</thead>
<tbody>
{% for ligne in actifs %}
<tr>
<td class="{{ ligne.bg_color }}">{{ ligne.classe }}</td>
{% if ligne.type=='ACTION' %}
<td><a href="actif_edit/{{ ligne.no_id }}">{{ ligne.libelle }}</a></td>
{% else %}
<td><a href="actif2_edit/{{ ligne.no_id }}">{{ ligne.libelle }}</a></td>
{% endif %}
{% if ligne.devise=='EUR' %}
<td>{{ '{0:0.2f} €'.format(ligne.cours) }}</td>
{% else %}
<td>{{ '{0:0.2f} $'.format(ligne.cours) }}</td>
{% endif %}
<td class="text-right">{{ ligne.nombre }}</td>
<td class="text-right">{{ '{0:0.2f} €'.format(ligne.valeur) }}</td>
{% if ligne.plus_value >= 0 %}
<td class="text-right" style="color: green;">{{ '{0:0.2f} €'.format(ligne.plus_value) }}</td>
<td class="text-right" style="color: green;">{{ '{0:0.1f}'.format(ligne.pc_plusvalue) }}</td>
{% else %}
<td class="text-right" style="color: red;">{{ '{0:0.2f} €'.format(ligne.plus_value) }}</td>
<td class="text-right" style="color: red;">{{ '{0:0.1f}'.format(ligne.pc_plusvalue) }}</td>
{% endif %}
<td class="text-right">{{ '{0:0.1f}'.format(ligne.ter) }}</td>
<td class="text-right">{{ '{0:0.1f}'.format(ligne.pc_allocation) }}</td>
</tr>
{% endfor %}
<tr>
<td class="text-right" colspan="4"><b>Total</b></td>
<td>{{ '{0:0.2f} €'.format(total_valeur) }}</td>
{% if total_pv >= 0 %}
<td class="text-right" style="color: green;">{{ '{0:0.1f}'.format(total_pv) }}</td>
<td class="text-right" style="color: green;">{{ '{0:0.2f} €'.format(total_pc_value) }}</td>
{% else %}
<td class="text-right" style="color: red;">{{ '{0:0.1f}'.format(total_pv) }}</td>
<td class="text-right" style="color: red;">{{ '{0:0.2f} €'.format(total_pc_value) }}</td>
{% endif %}
<td></td>
<td class="text-right"><b>100.0</b></td>
</tr>
</tbody>
</table>
</div>
<div class="row">
<div>
<!-- graphique evolution -->
<div id="curve_chart" style="width: 100%; height: 500px;"></div>
</div>
</div>
<!-- MODAL POPUP : Affichage de la FAQ -->
<div class="modal fade" id="choixTypeActif" tabindex="-1" role="dialog" aria-labelledby="choixTypeActif" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span class="glyphicon glyphicon-remove"></span></button>
<h4 class="modal-title" id="choixTypeActif">Choix du type d'actif à créer</h4>
</div>
<div class="modal-body">
<p>
Voulez-vous créer un actif de type <br />
<br />
<a href="/actif_edit/0" class="btn btn-primary" role="button">
ACTION</a> ou
<a href="/actif2_edit/0" class="btn btn-warning" role="button">
AUTRE</a>
<br />
<br />
<p>
</div>
</div>
</div>
</div>
<script type="text/javascript">
$('#updateButton').on('click', function(){
$('i.gly-spin').removeClass('gly-spin');
$('i').addClass('gly-spin');
});
</script>
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script type="text/javascript">
google.charts.load("current", {packages:["corechart"]});
google.charts.setOnLoadCallback(drawChart);
var dataSet_cible = {{ donut_cible | safe }};
var dataSet_actuel = {{ donut_actuel | safe }};
var dataSet_evoln = {{ courbe_evoln | safe }};
function drawChart() {
var data_cible = google.visualization.arrayToDataTable(dataSet_cible);
var data_actuel = google.visualization.arrayToDataTable(dataSet_actuel);
var data_evoln = google.visualization.arrayToDataTable(dataSet_evoln);
var options_cible = {
title: 'Allocation cible',
pieHole: 0.4,
slices: {
0: {color: 'SteelBlue'}, 1: {color: 'LightSteelBlue'},
2: {color: 'Maroon'}, 3: {color: 'Brown'},
5: {offset: 0.2, color: 'DarkGreen'}, 6: {offset: 0.3, color: 'Green'},
},
};
var options_actuel = {
title: 'Allocation actuelle',
pieHole: 0.4,
slices: {
0: {color: 'SteelBlue'}, 1: {color: 'LightSteelBlue'},
2: {color: 'Maroon'}, 3: {color: 'Brown'},
5: {offset: 0.2, color: 'DarkGreen'}, 6: {offset: 0.3, color: 'Green'},
},
};
var options_evoln = {
title: 'Evolution du portefeuille VS fond Carmignac Investissement',
curveType: 'function',
legend: { position: 'top' }
};
var chart_cible = new google.visualization.PieChart(document.getElementById('donutchart_cible'));
chart_cible.draw(data_cible, options_cible);
var chart_actuel = new google.visualization.PieChart(document.getElementById('donutchart_actuel'));
chart_actuel.draw(data_actuel, options_actuel);
var chart = new google.visualization.LineChart(document.getElementById('curve_chart'));
chart.draw(data_evoln, options_evoln);
}
</script>
</div><!-- content -->
</div>
{% endblock %}

View File

@@ -6,8 +6,7 @@ from ..forms import BlogCreateForm, BlogUpdateForm, BlogSearchForm, TagForm
import markdown
import datetime #<- will be used to set default dates on models
@view_config(route_name='blog',
renderer='cao_blogr:templates/blog.jinja2')
@view_config(route_name='blog', renderer='cao_blogr:templates/blog.jinja2')
def blog(request):
# get post id from request
blog_id = request.matchdict['id']
@@ -22,10 +21,17 @@ def blog(request):
# convertir de markdown en HTML
body_html = markdown.markdown(body, extensions=['footnotes'])
# si page mouvement de portfolio
if blog_id == '2':
return_url = "/portfolio"
else:
return_url = "/"
return {
'page_title': entry.title,
'entry': entry,
'body_html': body_html,
'return_url': return_url,
}

View File

@@ -1,12 +1,113 @@
from pyramid.view import (
view_config,
forbidden_view_config,
)
from pyramid.httpexceptions import HTTPFound
from pyramid.security import remember, forget
from ..services.portfolio import PFService
from ..forms import UserCreateForm
from ..models.portfolio import Histo
from ..forms import AllocationForm, HistoForm
from ..models.portfolio import Histo, Allocation
import datetime #<- will be used to set default dates on models
import yfinance as yf
import json
@view_config(route_name='portfolio', renderer='../templates/portfolio/portfolio.jinja2', permission='view')
def portfolio(request):
logged_in = request.authenticated_userid
url = request.route_url('portfolio')
# lire les categories
items = PFService.get_allocation(request, '0')
import pdb;pdb.set_trace()
# construire la liste pour donut
donut_cible=[]
donut_cible.append(('Allocation cible', 'Pourcent'))
donut_actuel=[]
donut_actuel.append(('Allocation actuelle', 'Pourcent'))
# calculer % total
total = 0
for item in items:
# construire la liste pour donut cible
d = (item.classe, item.pc_cible)
donut_cible.append(d)
# construire la liste pour donut actuel
d = (item.classe, int(item.pc_atteint * 10))
donut_actuel.append(d)
# totaliser les pourcentages
total += item.pc_cible
swr_rate = 3.5
if total != 100:
request.session.flash('Attention, le total de votre répartition cible dépasse 100% : ' + str(total), 'warning')
# lire les actifs
actifs = PFService.get_actifs(request, '0')
# MAJ du prtefeuille
if 'form.submitted' in request.params:
# lire le cours de EURUSD
ticker = yf.Ticker('EUR=X')
# maj des parités des devises
PFService.update_actif_devise(request, 'USD', ticker.info.get('regularMarketPrice'))
for item in actifs:
if item.type == 'ACTION':
# lire le cours de l'action
ticker = yf.Ticker(item.symbole)
# ticker delisted ?
if ticker.info == None:
price = 0
else:
price = ticker.info.get('regularMarketPrice')
# caluler son rendement
dividends = PFService.get_dividends(ticker)
PFService.update_actif_valeur(request, item.symbole, price, dividends)
# time.sleep(1) # attendre 2 secondes
# update du portefeuille
PFService.update_portefeuille(request, logged_in)
# relire les actifs
actifs = PFService.get_actifs(request, '0')
request.session.flash('Le portefeuille est mis à jour avec succès.', 'success')
total_valeur = 0
total_pv = 0
total_rdt = 0
for item in actifs:
total_valeur += item.valeur
total_pv += item.plus_value
if total_valeur == 0:
total_pc_value = 0
else:
total_pc_value = total_pv / total_valeur * 100
total_rdt += item.rendement
# lire l'historique
histos = PFService.get_histo(request,'0')
courbe_evoln=[]
courbe_evoln.append(('Date', 'Valeur part PF', 'Valeur part CARINVT:FP'))
for item in histos:
# construire la liste pour donut cible
d = (item.date.strftime('%d/%m/%Y'), int(item.val_part * 1000), int(item.val_part_ref * 1000))
courbe_evoln.append(d)
return {
'page_title': "Portefeuille",
'url': url,
'items': items,
'donut_cible': json.dumps(donut_cible),
'donut_actuel': json.dumps(donut_actuel),
'courbe_evoln': json.dumps(courbe_evoln),
'actifs': actifs,
'total_valeur': total_valeur,
'total_pv': total_pv,
'total_pc_value': total_pc_value,
'total_rdt': total_rdt,
'swr_rate' : swr_rate,
'swr_amount': float(total_valeur) * swr_rate / 100,
}
@view_config(route_name='histo_list', renderer='../templates/portfolio/histo_list.jinja2', permission='view')
@@ -15,8 +116,105 @@ def histo_list(request):
items = PFService.get_histo(request, '0')
return {
'page_title': 'Historique',
'page_title': 'Historique des parts',
'items': items,
}
@view_config(route_name='allocation_edit', renderer='../templates/portfolio/allocation_edit.jinja2', permission='view')
def allocation_edit(request):
no_cat = request.matchdict['no_cat']
url = request.route_url('allocation_edit', no_cat=no_cat)
# lire les classes
classes_list = PFService.get_classes(request, '0')
if no_cat == '0':
# create a new allocation
entry = Allocation()
form = AllocationForm(request.POST, entry)
form.classe.choices = [(row.classe, row.classe) for row in classes_list]
page_title = "Nouvelle allocation"
else:
# modify post
entry = PFService.get_allocation(request, no_cat)
if not entry:
request.session.flash(u"Allocation non trouvée : %s" % no_cat, 'warning')
return HTTPFound(location=request.route_url('portfolio'))
form = AllocationForm(request.POST, entry)
form.classe.choices = [(row.classe, row.classe) for row in classes_list]
page_title = "Modifier Allocation : " + str(entry.no_cat)
if 'form.submitted' in request.params and form.validate():
if no_cat == '0':
form.populate_obj(entry)
entry.pc_atteint = 0
entry.valeur = 0
request.dbsession.add(entry)
return HTTPFound(location=request.route_url('portfolio'))
else:
del form.no_cat # SECURITY: prevent overwriting of primary key
form.populate_obj(entry)
return HTTPFound(location=request.route_url('portfolio'))
if 'form.deleted' in request.params:
PFService.delete_allocation(request, entry.no_cat)
request.session.flash("La fiche a été supprimée avec succès.", 'success')
return HTTPFound(location=request.route_url('portfolio'))
return {
'page_title': page_title,
'url': url,
'form': form,
'item': entry,
}
@view_config(route_name='histo_edit', renderer='../templates/portfolio/histo_edit.jinja2', permission='view')
def histo_edit(request):
no_id = request.matchdict['no_id']
url = request.route_url('histo_edit', no_id=no_id)
if no_id == '0':
# create a new tag
entry = Histo()
entry.date = datetime.datetime.now()
form = HistoForm(request.POST, entry)
page_title = "Nouveau Histo"
else:
# modify post
entry = PFService.get_histo(request, no_id)
if not entry:
request.session.flash(u"Histo non trouvé : %s" % no_id, 'warning')
return HTTPFound(location=request.route_url('histo_list'))
form = HistoForm(request.POST, entry)
page_title = "Modifier histo no " + str(entry.no_id)
if 'form.submitted' in request.params and form.validate():
if no_id == '0':
form.populate_obj(entry)
request.dbsession.add(entry)
return HTTPFound(location=request.route_url('histo_list'))
else:
del form.no_id # SECURITY: prevent overwriting of primary key
form.populate_obj(entry)
# lire le cours de l'indice de réfence : Carmignac Investissement A EUR Acc
ticker = yf.Ticker('0P00000FB2.F')
entry.cours_ref = ticker.info.get('regularMarketPrice')
return HTTPFound(location=request.route_url('histo_list'))
if 'form.deleted' in request.params:
PFService.delete_histo(request, entry.no_id)
request.session.flash("La fiche a été supprimée avec succès.", 'success')
return HTTPFound(location=request.route_url('histo_list'))
return {
'page_title': page_title,
'url': url,
'form': form,
'item': entry,
}

View File

@@ -25,6 +25,7 @@ requires = [
'webhelpers2', # various web building related helpers 2.0
'passlib',
'markdown',
'yfinance',
]
tests_require = [