增加快速记录功能

This commit is contained in:
wjsjwr 2025-03-10 22:54:57 +08:00
parent 2b3b516f18
commit 083dda7118
6 changed files with 191 additions and 11 deletions

View File

@ -10,18 +10,18 @@
<cargoProject FILE="$PROJECT_DIR$/bookkeeper/Cargo.toml" />
</component>
<component name="ChangeListManager">
<list default="true" id="76b3b902-7a5c-4bcd-9c1b-8241d748fb44" name="更改" comment="添加登录页面">
<list default="true" id="76b3b902-7a5c-4bcd-9c1b-8241d748fb44" name="更改" comment="bug fix: zero division error">
<change afterPath="$PROJECT_DIR$/bookkeeper/templates/ftx.html.j2" afterDir="false" />
<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/Cargo.toml" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/Cargo.toml" 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/index.html.j2" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/templates/index.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>
@ -82,7 +82,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 />
@ -156,7 +156,9 @@
<workItem from="1736239592084" duration="2278000" />
<workItem from="1737809478583" duration="76000" />
<workItem from="1739197188908" duration="1873000" />
<workItem from="1740209396270" duration="6029000" />
<workItem from="1740209396270" duration="6333000" />
<workItem from="1741527809947" duration="5732000" />
<workItem from="1741617416981" duration="867000" />
</task>
<task id="LOCAL-00001" summary="Add bookkeeper">
<option name="closed" value="true" />
@ -278,7 +280,15 @@
<option name="project" value="LOCAL" />
<updated>1740214793630</updated>
</task>
<option name="localTasksCounter" value="16" />
<task id="LOCAL-00016" summary="bug fix: zero division error">
<option name="closed" value="true" />
<created>1740216327515</created>
<option name="number" value="00016" />
<option name="presentableId" value="LOCAL-00016" />
<option name="project" value="LOCAL" />
<updated>1740216327515</updated>
</task>
<option name="localTasksCounter" value="17" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@ -311,7 +321,8 @@
<MESSAGE value="清仓筛选" />
<MESSAGE value="添加总计" />
<MESSAGE value="添加登录页面" />
<option name="LAST_COMMIT_MESSAGE" value="添加登录页面" />
<MESSAGE value="bug fix: zero division error" />
<option name="LAST_COMMIT_MESSAGE" value="bug fix: zero division error" />
</component>
<component name="XSLT-Support.FileAssociations.UIState">
<expand />

2
bookkeeper/Cargo.lock generated
View File

