Refine gain algorithm and some UI update

This commit is contained in:
wjsjwr 2024-12-31 01:05:15 +08:00
parent d8aa433055
commit 41d7680920
8 changed files with 157 additions and 37 deletions

View File

@ -10,18 +10,21 @@
<cargoProject FILE="$PROJECT_DIR$/bookkeeper/Cargo.toml" />
</component>
<component name="ChangeListManager">
<list default="true" id="76b3b902-7a5c-4bcd-9c1b-8241d748fb44" name="更改" comment="Modify Tx and Delete Tx">
<change afterPath="$PROJECT_DIR$/bookkeeper/.cargo/config.toml" afterDir="false" />
<list default="true" id="76b3b902-7a5c-4bcd-9c1b-8241d748fb44" name="更改" comment="Release first version">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/bookkeeper/Cargo.lock" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/Cargo.lock" afterDir="false" />
<change beforePath="$PROJECT_DIR$/bookkeeper/migration/Cargo.toml" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/migration/Cargo.toml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/bookkeeper/migration/src/lib.rs" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/migration/src/lib.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/bookkeeper/src/main.rs" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/src/main.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/bookkeeper/templates/base.html.j2" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/templates/base.html.j2" afterDir="false" />
<change beforePath="$PROJECT_DIR$/bookkeeper/templates/index.html.j2" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/templates/index.html.j2" afterDir="false" />
<change beforePath="$PROJECT_DIR$/bookkeeper/templates/tx.html.j2" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/templates/tx.html.j2" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="ExecutionTargetManager" SELECTED_TARGET="RsBuildProfile:release" />
<component name="ExecutionTargetManager" SELECTED_TARGET="RsBuildProfile:dev" />
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
@ -81,7 +84,7 @@
</component>
<component name="RunManager" selected="Cargo.Run bookkeeper">
<configuration name="Run bookkeeper" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="buildProfileId" value="release" />
<option name="buildProfileId" value="dev" />
<option name="command" value="run --package bookkeeper --bin bookkeeper" />
<option name="workingDirectory" value="file://$PROJECT_DIR$/bookkeeper" />
<envs />
@ -150,7 +153,7 @@
<workItem from="1735392290940" duration="620000" />
<workItem from="1735392928494" duration="12910000" />
<workItem from="1735470376833" duration="16014000" />
<workItem from="1735553493290" duration="6862000" />
<workItem from="1735553493290" duration="15888000" />
</task>
<task id="LOCAL-00001" summary="Add bookkeeper">
<option name="closed" value="true" />
@ -216,7 +219,15 @@
<option name="project" value="LOCAL" />
<updated>1735562546317</updated>
</task>
<option name="localTasksCounter" value="9" />
<task id="LOCAL-00009" summary="Release first version">
<option name="closed" value="true" />
<created>1735565490274</created>
<option name="number" value="00009" />
<option name="presentableId" value="LOCAL-00009" />
<option name="project" value="LOCAL" />
<updated>1735565490274</updated>
</task>
<option name="localTasksCounter" value="10" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@ -242,7 +253,8 @@
<MESSAGE value="Implement add transaction" />
<MESSAGE value="Update some settings" />
<MESSAGE value="Modify Tx and Delete Tx" />
<option name="LAST_COMMIT_MESSAGE" value="Modify Tx and Delete Tx" />
<MESSAGE value="Release first version" />
<option name="LAST_COMMIT_MESSAGE" value="Release first version" />
</component>
<component name="XSLT-Support.FileAssociations.UIState">
<expand />

2
bookkeeper/Cargo.lock generated
View File

@ -1836,6 +1836,8 @@ name = "migration"
version = "0.1.0"
dependencies = [
"async-std",
"rust_decimal",
"sea-orm",
"sea-orm-migration",
]

View File

@ -10,6 +10,7 @@ path = "src/lib.rs"
[dependencies]
async-std = { version = "1", features = ["attributes", "tokio1"] }
rust_decimal = "1.36.0"
[dependencies.sea-orm-migration]
version = "1.1.0"
@ -22,3 +23,15 @@ features = [
"sqlx-sqlite",
"runtime-tokio-rustls",
]
[dependencies.sea-orm]
version = "1.1.0"
features = [
"sqlx-sqlite",
"runtime-tokio-rustls",
"macros",
"with-rust_decimal",
"with-chrono",
"with-uuid",
"with-json"
]

