Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
1b670a80d9
commit
95973ab28d
@ -9,11 +9,22 @@ fi
|
|||||||
# ManeshTrader quick deploy helper for Synology Docker host.
|
# ManeshTrader quick deploy helper for Synology Docker host.
|
||||||
# Defaults are set for the target provided by the user.
|
# Defaults are set for the target provided by the user.
|
||||||
|
|
||||||
|
ENV_FILE=".env.deploy_synology"
|
||||||
|
if [[ -f "$ENV_FILE" ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
set -a; source "$ENV_FILE"; set +a
|
||||||
|
fi
|
||||||
|
|
||||||
REMOTE_USER="${REMOTE_USER:-mbrucedogs}"
|
REMOTE_USER="${REMOTE_USER:-mbrucedogs}"
|
||||||
REMOTE_HOST="${REMOTE_HOST:-192.168.1.128}"
|
REMOTE_HOST="${REMOTE_HOST:-192.168.1.128}"
|
||||||
REMOTE_PORT="${REMOTE_PORT:-25}"
|
REMOTE_PORT="${REMOTE_PORT:-25}"
|
||||||
REMOTE_BASE="${REMOTE_BASE:-/volume1/docker/maneshtrader}"
|
REMOTE_BASE="${REMOTE_BASE:-/volume1/docker/maneshtrader}"
|
||||||
|
LOCAL_BASE="${LOCAL_BASE:-web/src}"
|
||||||
SYNO_PASSWORD="${SYNO_PASSWORD:-}"
|
SYNO_PASSWORD="${SYNO_PASSWORD:-}"
|
||||||
|
CONTAINER_NAME="${CONTAINER_NAME:-}"
|
||||||
|
SUDO_PASSWORD="${SUDO_PASSWORD:-${SYNO_PASSWORD:-}}"
|
||||||
|
SUDO_MODE="${SUDO_MODE:-auto}" # auto|always|never
|
||||||
|
SSH_PASS_WARNING_SHOWN="0"
|
||||||
|
|
||||||
MODE="bind" # bind: upload + restart; image: upload + rebuild
|
MODE="bind" # bind: upload + restart; image: upload + rebuild
|
||||||
NO_RESTART="0"
|
NO_RESTART="0"
|
||||||
@ -36,17 +47,24 @@ Options:
|
|||||||
--mode bind|image Deploy mode (default: bind)
|
--mode bind|image Deploy mode (default: bind)
|
||||||
--files "a b c" Space-separated file list to upload
|
--files "a b c" Space-separated file list to upload
|
||||||
--recent-minutes N Upload files under web/src modified in last N minutes
|
--recent-minutes N Upload files under web/src modified in last N minutes
|
||||||
|
--git-hours N Upload files changed in last N hours (git history + working tree)
|
||||||
|
--env-file PATH Load environment variables from file (default: .env.deploy_synology if present)
|
||||||
|
--sudo-mode MODE Docker privilege mode: auto|always|never (default: auto)
|
||||||
--no-restart Upload only; skip remote docker restart/rebuild
|
--no-restart Upload only; skip remote docker restart/rebuild
|
||||||
--dry-run Print actions but do not execute
|
--dry-run Print actions but do not execute
|
||||||
-h, --help Show help
|
-h, --help Show help
|
||||||
|
|
||||||
Environment overrides:
|
Environment overrides:
|
||||||
REMOTE_USER, REMOTE_HOST, REMOTE_PORT, REMOTE_BASE, SYNO_PASSWORD, CONTAINER_NAME
|
REMOTE_USER, REMOTE_HOST, REMOTE_PORT, REMOTE_BASE, LOCAL_BASE, SYNO_PASSWORD, CONTAINER_NAME,
|
||||||
|
SUDO_PASSWORD, SUDO_MODE
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
./deploy_synology.sh
|
./deploy_synology.sh
|
||||||
./deploy_synology.sh --mode image
|
./deploy_synology.sh --mode image
|
||||||
./deploy_synology.sh --recent-minutes 120
|
./deploy_synology.sh --recent-minutes 120
|
||||||
|
./deploy_synology.sh --git-hours 2
|
||||||
|
./deploy_synology.sh --env-file .env.deploy_synology --git-hours 2
|
||||||
|
./deploy_synology.sh --git-hours 2 --sudo-mode always
|
||||||
./deploy_synology.sh --files "web/src/web_core/ui/help_content.py"
|
./deploy_synology.sh --files "web/src/web_core/ui/help_content.py"
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
@ -62,8 +80,12 @@ run_cmd() {
|
|||||||
build_ssh_cmd() {
|
build_ssh_cmd() {
|
||||||
if [[ -n "${SYNO_PASSWORD:-}" ]]; then
|
if [[ -n "${SYNO_PASSWORD:-}" ]]; then
|
||||||
if ! command -v sshpass >/dev/null 2>&1; then
|
if ! command -v sshpass >/dev/null 2>&1; then
|
||||||
echo "Error: SYNO_PASSWORD is set but sshpass is not installed." >&2
|
if [[ "$SSH_PASS_WARNING_SHOWN" == "0" ]]; then
|
||||||
exit 1
|
echo "Warning: SYNO_PASSWORD is set but sshpass is not installed; falling back to interactive ssh/scp prompts." >&2
|
||||||
|
SSH_PASS_WARNING_SHOWN="1"
|
||||||
|
fi
|
||||||
|
printf "ssh -p '%s' '%s@%s'" "${REMOTE_PORT}" "${REMOTE_USER}" "${REMOTE_HOST}"
|
||||||
|
return
|
||||||
fi
|
fi
|
||||||
printf "SSHPASS='%s' sshpass -e ssh -o StrictHostKeyChecking=accept-new -p '%s' '%s@%s'" \
|
printf "SSHPASS='%s' sshpass -e ssh -o StrictHostKeyChecking=accept-new -p '%s' '%s@%s'" \
|
||||||
"${SYNO_PASSWORD}" "${REMOTE_PORT}" "${REMOTE_USER}" "${REMOTE_HOST}"
|
"${SYNO_PASSWORD}" "${REMOTE_PORT}" "${REMOTE_USER}" "${REMOTE_HOST}"
|
||||||
@ -75,8 +97,12 @@ build_ssh_cmd() {
|
|||||||
build_scp_cmd() {
|
build_scp_cmd() {
|
||||||
if [[ -n "${SYNO_PASSWORD:-}" ]]; then
|
if [[ -n "${SYNO_PASSWORD:-}" ]]; then
|
||||||
if ! command -v sshpass >/dev/null 2>&1; then
|
if ! command -v sshpass >/dev/null 2>&1; then
|
||||||
echo "Error: SYNO_PASSWORD is set but sshpass is not installed." >&2
|
if [[ "$SSH_PASS_WARNING_SHOWN" == "0" ]]; then
|
||||||
exit 1
|
echo "Warning: SYNO_PASSWORD is set but sshpass is not installed; falling back to interactive ssh/scp prompts." >&2
|
||||||
|
SSH_PASS_WARNING_SHOWN="1"
|
||||||
|
fi
|
||||||
|
printf "scp -O -P '%s'" "${REMOTE_PORT}"
|
||||||
|
return
|
||||||
fi
|
fi
|
||||||
printf "SSHPASS='%s' sshpass -e scp -O -o StrictHostKeyChecking=accept-new -P '%s'" \
|
printf "SSHPASS='%s' sshpass -e scp -O -o StrictHostKeyChecking=accept-new -P '%s'" \
|
||||||
"${SYNO_PASSWORD}" "${REMOTE_PORT}"
|
"${SYNO_PASSWORD}" "${REMOTE_PORT}"
|
||||||
@ -85,12 +111,77 @@ build_scp_cmd() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
to_remote_rel_path() {
|
||||||
|
local file="$1"
|
||||||
|
local prefix="${LOCAL_BASE%/}/"
|
||||||
|
if [[ "$file" == "$prefix"* ]]; then
|
||||||
|
printf "%s" "${file#$prefix}"
|
||||||
|
else
|
||||||
|
printf "%s" "$file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_git_hours_files() {
|
||||||
|
local hours="$1"
|
||||||
|
local base_commit
|
||||||
|
local combined
|
||||||
|
|
||||||
|
if ! command -v git >/dev/null 2>&1; then
|
||||||
|
echo "Error: git is required for --git-hours." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
echo "Error: --git-hours must be run from inside a git repository." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
base_commit="$(git rev-list -n 1 --before="${hours} hours ago" HEAD 2>/dev/null || true)"
|
||||||
|
|
||||||
|
combined="$(
|
||||||
|
{
|
||||||
|
if [[ -n "$base_commit" ]]; then
|
||||||
|
git diff --name-only --diff-filter=ACMRTUXB "${base_commit}..HEAD" -- web/src
|
||||||
|
fi
|
||||||
|
git diff --name-only -- web/src
|
||||||
|
git diff --cached --name-only -- web/src
|
||||||
|
git ls-files -m -o --exclude-standard -- web/src
|
||||||
|
} | sed '/^$/d' | sort -u
|
||||||
|
)"
|
||||||
|
|
||||||
|
FILES=()
|
||||||
|
while IFS= read -r candidate; do
|
||||||
|
[[ -z "$candidate" ]] && continue
|
||||||
|
FILES+=("$candidate")
|
||||||
|
done <<EOF
|
||||||
|
$combined
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--mode)
|
--mode)
|
||||||
MODE="${2:-}"
|
MODE="${2:-}"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
--env-file)
|
||||||
|
ENV_FILE="${2:-}"
|
||||||
|
if [[ ! -f "$ENV_FILE" ]]; then
|
||||||
|
echo "Error: env file not found: $ENV_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
set -a; source "$ENV_FILE"; set +a
|
||||||
|
REMOTE_USER="${REMOTE_USER:-mbrucedogs}"
|
||||||
|
REMOTE_HOST="${REMOTE_HOST:-192.168.1.128}"
|
||||||
|
REMOTE_PORT="${REMOTE_PORT:-25}"
|
||||||
|
REMOTE_BASE="${REMOTE_BASE:-/volume1/docker/maneshtrader}"
|
||||||
|
LOCAL_BASE="${LOCAL_BASE:-web/src}"
|
||||||
|
SYNO_PASSWORD="${SYNO_PASSWORD:-}"
|
||||||
|
CONTAINER_NAME="${CONTAINER_NAME:-}"
|
||||||
|
SUDO_PASSWORD="${SUDO_PASSWORD:-${SYNO_PASSWORD:-}}"
|
||||||
|
SUDO_MODE="${SUDO_MODE:-auto}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
--files)
|
--files)
|
||||||
read -r -a FILES <<< "${2:-}"
|
read -r -a FILES <<< "${2:-}"
|
||||||
shift 2
|
shift 2
|
||||||
@ -110,10 +201,23 @@ $(find web/src -type f -mmin "-$minutes" ! -name ".DS_Store" | sort)
|
|||||||
EOF
|
EOF
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
--git-hours)
|
||||||
|
hours="${2:-}"
|
||||||
|
if ! [[ "$hours" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "Error: --git-hours requires an integer." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
collect_git_hours_files "$hours"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
--no-restart)
|
--no-restart)
|
||||||
NO_RESTART="1"
|
NO_RESTART="1"
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
--sudo-mode)
|
||||||
|
SUDO_MODE="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
--dry-run)
|
--dry-run)
|
||||||
DRY_RUN="1"
|
DRY_RUN="1"
|
||||||
shift
|
shift
|
||||||
@ -134,6 +238,10 @@ if [[ "$MODE" != "bind" && "$MODE" != "image" ]]; then
|
|||||||
echo "Error: --mode must be 'bind' or 'image'." >&2
|
echo "Error: --mode must be 'bind' or 'image'." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
if [[ "$SUDO_MODE" != "auto" && "$SUDO_MODE" != "always" && "$SUDO_MODE" != "never" ]]; then
|
||||||
|
echo "Error: --sudo-mode must be auto|always|never." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ ${#FILES[@]} -eq 0 ]]; then
|
if [[ ${#FILES[@]} -eq 0 ]]; then
|
||||||
echo "Error: No files selected for upload." >&2
|
echo "Error: No files selected for upload." >&2
|
||||||
@ -156,7 +264,8 @@ for file in "${FILES[@]}"; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
for file in "${FILES[@]}"; do
|
for file in "${FILES[@]}"; do
|
||||||
remote_path="${REMOTE_BASE}/${file}"
|
remote_rel="$(to_remote_rel_path "$file")"
|
||||||
|
remote_path="${REMOTE_BASE}/${remote_rel}"
|
||||||
remote_dir="$(dirname "$remote_path")"
|
remote_dir="$(dirname "$remote_path")"
|
||||||
SSH_CMD="$(build_ssh_cmd)"
|
SSH_CMD="$(build_ssh_cmd)"
|
||||||
SCP_CMD="$(build_scp_cmd)"
|
SCP_CMD="$(build_scp_cmd)"
|
||||||
@ -174,6 +283,8 @@ if [[ "$MODE" == "bind" ]]; then
|
|||||||
run_cmd "${SSH_CMD} '
|
run_cmd "${SSH_CMD} '
|
||||||
set -e
|
set -e
|
||||||
cd \"${REMOTE_BASE}\"
|
cd \"${REMOTE_BASE}\"
|
||||||
|
SUDO_MODE=\"${SUDO_MODE}\"
|
||||||
|
SUDO_PASSWORD=\"${SUDO_PASSWORD}\"
|
||||||
DOCKER_BIN=\"\"
|
DOCKER_BIN=\"\"
|
||||||
if command -v docker >/dev/null 2>&1; then
|
if command -v docker >/dev/null 2>&1; then
|
||||||
DOCKER_BIN=\"\$(command -v docker)\"
|
DOCKER_BIN=\"\$(command -v docker)\"
|
||||||
@ -185,21 +296,42 @@ elif [[ -x /var/packages/ContainerManager/target/usr/bin/docker ]]; then
|
|||||||
DOCKER_BIN=\"/var/packages/ContainerManager/target/usr/bin/docker\"
|
DOCKER_BIN=\"/var/packages/ContainerManager/target/usr/bin/docker\"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n \"\$DOCKER_BIN\" ]] && \"\$DOCKER_BIN\" compose version >/dev/null 2>&1; then
|
docker_cmd() {
|
||||||
\"\$DOCKER_BIN\" compose restart
|
if [[ \"\$USE_SUDO\" == \"1\" ]]; then
|
||||||
|
if [[ -n \"\$SUDO_PASSWORD\" ]]; then
|
||||||
|
printf \"%s\\n\" \"\$SUDO_PASSWORD\" | sudo -S -p \"\" \"\$DOCKER_BIN\" \"\$@\"
|
||||||
|
else
|
||||||
|
sudo \"\$DOCKER_BIN\" \"\$@\"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
\"\$DOCKER_BIN\" \"\$@\"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
USE_SUDO=\"0\"
|
||||||
|
if [[ \"\$SUDO_MODE\" == \"always\" ]]; then
|
||||||
|
USE_SUDO=\"1\"
|
||||||
|
elif [[ \"\$SUDO_MODE\" == \"auto\" ]]; then
|
||||||
|
if ! \"\$DOCKER_BIN\" info >/dev/null 2>&1; then
|
||||||
|
USE_SUDO=\"1\"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n \"\$DOCKER_BIN\" ]] && docker_cmd compose version >/dev/null 2>&1; then
|
||||||
|
docker_cmd compose restart
|
||||||
elif command -v docker-compose >/dev/null 2>&1; then
|
elif command -v docker-compose >/dev/null 2>&1; then
|
||||||
docker-compose restart
|
docker-compose restart
|
||||||
elif [[ -n \"\$DOCKER_BIN\" ]]; then
|
elif [[ -n \"\$DOCKER_BIN\" ]]; then
|
||||||
if [[ -n \"${CONTAINER_NAME:-}\" ]]; then
|
if [[ -n \"${CONTAINER_NAME:-}\" ]]; then
|
||||||
\"\$DOCKER_BIN\" restart \"${CONTAINER_NAME}\"
|
docker_cmd restart \"${CONTAINER_NAME}\"
|
||||||
else
|
else
|
||||||
ids=\$(\"\$DOCKER_BIN\" ps --filter \"name=maneshtrader\" -q)
|
ids=\$(docker_cmd ps --filter \"name=maneshtrader\" -q)
|
||||||
if [[ -z \"\$ids\" ]]; then
|
if [[ -z \"\$ids\" ]]; then
|
||||||
echo \"No docker compose command found, and no running containers matched name=maneshtrader.\" >&2
|
echo \"No docker compose command found, and no running containers matched name=maneshtrader.\" >&2
|
||||||
echo \"Set CONTAINER_NAME=<your-container-name> and rerun.\" >&2
|
echo \"Set CONTAINER_NAME=<your-container-name> and rerun.\" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
\"\$DOCKER_BIN\" restart \$ids
|
docker_cmd restart \$ids
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo \"No docker or docker compose command found on remote host.\" >&2
|
echo \"No docker or docker compose command found on remote host.\" >&2
|
||||||
@ -212,6 +344,8 @@ else
|
|||||||
run_cmd "${SSH_CMD} '
|
run_cmd "${SSH_CMD} '
|
||||||
set -e
|
set -e
|
||||||
cd \"${REMOTE_BASE}\"
|
cd \"${REMOTE_BASE}\"
|
||||||
|
SUDO_MODE=\"${SUDO_MODE}\"
|
||||||
|
SUDO_PASSWORD=\"${SUDO_PASSWORD}\"
|
||||||
DOCKER_BIN=\"\"
|
DOCKER_BIN=\"\"
|
||||||
if command -v docker >/dev/null 2>&1; then
|
if command -v docker >/dev/null 2>&1; then
|
||||||
DOCKER_BIN=\"\$(command -v docker)\"
|
DOCKER_BIN=\"\$(command -v docker)\"
|
||||||
@ -223,8 +357,29 @@ elif [[ -x /var/packages/ContainerManager/target/usr/bin/docker ]]; then
|
|||||||
DOCKER_BIN=\"/var/packages/ContainerManager/target/usr/bin/docker\"
|
DOCKER_BIN=\"/var/packages/ContainerManager/target/usr/bin/docker\"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n \"\$DOCKER_BIN\" ]] && \"\$DOCKER_BIN\" compose version >/dev/null 2>&1; then
|
docker_cmd() {
|
||||||
\"\$DOCKER_BIN\" compose up -d --build
|
if [[ \"\$USE_SUDO\" == \"1\" ]]; then
|
||||||
|
if [[ -n \"\$SUDO_PASSWORD\" ]]; then
|
||||||
|
printf \"%s\\n\" \"\$SUDO_PASSWORD\" | sudo -S -p \"\" \"\$DOCKER_BIN\" \"\$@\"
|
||||||
|
else
|
||||||
|
sudo \"\$DOCKER_BIN\" \"\$@\"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
\"\$DOCKER_BIN\" \"\$@\"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
USE_SUDO=\"0\"
|
||||||
|
if [[ \"\$SUDO_MODE\" == \"always\" ]]; then
|
||||||
|
USE_SUDO=\"1\"
|
||||||
|
elif [[ \"\$SUDO_MODE\" == \"auto\" ]]; then
|
||||||
|
if ! \"\$DOCKER_BIN\" info >/dev/null 2>&1; then
|
||||||
|
USE_SUDO=\"1\"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n \"\$DOCKER_BIN\" ]] && docker_cmd compose version >/dev/null 2>&1; then
|
||||||
|
docker_cmd compose up -d --build
|
||||||
elif command -v docker-compose >/dev/null 2>&1; then
|
elif command -v docker-compose >/dev/null 2>&1; then
|
||||||
docker-compose up -d --build
|
docker-compose up -d --build
|
||||||
else
|
else
|
||||||
|
|||||||
@ -96,6 +96,8 @@ Tip: sign and notarize before sharing broadly, so macOS trust prompts are reduce
|
|||||||
13. Read `Trend Events` for starts and reversals.
|
13. Read `Trend Events` for starts and reversals.
|
||||||
14. Use `Live Decision Guide` to translate trend state into a practical bias/action/invalidation workflow.
|
14. Use `Live Decision Guide` to translate trend state into a practical bias/action/invalidation workflow.
|
||||||
15. Keep `Show past behavior examples` ON while learning to review historical entry/exit outcomes.
|
15. Keep `Show past behavior examples` ON while learning to review historical entry/exit outcomes.
|
||||||
|
16. Set `Display Timezone (US)` to your preferred timezone (default is `America/Chicago`, CST/CDT).
|
||||||
|
17. Choose `Use 24-hour time` ON for `13:00` style, or OFF for `1:00 PM` style.
|
||||||
|
|
||||||
## 4.1) Advanced Features (Optional)
|
## 4.1) Advanced Features (Optional)
|
||||||
- `Advanced Signals`:
|
- `Advanced Signals`:
|
||||||
@ -105,6 +107,9 @@ Tip: sign and notarize before sharing broadly, so macOS trust prompts are reduce
|
|||||||
- `Regime filter (stand aside in choppy periods)`
|
- `Regime filter (stand aside in choppy periods)`
|
||||||
- `Training & Guidance`:
|
- `Training & Guidance`:
|
||||||
- `Replay mode (hide future bars)` + `Replay bars shown`
|
- `Replay mode (hide future bars)` + `Replay bars shown`
|
||||||
|
- `Monitoring`:
|
||||||
|
- `Display Timezone (US)`: controls audit/event/training timestamp timezone (`America/Chicago` default)
|
||||||
|
- `Use 24-hour time`: switches between `13:00` and `1:00 PM` time styles
|
||||||
- `Compare Symbols`:
|
- `Compare Symbols`:
|
||||||
- enable panel and provide comma-separated symbols (`AAPL, MSFT, NVDA`)
|
- enable panel and provide comma-separated symbols (`AAPL, MSFT, NVDA`)
|
||||||
- `Alerts`:
|
- `Alerts`:
|
||||||
|
|||||||
@ -102,6 +102,8 @@ Normalization constraints:
|
|||||||
- `backtest_take_profit_pct`: `[0.0, 25.0]`, fallback `0.0` (0 disables)
|
- `backtest_take_profit_pct`: `[0.0, 25.0]`, fallback `0.0` (0 disables)
|
||||||
- `backtest_min_hold_bars`: `[1, 20]`, fallback `1`
|
- `backtest_min_hold_bars`: `[1, 20]`, fallback `1`
|
||||||
- `backtest_max_hold_bars`: `[1, 40]`, fallback `1` and clamped to `>= min_hold`
|
- `backtest_max_hold_bars`: `[1, 40]`, fallback `1` and clamped to `>= min_hold`
|
||||||
|
- `display_timezone`: one of `America/New_York`, `America/Chicago`, `America/Denver`, `America/Los_Angeles`, `America/Phoenix`, `America/Anchorage`, `Pacific/Honolulu`; fallback `America/Chicago`
|
||||||
|
- `use_24h_time`: boolean, fallback `false` (`false` => `1:00 PM`, `true` => `13:00`)
|
||||||
- booleans normalized from common truthy/falsy strings and numbers
|
- booleans normalized from common truthy/falsy strings and numbers
|
||||||
|
|
||||||
## 5. Classification Rules
|
## 5. Classification Rules
|
||||||
@ -181,13 +183,13 @@ Gap handling (`hide_market_closed_gaps`):
|
|||||||
- signal confirmation status
|
- signal confirmation status
|
||||||
- latest bar interpretation
|
- latest bar interpretation
|
||||||
- action + invalidation guidance
|
- action + invalidation guidance
|
||||||
- Trend events table (latest events)
|
- Trend events table (latest events), rendered in selected US display timezone and 12h/24h format
|
||||||
- Backtest snapshot:
|
- Backtest snapshot:
|
||||||
- signal at trend-change rows to active bull/bear states
|
- signal at trend-change rows to active bull/bear states
|
||||||
- advanced mode supports configurable costs/holds/stop/target
|
- advanced mode supports configurable costs/holds/stop/target
|
||||||
- Past behavior examples (optional training panel):
|
- Past behavior examples (optional training panel):
|
||||||
- historical examples using trend-confirmation entries and opposite-confirmation exits
|
- historical examples using trend-confirmation entries and opposite-confirmation exits
|
||||||
- per-example direction, entry/exit timestamps, bars held, P/L%, and outcome
|
- per-example direction, entry/exit timestamps (rendered in selected US display timezone and 12h/24h format), bars held, P/L%, and outcome
|
||||||
- aggregate example metrics (count, win/loss, win rate, average P/L)
|
- aggregate example metrics (count, win/loss, win rate, average P/L)
|
||||||
- selectable table rows that drive chart highlight of chosen example
|
- selectable table rows that drive chart highlight of chosen example
|
||||||
- plain-language explanation for selected example
|
- plain-language explanation for selected example
|
||||||
|
|||||||
@ -37,6 +37,7 @@ from web_core.ui.sidebar_ui import render_sidebar
|
|||||||
from web_core.strategy import classify_bars, detect_trends
|
from web_core.strategy import classify_bars, detect_trends
|
||||||
from web_core.market.symbols import resolve_symbol_identity
|
from web_core.market.symbols import resolve_symbol_identity
|
||||||
from web_core.ui.training_ui import render_training_panel
|
from web_core.ui.training_ui import render_training_panel
|
||||||
|
from web_core.time_display import format_timestamp
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
@ -100,6 +101,8 @@ def main() -> None:
|
|||||||
interval = str(sidebar_settings["interval"])
|
interval = str(sidebar_settings["interval"])
|
||||||
period = str(sidebar_settings["period"])
|
period = str(sidebar_settings["period"])
|
||||||
max_bars = int(sidebar_settings["max_bars"])
|
max_bars = int(sidebar_settings["max_bars"])
|
||||||
|
display_timezone = str(sidebar_settings.get("display_timezone", "America/Chicago"))
|
||||||
|
use_24h_time = bool(sidebar_settings.get("use_24h_time", False))
|
||||||
|
|
||||||
if not symbol:
|
if not symbol:
|
||||||
st.error("Please enter a symbol.")
|
st.error("Please enter a symbol.")
|
||||||
@ -234,6 +237,8 @@ def main() -> None:
|
|||||||
show_past_behavior=bool(sidebar_settings["show_past_behavior"]),
|
show_past_behavior=bool(sidebar_settings["show_past_behavior"]),
|
||||||
example_trades=example_trades,
|
example_trades=example_trades,
|
||||||
alert_key=alert_key,
|
alert_key=alert_key,
|
||||||
|
display_timezone=display_timezone,
|
||||||
|
use_24h_time=use_24h_time,
|
||||||
)
|
)
|
||||||
|
|
||||||
fig = build_figure(
|
fig = build_figure(
|
||||||
@ -241,6 +246,8 @@ def main() -> None:
|
|||||||
gray_fake=bool(sidebar_settings["gray_fake"]),
|
gray_fake=bool(sidebar_settings["gray_fake"]),
|
||||||
interval=interval,
|
interval=interval,
|
||||||
hide_market_closed_gaps=bool(sidebar_settings["hide_market_closed_gaps"]),
|
hide_market_closed_gaps=bool(sidebar_settings["hide_market_closed_gaps"]),
|
||||||
|
display_timezone=display_timezone,
|
||||||
|
use_24h_time=use_24h_time,
|
||||||
)
|
)
|
||||||
if bool(sidebar_settings["show_trade_markers"]):
|
if bool(sidebar_settings["show_trade_markers"]):
|
||||||
add_example_trade_markers(fig, example_trades)
|
add_example_trade_markers(fig, example_trades)
|
||||||
@ -307,7 +314,10 @@ def main() -> None:
|
|||||||
if events_view:
|
if events_view:
|
||||||
event_df = pd.DataFrame(
|
event_df = pd.DataFrame(
|
||||||
{
|
{
|
||||||
"timestamp": [str(e.timestamp) for e in events_view[-25:]][::-1],
|
"timestamp": [
|
||||||
|
format_timestamp(e.timestamp, display_timezone=display_timezone, use_24h_time=use_24h_time)
|
||||||
|
for e in events_view[-25:]
|
||||||
|
][::-1],
|
||||||
"event": [e.event for e in events_view[-25:]][::-1],
|
"event": [e.event for e in events_view[-25:]][::-1],
|
||||||
"trend_after": [e.trend_after for e in events_view[-25:]][::-1],
|
"trend_after": [e.trend_after for e in events_view[-25:]][::-1],
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,6 +65,8 @@ def test_build_figure_adds_missing_day_rangebreak_values() -> None:
|
|||||||
gray_fake=False,
|
gray_fake=False,
|
||||||
interval="1d",
|
interval="1d",
|
||||||
hide_market_closed_gaps=True,
|
hide_market_closed_gaps=True,
|
||||||
|
display_timezone="America/Chicago",
|
||||||
|
use_24h_time=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
rangebreak_values: list[str] = []
|
rangebreak_values: list[str] = []
|
||||||
@ -83,6 +85,8 @@ def test_build_figure_intraday_uses_category_axis_when_hiding_gaps() -> None:
|
|||||||
gray_fake=False,
|
gray_fake=False,
|
||||||
interval="15m",
|
interval="15m",
|
||||||
hide_market_closed_gaps=True,
|
hide_market_closed_gaps=True,
|
||||||
|
display_timezone="America/Chicago",
|
||||||
|
use_24h_time=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert fig.layout.xaxis.type == "category"
|
assert fig.layout.xaxis.type == "category"
|
||||||
|
|||||||
@ -29,3 +29,23 @@ def test_normalize_watchlist_splits_legacy_escaped_newline_values() -> None:
|
|||||||
def test_normalize_watchlist_splits_escaped_newlines_inside_list_items() -> None:
|
def test_normalize_watchlist_splits_escaped_newlines_inside_list_items() -> None:
|
||||||
out = normalize_web_settings({"watchlist": ["AMD\\NTSLA"]})
|
out = normalize_web_settings({"watchlist": ["AMD\\NTSLA"]})
|
||||||
assert out["watchlist"] == ["AMD", "TSLA"]
|
assert out["watchlist"] == ["AMD", "TSLA"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_display_timezone_defaults_to_central_when_invalid() -> None:
|
||||||
|
out = normalize_web_settings({"display_timezone": "Europe/London"})
|
||||||
|
assert out["display_timezone"] == "America/Chicago"
|
||||||
|
|
||||||
|
|
||||||
|
def test_display_timezone_accepts_supported_us_timezone() -> None:
|
||||||
|
out = normalize_web_settings({"display_timezone": "America/Los_Angeles"})
|
||||||
|
assert out["display_timezone"] == "America/Los_Angeles"
|
||||||
|
|
||||||
|
|
||||||
|
def test_use_24h_time_defaults_false() -> None:
|
||||||
|
out = normalize_web_settings({})
|
||||||
|
assert out["use_24h_time"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_use_24h_time_normalizes_truthy_values() -> None:
|
||||||
|
out = normalize_web_settings({"use_24h_time": "true"})
|
||||||
|
assert out["use_24h_time"] is True
|
||||||
|
|||||||
27
web/src/tests/test_time_display.py
Normal file
27
web/src/tests/test_time_display.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from web_core.time_display import format_timestamp, normalize_display_timezone
|
||||||
|
|
||||||
|
|
||||||
|
def test_normalize_display_timezone_defaults_to_central() -> None:
|
||||||
|
assert normalize_display_timezone("bad-zone") == "America/Chicago"
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_timestamp_converts_utc_to_central() -> None:
|
||||||
|
stamp = pd.Timestamp("2026-02-17 19:00:00", tz="UTC")
|
||||||
|
out = format_timestamp(stamp, display_timezone="America/Chicago", use_24h_time=True)
|
||||||
|
assert out.endswith("CST")
|
||||||
|
assert "2026-02-17 13:00" in out
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_timestamp_12h_mode_uses_am_pm() -> None:
|
||||||
|
stamp = pd.Timestamp("2026-02-17 19:00:00", tz="UTC")
|
||||||
|
out = format_timestamp(stamp, display_timezone="America/Chicago", use_24h_time=False)
|
||||||
|
assert out.endswith("PM CST")
|
||||||
|
assert "2026-02-17 1:00" in out
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_timestamp_returns_na_for_bad_value() -> None:
|
||||||
|
assert format_timestamp("not-a-timestamp", display_timezone="America/Chicago", use_24h_time=False) == "n/a"
|
||||||
@ -21,6 +21,7 @@ from web_core.auth.profile_auth import (
|
|||||||
resolve_login_profile,
|
resolve_login_profile,
|
||||||
)
|
)
|
||||||
from web_core.settings.settings_schema import normalize_web_settings
|
from web_core.settings.settings_schema import normalize_web_settings
|
||||||
|
from web_core.time_display import format_timestamp
|
||||||
|
|
||||||
SETTINGS_PATH = Path.home() / ".web_local_shell" / "settings.json"
|
SETTINGS_PATH = Path.home() / ".web_local_shell" / "settings.json"
|
||||||
LEGACY_SETTINGS_PATH = Path.home() / ".manesh_trader" / "settings.json"
|
LEGACY_SETTINGS_PATH = Path.home() / ".manesh_trader" / "settings.json"
|
||||||
@ -33,14 +34,19 @@ def _normalize_epoch(value: Any) -> int | None:
|
|||||||
return parsed if parsed >= 0 else None
|
return parsed if parsed >= 0 else None
|
||||||
|
|
||||||
|
|
||||||
def format_epoch(value: Any) -> str:
|
def format_epoch(
|
||||||
|
value: Any,
|
||||||
|
display_timezone: str = "America/Chicago",
|
||||||
|
use_24h_time: bool = False,
|
||||||
|
) -> str:
|
||||||
parsed = _normalize_epoch(value)
|
parsed = _normalize_epoch(value)
|
||||||
if parsed is None:
|
if parsed is None:
|
||||||
return "n/a"
|
return "n/a"
|
||||||
try:
|
return format_timestamp(
|
||||||
return pd.to_datetime(parsed, unit="s", utc=True).strftime("%Y-%m-%d %H:%M UTC")
|
pd.to_datetime(parsed, unit="s", utc=True),
|
||||||
except Exception:
|
display_timezone=display_timezone,
|
||||||
return "n/a"
|
use_24h_time=use_24h_time,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _load_raw_settings_payload() -> dict[str, Any] | None:
|
def _load_raw_settings_payload() -> dict[str, Any] | None:
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import plotly.graph_objects as go
|
|||||||
from plotly.subplots import make_subplots
|
from plotly.subplots import make_subplots
|
||||||
|
|
||||||
from .constants import TREND_BEAR, TREND_BULL, TREND_NEUTRAL
|
from .constants import TREND_BEAR, TREND_BULL, TREND_NEUTRAL
|
||||||
|
from .time_display import format_timestamp
|
||||||
|
|
||||||
|
|
||||||
def _is_intraday_interval(interval: str) -> bool:
|
def _is_intraday_interval(interval: str) -> bool:
|
||||||
@ -53,6 +54,8 @@ def build_figure(
|
|||||||
*,
|
*,
|
||||||
interval: str,
|
interval: str,
|
||||||
hide_market_closed_gaps: bool,
|
hide_market_closed_gaps: bool,
|
||||||
|
display_timezone: str,
|
||||||
|
use_24h_time: bool,
|
||||||
) -> go.Figure:
|
) -> go.Figure:
|
||||||
fig = make_subplots(
|
fig = make_subplots(
|
||||||
rows=2,
|
rows=2,
|
||||||
@ -64,6 +67,9 @@ def build_figure(
|
|||||||
|
|
||||||
bull_mask = df["classification"] == "real_bull"
|
bull_mask = df["classification"] == "real_bull"
|
||||||
bear_mask = df["classification"] == "real_bear"
|
bear_mask = df["classification"] == "real_bear"
|
||||||
|
time_labels = [
|
||||||
|
format_timestamp(ts, display_timezone=display_timezone, use_24h_time=use_24h_time) for ts in df.index
|
||||||
|
]
|
||||||
|
|
||||||
if gray_fake:
|
if gray_fake:
|
||||||
fig.add_trace(
|
fig.add_trace(
|
||||||
@ -77,6 +83,14 @@ def build_figure(
|
|||||||
increasing_line_color="#B0B0B0",
|
increasing_line_color="#B0B0B0",
|
||||||
decreasing_line_color="#808080",
|
decreasing_line_color="#808080",
|
||||||
opacity=0.35,
|
opacity=0.35,
|
||||||
|
customdata=time_labels,
|
||||||
|
hovertemplate=(
|
||||||
|
"Time: %{customdata}<br>"
|
||||||
|
"Open: %{open:.2f}<br>"
|
||||||
|
"High: %{high:.2f}<br>"
|
||||||
|
"Low: %{low:.2f}<br>"
|
||||||
|
"Close: %{close:.2f}<extra>All Bars</extra>"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
row=1,
|
row=1,
|
||||||
col=1,
|
col=1,
|
||||||
@ -93,11 +107,20 @@ def build_figure(
|
|||||||
increasing_line_color="#2E8B57",
|
increasing_line_color="#2E8B57",
|
||||||
decreasing_line_color="#B22222",
|
decreasing_line_color="#B22222",
|
||||||
opacity=0.6,
|
opacity=0.6,
|
||||||
|
customdata=time_labels,
|
||||||
|
hovertemplate=(
|
||||||
|
"Time: %{customdata}<br>"
|
||||||
|
"Open: %{open:.2f}<br>"
|
||||||
|
"High: %{high:.2f}<br>"
|
||||||
|
"Low: %{low:.2f}<br>"
|
||||||
|
"Close: %{close:.2f}<extra>All Bars</extra>"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
row=1,
|
row=1,
|
||||||
col=1,
|
col=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
bull_time_labels = [time_labels[idx] for idx, is_bull in enumerate(bull_mask) if bool(is_bull)]
|
||||||
fig.add_trace(
|
fig.add_trace(
|
||||||
go.Scatter(
|
go.Scatter(
|
||||||
x=df.index[bull_mask],
|
x=df.index[bull_mask],
|
||||||
@ -105,11 +128,14 @@ def build_figure(
|
|||||||
mode="markers",
|
mode="markers",
|
||||||
name="Real Bullish",
|
name="Real Bullish",
|
||||||
marker=dict(color="#00C853", size=9, symbol="triangle-up"),
|
marker=dict(color="#00C853", size=9, symbol="triangle-up"),
|
||||||
|
customdata=bull_time_labels,
|
||||||
|
hovertemplate="Time: %{customdata}<br>Close: %{y:.2f}<extra>Real Bullish</extra>",
|
||||||
),
|
),
|
||||||
row=1,
|
row=1,
|
||||||
col=1,
|
col=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
bear_time_labels = [time_labels[idx] for idx, is_bear in enumerate(bear_mask) if bool(is_bear)]
|
||||||
fig.add_trace(
|
fig.add_trace(
|
||||||
go.Scatter(
|
go.Scatter(
|
||||||
x=df.index[bear_mask],
|
x=df.index[bear_mask],
|
||||||
@ -117,6 +143,8 @@ def build_figure(
|
|||||||
mode="markers",
|
mode="markers",
|
||||||
name="Real Bearish",
|
name="Real Bearish",
|
||||||
marker=dict(color="#D50000", size=9, symbol="triangle-down"),
|
marker=dict(color="#D50000", size=9, symbol="triangle-down"),
|
||||||
|
customdata=bear_time_labels,
|
||||||
|
hovertemplate="Time: %{customdata}<br>Close: %{y:.2f}<extra>Real Bearish</extra>",
|
||||||
),
|
),
|
||||||
row=1,
|
row=1,
|
||||||
col=1,
|
col=1,
|
||||||
@ -136,6 +164,8 @@ def build_figure(
|
|||||||
marker_color=trend_color,
|
marker_color=trend_color,
|
||||||
name="Volume",
|
name="Volume",
|
||||||
opacity=0.65,
|
opacity=0.65,
|
||||||
|
customdata=time_labels,
|
||||||
|
hovertemplate="Time: %{customdata}<br>Volume: %{y}<extra>Volume</extra>",
|
||||||
),
|
),
|
||||||
row=2,
|
row=2,
|
||||||
col=1,
|
col=1,
|
||||||
|
|||||||
@ -4,6 +4,7 @@ from typing import Any
|
|||||||
|
|
||||||
from web_core.constants import INTERVAL_OPTIONS, PERIOD_OPTIONS
|
from web_core.constants import INTERVAL_OPTIONS, PERIOD_OPTIONS
|
||||||
from web_core.market.presets import MARKET_PRESET_OPTIONS
|
from web_core.market.presets import MARKET_PRESET_OPTIONS
|
||||||
|
from web_core.time_display import DEFAULT_DISPLAY_TIMEZONE, normalize_display_timezone
|
||||||
|
|
||||||
|
|
||||||
def _clamp_int(value: Any, fallback: int, minimum: int, maximum: int) -> int:
|
def _clamp_int(value: Any, fallback: int, minimum: int, maximum: int) -> int:
|
||||||
@ -124,6 +125,8 @@ def normalize_web_settings(raw: dict[str, Any] | None) -> dict[str, Any]:
|
|||||||
"backtest_take_profit_pct": 0.0,
|
"backtest_take_profit_pct": 0.0,
|
||||||
"backtest_min_hold_bars": 1,
|
"backtest_min_hold_bars": 1,
|
||||||
"backtest_max_hold_bars": 1,
|
"backtest_max_hold_bars": 1,
|
||||||
|
"display_timezone": DEFAULT_DISPLAY_TIMEZONE,
|
||||||
|
"use_24h_time": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
symbol = str(raw.get("symbol", defaults["symbol"])).strip().upper()
|
symbol = str(raw.get("symbol", defaults["symbol"])).strip().upper()
|
||||||
@ -249,4 +252,9 @@ def normalize_web_settings(raw: dict[str, Any] | None) -> dict[str, Any]:
|
|||||||
),
|
),
|
||||||
"backtest_min_hold_bars": backtest_min_hold_bars,
|
"backtest_min_hold_bars": backtest_min_hold_bars,
|
||||||
"backtest_max_hold_bars": backtest_max_hold_bars,
|
"backtest_max_hold_bars": backtest_max_hold_bars,
|
||||||
|
"display_timezone": normalize_display_timezone(
|
||||||
|
raw.get("display_timezone"),
|
||||||
|
fallback=str(defaults["display_timezone"]),
|
||||||
|
),
|
||||||
|
"use_24h_time": _to_bool(raw.get("use_24h_time"), fallback=bool(defaults["use_24h_time"])),
|
||||||
}
|
}
|
||||||
|
|||||||
46
web/src/web_core/time_display.py
Normal file
46
web/src/web_core/time_display.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
US_TIMEZONE_OPTIONS: list[str] = [
|
||||||
|
"America/New_York",
|
||||||
|
"America/Chicago",
|
||||||
|
"America/Denver",
|
||||||
|
"America/Los_Angeles",
|
||||||
|
"America/Phoenix",
|
||||||
|
"America/Anchorage",
|
||||||
|
"Pacific/Honolulu",
|
||||||
|
]
|
||||||
|
|
||||||
|
DEFAULT_DISPLAY_TIMEZONE = "America/Chicago"
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_display_timezone(value: Any, fallback: str = DEFAULT_DISPLAY_TIMEZONE) -> str:
|
||||||
|
candidate = str(value or "").strip()
|
||||||
|
if candidate in US_TIMEZONE_OPTIONS:
|
||||||
|
return candidate
|
||||||
|
return fallback if fallback in US_TIMEZONE_OPTIONS else DEFAULT_DISPLAY_TIMEZONE
|
||||||
|
|
||||||
|
|
||||||
|
def format_timestamp(
|
||||||
|
value: Any,
|
||||||
|
display_timezone: str = DEFAULT_DISPLAY_TIMEZONE,
|
||||||
|
use_24h_time: bool = False,
|
||||||
|
) -> str:
|
||||||
|
try:
|
||||||
|
ts = pd.Timestamp(value)
|
||||||
|
except Exception:
|
||||||
|
return "n/a"
|
||||||
|
|
||||||
|
tz_name = normalize_display_timezone(display_timezone)
|
||||||
|
try:
|
||||||
|
if ts.tzinfo is None:
|
||||||
|
ts = ts.tz_localize("UTC")
|
||||||
|
converted = ts.tz_convert(tz_name)
|
||||||
|
if use_24h_time:
|
||||||
|
return converted.strftime("%Y-%m-%d %H:%M %Z")
|
||||||
|
return converted.strftime("%Y-%m-%d %I:%M %p %Z").replace(" 0", " ", 1)
|
||||||
|
except Exception:
|
||||||
|
return "n/a"
|
||||||
@ -139,6 +139,10 @@ then checking if your guess was right.
|
|||||||
Why care: keeps data fresh while you watch.
|
Why care: keeps data fresh while you watch.
|
||||||
- `Refresh interval (seconds)`
|
- `Refresh interval (seconds)`
|
||||||
Why care: lower is faster updates, but more load.
|
Why care: lower is faster updates, but more load.
|
||||||
|
- `Display Timezone (US)`
|
||||||
|
Why care: shows audit/event/training times in your preferred U.S. timezone (default CST/CDT).
|
||||||
|
- `Use 24-hour time`
|
||||||
|
Why care: choose `13:00` style (ON) or `1:00 PM` style (OFF).
|
||||||
|
|
||||||
### Advanced Signals
|
### Advanced Signals
|
||||||
- `Auto-run advanced panels (slower)`
|
- `Auto-run advanced panels (slower)`
|
||||||
|
|||||||
@ -21,6 +21,8 @@ from web_core.auth.profile_store import (
|
|||||||
|
|
||||||
def render_profile_login(now_epoch: float, query_params: Any, session_expired: bool) -> None:
|
def render_profile_login(now_epoch: float, query_params: Any, session_expired: bool) -> None:
|
||||||
existing_profiles = set(list_web_profiles())
|
existing_profiles = set(list_web_profiles())
|
||||||
|
_, centered_col, _ = st.columns([1, 1.8, 1])
|
||||||
|
with centered_col:
|
||||||
st.subheader("Profile Login")
|
st.subheader("Profile Login")
|
||||||
st.info("Login with an existing profile or create a new one. Settings are isolated per profile.")
|
st.info("Login with an existing profile or create a new one. Settings are isolated per profile.")
|
||||||
if session_expired:
|
if session_expired:
|
||||||
|
|||||||
@ -9,6 +9,7 @@ from web_core.constants import INTERVAL_OPTIONS, PERIOD_OPTIONS
|
|||||||
from web_core.ui.help_content import help_dialog
|
from web_core.ui.help_content import help_dialog
|
||||||
from web_core.market.presets import MARKET_PRESET_OPTIONS, apply_market_preset
|
from web_core.market.presets import MARKET_PRESET_OPTIONS, apply_market_preset
|
||||||
from web_core.market.symbols import lookup_symbol_candidates
|
from web_core.market.symbols import lookup_symbol_candidates
|
||||||
|
from web_core.time_display import US_TIMEZONE_OPTIONS
|
||||||
from web_core.auth.profile_store import (
|
from web_core.auth.profile_store import (
|
||||||
first_query_param_value,
|
first_query_param_value,
|
||||||
format_epoch,
|
format_epoch,
|
||||||
@ -33,6 +34,25 @@ def _parse_watchlist(raw: str) -> list[str]:
|
|||||||
|
|
||||||
def render_sidebar(active_profile: str, query_params: Any) -> dict[str, Any]:
|
def render_sidebar(active_profile: str, query_params: Any) -> dict[str, Any]:
|
||||||
with st.sidebar:
|
with st.sidebar:
|
||||||
|
persisted_settings = load_web_settings(profile_id=active_profile)
|
||||||
|
query_overrides: dict[str, Any] = {}
|
||||||
|
for key in persisted_settings:
|
||||||
|
candidate = first_query_param_value(query_params, key)
|
||||||
|
if candidate is not None:
|
||||||
|
query_overrides[key] = candidate
|
||||||
|
effective_defaults = normalize_web_settings({**persisted_settings, **query_overrides})
|
||||||
|
display_timezone = st.selectbox(
|
||||||
|
"Display Timezone (US)",
|
||||||
|
US_TIMEZONE_OPTIONS,
|
||||||
|
index=US_TIMEZONE_OPTIONS.index(str(effective_defaults["display_timezone"])),
|
||||||
|
help="Controls how timestamps are shown in the sidebar and training/event tables.",
|
||||||
|
)
|
||||||
|
use_24h_time = st.checkbox(
|
||||||
|
"Use 24-hour time",
|
||||||
|
value=bool(effective_defaults["use_24h_time"]),
|
||||||
|
help="On: 13:00 format. Off: 1:00 PM format.",
|
||||||
|
)
|
||||||
|
|
||||||
st.header("Profile")
|
st.header("Profile")
|
||||||
st.success(f"Logged in as: {active_profile}")
|
st.success(f"Logged in as: {active_profile}")
|
||||||
if st.button("Switch profile", use_container_width=True):
|
if st.button("Switch profile", use_container_width=True):
|
||||||
@ -50,9 +70,9 @@ def render_sidebar(active_profile: str, query_params: Any) -> dict[str, Any]:
|
|||||||
)
|
)
|
||||||
st.caption(
|
st.caption(
|
||||||
"Audit: created "
|
"Audit: created "
|
||||||
f"{format_epoch(profile_audit.get('created_at'))}, "
|
f"{format_epoch(profile_audit.get('created_at'), display_timezone=display_timezone, use_24h_time=use_24h_time)}, "
|
||||||
f"last login {format_epoch(profile_audit.get('last_login_at'))}, "
|
f"last login {format_epoch(profile_audit.get('last_login_at'), display_timezone=display_timezone, use_24h_time=use_24h_time)}, "
|
||||||
f"updated {format_epoch(profile_audit.get('updated_at'))}, "
|
f"updated {format_epoch(profile_audit.get('updated_at'), display_timezone=display_timezone, use_24h_time=use_24h_time)}, "
|
||||||
f"last symbol {str(profile_audit.get('last_symbol') or 'n/a')}"
|
f"last symbol {str(profile_audit.get('last_symbol') or 'n/a')}"
|
||||||
)
|
)
|
||||||
st.divider()
|
st.divider()
|
||||||
@ -62,14 +82,6 @@ def render_sidebar(active_profile: str, query_params: Any) -> dict[str, Any]:
|
|||||||
help_dialog()
|
help_dialog()
|
||||||
st.divider()
|
st.divider()
|
||||||
|
|
||||||
persisted_settings = load_web_settings(profile_id=active_profile)
|
|
||||||
query_overrides: dict[str, Any] = {}
|
|
||||||
for key in persisted_settings:
|
|
||||||
candidate = first_query_param_value(query_params, key)
|
|
||||||
if candidate is not None:
|
|
||||||
query_overrides[key] = candidate
|
|
||||||
effective_defaults = normalize_web_settings({**persisted_settings, **query_overrides})
|
|
||||||
|
|
||||||
st.subheader("Market Preset")
|
st.subheader("Market Preset")
|
||||||
preset_default = str(effective_defaults["market_preset"])
|
preset_default = str(effective_defaults["market_preset"])
|
||||||
preset_index = MARKET_PRESET_OPTIONS.index(preset_default) if preset_default in MARKET_PRESET_OPTIONS else 0
|
preset_index = MARKET_PRESET_OPTIONS.index(preset_default) if preset_default in MARKET_PRESET_OPTIONS else 0
|
||||||
@ -347,4 +359,6 @@ def render_sidebar(active_profile: str, query_params: Any) -> dict[str, Any]:
|
|||||||
"backtest_take_profit_pct": float(backtest_take_profit_pct),
|
"backtest_take_profit_pct": float(backtest_take_profit_pct),
|
||||||
"backtest_min_hold_bars": int(backtest_min_hold_bars),
|
"backtest_min_hold_bars": int(backtest_min_hold_bars),
|
||||||
"backtest_max_hold_bars": int(backtest_max_hold_bars),
|
"backtest_max_hold_bars": int(backtest_max_hold_bars),
|
||||||
|
"display_timezone": str(display_timezone),
|
||||||
|
"use_24h_time": bool(use_24h_time),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,16 @@ from __future__ import annotations
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
|
|
||||||
|
from web_core.time_display import format_timestamp
|
||||||
|
|
||||||
def render_training_panel(show_past_behavior: bool, example_trades: pd.DataFrame, alert_key: str) -> pd.Series | None:
|
|
||||||
|
def render_training_panel(
|
||||||
|
show_past_behavior: bool,
|
||||||
|
example_trades: pd.DataFrame,
|
||||||
|
alert_key: str,
|
||||||
|
display_timezone: str,
|
||||||
|
use_24h_time: bool,
|
||||||
|
) -> pd.Series | None:
|
||||||
selected_trade: pd.Series | None = None
|
selected_trade: pd.Series | None = None
|
||||||
if not show_past_behavior:
|
if not show_past_behavior:
|
||||||
return selected_trade
|
return selected_trade
|
||||||
@ -27,10 +35,20 @@ def render_training_panel(show_past_behavior: bool, example_trades: pd.DataFrame
|
|||||||
t4.metric("Avg P/L per Example", f"{avg_pnl}%")
|
t4.metric("Avg P/L per Example", f"{avg_pnl}%")
|
||||||
|
|
||||||
latest_example = example_trades.iloc[-1]
|
latest_example = example_trades.iloc[-1]
|
||||||
|
latest_entry_ts = format_timestamp(
|
||||||
|
latest_example["entry_timestamp"],
|
||||||
|
display_timezone=display_timezone,
|
||||||
|
use_24h_time=use_24h_time,
|
||||||
|
)
|
||||||
|
latest_exit_ts = format_timestamp(
|
||||||
|
latest_example["exit_timestamp"],
|
||||||
|
display_timezone=display_timezone,
|
||||||
|
use_24h_time=use_24h_time,
|
||||||
|
)
|
||||||
st.caption(
|
st.caption(
|
||||||
"Latest closed example: "
|
"Latest closed example: "
|
||||||
f"{latest_example['direction']} from {latest_example['entry_timestamp']} "
|
f"{latest_example['direction']} from {latest_entry_ts} "
|
||||||
f"to {latest_example['exit_timestamp']} "
|
f"to {latest_exit_ts} "
|
||||||
f"({latest_example['bars_held']} bars, {latest_example['pnl_pct']}%)."
|
f"({latest_example['bars_held']} bars, {latest_example['pnl_pct']}%)."
|
||||||
)
|
)
|
||||||
st.caption("Click a row below to highlight that specific example on the chart.")
|
st.caption("Click a row below to highlight that specific example on the chart.")
|
||||||
@ -38,8 +56,12 @@ def render_training_panel(show_past_behavior: bool, example_trades: pd.DataFrame
|
|||||||
|
|
||||||
display_examples_raw = example_trades.iloc[::-1].reset_index(drop=True)
|
display_examples_raw = example_trades.iloc[::-1].reset_index(drop=True)
|
||||||
display_examples = display_examples_raw.copy()
|
display_examples = display_examples_raw.copy()
|
||||||
display_examples["entry_timestamp"] = display_examples["entry_timestamp"].astype(str)
|
display_examples["entry_timestamp"] = display_examples["entry_timestamp"].map(
|
||||||
display_examples["exit_timestamp"] = display_examples["exit_timestamp"].astype(str)
|
lambda value: format_timestamp(value, display_timezone=display_timezone, use_24h_time=use_24h_time)
|
||||||
|
)
|
||||||
|
display_examples["exit_timestamp"] = display_examples["exit_timestamp"].map(
|
||||||
|
lambda value: format_timestamp(value, display_timezone=display_timezone, use_24h_time=use_24h_time)
|
||||||
|
)
|
||||||
|
|
||||||
table_event = st.dataframe(
|
table_event = st.dataframe(
|
||||||
display_examples,
|
display_examples,
|
||||||
@ -66,8 +88,16 @@ def render_training_panel(show_past_behavior: bool, example_trades: pd.DataFrame
|
|||||||
|
|
||||||
selected_trade = display_examples_raw.iloc[selected_row_idx]
|
selected_trade = display_examples_raw.iloc[selected_row_idx]
|
||||||
direction = str(selected_trade["direction"])
|
direction = str(selected_trade["direction"])
|
||||||
entry_ts = str(selected_trade["entry_timestamp"])
|
entry_ts = format_timestamp(
|
||||||
exit_ts = str(selected_trade["exit_timestamp"])
|
selected_trade["entry_timestamp"],
|
||||||
|
display_timezone=display_timezone,
|
||||||
|
use_24h_time=use_24h_time,
|
||||||
|
)
|
||||||
|
exit_ts = format_timestamp(
|
||||||
|
selected_trade["exit_timestamp"],
|
||||||
|
display_timezone=display_timezone,
|
||||||
|
use_24h_time=use_24h_time,
|
||||||
|
)
|
||||||
entry_price = float(selected_trade["entry_price"])
|
entry_price = float(selected_trade["entry_price"])
|
||||||
exit_price = float(selected_trade["exit_price"])
|
exit_price = float(selected_trade["exit_price"])
|
||||||
bars_held = int(selected_trade["bars_held"])
|
bars_held = int(selected_trade["bars_held"])
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user