@ -481,7 +481,7 @@ dependencies = [
[[package]]
name = "bookkeeper"
version = "0.2.1"
version = "0.3.0"
dependencies = [
"bcrypt",
"chrono",

View File

@ -1,6 +1,6 @@
[package]
name = "bookkeeper"
version = "0.2.1"
version = "0.3.0"
edition = "2021"
[dependencies]

View File

@ -350,6 +350,118 @@ async fn delete_tx(
}
}
#[derive(FromForm, Debug)]
struct FastTransModel<'r> {
code: usize,
name: &'r str,
sell: &'r str,
#[field(validate = range(100..))]
volume: i32,
net_sell: &'r str,
other_fee: &'r str,
date: &'r str,
}
/// Fast Transaction 模式:
/// 必要条件:
/// 1. 买入单存在
/// 算法:
/// 1. 买入时间早于FTX时间且没有分割订单按买入价格从低到高排列未完成订单。
/// 2. 如果没有满足条件的买入单,则报错退出。
/// 3. 如果当前买入单的成交量刚好等于FTX的剩余成交量完成循环并进入6。
/// 4. 如果当前买入单的成交量大于FTX的剩余成交量则报错退出。
/// 5. FTX剩余成交量减少当前买入单的成交量切换下一个买入单并进入3。
/// 6. 按照满足条件的买入单的成交量进行分配,并更新结果。
#[post("/ftx", data = "<data>")]
async fn post_ftx(
data: Form<FastTransModel<'_>>,
conn: Connection<'_, Db>,
cookies: &CookieJar<'_>,
) -> Result<Redirect, Status> {
if !verify(cookies) {
return Err(Status::Forbidden);
}
let db = conn.into_inner();
let date = NaiveDate::parse_from_str(data.date, "%Y-%m-%d").unwrap();
let rows: Vec<transaction::Model> = TE::find()
.filter(transaction::Column::Code.eq(data.code as i32))
.filter(transaction::Column::Name.eq(data.name))
.filter(transaction::Column::IsDone.eq(false))
.filter(transaction::Column::Date.lte(date))
.filter(transaction::Column::SplitPieces.is_null())
.order_by_asc(transaction::Column::Buy)
.order_by_asc(transaction::Column::Date)
.all(db)
.await
.unwrap();
let mut vol = data.volume;
let mut selected = 0;
for r in &rows {
if vol < r.volume {
println!("volume {} is less than {}", vol, r.volume);
return Err(Status::BadRequest);
}
vol -= r.volume;
selected += 1;
if vol == 0 {
break;
}
}
if vol > 0 {
println!("remaining volume {}", vol);
return Err(Status::BadRequest);
}
if selected <= 0 {
println!("No transaction selected");
return Err(Status::BadRequest);
}
let after_fee =
Decimal::from_str(data.net_sell).unwrap() - Decimal::from_str(data.other_fee).unwrap();
for i in 0..selected {
let mut sp: Vec<SplitPiece> = Vec::new();
sp.push(SplitPiece {
date,
sell: Decimal::from_str(data.sell).unwrap(),
net_sell: after_fee * Decimal::from(rows[i].volume) / Decimal::from(data.volume),
volume: rows[i].volume,
});
let v: serde_json::value::Value = serde_json::to_value(sp).unwrap();
let mut rec: transaction::ActiveModel = rows[i].clone().into();
rec.split_pieces = Set(Some(v));
rec.is_done = Set(true);
rec.gain = Set(Some(
(Decimal::from_str(data.sell).unwrap() - rows[i].buy) * Decimal::from(rows[i].volume),
));
rec.net_gain = Set(Some(
after_fee * Decimal::from(rows[i].volume) / Decimal::from(data.volume)
- rows[i].net_buy,
));
match rec.update(db).await {
Err(_) => return Err(Status::InternalServerError),
Ok(_) => continue,
}
}
Ok(Redirect::to("/"))
}
#[get("/ftx")]
async fn get_ftx(cookies: &CookieJar<'_>) -> Result<Template, Status> {
if !verify(cookies) {
return Err(Status::Forbidden);
}
Ok(Template::render("ftx", ()))
}
#[get("/add")]
async fn get_add(cookies: &CookieJar<'_>) -> Result<Template, Redirect> {
if !verify(cookies) {
@ -451,7 +563,10 @@ async fn rocket() -> _ {
}))
.mount(
"/",
routes![index, add, get_add, get_tx, post_tx, delete_tx, login_page, login_post],
routes![
index, add, get_add, get_tx, post_tx, delete_tx, login_page, login_post, post_ftx,
get_ftx
],
)
.mount("/static", FileServer::from("static"))
}

View File

@ -0,0 +1,42 @@
<div class="box">
<form action="/ftx" method="post">
<div class="field">
<label class="label" for="code">代码</label>
<div class="control">
<input id="code" name="code" class="input" type="number" placeholder="Code" value="">
</div>
</div>
<div class="field">
<label class="label" for="name">名称</label>
<div class="control">
<input id="name" name="name" class="input" type="text" placeholder="Name" value="">
</div>
</div>
<table class="table is-fullwidth is-bordered">
<thead>
<tr>
<th><label class="label" for="date">卖出日期</label></th>
<th><label class="label" for="sell">卖出价</label></th>
<th><label class="label" for="volume">成交量(卖)</label></th>
<th><label class="label" for="net_sell">实际卖出</label></th>
<th><label class="label" for="other_fee">额外手续费</label></th>
</tr>
</thead>
<tbody>
<tr>
<td><input id="date" name="date" class="input" type="date" placeholder="YYYY-MM-DD" value="0"></td>
<td><input id="sell" name="sell" class="input" type="text" placeholder="卖出价" value="0"></td>
<td><input id="volume" name="volume" class="input" type="number" placeholder="Volume" value="0"></td>
<td><input id="net_sell" name="net_sell" class="input" type="text" placeholder="实际卖出" value="0"></td>
<td><input id="other_fee" name="other_fee" class="input" type="text" placeholder="额外手续费" value="0"></td>
</tr>
</tbody>
</table>
<div class="field is-grouped">
<div class="control">
<button class="button is-link">Submit</button>
</div>
</div>
</form>
</div>

View File

@ -86,6 +86,8 @@
<span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
<a id="filterEntry">高级筛选</a>
<span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
<a id="ftx">快速记录</a>
<span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
<a href="/?is_done=false">未清仓</a>
<span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
<a href="/?is_done=true">已清仓</a>
@ -130,6 +132,16 @@
openModal($target);
});
document.getElementById('ftx').addEventListener('click', async () => {
const $target = document.getElementById("modal");
const response = await fetch("/ftx");
if (!response.ok) {
return;
}
document.getElementById('modal-content').innerHTML = await response.text();
openModal($target);
});
(document.querySelectorAll('.modify-row') || []).forEach(($cl) => {
$cl.addEventListener('click', async () => {
const rowId = $cl.attributes.getNamedItem("row-id").value;