View File

@ -1,12 +1,16 @@
pub use sea_orm_migration::prelude::*;
mod m20220101_000001_create_table;
mod m20241230_160326_gain_evolution;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![Box::new(m20220101_000001_create_table::Migration)]
vec![
Box::new(m20220101_000001_create_table::Migration),
Box::new(m20241230_160326_gain_evolution::Migration),
]
}
}

View File

@ -0,0 +1,63 @@
use std::str::FromStr;
use sea_orm_migration::prelude::*;
use rust_decimal::Decimal;
use sea_orm::entity::prelude::*;
use sea_orm::ActiveValue::Set;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq,)]
#[sea_orm(table_name = "transaction")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub code: i32,
pub name: String,
pub date: Date,
pub buy: Decimal,
pub volume: i32,
pub net_buy: Decimal,
#[sea_orm(column_type = "JsonBinary", nullable)]
pub split_pieces: Option<Json>,
pub gain: Option<Decimal>,
pub net_gain: Option<Decimal>,
pub is_done: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
let records: Vec<Model> = Entity::find()
.filter(Column::Gain.is_not_null())
.all(db)
.await?;
for record in records {
let mut r: ActiveModel = record.into();
let pcs = r.split_pieces.clone().unwrap().unwrap();
let pieces = pcs.as_array().unwrap();
let mut total_sell = Decimal::from_str("0.0").unwrap();
let mut total_net_sell = Decimal::from_str("0.0").unwrap();
for piece in pieces {
let p = piece.as_object().unwrap();
total_sell += Decimal::from_str(p.get("sell").unwrap().as_str().unwrap()).unwrap() *
Decimal::from(p.get("volume").unwrap().as_i64().unwrap());
total_net_sell += Decimal::from_str(p.get("net_sell").unwrap().as_str().unwrap()).unwrap();
}
r.gain = Set(Some(total_sell - r.buy.clone().unwrap() * Decimal::from(r.volume.clone().unwrap())));
r.net_gain = Set(Some(total_net_sell - r.net_buy.clone().unwrap()));
r.update(db).await?;
}
Ok(())
}
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
// Replace the sample below with your own migration scripts
Ok(())
}
}

View File

@ -11,7 +11,7 @@ use rocket::fairing::{self, AdHoc};
use rocket::form::Form;
use rocket::response::{Flash, Redirect};
use rocket::{Build, Rocket};
use rocket_dyn_templates::{context, Template};
use rocket_dyn_templates::{context, Engines, Template};
use rust_decimal::Decimal;
use sea_orm::query::*;
use sea_orm::ActiveValue::Set;
@ -57,10 +57,15 @@ impl sea_orm_rocket::Pool for SeaOrmPool {
}
}
#[get("/")]
async fn index(conn: Connection<'_, Db>) -> Template {
let rows: Vec<transaction::Model> = TE::find()
.filter(transaction::Column::Date.gte(Local::now() - TimeDelta::days(30)))
#[get("/?<last>")]
async fn index(conn: Connection<'_, Db>, last: Option<u32>) -> Template {
let mut partial = TE::find();
if let Some(last) = last {
partial = partial.filter(transaction::Column::Date.gte(Local::now() - TimeDelta::days(last as i64)));
}
let rows: Vec<transaction::Model> = partial
.order_by_desc(transaction::Column::Id)
.all(conn.into_inner())
.await
@ -189,15 +194,15 @@ fn translate(trans: Form<TransModel>) -> transaction::ActiveModel {
acc + e.sell * Decimal::from(e.volume)
});
let buy = Decimal::from_str(trans.buy).unwrap() * Decimal::from(total);
record.gain = Set(Option::from((sell_int - buy) / buy * Decimal::from(100)));
record.gain = Set(Some(sell_int - buy));
let net_sell_int = sp.iter().fold(Decimal::from(0), |acc, e| { acc + e.net_sell });
let net_buy = Decimal::from_str(trans.net_buy).unwrap();
record.net_gain = Set(Option::from((net_sell_int - net_buy) / net_buy * Decimal::from(100)));
record.net_gain = Set(Some(net_sell_int - net_buy));
}
let v: serde_json::value::Value = serde_json::to_value(sp).unwrap();
record.split_pieces = Set(Option::from(v));
record.split_pieces = Set(Some(v));
}
record
@ -214,7 +219,15 @@ async fn rocket() -> _ {
rocket::build()
.attach(Db::init())
.attach(AdHoc::try_on_ignite("Migrations", run_migrations))
.attach(Template::fairing())
.attach(Template::custom(|engines: &mut Engines| {
engines.minijinja.add_filter("padLeft", |value: String, length: usize| -> String {
if value.len() < length {
"0".repeat(length - value.len()) + value.as_str()
} else {
value
}
})
}))
.mount("/", routes![index, add, get_add, get_tx, post_tx, delete_tx])
.mount("/static", FileServer::from("static"))
}

