diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index ca1bf27..73cf921 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -10,18 +10,18 @@
-
+
+
-
-
+
-
+
-
+
@@ -156,7 +156,9 @@
-
+
+
+
@@ -278,7 +280,15 @@
1740214793630
-
+
+
+ 1740216327515
+
+
+
+ 1740216327515
+
+
@@ -311,7 +321,8 @@
-
+
+
diff --git a/bookkeeper/Cargo.lock b/bookkeeper/Cargo.lock
index f5d29db..972b434 100644
--- a/bookkeeper/Cargo.lock
+++ b/bookkeeper/Cargo.lock
@@ -481,7 +481,7 @@ dependencies = [
[[package]]
name = "bookkeeper"
-version = "0.2.1"
+version = "0.3.0"
dependencies = [
"bcrypt",
"chrono",
diff --git a/bookkeeper/Cargo.toml b/bookkeeper/Cargo.toml
index a7d9eab..49dea79 100644
--- a/bookkeeper/Cargo.toml
+++ b/bookkeeper/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "bookkeeper"
-version = "0.2.1"
+version = "0.3.0"
edition = "2021"
[dependencies]
diff --git a/bookkeeper/src/main.rs b/bookkeeper/src/main.rs
index 7c88657..f5a02b7 100644
--- a/bookkeeper/src/main.rs
+++ b/bookkeeper/src/main.rs
@@ -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 = "")]
+async fn post_ftx(
+ data: Form>,
+ conn: Connection<'_, Db>,
+ cookies: &CookieJar<'_>,
+) -> Result {
+ 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 = 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 = 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 {
+ if !verify(cookies) {
+ return Err(Status::Forbidden);
+ }
+ Ok(Template::render("ftx", ()))
+}
+
#[get("/add")]
async fn get_add(cookies: &CookieJar<'_>) -> Result {
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"))
}
diff --git a/bookkeeper/templates/ftx.html.j2 b/bookkeeper/templates/ftx.html.j2
new file mode 100644
index 0000000..809633a
--- /dev/null
+++ b/bookkeeper/templates/ftx.html.j2
@@ -0,0 +1,42 @@
+
+
diff --git a/bookkeeper/templates/index.html.j2 b/bookkeeper/templates/index.html.j2
index 82a8f3f..7177a40 100644
--- a/bookkeeper/templates/index.html.j2
+++ b/bookkeeper/templates/index.html.j2
@@ -86,6 +86,8 @@
高级筛选
+ 快速记录
+
未清仓
已清仓
@@ -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;