From edcc04e88c180718cb61b9ca4cc5080acf5f87bb Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 17 Jul 2026 09:45:44 +0800 Subject: [PATCH] Improve options review tabs and multi-timeframe uploads. Add source-type tabs, open an inline journal form from list rows, and match contract-style 5m/15m/1h/4h screenshot slots. Co-authored-by: Cursor --- docs/期权用法.md | 6 +- lib/common/static/options_review.js | 508 ++++++++++-------- lib/options/options_review_images_lib.py | 6 +- .../templates/options_review_panel.html | 120 ++++- tests/test_options_review_lib.py | 6 +- 5 files changed, 395 insertions(+), 251 deletions(-) diff --git a/docs/期权用法.md b/docs/期权用法.md index 9ed3158..c870737 100644 --- a/docs/期权用法.md +++ b/docs/期权用法.md @@ -118,9 +118,13 @@ OKX_OPTIONS_API_PASSPHRASE=... ### 图片 - 目录:`static/images/options_journal/` -- 文件名:`options_journal_{draftId}_{chart|entry|exit|other}.ext` +- 文件名:`options_journal_{draftId}_{5m|15m|1h|4h}.ext`(与合约复盘同周期槽位) - 备份时与 `crypto.db` 一并打包即可;勿与合约 `journal_*` 截图混用. +### 页面 + +顶部三个 Tab:**期权交易记录** / **期期对冲记录** / **永期对冲记录**.点击列表行后在下方打开「复盘记录上传」,支持四周期即时截图与情绪标签. + ### 统计 同页 KPI + 分组:类型、标的、策略标签、对冲结束原因、持有周期、Call/Put.策略维度仅统计已填策略标签的记录. diff --git a/lib/common/static/options_review.js b/lib/common/static/options_review.js index 8c55e53..346a850 100644 --- a/lib/common/static/options_review.js +++ b/lib/common/static/options_review.js @@ -1,13 +1,19 @@ /** - * OKX 期权复盘(含对冲):列表 / 同步 / 统计 / 编辑上传. + * OKX 期权复盘:三类 Tab + 列表点选 + 下方多周期(5m/15m/1h/4h)复盘表单. */ (function (global) { "use strict"; - var SLOT_TFS = ["chart", "entry", "exit", "other"]; - var SLOT_LABELS = { chart: "走势图", entry: "入场", exit: "离场", other: "其他" }; + var SLOT_TFS = ["5m", "15m", "1h", "4h"]; + var TAB_LABELS = { + option_spot: "期权交易记录", + options_options: "期期对冲记录", + perp_options: "永期对冲记录", + }; + var activeSource = "option_spot"; var currentTradeId = null; var draftId = ""; + var tradesCache = {}; function $(id) { return document.getElementById(id); @@ -17,8 +23,7 @@ if (v == null || v === "") return "—"; var n = Number(v); if (Number.isNaN(n)) return "—"; - var s = (n >= 0 ? "+" : "") + n.toFixed(2); - return s; + return (n >= 0 ? "+" : "") + n.toFixed(2); } function fmtHold(sec) { @@ -29,24 +34,11 @@ return (s / 86400).toFixed(1) + "d"; } - function qs() { - var p = new URLSearchParams(); - var source = ($("or-filter-source") || {}).value || ""; - var uly = ($("or-filter-uly") || {}).value || ""; - var opt = ($("or-filter-opt") || {}).value || ""; - var reviewed = ($("or-filter-reviewed") || {}).value || ""; - var strategy = (($("or-filter-strategy") || {}).value || "").trim(); - var from = ($("or-filter-from") || {}).value || ""; - var to = ($("or-filter-to") || {}).value || ""; - if (source) p.set("source_type", source); - if (uly) p.set("underlying", uly); - if (opt) p.set("opt_type", opt); - if (reviewed) p.set("reviewed", reviewed); - if (strategy) p.set("strategy_tag", strategy); - if (from) p.set("closed_from", from.replace("T", " ") + ":00"); - if (to) p.set("closed_to", to.replace("T", " ") + ":00"); - if (($("or-include-hedge-legs") || {}).checked) p.set("include_hedge_legs", "1"); - return p.toString(); + function toLocalInput(ts) { + if (!ts) return ""; + var s = String(ts).trim().replace(" ", "T"); + if (s.length >= 16) return s.slice(0, 16); + return s; } function newDraftId() { @@ -58,6 +50,25 @@ return s; } + function qs() { + var p = new URLSearchParams(); + p.set("source_type", activeSource); + var uly = ($("or-filter-uly") || {}).value || ""; + var opt = ($("or-filter-opt") || {}).value || ""; + var reviewed = ($("or-filter-reviewed") || {}).value || ""; + var strategy = (($("or-filter-strategy") || {}).value || "").trim(); + var from = ($("or-filter-from") || {}).value || ""; + var to = ($("or-filter-to") || {}).value || ""; + if (uly) p.set("underlying", uly); + if (opt) p.set("opt_type", opt); + if (reviewed) p.set("reviewed", reviewed); + if (strategy) p.set("strategy_tag", strategy); + if (from) p.set("closed_from", from.replace("T", " ") + ":00"); + if (to) p.set("closed_to", to.replace("T", " ") + ":00"); + if (($("or-include-hedge-legs") || {}).checked) p.set("include_hedge_legs", "1"); + return p.toString(); + } + function setSyncStatus(text) { var el = $("or-sync-status"); if (el) el.textContent = text || ""; @@ -93,6 +104,17 @@ }); } + function setActiveTab(source) { + activeSource = source || "option_spot"; + document.querySelectorAll(".or-tab").forEach(function (btn) { + btn.classList.toggle("active", btn.getAttribute("data-source") === activeSource); + }); + var title = $("or-list-title"); + if (title) title.textContent = TAB_LABELS[activeSource] || "记录"; + hideJournalForm(); + reloadAll(); + } + function reloadAll() { loadTrades(); loadStats(); @@ -101,23 +123,25 @@ function loadTrades() { var tbody = $("or-trades-tbody"); if (!tbody) return; - tbody.innerHTML = '加载中…'; + tbody.innerHTML = '加载中…'; fetch("/api/options/review/trades?" + qs(), { credentials: "same-origin" }) .then(function (r) { return r.json(); }) .then(function (data) { if (!data.ok) { - tbody.innerHTML = '加载失败'; + tbody.innerHTML = '加载失败'; return; } var rows = data.trades || []; + tradesCache = {}; if (!rows.length) { - tbody.innerHTML = '暂无记录,请先同步'; + tbody.innerHTML = '暂无记录,请先同步'; return; } tbody.innerHTML = rows .map(function (t) { + tradesCache[t.id] = t; var title = t.source_type === "option_spot" ? t.inst_id || "—" @@ -130,22 +154,25 @@ : Number(t.realized_pnl_total) < 0 ? "color:#f07178" : ""; + var active = currentTradeId === t.id ? " or-row-active" : ""; return ( - "" + + '">' + "" + (t.source_label || t.source_type) + "" + "" + title + "" + - "" + + '">' + fmtPnl(t.realized_pnl_total) + "" + - "" + + '' + (t.opened_at || "—") + "
" + (t.closed_at || "—") + @@ -159,21 +186,18 @@ "" + (t.reviewed ? "已复盘" : "待复盘") + "" + - "" + "" ); }) .join(""); - tbody.querySelectorAll(".or-edit-btn").forEach(function (btn) { - btn.addEventListener("click", function () { - openEdit(Number(btn.getAttribute("data-id"))); + tbody.querySelectorAll(".or-trade-row").forEach(function (tr) { + tr.addEventListener("click", function () { + openJournalForm(Number(tr.getAttribute("data-id"))); }); }); }) .catch(function () { - tbody.innerHTML = '加载失败'; + tbody.innerHTML = '加载失败'; }); } @@ -189,7 +213,7 @@ .slice(0, 8) .map(function (g) { return ( - "
" + + '
' + "" + g.key + " · " + @@ -204,7 +228,13 @@ ); }) .join(""); - return '
' + title + "
" + lines + "
"; + return ( + '
' + + title + + "
" + + lines + + "
" + ); } function loadStats() { @@ -248,197 +278,231 @@ .catch(function () {}); } - function uploadSlot(input, file) { - if (!draftId || !file) return; - var status = input.parentElement && input.parentElement.querySelector(".or-upload-status"); - var hidden = input.parentElement && input.parentElement.querySelector(".or-upload-hidden"); - if (status) status.textContent = "上传中…"; - var fd = new FormData(); - fd.append("draft_id", draftId); - fd.append("tf", input.getAttribute("data-tf") || ""); - fd.append("file", file); - fetch("/api/options/review/upload_slot", { method: "POST", body: fd, credentials: "same-origin" }) - .then(function (r) { - return r.json(); - }) - .then(function (data) { - if (!data.ok) throw new Error(data.error || "fail"); - if (hidden) hidden.value = data.file; - if (status) status.textContent = "已上传"; - input.value = ""; - }) - .catch(function () { - if (hidden) hidden.value = ""; - if (status) status.textContent = "失败"; - }); + function resetUploadSlots() { + draftId = newDraftId(); + var draftEl = $("or-draft-id"); + if (draftEl) draftEl.value = draftId; + document.querySelectorAll("#or-upload-slots .or-upload-hidden").forEach(function (el) { + el.value = ""; + }); + document.querySelectorAll("#or-upload-slots .or-upload-input").forEach(function (el) { + el.value = ""; + }); + document.querySelectorAll("#or-upload-slots .or-upload-status").forEach(function (el) { + el.textContent = ""; + }); } - function openEdit(tradeId) { - currentTradeId = tradeId; - draftId = newDraftId(); - var modal = $("or-edit-modal"); - var body = $("or-edit-body"); - var title = $("or-edit-title"); - if (!modal || !body) return; - body.innerHTML = '
加载中…
'; - modal.style.display = "flex"; - fetch("/api/options/review/trades/" + tradeId, { credentials: "same-origin" }) - .then(function (r) { - return r.json(); - }) - .then(function (data) { - if (!data.ok || !data.trade) { - body.innerHTML = '
加载失败
'; + function bindUploadSlots() { + document.querySelectorAll("#or-upload-slots .or-upload-input").forEach(function (input) { + if (input.dataset.orBound === "1") return; + input.dataset.orBound = "1"; + input.addEventListener("change", function () { + var file = input.files && input.files[0]; + var row = input.closest(".or-upload-row"); + var status = row && row.querySelector(".or-upload-status"); + var hidden = row && row.querySelector(".or-upload-hidden"); + if (!file) { + if (hidden) hidden.value = ""; + if (status) status.textContent = ""; return; } - var t = data.trade; - var e = t.entry || {}; - if (title) title.textContent = (t.source_label || "") + " · 复盘 #" + t.id; - var legsHtml = ""; - if (t.legs && t.legs.length) { - legsHtml = - '' + - t.legs - .map(function (leg) { - return ( - "" - ); - }) - .join("") + - "
合约盈亏原因
" + - (leg.leg_role || "") + - "" + - (leg.inst_id || leg.symbol || "") + - "" + - fmtPnl(leg.realized_pnl) + - "" + - (leg.close_reason || "") + - "
"; - } - var slots = SLOT_TFS.map(function (tf) { - return ( - '
' + - "" + - '' + - '
' - ); - }).join(""); - body.innerHTML = - '
' + - (t.inst_id || t.underlying || "") + - " · 盈亏 " + - fmtPnl(t.realized_pnl_total) + - (t.is_hedge - ? " (永续 " + - fmtPnl(t.realized_pnl_perp) + - " / 期权 " + - fmtPnl(t.realized_pnl_options) + - ")" - : "") + - "
开 " + - (t.opened_at || "—") + - " → 平 " + - (t.closed_at || "—") + - "
" + - legsHtml + - '
' + - '' + - '' + - '' + - '" + - '' + - '' + - "
" + - '" + - '" + - '
截图' + - slots + - "
" + - '
' + - '' + - '' + - "
"; - body.querySelectorAll(".or-upload-input").forEach(function (input) { - input.addEventListener("change", function () { - var file = input.files && input.files[0]; - if (file) uploadSlot(input, file); + if (!draftId) draftId = newDraftId(); + if (status) status.textContent = "上传中…"; + var fd = new FormData(); + fd.append("draft_id", draftId); + fd.append("tf", input.getAttribute("data-tf") || ""); + fd.append("file", file); + fetch("/api/options/review/upload_slot", { + method: "POST", + body: fd, + credentials: "same-origin", + }) + .then(function (r) { + return r.json(); + }) + .then(function (data) { + if (!data.ok) throw new Error(data.error || "fail"); + if (hidden) hidden.value = data.file; + if (status) status.textContent = "上传成功 " + data.file; + input.value = ""; + }) + .catch(function () { + if (hidden) hidden.value = ""; + if (status) status.textContent = "上传失败"; }); - }); - // 回填已有图片文件名到 hidden(仅展示状态) - (e.images || []).forEach(function (img) { - var hidden = body.querySelector('.or-upload-hidden[data-tf="' + img.tf + '"]'); - var status = hidden && hidden.parentElement.querySelector(".or-upload-status"); - if (hidden && img.file) { - hidden.value = img.file; - if (status) status.textContent = "已有 " + img.file; - } - }); - $("or-save-btn").addEventListener("click", saveEntry); - $("or-del-btn").addEventListener("click", deleteEntry); }); + }); } - function esc(s) { - return String(s == null ? "" : s) - .replace(/&/g, "&") - .replace(/" + v + ""; + function collectMoodTags() { + var out = []; + document.querySelectorAll(".or-mood:checked").forEach(function (cb) { + out.push(cb.value); + }); + return out.join(","); } function collectImages() { - var body = $("or-edit-body"); - if (!body) return []; var out = []; - body.querySelectorAll(".or-upload-hidden").forEach(function (el) { + document.querySelectorAll("#or-upload-slots .or-upload-hidden").forEach(function (el) { var file = (el.value || "").trim(); if (file) out.push({ tf: el.getAttribute("data-tf") || "", file: file }); }); return out; } + function hideJournalForm() { + currentTradeId = null; + var card = $("or-journal-card"); + if (card) card.hidden = true; + document.querySelectorAll(".or-trade-row").forEach(function (tr) { + tr.classList.remove("or-row-active"); + }); + } + + function openJournalForm(tradeId) { + currentTradeId = tradeId; + var card = $("or-journal-card"); + if (!card) return; + card.hidden = false; + card.scrollIntoView({ behavior: "smooth", block: "start" }); + document.querySelectorAll(".or-trade-row").forEach(function (tr) { + tr.classList.toggle("or-row-active", Number(tr.getAttribute("data-id")) === tradeId); + }); + resetUploadSlots(); + bindUploadSlots(); + ($("or-save-status") || {}).textContent = "加载中…"; + + fetch("/api/options/review/trades/" + tradeId, { credentials: "same-origin" }) + .then(function (r) { + return r.json(); + }) + .then(function (data) { + if (!data.ok || !data.trade) { + ($("or-save-status") || {}).textContent = "加载失败"; + return; + } + fillForm(data.trade); + ($("or-save-status") || {}).textContent = ""; + }) + .catch(function () { + ($("or-save-status") || {}).textContent = "加载失败"; + }); + } + + function fillForm(t) { + var e = t.entry || {}; + ($("or-trade-id") || {}).value = String(t.id || ""); + ($("or-f-open") || {}).value = toLocalInput(t.opened_at); + ($("or-f-close") || {}).value = toLocalInput(t.closed_at); + ($("or-f-coin") || {}).value = t.underlying || ""; + ($("or-f-inst") || {}).value = + t.source_type === "option_spot" + ? t.inst_id || "" + : (t.source_label || "") + (t.plan_close_reason ? " · " + t.plan_close_reason : ""); + ($("or-f-pnl") || {}).value = fmtPnl(t.realized_pnl_total); + ($("or-f-hold") || {}).value = fmtHold(t.hold_seconds); + ($("or-f-strategy") || {}).value = e.strategy_tag || ""; + ($("or-f-direction") || {}).value = e.direction_view || t.direction || ""; + ($("or-f-exit") || {}).value = e.exit_reason || t.plan_close_reason || ""; + ($("or-f-followed") || {}).value = e.followed_plan || ""; + ($("or-f-result") || {}).value = e.result_tag || ""; + ($("or-f-entry") || {}).value = e.entry_logic || ""; + ($("or-f-note") || {}).value = e.note || ""; + setMoodTags(e.mistake_tags); + + var summary = $("or-journal-summary"); + if (summary) { + summary.textContent = + (t.source_label || "") + + " · " + + (t.inst_id || t.underlying || "#" + t.id) + + " · 盈亏 " + + fmtPnl(t.realized_pnl_total) + + (t.is_hedge + ? " (永续 " + fmtPnl(t.realized_pnl_perp) + " / 期权 " + fmtPnl(t.realized_pnl_options) + ")" + : ""); + } + + // 已有截图回填状态(文件名保留,新上传会换 draft) + (e.images || []).forEach(function (img) { + var hidden = document.querySelector( + '#or-upload-slots .or-upload-hidden[data-tf="' + img.tf + '"]' + ); + var status = document.querySelector( + '#or-upload-slots .or-upload-status[data-tf="' + img.tf + '"]' + ); + if (hidden && img.file) { + hidden.value = img.file; + if (status) status.textContent = "已有 " + img.file; + } + }); + + var legsHost = $("or-legs-host"); + if (legsHost) { + if (t.legs && t.legs.length) { + legsHost.innerHTML = + "

计划腿

" + + t.legs + .map(function (leg) { + return ( + "" + ); + }) + .join("") + + "
合约盈亏原因
" + + (leg.leg_role || "") + + "" + + (leg.inst_id || leg.symbol || "") + + "" + + fmtPnl(leg.realized_pnl) + + "" + + (leg.close_reason || "") + + "
"; + } else { + legsHost.innerHTML = ""; + } + } + } + function saveEntry() { - if (!currentTradeId) return; + var tradeId = Number(($("or-trade-id") || {}).value || 0); + if (!tradeId) return; + var strategy = (($("or-f-strategy") || {}).value || "").trim(); + if (!strategy) { + alert("请填写策略标签"); + return; + } var payload = { - trade_id: currentTradeId, - strategy_tag: ($("or-f-strategy") || {}).value || "", + trade_id: tradeId, + strategy_tag: strategy, direction_view: ($("or-f-direction") || {}).value || "", exit_reason: ($("or-f-exit") || {}).value || "", followed_plan: ($("or-f-followed") || {}).value || "", result_tag: ($("or-f-result") || {}).value || "", - mistake_tags: ($("or-f-mistakes") || {}).value || "", + mistake_tags: collectMoodTags(), entry_logic: ($("or-f-entry") || {}).value || "", note: ($("or-f-note") || {}).value || "", images: collectImages(), }; + ($("or-save-status") || {}).textContent = "保存中…"; fetch("/api/options/review/entry", { method: "POST", credentials: "same-origin", @@ -450,21 +514,22 @@ }) .then(function (data) { if (!data.ok) { - alert(data.msg || "保存失败"); + ($("or-save-status") || {}).textContent = data.msg || "保存失败"; return; } - closeModal(); + ($("or-save-status") || {}).textContent = "已保存"; reloadAll(); }) .catch(function () { - alert("保存失败"); + ($("or-save-status") || {}).textContent = "保存失败"; }); } function deleteEntry() { - if (!currentTradeId) return; + var tradeId = Number(($("or-trade-id") || {}).value || 0); + if (!tradeId) return; if (!confirm("删除该条复盘内容与图片?")) return; - fetch("/api/options/review/entry/" + currentTradeId, { + fetch("/api/options/review/entry/" + tradeId, { method: "DELETE", credentials: "same-origin", }) @@ -472,34 +537,43 @@ return r.json(); }) .then(function () { - closeModal(); + hideJournalForm(); reloadAll(); }); } - function closeModal(ev) { - if (ev && ev.target && ev.target.id !== "or-edit-modal") return; - var modal = $("or-edit-modal"); - if (modal) modal.style.display = "none"; - currentTradeId = null; - } - function init() { if (!$("options-review-root")) return; + document.querySelectorAll(".or-tab").forEach(function (btn) { + btn.addEventListener("click", function () { + setActiveTab(btn.getAttribute("data-source")); + }); + }); var syncBtn = $("or-sync-btn"); var reloadBtn = $("or-reload-btn"); + var saveBtn = $("or-save-btn"); + var clearBtn = $("or-clear-btn"); + var delBtn = $("or-del-btn"); if (syncBtn) syncBtn.addEventListener("click", syncNow); if (reloadBtn) reloadBtn.addEventListener("click", reloadAll); - ["or-filter-source", "or-filter-uly", "or-filter-opt", "or-filter-reviewed", "or-include-hedge-legs"].forEach( + if (saveBtn) saveBtn.addEventListener("click", saveEntry); + if (clearBtn) clearBtn.addEventListener("click", hideJournalForm); + if (delBtn) delBtn.addEventListener("click", deleteEntry); + ["or-filter-uly", "or-filter-opt", "or-filter-reviewed", "or-include-hedge-legs"].forEach( function (id) { var el = $(id); if (el) el.addEventListener("change", reloadAll); } ); - reloadAll(); + bindUploadSlots(); + setActiveTab("option_spot"); } - global.OptionsReview = { init: init, closeModal: closeModal, openEdit: openEdit }; + global.OptionsReview = { + init: init, + openJournalForm: openJournalForm, + hideJournalForm: hideJournalForm, + }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); diff --git a/lib/options/options_review_images_lib.py b/lib/options/options_review_images_lib.py index d22d5f0..b1845a8 100644 --- a/lib/options/options_review_images_lib.py +++ b/lib/options/options_review_images_lib.py @@ -1,4 +1,4 @@ -"""期权复盘截图:独立命名空间,不与合约 journal 混用.""" +"""期权复盘截图:独立命名空间,与合约同款四周期 5m/15m/1h/4h.""" from __future__ import annotations import json @@ -6,11 +6,11 @@ import os import re from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence -OPTIONS_REVIEW_UPLOAD_TFS: tuple[str, ...] = ("chart", "entry", "exit", "other") +OPTIONS_REVIEW_UPLOAD_TFS: tuple[str, ...] = ("5m", "15m", "1h", "4h") OPTIONS_REVIEW_ALLOWED_EXT = frozenset({".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp"}) _DRAFT_ID_RE = re.compile(r"^[a-f0-9]{32}$") _SLOT_FILE_RE = re.compile( - r"^options_journal_([a-f0-9]{32})_(chart|entry|exit|other)\.(png|jpg|jpeg|webp|gif|bmp)$", + r"^options_journal_([a-f0-9]{32})_(5m|15m|1h|4h)\.(png|jpg|jpeg|webp|gif|bmp)$", re.I, ) diff --git a/lib/options/templates/options_review_panel.html b/lib/options/templates/options_review_panel.html index c66384f..2753a20 100644 --- a/lib/options/templates/options_review_panel.html +++ b/lib/options/templates/options_review_panel.html @@ -1,28 +1,38 @@ -{# OKX 期权复盘(含对冲):独立页,不混合约 journal #} +{# OKX 期权复盘:三类 Tab + 列表 + 下方多周期截图复盘表单 #}
{% if not options_enabled %}
期权未启用:请设置 OKX_OPTIONS_ENABLED=true 后重启.对冲计划仍可单独同步(若本地有已结束计划).
{% endif %} + +

期权复盘

-

同一列表三类:纯期权(交易所已平仓)、永期对冲 / 期期对冲(本地已结束计划,计划级一条).对冲盈亏用合计;归属对冲的期权腿默认不重复计入.

- +
+ + + +
+

点列表行进入下方「复盘记录上传」;截图槽位与合约一致(5m / 15m / 1h / 4h).对冲盈亏用计划合计;归属对冲的期权腿默认不重复计入.

- + +
+ + + + + + +
+
+ + + + + +
+ + +
+ {% for tf in ['5m', '15m', '1h', '4h'] %} +
+ {{ tf }} + + + +
+ {% endfor %} +
+

可只传部分周期;选文件后即时上传,保存后详情可查看

+ +
+ + + + + + +
+ +
+ + + + +
+
+
- + diff --git a/tests/test_options_review_lib.py b/tests/test_options_review_lib.py index e2c1c4e..9d68a3b 100644 --- a/tests/test_options_review_lib.py +++ b/tests/test_options_review_lib.py @@ -230,14 +230,14 @@ class OptionsReviewTests(unittest.TestCase): with tempfile.TemporaryDirectory() as tmp: folder = options_review_upload_dir(tmp) fname = build_options_review_slot_filename( - "a" * 32, "chart", ".png", secure_filename_fn=lambda x: x + "a" * 32, "5m", ".png", secure_filename_fn=lambda x: x ) self.assertTrue(fname.startswith("options_journal_")) - self.assertTrue(is_valid_options_review_file(fname, "a" * 32, "chart")) + self.assertTrue(is_valid_options_review_file(fname, "a" * 32, "5m")) item = save_options_review_slot_file( _FakeFile("x.png"), "a" * 32, - "chart", + "5m", folder, secure_filename_fn=lambda x: x, )