View File

@ -4,7 +4,7 @@
<div class="">
<table class="table is-fullwidth is-bordered is-hoverable">
<thead>
<tr>
<tr style="position: sticky; top: 0; background-color: white;">
<th style="width: fit-content">代码</th>
<th style="width: fit-content">名称</th>
<th>买入日期</th>
@ -26,18 +26,30 @@
{% for p in r.split_pieces %}
<tr>
{% if loop.index == 1 %}
<td rowspan="{{ loop.length }}" style="vertical-align: middle;">{{ r.code }}</td>
<td rowspan="{{ loop.length }}" style="vertical-align: middle;">{{ r.code|string|padLeft(6) }}</td>
<td rowspan="{{ loop.length }}" style="vertical-align: middle;">{{ r.name }}</td>
<td rowspan="{{ loop.length }}" style="vertical-align: middle;" class="{% if not r.is_done %}is-warning{% endif %}">{{ r.date }}</td>
<td rowspan="{{ loop.length }}" style="vertical-align: middle;">{{ r.buy }}</td>
<td rowspan="{{ loop.length }}" style="vertical-align: middle;">{{ r.buy|float|round(4) }}</td>
<td rowspan="{{ loop.length }}" style="vertical-align: middle;">{{ r.volume }}</td>
<td rowspan="{{ loop.length }}" style="vertical-align: middle;">{{ r.net_buy }}</td>
<td rowspan="{{ loop.length }}" style="vertical-align: middle;">{{ r.net_buy|float|round(2) }}</td>
<td>{{ p.date or "" }}</td>
<td>{{ p.sell or "" }}</td>
<td>{{ (p.sell or 0)|float|round(4) }}</td>
<td>{{ p.volume or "" }}</td>
<td>{{ p.net_sell or "" }}</td>
<td rowspan="{{ loop.length }}" style="vertical-align: middle;" class="{% if r.gain|float > 0 %}is-success{% else %}is-danger{% endif %}">{{ (r.gain or 0)|float|round(4) }}%</td>
<td rowspan="{{ loop.length }}" style="vertical-align: middle;" class="{% if r.net_gain|float > 0 %}is-success{% else %}is-danger{% endif %}">{{ (r.net_gain or 0)|float|round(4) }}%</td>
<td>{{ (p.net_sell or 0)|float|round(2) }}</td>
<td rowspan="{{ loop.length }}" style="vertical-align: middle;" class="{% if r.gain|float > 0 %}is-success{% else %}is-danger{% endif %}">
{% if r.gain %}
{{ r.gain|float|round(2) }} ({{ ((r.gain|float)/(r.buy|float)/(r.volume|float)*100)|round(2) }}%)
{% else %}
-
{% endif %}
</td>
<td rowspan="{{ loop.length }}" style="vertical-align: middle;" class="{% if r.net_gain|float > 0 %}is-success{% else %}is-danger{% endif %}">
{% if r.net_gain %}
{{ r.net_gain|float|round(2) }} ({{ ((r.net_gain|float)/(r.net_buy|float)*100)|round(2) }}%)
{% else %}
-
{% endif %}
</td>
<td rowspan="{{ loop.length }}" style="vertical-align: middle;">
<a row-id="{{ r.id }}" class="modify-row">改</a>&nbsp;<a row-id="{{ r.id }}" class="delete-row has-text-danger">删</a>
</td>
@ -51,12 +63,12 @@
{% endfor %}
{% else %}
<tr>
<td>{{ r.code }}</td>
<td>{{ r.code|string|padLeft(6) }}</td>
<td>{{ r.name }}</td>
<td class="is-warning">{{ r.date }}</td>
<td>{{ r.buy }}</td>
<td>{{ r.buy|float|round(4) }}</td>
<td>{{ r.volume }}</td>
<td>{{ r.net_buy }}</td>
<td>{{ r.net_buy|float|round(2) }}</td>
<td></td>
<td></td>
<td></td>
@ -68,8 +80,9 @@
</tr>
{% endif %}
{% endfor %}
<tr class="is-light">
<td colspan="14" style="text-align: center"><a id="addEntry">添加</a></td>
<tr class="is-light" style="position: sticky; bottom: 0;">
<td colspan="14" style="text-align: center"><a id="addEntry">添加</a><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><a
id="filterEntry">高级筛选</a></td>
</tr>
</tbody>
</table>

View File

@ -24,9 +24,9 @@
<tbody>
<tr>
<td><input id="date" name="date" class="input" type="date" placeholder="YYYY-MM-DD" value="{{ r.date or '' }}"></td>
<td><input id="buy" name="buy" class="input" type="number" placeholder="Buy" value="{{ r.buy or 0 }}"></td>
<td><input id="buy" name="buy" class="input" type="text" placeholder="Buy" value="{{ r.buy or 0 }}"></td>
<td><input id="volume" name="volume" class="input" type="number" placeholder="Volume" value="{{ r.volume or 0 }}"></td>
<td><input id="net_buy" name="net_buy" class="input" type="number" placeholder="Net Buy" value="{{ r.net_buy or 0 }}"></td>
<td><input id="net_buy" name="net_buy" class="input" type="text" placeholder="Net Buy" value="{{ r.net_buy or 0 }}"></td>
</tr>
</tbody>
</table>
@ -45,18 +45,18 @@
{% for split in r.split_pieces %}
<tr>
<td><input name="split_pieces[{{ loop.index0 }}].date" class="input" type="date" placeholder="YYYY-MM-DD" value="{{ split.date or '' }}"></td>
<td><input name="split_pieces[{{ loop.index0 }}].sell" class="input" type="number" placeholder="Sell" value="{{ split.sell or '' }}"></td>
<td><input name="split_pieces[{{ loop.index0 }}].sell" class="input" type="text" placeholder="Sell" value="{{ split.sell or '' }}"></td>
<td><input name="split_pieces[{{ loop.index0 }}].volume" class="input" type="number" placeholder="Volume" value="{{ split.volume or '' }}"></td>
<td><input name="split_pieces[{{ loop.index0 }}].net_sell" class="input" type="number" placeholder="Net Sell" value="{{ split.net_sell or '' }}"></td>
<td><input name="split_pieces[{{ loop.index0 }}].net_sell" class="input" type="text" placeholder="Net Sell" value="{{ split.net_sell or '' }}"></td>
<td><a onclick="deleteRow(this)">删除</a></td>
</tr>
{% endfor %}
{% else %}
<tr>
<td><input name="split_pieces[0].date" class="input" type="date" placeholder="YYYY-MM-DD"></td>
<td><input name="split_pieces[0].sell" class="input" type="number" placeholder="Sell"></td>
<td><input name="split_pieces[0].sell" class="input" type="text" placeholder="Sell"></td>
<td><input name="split_pieces[0].volume" class="input" type="number" placeholder="Volume"></td>
<td><input name="split_pieces[0].net_sell" class="input" type="number" placeholder="Net Sell"></td>
<td><input name="split_pieces[0].net_sell" class="input" type="text" placeholder="Net Sell"></td>
<td style="vertical-align: middle;"><a onclick="deleteRow(this)">删除</a></td>
</tr>
{% endif %}