Compare commits

...

238 Commits

Author SHA1 Message Date
dekun 61481d5749 Fix recommend table missing trend/daily stats when CTP klines are sparse.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-03 13:00:03 +08:00
dekun 08be5f34c8 Fix dashboard daily loss risk cell text overlap
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-03 12:49:51 +08:00
dekun 2081bf2da9 Add daily loss force-flatten at configurable equity limit
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-03 12:42:13 +08:00
dekun b6c3266a9e Fix daily risk and open count deduping by position slot.
Count each symbol+direction once per trading day; use realized loss for closed slots instead of summing duplicate monitor rows.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-03 09:42:00 +08:00
dekun d46cd7c3e1 Use VeighNa OffsetConverter for SHFE close today/yesterday split.
Feed CTP PositionDate-corrected positions to converter like CTA engine sell/cover.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-03 09:35:20 +08:00
dekun 2ec534c547 Use CTP PositionDate from investor position query for SHFE close offset.
Parse official today/history legs in onRspQryInvestorPosition instead of guessing or retrying alternate offsets.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-03 09:30:38 +08:00
dekun 0bade8a01f Fix SHFE close by retrying alternate today/yesterday offset on CTP reject.
Split mixed positions and auto-fallback when vnpy td/yd volumes disagree with the exchange.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-03 09:26:38 +08:00
dekun 1cd3039605 Fix roll margin validation and SHFE close offset for night positions.
Use CTP account margin for roll cap checks and prefer close-today on SHFE when yesterday close is rejected.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-03 09:22:17 +08:00
dekun 50bb04e2bb Fix stop-loss close loop spamming WeChat and blocking manual close.
Throttle close retries, skip monitor revive while pending, and dedupe notifications when CTP already has a close order.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-03 09:12:27 +08:00
dekun 6b2c7ade95 Use sina quote fallback for recommend list when CTP tick missing.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-03 06:55:14 +08:00
dekun 16d90814a1 Fix empty recommend list when CTP ticks unavailable off-hours.
Fall back to daily prev_close for margin estimates; keep previous cache when refresh gets all no_price; stop re-fetching only for missing turnover.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-03 06:43:20 +08:00
dekun 125cc60a3d Fix positions 500 when recommend row lacks turnover field.
Use Jinja defined check in trade template and always set turnover key (or null) when enriching recommend rows.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-03 06:34:12 +08:00
dekun c5b822a92e Speed up strategy page by removing blocking CTP sync on load.
Replace per-request CTP monitor sync with a lightweight roll-monitor revive; skip mark-price enrichment on pending legs during HTML render.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 23:16:53 +08:00
dekun ae28bc6273 Count roll groups as one position slot and protect monitors during roll.
Include active roll groups in deduped position slots; normalize CTP position keys in reconcile; skip closing monitors tied to active roll groups.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 23:04:34 +08:00
dekun 93bdce3568 Prefer monitor slots for position limit and fix dashboard sticky count.
When active monitors exist, use their deduped symbol+direction slots so roll does not inflate the limit; only hold previous active_count on SSE when incoming drops to zero during sync.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 23:01:50 +08:00
dekun 6710692dfb Fix position limit to count distinct slots and speed up strategy page.
Roll/add-on shares one monitor per symbol+direction so it no longer inflates the active position count; strategy page skips full CTP sync when monitors already exist.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 22:49:32 +08:00
dekun 530738ae93 Fix roll first-lots display and make market add use pending orders.
Store initial_lots on roll groups, submit market roll as CTP pending legs with cancel closing empty groups, and backfill first-lots for existing active groups.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 22:12:04 +08:00
dekun 3c53b2063f Sync CTP monitors on strategy page load for roll trading.
Revive and sync active monitors from live positions before listing roll candidates, show ineligible monitors with reasons, and validate eligibility on preview.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 21:55:50 +08:00
dekun 4657d26f5e Fix strategy roll page JS broken by Jinja in static file.
Pass trading_session via inline page config, restore roll preview/execute handlers, and validate new stop-loss before preview.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 21:46:08 +08:00
dekun 075fae37ec Fix float P/L when using OpenCost entry and clear CTP hint on connect.
Recalculate position quotes from resolved entry instead of stale CTP PositionProfit, and hide the auto-connect disabled banner once CTP is connected.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 21:24:48 +08:00
dekun dca773d6be Fix CTP open average price direction mapping and resolution order.
Correct PosiDirection 2=long/3=short so OpenCost caches under the right key, prefer open_cost over PositionCost for entry and float P/L, and refresh the cache when incomplete.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 21:13:28 +08:00
dekun 870dfb3bc0 Fix CTP position average price using OpenCost instead of PositionCost.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 20:53:15 +08:00
dekun b0afff53af Ensure scheduled auto backups explicitly include .env in backup archives.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 16:38:51 +08:00
dekun 8ebe1a3c77 Remove PostgreSQL support and standardize on SQLite only.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 16:21:27 +08:00
dekun 9379bc4f4f Add frontend backup upload and list-based restore with validation.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 16:03:18 +08:00
dekun 481086eddc Collapse dashboard server status into top bar and include .env in backup restore.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 15:43:48 +08:00
dekun aae897b7eb Add server status card to dashboard with public and private IP display.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 15:36:55 +08:00
dekun 5328673ce8 Add separate kline.db and pre-seed small-account four-product K-lines on startup.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 15:22:52 +08:00
dekun 972ab5d08b Route business quotes and K-lines to CTP; keep Sina only for market chart page.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 15:14:13 +08:00
dekun 98d63f38bf Fix dashboard position limit flicker by unifying active count across APIs.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 14:53:04 +08:00
dekun 658982d2b9 Speed up CTP account display: refresh equity on fast snapshot without waiting for full position sync.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 15:02:37 +08:00
dekun a34bcb6bfc Fix test_simnow.py imports for modules layout and config/.env.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 14:47:27 +08:00
dekun e5a586f903 Restructure into modules/ with single-process CTP and config/ layout.
Move business code under modules/, env template to config/, PM2 single qihuo process, and _legacy shims for old imports.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 14:42:16 +08:00
dekun b354d6c701 Document Git-only deploy workflow and reduce positions page IPC blocking.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 14:05:37 +08:00
dekun 7748a88219 Fix positions page hang from blocking CTP IPC and SSE lock wait.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 13:33:59 +08:00
dekun 5aba31f530 Fix position SSE JSON serialization for datetime fields.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 13:27:46 +08:00
dekun 95156ca595 Update docs for CTP worker split and roll breakout off-session.
Refresh DEPLOY, TRADING, STRATEGY, CTP_LIVE, FEATURES, INDEX, and README to document qihuo-ctp architecture, dual PM2 restarts, and休盘突破加仓.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 12:56:27 +08:00
dekun 9cd81a3ea7 Isolate CTP in worker process and improve strategy roll UX.
Split vn.py into qihuo-ctp worker with IPC client bridge, keep CTP connected during breaks with cached account fallback, speed up strategy page loads, and allow off-session breakout roll submissions.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 12:37:44 +08:00
dekun 08d55411aa Simplify account_risk_state DDL for PostgreSQL compatibility.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 08:31:17 +08:00
dekun cd636ff1c3 Fix account_risk_state missing on PostgreSQL: probe table before cache skip.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 08:29:46 +08:00
dekun d7ea7b9e8a Inline account_risk_state DDL in init_db; use autocommit for migration.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 08:26:13 +08:00
dekun 06fbff04a7 Ensure account_risk_state table exists even if schema flag was set.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 08:25:07 +08:00
dekun ef790c8a80 Fix migration: per-table connections and skip non-serial setval.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 08:24:27 +08:00
dekun 6abe06d935 Ensure all PG tables on init; fix migration commits per table.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 08:22:54 +08:00
dekun e418c2dcec Fix SQLite migration: no app import, commit per table.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 08:22:21 +08:00
dekun 1bb0028402 Import rollback_if_postgres in strategy_db for PostgreSQL migrations.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 08:21:35 +08:00
dekun 8e1baf9a73 Commit each schema migration separately on PostgreSQL.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 08:20:49 +08:00
dekun 47ee9602d1 Use direct PostgreSQL connections instead of pool to fix init deploy.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 08:19:46 +08:00
dekun 843b68b412 Stop PM2 before PostgreSQL init to free connection pool.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 08:17:29 +08:00
dekun 2347276bf4 Fix PostgreSQL schema init: commit DDL before ALTER migrations.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 08:15:31 +08:00
dekun 008f042fcc Fix init-only mode for PostgreSQL deploy script imports.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 08:14:43 +08:00
dekun a1f22624de Harden PostgreSQL deploy: auto-rollback on SQL errors, clean DB on migrate.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 08:13:51 +08:00
dekun bc79ad308c Fix PostgreSQL init_db: rollback after benign schema migration errors.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 08:13:14 +08:00
dekun 1b276b5897 Fix PostgreSQL DDL: use single-quoted DEFAULT literals.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 08:12:34 +08:00
dekun 52aca456e9 Add PostgreSQL production backend to eliminate SQLite lock contention.
Support DATABASE_URL with connection pooling, pg_dump backups, SQLite migration script, and deploy_postgres.sh with docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 08:11:42 +08:00
dekun 39eac983ff Fix positions and records pages hanging on load.
Stop blocking page render on full trading live rebuild and CTP trade sync; use cached snapshot and background prime instead.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 23:31:13 +08:00
dekun 1b3a7f1bdc Fix position flicker, drop futures cooloff, prioritize startup display.
Preserve trading state when CTP memory is empty, bootstrap equity/positions on page load, stabilize risk status from DB monitors, and remove app-layer manual close cooling periods.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 23:17:18 +08:00
dekun 2386eca324 Fix stats max profit/loss to use win/loss subsets only.
Corrects misleading breakdown rows and summary max amounts after trade log edits.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 22:28:46 +08:00
dekun d324d30332 Schedule CTP disconnect 30 minutes after day and night session close.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 22:14:39 +08:00
dekun 32838daae0 Align trade record verify/edit UX with Binance instance single-row prompts.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 22:02:48 +08:00
dekun b6b7bfb248 Add close price column to trade records for overnight position PnL review.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 21:51:53 +08:00
dekun 4552f4ef9c Default equity chain baseline to 100k when live_capital unset.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 21:44:15 +08:00
dekun 0b924fca87 Fix trade log equity_after to chain from initial capital by close time.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 21:41:37 +08:00
dekun 8d2d09396b Fix missing trade log after manual close when CTP is connected.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 21:23:44 +08:00
dekun e5f264b774 Increase trade calendar cell font sizes for readability. 2026-06-30 12:11:57 +08:00
dekun 726ed7adef Add standalone trade calendar page with solar and lunar dates.
Move calendar from stats to /calendar nav page; show Gregorian and lunar on cells, month title, and day detail.
2026-06-30 12:07:58 +08:00
dekun 8ebad6e8a2 Add stats trading calendar and fix CTP position avg/sync.
Calendar shows daily closed trade count and PnL with emotion-day highlighting; day click loads review-first trade list. Use exchange-only entry average and improve vnpy position sync after CTP reconnect.
2026-06-30 11:59:25 +08:00
dekun d07fc4b70d Prefer CTP PnL-consistent entry when vnpy avg differs from SimNow.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 10:51:16 +08:00
dekun e6208e403e Fix roll average entry: CTP trade-weighted avg, sync after fill, live entry for preview.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 10:42:56 +08:00
dekun 6e954da4e1 Lock CTP entry price from position PnL snapshot; match SimNow avg and float PnL.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 10:36:06 +08:00
dekun 6c68808318 Stop entry price jumping: use fixed CTP position avg, not tick-derived.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 10:10:54 +08:00
dekun fb23ee891c Resolve position average entry from CTP trades and PnL instead of stale monitor cache.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 09:59:02 +08:00
dekun 2f58159372 Fix slow positions and DB lock on fast live refresh by skipping writes.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 09:29:42 +08:00
dekun ec4199b82c Restore stop-loss and take-profit monitors after restart and CTP reconnect.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 09:24:37 +08:00
dekun 963ed141e5 Persist CTP average entry price to monitor DB on every position sync.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 09:22:10 +08:00
dekun f41276806e Sync displayed entry price from CTP exchange position average.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 09:19:27 +08:00
dekun c8fef65de7 Fix float PnL to match displayed entry price and contract multiplier.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 09:14:13 +08:00
dekun 92c222584e Show tablet trade records as close-record table with action column.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 00:12:10 +08:00
dekun a6b3c4a657 Align tablet trade records with close-record layout and row actions.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 00:05:33 +08:00
dekun 21d68f6269 Use compact single-row trade records on tablet with detail modal.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 23:59:02 +08:00
dekun 9dbf6b1f1e Fix position cards, breakeven badge, and tablet equal-height layout.
Stop clipping pos cards and match trailing-BE stop detection for the badge. On tablet, align order and live-trading panels to equal height with internal scroll; keep desktop positions scrollable after three cards.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 23:47:59 +08:00
dekun c6c6c3fe83 Replace K-line SMA with configurable EMA periods.
Default to EMA 21/55 with editable period inputs and localStorage persistence on the market chart.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 23:17:18 +08:00
dekun 2f5b5c4aae Fix dashboard mobile load issues and simplify card layout.
Reduce poll pressure on phone/tablet, cache key prices, and handle live API errors gracefully. Rework mobile position and close cards with inline direction, compact P/L line, and detail modal.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 23:10:20 +08:00
dekun d1ad0f9253 Improve dashboard responsive layout, collapsible risk section, and breakeven badge.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 22:50:48 +08:00
dekun 8b4b1a875c Fix composite margin ratio cap at 50% and add risk guide page with nav toggle.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 22:29:13 +08:00
dekun 9a10ac8a51 Improve dashboard risk card styling, colors, and add risk control docs.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 22:14:11 +08:00
dekun d8c6428eb5 Enhance dashboard with exchange labels, split SL/TP columns, and daily risk limits.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 22:04:44 +08:00
dekun b460c6c4e5 Enhance dashboard with contract symbols and risk control overview.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 21:40:53 +08:00
dekun 28c54b1a3f Add real-time data dashboard with account, positions, keys, and closes.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 21:30:33 +08:00
dekun df79017b30 Stream real-time position quotes via tick-driven SSE with incremental UI updates.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 21:14:41 +08:00
dekun 94c566fbe5 Allow scheduled CTP connect when auto-connect setting is off.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 20:38:30 +08:00
dekun c5262a0a54 Add responsive mobile layout, records cards, and tablet settings fold fix.
Mobile gets compact trade/records UI with detail modals; static assets are cache-busted and settings cards fold correctly on tablet grid layout.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 16:42:38 +08:00
dekun 44bec23296 Add futures roll strategy with breakout monitoring and fixed-amount sizing.
Replace percent-based risk with system fixed amount, support market/breakout add modes only, allow pending submission outside trading hours, and fix short breakout geometry plus route registration.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 12:05:21 +08:00
dekun 7ce59d2d71 Detect 10:15-10:30 morning break in trading session clock.
Split day session into 9:00-10:15 and 10:30-11:30; show 上午休盘 status and countdown to 10:30 reopen.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 10:27:21 +08:00
dekun 19676943d0 Align margin display with CTP counter rates and position margin.
Read margin ratios from CTP instrument query and margin-rate API instead of vnpy ContractData (which lacks ratios). Keep occupied margin on position UseMargin; use per-lot max rate for recommend table.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 10:21:44 +08:00
dekun 71c480a587 Deduct open position margin from recommend max lots.
Recalculate tradable symbol budgets from remaining margin after CTP usage and refresh the table on position updates.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 09:39:42 +08:00
dekun fd2dba22fd Add trailing BE to SL/TP dialog and speed up position refresh.
Use modal for monitor upsert with trailing checkbox, refresh CTP tick every second, and push full snapshot after orders.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 09:32:56 +08:00
dekun d366344b0f Fix trailing break-even stop loss not linked to CTP positions.
Match pending monitors to filled positions, reconcile pending on fast refresh, and refresh CTP before promote.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 09:26:54 +08:00
dekun 45ae57ed43 Fix lot preview crash and improve key monitor WeChat alerts.
Unpack margin_one_lot correctly for fixed-amount sizing, use exchange klines for breakout detection, and notify on pending auto orders.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 09:21:27 +08:00
dekun b02c9d6ca0 Fix empty recommend list and CTP premarket auto-connect.
Correct main_code order in product refresh, refresh on CTP connect, and limit reconnect to trading or premarket windows.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 08:24:10 +08:00
dekun f8ff97f93d Add collapsible rule descriptions to trade, strategy, and key pages.
Shared module-rules styling; key monitor rules collapsed by default like other modules.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 07:46:58 +08:00
dekun 4d2463c9a9 Show Tonghuashun symbol meta inline after the variety label.
Move symbol-selected into the label row across all symbol pickers without shifting the input field.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 07:34:05 +08:00
dekun bfb1b95471 Improve key monitor form with bar period, box direction, and labeled fields.
Match order-monitor layout; persist bar_period and enforce upper-direction filter for box breakouts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 07:24:36 +08:00
dekun 169136dd4a Add modular docs with index, WeChat templates, and AI guide.
Document per-module order logic, risk rules, and WeChat message templates with a central INDEX for navigation.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 10:49:10 +08:00
dekun 840e88daad Add key-level auto trade, AI analysis, and trading UX improvements.
Key monitors use 5m close triggers with WeChat alerts and box/convergence auto orders; add pending-order worker, structured WeChat notify, AI settings/messages, session clock, CTP margin sizing, and dual-layer position limits.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 10:36:56 +08:00
dekun 0109b59f27 Use 100k equity for recommend list when CTP is offline.
When SimNow or live CTP is disconnected, the tradable-products section shows four whitelisted symbols and calculates max lots from a fixed 100,000 capital instead of reference capital in settings.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 09:16:22 +08:00
dekun e18d5feb72 Apply 200k scope when CTP offline; trailing breakeven order UX.
When SimNow or live CTP is disconnected, default to the four-product whitelist regardless of reference capital. Trailing breakeven defaults off; when enabled hide take-profit and risk-reward, monitor exits via trailing stop only. Document both behaviors in TRADING.md and FEATURES.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-27 23:57:11 +08:00
dekun 4f4c4bb9fc Fix missing recommend_payload import; raise small-account cap to 200k.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-27 23:27:19 +08:00
dekun 7bb80ba538 Limit tradable products to four varieties for accounts at or below 100k.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-27 23:18:27 +08:00
dekun 24190bf679 Add period selector and crypto-style trend plan preview table.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 22:52:47 +08:00
dekun d3b92de703 Fix trend strategy symbol input clearing on every keystroke.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 22:40:33 +08:00
dekun 69cf94d1df Fix symbol dropdown broken by JS syntax error in symbol.js.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 22:31:46 +08:00
dekun 7f8b4cfefd Label night-session products and hide day-only symbols at night.
Mark tradable varieties with a night tag; during 21:00-02:30 filter out index futures and other products without night sessions from symbol picker and recommend list.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 22:27:47 +08:00
dekun f2940d41e9 Fix stale pending orders after CTP rejection or cancel.
When the exchange rejects or cancels an order, close local pending monitors once the order leaves CTP active list instead of waiting for the full timeout.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 22:21:15 +08:00
dekun fd49b08c08 Disable turbo client navigation to fix broken page layouts.
Turbo swapping broke page CSS/JS across stats, settings, market, and trade. Restore full page loads; keep external base.css and link prefetch.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 22:05:04 +08:00
dekun 9613fb0737 Fix turbo nav for settings and stats pages.
Extract settings.js, preserve inline scripts from raw HTML (DOMParser strips them), and load trade config via JSON script tag.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 21:26:15 +08:00
dekun 6d55a54946 Fix turbo nav layout flash and stats page not loading.
Wait for page CSS before swapping content, hoist inline styles to head, and boot page scripts immediately when DOM markers exist.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 20:55:36 +08:00
dekun c79bb2ea4b Speed up top nav with turbo routing and external base CSS.
Remove view-transition lag, swap main content without full reload, prefetch pages, and tear down SSE timers on leave.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 20:47:22 +08:00
dekun ddfe2a52aa Merge orders and positions into one card and hide stale pending when CTP is off.
Stop showing DB pending orders while disconnected, invalidate session cache when CTP is down, and add a local DB clear script without embedded credentials.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 19:27:02 +08:00
dekun 631aa2c0ab Add CTP auto-connect toggle to stop off-hours reconnect attempts.
When disabled, disconnect immediately and skip auto-reconnect, premarket connect, and TCP probes that fail outside SimNow trading hours.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 18:53:49 +08:00
dekun 3a150dd3d6 Implement CTP-authoritative trading UI with event-driven state.
Add in-memory order/position books fed by CTP events, split active orders above positions in the UI, tick-triggered local SL/TP, and 30-second full calibration.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 18:12:11 +08:00
dekun 4ef33a367f Fix CTP exchange routing for non-SHFE contracts and duplicate trade closes.
Resolve CZCE/DCE symbols to the correct exchange for orders, dedupe stop-loss closes and trade logs, and rely on CTP sync for authoritative records.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 14:06:49 +08:00
dekun 382a9a0e14 Use Sina-only market K-lines and editable admin login synced to .env.
Market page uses Sina for quotes and bars with an auto-follow toggle and incremental chart updates while panning. Settings lets users change username and password, persisting to the database and .env.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 13:53:12 +08:00
dekun 6905373401 Make settings cards collapsible and tighten backup layout.
All settings sections fold by default with persisted state; backup and password sit in one compact row.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 13:10:43 +08:00
dekun 98239d29c1 Add automatic database backup with download and restore docs.
Back up futures.db and uploads to /root/qihuo_backup on a daily schedule, expose backup downloads in settings, and document cross-server restore.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 13:04:48 +08:00
dekun 508d85a282 Fix SQLite lock errors on /api/stats under concurrent writes.
Retry stats cache commits, serialize refresh, and fall back to read-only compute so the stats API does not return 500 when the database is briefly locked.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 12:23:33 +08:00
dekun d3955309d9 Persist CTP margin, ratio, and fees to DB; use exchange commission in trade logs.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 11:36:56 +08:00
dekun 9a55c61678 Align position margin with account balance and show deducted open commission only.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 11:26:41 +08:00
dekun 038eb9a403 Use CTP contract margin rates for position margin and ratio display.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 11:14:01 +08:00
dekun 7a4a3f08e5 Fix slow CTP position sync after restart and link positions to 15m K-line.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 11:05:13 +08:00
dekun deb9501cbe Show product name, main contract badge, and exchange on position cards.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 10:55:37 +08:00
dekun 42f2dad52a Remove contract profile from navigation and retire its routes.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 03:32:30 +08:00
dekun cababd67f5 Reduce navigation flash with instant theme background and view transitions.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 03:26:57 +08:00
dekun aaf69329cb Use English tagline and open daily K-line when jumping from tradable symbols.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 03:23:52 +08:00
dekun 4eb5709d71 Rebrand product and enhance tradable symbols table with spec columns and K-line links.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 03:18:42 +08:00
dekun ab9987e4c7 Add personal license agreement and rename product section to tradable symbols.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 02:52:45 +08:00
dekun 7b60f0dce5 Update documentation to match current product features.
Rewrite module docs for order monitor, CTP sync, and stats; remove obsolete simulated-position and UI descriptions.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 02:08:01 +08:00
dekun bdfa21def8 Rename positions nav to order monitor and set as default landing page.
Remove stats recalculate button; login and home now open /positions without affecting refresh on other routes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 01:51:22 +08:00
dekun 1cc0cd5f8d Flatten records visuals and show stats summary in one row.
Use transparent chart/table surfaces on records page; merge 14 stat metrics into a single compact card row.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 01:41:35 +08:00
dekun 64adce0e24 Flatten records page styling to match outer card background.
Use card-bg for equity curve and trade table instead of nested inner card colors.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 01:35:50 +08:00
dekun f457e7d6c9 Fix trade records table scroll with sticky header and themed background.
Use card CSS variables for 10-row viewport, sticky column headers, and dark/light theme sync.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 01:31:06 +08:00
dekun aad88a9e98 Add industry filter to recommendations and fix verify button width.
Show category, turnover, and per-industry counts; clarify volume is in lots. Prevent trade-save button from stretching full column width.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 01:21:53 +08:00
dekun e5f675c6ca Sync equity curve with card theme and keep trade actions on one row.
Use CSS variables for chart colors with dark/light auto-switch; prevent trade action buttons from wrapping.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 01:11:21 +08:00
dekun eb4414f741 Fix trade sync when get_all_trades returns a list.
Handle both dict and list from vnpy so records page CTP sync no longer warns.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 00:58:16 +08:00
dekun 7133a0e448 Fix CTP vnctptd segfault restart loop by serializing reconnect.
Skip duplicate auto-connect when TD is logged in, stop aggressive query_position hooks, and throttle position refresh.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 00:49:17 +08:00
dekun 9f48f22d16 Gate order cancel to trading hours and sync trade logs from CTP.
Disable cancel UI outside sessions, query exchange fills for records, and label local vs counterparty rows.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 00:35:51 +08:00
dekun a23f2c80ca Track open orders as pending until CTP fill, with cancel and timeout.
Add configurable pending timeout in settings and clearer CTP password save feedback.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 00:05:45 +08:00
dekun 7ea8fb6301 Remove pm2 300M memory restart limit for 8GB server
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 18:04:17 +08:00
dekun ca541d5fc3 Auto-reconnect CTP with new front-end addresses after saving settings
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 17:51:18 +08:00
dekun b641a4eaa0 Expand recommend table with gap, daily stats, and client-side sorting
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 17:36:31 +08:00
dekun a56370d2af Fix positions page hang by moving recommend refresh to background
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 17:29:10 +08:00
dekun 04b6f5e72d Add daily trend status to product recommendations with breakout priority
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 17:25:34 +08:00
dekun aba67a3d16 Compact CTP settings form into 3-row grid per card
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 17:16:52 +08:00
dekun 47dd946d66 fix: CTP 设置 SimNow/实盘卡片改为左右并列显示
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 17:11:54 +08:00
dekun 480000e195 feat: 系统设置 CTP 连接拆分为 SimNow/实盘可折叠卡片
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 17:09:56 +08:00
dekun 259d9e812d fix: CTP登录冷却持久化到数据库,取消页面自动连并刷新JS缓存
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 17:01:36 +08:00
dekun 9c8b92d2bd fix: 登录冷却期间不再显示 CTP 连接中,优化前端状态同步
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 16:57:06 +08:00
dekun 4aebc2df49 fix: SimNow 登录封禁(错误75)时冷却退避,停止自动重连
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 16:50:09 +08:00
dekun 5a6c89c662 feat: CTP/SimNow 配置迁入系统设置,登录失败即时报错
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 16:46:01 +08:00
dekun 72361233a0 fix: CTP重连前探测前置可达性,失败时关闭网关并明确报错
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 16:30:35 +08:00
dekun 240fbe7994 feat: 交易记录增加保证金占比与最新资金,上方展示资金曲线
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 16:17:22 +08:00
dekun 649c064c2f feat: 期货下单写入DB来源与开仓时间,CTP同步均价保证金现价
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 15:59:15 +08:00
dekun 0741997818 fix: 持仓手数均价分列、止损止盈显示金额、CTP开仓时间含OpenTime
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 15:48:52 +08:00
dekun de74ffe5b9 fix: 持仓卡片布局优化、闭盘禁用平仓、固定金额step修复
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 15:41:35 +08:00
dekun 9772f3d986 feat: 计仓改为固定手数/固定金额,推荐过滤与CTP保证金,下单与持仓UI优化
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 15:31:34 +08:00
dekun c302e1f3ca fix: avoid SQLite lock on fast position poll by skipping DB writes
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 15:14:22 +08:00
dekun 12a30d4f0c fix: ctp_list_positions pass refresh_margin to fix empty positions
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 15:09:23 +08:00
dekun 4d60b958ce fix: 开仓时间读CTP OpenDate,止盈止损持久化且重启不丢失
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 15:05:58 +08:00
dekun 7daed9bd3a fix: 重启后立即读库展示持仓,CTP异步重连不再阻塞
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 14:57:39 +08:00
dekun 01de8dfb69 feat: 持仓委托改止盈止损,保证金改读CTP柜台UseMargin
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 14:50:45 +08:00
dekun 63beda3c71 feat: 盈亏比与亏损额度展示,市价FAK报单,修复止盈止损保存失败
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 14:33:05 +08:00
dekun 367f32dd82 fix: 持仓接口实时拉取并回写本地监控,修复有仓不显示
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 14:14:21 +08:00
dekun 040436e9cc feat: 持仓监控数据库优先显示,修复开仓重复与同步前空白
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 14:06:19 +08:00
dekun 86e61df993 fix: 修正持仓 worker 中 bool 优先级导致 .get 报错。
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 13:52:09 +08:00
dekun bbcc5607ad feat: 持仓监控后台 SSE 推送与浏览器缓存,刷新不再阻塞读柜台。
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 13:49:44 +08:00
dekun f31164076f feat: 非交易时段禁开仓、移动保本与交易结果分类。
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 13:33:17 +08:00
dekun 598a1407e1 feat: 止盈止损秒级监控市价平仓记交易记录,并加手数超限提醒。
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 13:16:19 +08:00
dekun f05362ea74 fix: 推荐品种下拉改用缓存 main_code,避免加载卡住。
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 13:05:26 +08:00
dekun fc425c0e9f feat: 品种下拉统一展示推荐列表,与下方品种推荐表一致。
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 13:01:20 +08:00
dekun d127a53870 fix: 修正 d.minute() 调用导致盘前连接 worker 报错。
datetime.minute 是属性而非方法,修复后交易时段与盘前自动连 CTP 可正常工作。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 12:36:01 +08:00
dekun 32f1fa2c66 fix: TradingView K线图表并修复品种推荐为空。
- 行情页改用 Lightweight Charts 标准蜡烛图(红跌绿涨)
- 修复 fee_rates 缺 source 列导致推荐刷新失败
- 空缓存自动重试,持仓页实时兜底计算推荐列表

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 12:33:49 +08:00
dekun 074551490f fix: 品种推荐改为最大手数并补算旧缓存。
- 最大手数 = floor(权益×保证金上限%÷1手保证金)
- 加载与 SSE 推送时实时补算,旧缓存缺字段时自动刷新

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 12:24:10 +08:00
dekun 9875ee6d44 本地监控止盈止损、盘前自动连CTP,并完善保证金与推荐手数。
- 止盈止损改为程序本地监控,触发后市价平仓(含跳空)
- 交易前30分钟后台自动连接 CTP
- 保证金占用上限默认30%,可在系统设置修改
- K线标准蜡烛图红跌绿涨,费率表全宽固定表头
- 品种推荐按保证金比例×总资金计算推荐手数

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 12:18:18 +08:00
dekun fe1b651900 fix: 止盈止损委托校验现价并在平仓后撤余单
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 14:35:20 +08:00
dekun 397e9cd9d8 fix: SHFE止盈止损平仓改平今并限制重复报单
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 14:30:41 +08:00
dekun f6ee13765d fix: sl_tp_guard 从 ctp_symbol 导入 ths_to_vnpy_symbol
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 14:23:06 +08:00
dekun 73b9dfdfdb feat: 持仓保证金占比与止盈止损自动委托守护
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 14:20:15 +08:00
dekun 23d0f1d6fa fix: 清理幽灵止盈止损监控并修正仓位上限冻结误触发
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 14:09:42 +08:00
dekun 5af04ef661 fix: 限制单笔报单最大50手,防止以损定仓计算出超大委托
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 14:01:55 +08:00
dekun 3aa3e1ad30 fix: CTP报单校验连接与tick价,市价改限价,郑商所平今平昨
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 13:53:10 +08:00
dekun 049aaffdcf fix: CTP连接改后台异步,避免多路重连互相阻塞
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 13:49:25 +08:00
dekun 1d95950b5c fix: 手续费同步改为后台执行,避免阻塞 Web 请求
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 13:45:39 +08:00
dekun e01c011df5 feat: 手续费仅CTP每日后台同步入库,前端只读展示
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 13:33:14 +08:00
dekun de6815d481 fix: K线新浪历史补齐与手续费页布局及CTP批量同步
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 13:26:53 +08:00
dekun 3fe4add8e1 feat: 行情K线优先CTP tick聚合,修复手续费同步主力列表解析
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 13:18:43 +08:00
dekun 09f4649d79 ui: 系统设置卡片对齐,策略页说明与布局完善
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 13:10:21 +08:00
dekun 59420e0550 ui: 持仓监控 SimNow 标签去掉「模拟」
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 13:05:22 +08:00
dekun eaca3d43ec ui: 手续费/设置布局优化,行情优先 CTP
手续费数据源与本地倍率并列双列;设置页去掉参考资金、缩小改密表单;CTP 连接时订阅柜台 tick 作为行情源。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 13:04:11 +08:00
dekun 9d4aea60f0 fix: start_recommend_worker 参数名 quote_fn 与调用方一致
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 12:57:55 +08:00
dekun aea9aca472 feat: 品种推荐与下单显示主力合约
推荐列表展示当前主力代码;下单品种支持中文/代码搜索并按交易所分组选择主力合约。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 12:56:06 +08:00
dekun 3ba3be6035 ui: 持仓监控卡片与关键位同宽,完善多端与 PWA
改用 split-grid 全宽布局;手机/平板/电脑断点适配;更新 manifest 与 Service Worker 支持安装为 App。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 12:50:58 +08:00
dekun 67683f5562 ui: 顶栏透明、设置两列、下单与持仓监控优化
导航栏与页面背景一致;系统设置双列布局;下单三行表单与开仓状态反馈;持仓卡片增加平仓与止盈止损挂单展示。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 12:46:23 +08:00
dekun 528d9811e3 feat: 导航开关与 CTP 柜台手续费
系统设置可开关五类导航;手续费默认从 CTP 查询同步,本地/AKShare 作离线兜底;补充 FEES.md。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 12:19:56 +08:00
dekun ca894dfd4d deploy: 一键部署内置 locale/时区/SimNow 前置探测与 CTP 验证
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 11:59:02 +08:00
dekun b4250171d5 feat: CTP 断线重连、下单卡片优化、手数自动计算
后台每 30s 检测并重连;以损定仓填止损后自动算手数;开仓/平仓按钮并排对齐。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 11:56:20 +08:00
dekun 36e26973fb fix: CTP 需 zh_CN.GB18030 中文 locale 而非仅 UTF-8
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 11:42:15 +08:00
dekun d368317c1b fix: 强制设置有效 locale 修复 vnpy_ctp CTP 登录崩溃
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 11:38:19 +08:00
dekun f73d436077 fix: CTP 连接后 locale 崩溃,PM2 设置 LANG=C.UTF-8
vnpy_ctp C++ 扩展在缺 locale 时会 terminate;补充 SimNow 备用前置说明。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 11:32:20 +08:00
dekun 350d0fed6b docs: TRADING 增加 SimNow 注册文档链接
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 11:22:44 +08:00
dekun 28d6ae52b2 fix: CTP 连接捕获 SimNow 真实报错并增加诊断脚本
显式设置柜台环境=实盘;连接失败时解析 4097/登录拒单;scripts/test_simnow.py 供服务器排查。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 11:20:30 +08:00
dekun 8726760b12 fix: deploy 安装 pkg-config 以编译 vnpy_ctp
Meson 需 pkg-config 查找 python3-dev;deploy.sh 保留已有 venv 并验证 vnpy_ctp 导入。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 10:56:39 +08:00
dekun 62cd868f79 完善下单表单与 CTP 持仓,requirements 加入 vnpy 并更新部署文档
以损定仓/固定张数分栏下单、限价市价、持仓仅读柜台;DEPLOY 补充 SimNow 与 vnpy 安装说明。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 10:52:25 +08:00
dekun 709801305f 恢复下单界面并排布局,品种推荐数据库缓存与 SSE 推送。
期货下单与持仓监控左右并排,推荐按资金过滤存库,后台刷新并通过 EventSource 推送。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 10:41:26 +08:00
dekun 38a38cb51d 修复持仓监控页长时间空白:品种推荐改为异步加载。
页面先渲染三卡片,推荐表并行拉行情,持仓与推荐分别通过 API 加载。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 10:34:34 +08:00
dekun 55d95b4c39 进一步修复 SQLite 并发锁冲突,统一连接与重试机制。
新增 db_conn 模块、缓存 schema 初始化、positions 页 commit,风控读库自动重试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 10:30:26 +08:00
dekun 1688452f3f 修复持仓轮询时 SQLite database is locked 错误。
单连接复用并提交风控写入,启用 WAL 与 busy_timeout,缓存风控表 schema 初始化。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 10:27:04 +08:00
dekun 87aef80594 持仓监控页整合期货下单、持仓与品种推荐三卡片。
程序报单状态与推荐表内嵌同一页面,/recommend 跳转至 #recommend。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 10:24:41 +08:00
dekun 7b8a660309 合并期货下单与持仓监控为统一界面,移除手工录入。
策略与 CTP 自动同步持仓,新增 /api/trading/live 聚合展示与平仓接口。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 10:18:00 +08:00
dekun 6e423eebfb 接入 SimNow 模拟盘与期货下单、策略及品种推荐功能。
新增 vnpy CTP 桥接、以损定仓/固定张数、趋势回调与滚仓策略、按资金推荐品种及交易风控;模拟盘走 SimNow,实盘预留期货公司配置。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 10:04:37 +08:00
dekun 9c0e5d9c57 修复主力合约识别:按持仓量判定并移除当月占位
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 18:28:51 +08:00
dekun 404872007f 行情K线:分类主力选择、图表指标与布局稳定
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 18:23:39 +08:00
dekun 65992eb35e K线后台自动刷新并通过SSE推送到前端,移除轮询
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 17:33:31 +08:00
dekun b804bd19a7 K线本地缓存、图表交互优化与交易记录表格修复
新增 kline_store 优先读本地库;修复加载中遮挡、支持缩放与交易时段刷新;修复交易记录操作列被裁切。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 17:27:31 +08:00
dekun a9f4e2b1a5 修复行情页品种下拉列表被遮挡的问题
展开下拉时取消卡片 overflow 裁切,并提高下拉与工具栏层级。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 17:11:58 +08:00
dekun 6f3ac3deb6 新增行情K线页,支持分时与多周期图表
扩展新浪K线拉取与合成逻辑,提供 ECharts 交互图表及实时报价 API。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 17:07:04 +08:00
dekun 28875078f1 三端自适应布局与 PWA 可安装支持
新增响应式样式、手机侧滑导航、manifest 与 Service Worker;补充根路径重定向与安装 App 入口。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 16:58:13 +08:00
dekun e8b4dbbaca 重构统计分析页:汇总指标、分项下拉与后台缓存
新增 stats_engine 与 stats_cache,提供 API 自动加载 8 种统计维度;交易与复盘变更时自动刷新缓存。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 16:46:06 +08:00
dekun 0e385b057d 修复深色与浅色主题无法切换的问题
修正 CSS 选择器避免 :root 始终应用深色变量,并改进 theme.js 点击绑定与首屏主题恢复。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 16:29:34 +08:00
dekun 2b383b84ce 移除卡片四角装饰,主题切换改为深色/浅色分段按钮
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 16:15:35 +08:00
dekun a12da042cc 整体界面科技感增强:动态背景、霓虹导航与卡片光效
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 16:07:55 +08:00
dekun b77f30b3ff 新增品种简介查询页,支持东方财富/新浪合约规格展示
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 15:54:38 +08:00
dekun 706a0fd1a3 将 akshare 加入 requirements.txt 以支持手续费第三方同步
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 15:36:21 +08:00
dekun 613b88f2d3 补充功能说明与独立部署文档,精简 README 为文档入口
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 15:30:34 +08:00
dekun bea7804d47 本地手续费配置(标准×2),持仓/交易记录/复盘/统计展示扣费后盈亏
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 15:22:40 +08:00
dekun 9ba9733523 持仓监控独立导航页,交易记录与复盘合并为同一页
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 15:06:18 +08:00
dekun 5aa9f11733 持仓监控平仓自动记入交易记录,新增交易记录页与实盘资金设置
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 14:56:21 +08:00
dekun 38ff40111a 关键位与今日计划列表实时现价及距区间距离(1s轮询)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 14:30:01 +08:00
dekun 58020b6e9c 修复关键位sina_code字段;复盘详情全屏两行;开单计划表单布局优化
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 14:22:42 +08:00
270 changed files with 45314 additions and 2756 deletions
+2 -21
View File
@@ -1,21 +1,2 @@
# 服务配置 # 环境变量模板已迁移至 config/.env.example
HOST=0.0.0.0 # 使用: cp config/.env.example config/.env
PORT=6600
DEBUG=false
# Flask Session 密钥(部署时务必改为随机字符串,deploy.sh 首次会自动生成)
SECRET_KEY=change-this-to-a-random-secret-key
# 初始管理员(首次建库自动写入;已建库后修改需设 ADMIN_SYNC_FROM_ENV=true 并重启)
ADMIN_USERNAME=admin
ADMIN_PASSWORD=change-me-on-first-login
ADMIN_SYNC_FROM_ENV=false
# 企业微信 Webhook(也可在系统设置页面修改)
WECHAT_WEBHOOK=
# 行情数据源: sina(默认,免费)| auto(有机构 token 时优先同花顺)| ths
QUOTE_SOURCE=sina
# 同花顺 iFinD refresh_token(仅机构用户,普通用户留空即可)
THS_REFRESH_TOKEN=
+1
View File
@@ -1,4 +1,5 @@
.env .env
config/.env
*.db *.db
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
+42
View File
@@ -0,0 +1,42 @@
国内期货 · 交易复盘系统 — 软件使用许可与版权声明
著作权人:马建军
Copyright (c) 2025-2026 马建军. All rights reserved.
【权利声明】
本软件(含源代码、文档、界面、脚本及后续更新版本)之著作权及相关知识产权,
均归马建军所有。除本许可明确允许的范围外,保留一切权利。
【授权范围 — 个人版】
经著作权人书面或付费交付同意的自然人购买者,仅可在本人名下单一服务器或
个人设备上部署并使用本软件,用于个人期货交易纪律管理、记录与复盘,且须
遵守中华人民共和国相关法律法规及期货监管规定。
【严禁用途】
未经著作权人事先书面许可,严禁将本软件用于包括但不限于以下用途:
(1)带单、代客理财、代客下单、跟单室、信号群、付费喊单、向他人推荐具体
期货买卖方向或具体合约;
(2)向他人推荐、介绍、引导参与特定期货品种或交易机会(若构成投资咨询或
其他需许可之业务,使用者依法另行承担法律责任);
(3)融资、配资、分仓、分润、对赌、非法吸收资金等与期货相关的资金融通
或变相配资业务;
(4)复制、传播、转售、出租、出借源代码或编译产物,或授权第三方使用;
(5)搭建共享交易室、多租户 SaaS、白标系统对外经营(须另行签订机构版协议);
(6)删除、篡改或隐藏本版权及许可声明。
【免责声明】
本软件为交易纪律与记录辅助工具,不构成任何投资建议、咨询或收益承诺。
期货交易具有高风险,使用者须独立决策并自行承担全部盈亏及法律责任。
因使用者违反法律法规、监管规定或本许可导致的后果,由使用者自行承担。
【更新与维护】
源代码更新、部署服务及共享交易室等机构授权,以双方另行书面约定为准。
未经约定,不视为自动授予新版本或扩展用途之权利。
【联系】
著作权人:马建军
手机:18364911125
微信:dekun03
详细购买条款见 docs/软件购买与使用协议.md。
本许可之解释与适用以中华人民共和国法律为准(法律强制性规定除外)。
+56 -140
View File
@@ -1,178 +1,94 @@
# 国内期货交易监控复盘系统 # 国内期货 · 交易复盘系统
基于 Flask 的国内期货监控与复盘 Web 应用,支持开单计划、关键位监控、止盈止损自动跟踪、企业微信推送与统计分析 基于 Flask 的国内期货 **CTP 下单 + 监控 + 复盘 + 统计** Web 应用。模拟盘连接 SimNow,实盘连接期货公司 CTP;支持关键位/计划提醒、交易记录同步、资金曲线、可开仓品种(仓位纪律)与企业微信推送。
## 功能模块 ## 文档
| 模块 | 说明 | | 文档 | 说明 |
|------|------| |------|------|
| **开单计划** | 品种、方向、决策区间、止损/止盈;价格进入区间后激活并推送 | | **[功能说明](docs/FEATURES.md)** | 各模块功能、页面路径、数据库与后台任务 |
| **关键位监控** | 箱体/收敛突破、阻力/支撑位突破提醒(触发后去重) | | **[部署文档](docs/DEPLOY.md)** | 一键部署、更新、PM2、故障排查 |
| **交易记录与复盘** | 自动记录止盈/止损结果 | | **[备份与恢复](docs/BACKUP.md)** | 自动备份、下载、跨服务器恢复 |
| **统计分析** | 总交易、胜率,按品种/类型/方向统计 | | **[SimNow 接入](docs/SIMNOW.md)** | 仿真账号注册与 CTP 前置 |
| **系统设置** | 修改密码、配置企业微信 Webhook | | **[期货公司实盘 CTP](docs/CTP_LIVE.md)** | 实盘接入、与 SimNow 开平仓对比 |
| **[交易与策略](docs/TRADING.md)** | 下单、持仓、可开仓品种、策略 API |
| **[手续费与导航](docs/FEES.md)** | CTP 费率同步、导航开关 |
| **[软件购买与使用协议](docs/软件购买与使用协议.md)** | 个人版授权模板(含签署栏) |
## 品种与合约代码(同花顺格式) ## 功能一览
输入中文品种名(如「白银」「螺纹钢」)或同花顺合约代码(如 `ag2606``SR609``IF2606`),系统自动匹配**当前主力月份合约**。 | 模块 | 路径 | 说明 |
|------|------|------|
| **下单监控**(默认首页) | `/positions` | CTP 连接、期货下单、当前持仓、可开仓品种 |
| **策略交易** | `/strategy` | 趋势回调 / 顺势加仓(可导航开关) |
| **开单计划** | `/plans` | 当日决策区间、触发推送(可开关) |
| **关键位监控** | `/keys` | 箱体/阻力支撑突破提醒 |
| **行情 K 线** | `/market` | 多周期 K 线(可开关) |
| **交易记录与复盘** | `/records` | 资金曲线、CTP 成交同步、复盘上传 |
| **统计分析** | `/stats` | 汇总指标 + 多维度分项统计 |
| **手续费配置** | `/fees` | CTP / 本地费率(可开关) |
| **系统设置** | `/settings` | 交易模式、CTP、计仓、微信、主题 |
| 交易所 | 同花顺示例 | 说明 | 登录后默认进入 **下单监控**;刷新当前页不会跳转,仅访问根路径 `/` 或新登录时进入默认页。
|--------|-----------|------|
| 上期所 / 大商所 / 上期能源 | `ag2606``rb2605``m2609` | 小写品种 + 4 位年月 |
| 郑商所 | `SR609``MA606` | 大写品种 + 3 位年月 |
| 中金所 | `IF2606``IH2606` | 大写品种 + 4 位年月 |
界面展示**同花顺合约代码**(ag2608、IF2606),与看盘软件一致;**行情默认走新浪财经**(免费,普通用户无需 token)。 ## 快速开始
## 行情说明 > **发布铁律**:本地改代码 → 提交并 `git push` → 服务器 **仅** `git pull` / `git reset --hard origin/main` 更新。**禁止 SCP 复制代码到服务器。** 详见 [部署文档 · 代码发布铁律](docs/DEPLOY.md#代码发布铁律强制不容置疑)。
| 项目 | 说明 | **服务器(Ubuntu**
|------|------|
| 合约代码 | 同花顺格式,输入中文自动匹配主力月份 |
| 价格数据 | 新浪财经 API(免费) |
| 同花顺 iFinD | 仅机构/付费数据接口用户可用,**普通期货通用户无 refresh_token** |
因此个人用户使用本系统:**看同花顺代码,价格走新浪**,两者在主力合约价格上基本一致,满足监控需求。
## 快速部署(Ubuntu root + /opt/qihuo
```bash ```bash
# root 登录后执行 cd /opt/qihuo && bash deploy.sh
cd /opt/qihuo # 或先 git clone 再 bash deploy.sh # 访问 http://<IP>:6600
bash deploy.sh
``` ```
默认安装路径:`/opt/qihuo`,服务端口:`6600` **更新**(须先在本机 `git push`
部署完成后访问:`http://服务器IP:6600`
## 环境要求
- Ubuntu 20.04+(推荐)
- **root 用户**运行(部署目录 `/opt/qihuo`
- Python 3.10+
- Node.js + PM2(进程守护)
- 网络可访问 `hq.sinajs.cn`(行情)及企业微信 API
## 手动部署
### 1. 安装系统依赖
```bash ```bash
apt update
apt install -y python3 python3-venv python3-pip git nodejs npm
npm install -g pm2
```
### 2. 克隆到 /opt/qihuo
```bash
git clone https://git.bz121.com/dekun/qihuo.git /opt/qihuo
cd /opt/qihuo cd /opt/qihuo
git fetch origin && git reset --hard origin/main
source venv/bin/activate && pip install -r requirements.txt
python scripts/run_schema_migrate.py
pm2 restart ecosystem.config.cjs --update-env
``` ```
### 3. 虚拟环境与依赖 **禁止** 使用 `scp` / 手工复制更新服务器代码。
source venv/bin/activate
pip install -r requirements.txt
```
### 4. 配置环境变量 生产环境须同时维护 **`qihuo`**Web)与 **`qihuo-ctp`**CTP Worker)两个 PM2 进程。
```bash 详见 [部署文档](docs/DEPLOY.md)。
cp .env.example .env
# 编辑 .env,至少修改 SECRET_KEY 和 ADMIN_PASSWORD
nano .env
```
`.env` 主要字段: **本地开发**
```env
HOST=0.0.0.0
PORT=6600
SECRET_KEY=随机长字符串
ADMIN_USERNAME=admin
ADMIN_PASSWORD=你的密码
ADMIN_SYNC_FROM_ENV=false
WECHAT_WEBHOOK=企业微信机器人地址(可选)
QUOTE_SOURCE=sina
```
**改密码说明**:账号存在 `futures.db` 里,改 `.env` 后不会自动生效。
- **首次部署**:写好 `ADMIN_USERNAME` / `ADMIN_PASSWORD` 后启动即可。
- **已部署后**:在 `.env``ADMIN_SYNC_FROM_ENV=true`,改密码后 `pm2 restart qihuo`;或在网页「系统设置」改密。
- **忘记密码**`source venv/bin/activate && python reset_admin.py`
普通用户保持 `QUOTE_SOURCE=sina` 即可。
### 5. PM2 启动
```bash
pm2 start ecosystem.config.cjs
pm2 save
pm2 startup # 按提示执行生成的命令,实现开机自启
```
### 6. 常用 PM2 命令
```bash
pm2 status
pm2 logs qihuo
pm2 restart qihuo
pm2 stop qihuo
```
## 本地开发
```bash ```bash
python3 -m venv venv python3 -m venv venv
source venv/bin/activate source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt pip install -r requirements.txt
cp .env.example .env cp .env.example .env
python app.py python app.py
``` ```
## 目录结构 ## 品种与行情
``` - 合约代码:**同花顺格式**`ag2606``SR609``IF2606`),中文联想匹配主力
qihuo/ - 行情:默认 **新浪财经**;机构用户可配置同花顺 iFinD token
├── app.py # 主程序
├── market.py # 同花顺/新浪行情拉取
├── symbols.py # 期货品种与同花顺代码映射
├── requirements.txt
├── .env.example
├── deploy.sh # Ubuntu 一键部署
├── ecosystem.config.cjs # PM2 配置
├── static/js/symbol.js # 品种联想
├── templates/ # 页面模板
└── futures.db # SQLite 数据库(运行后生成)
```
## 监控逻辑说明 ## 环境要求
### 开单计划 - Python 3.10+vnpy_ctp
- PM2(生产部署:**qihuo** + **qihuo-ctp** 两个进程)
- 网络:新浪行情、Git 仓库、SimNow/CTP 前置(见部署文档)
1. **待触发**:当前价进入「决策区间 [下限, 上限]」→ 企业微信通知,状态变为「已激活」 ## 仓库
2. **已激活**:监控止盈止损直至触发,写入交易记录,计划关闭
### 关键位监控
- 箱体/收敛:突破上沿或跌破下沿各推送一次
- 阻力/支撑:单向突破推送一次
后台线程每 3 秒轮询行情。
## 安全建议
- 部署后立即修改默认密码
- 勿将 `.env` 提交到仓库
- 生产环境建议用 Nginx 反代并配置 HTTPS
- 限制 6600 端口仅内网或 VPN 访问
## 仓库地址
https://git.bz121.com/dekun/qihuo.git https://git.bz121.com/dekun/qihuo.git
## License ## 版权与授权
Private / 个人使用 - 著作权人:**马建军**
- 许可说明:[LICENSE.zh-CN.txt](LICENSE.zh-CN.txt)
- 个人购买协议模板:[docs/软件购买与使用协议.md](docs/软件购买与使用协议.md)
本软件为 **专有软件**,仅供经授权的个人自用部署。严禁用于带单、向他人推荐期货品种或买卖建议、融资配资、转售源码或搭建共享交易室等用途。本软件不构成投资建议,期货交易风险由使用者自行承担。
联系:手机 18364911125 · 微信 dekun03
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.settings.admin_settings
from modules.settings.admin_settings import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.notify.ai_client
from modules.notify.ai_client import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.notify.ai_messages
from modules.notify.ai_messages import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.notify.ai_worker
from modules.notify.ai_worker import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.core.contract_profile
from modules.core.contract_profile import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.core.contract_specs
from modules.core.contract_specs import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.ctp.ctp_entry_price
from modules.ctp.ctp_entry_price import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.ctp.ctp_fee_sync
from modules.ctp.ctp_fee_sync import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.ctp.ctp_fee_worker
from modules.ctp.ctp_fee_worker import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.ctp.ctp_ipc_client
from modules.ctp.ctp_ipc_client import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.ctp.ctp_kline
from modules.ctp.ctp_kline import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.ctp.ctp_premarket_connect
from modules.ctp.ctp_premarket_connect import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.ctp.ctp_reconnect
from modules.ctp.ctp_reconnect import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.ctp.ctp_settings
from modules.ctp.ctp_settings import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.ctp.ctp_symbol
from modules.ctp.ctp_symbol import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.ctp.ctp_trade_sync
from modules.ctp.ctp_trade_sync import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.ctp.ctp_trading_state
from modules.ctp.ctp_trading_state import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.ctp.ctp_worker
from modules.ctp.ctp_worker import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.stats.dashboard_lib
from modules.stats.dashboard_lib import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.backup.db_backup
from modules.backup.db_backup import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.core.db_conn
from modules.core.db_conn import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.core.doc_render
from modules.core.doc_render import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.core.env_file
from modules.core.env_file import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.fees.fee_specs
from modules.fees.fee_specs import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.fees.fee_sync
from modules.fees.fee_sync import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.keys.key_monitor_lib
from modules.keys.key_monitor_lib import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.market.kline_chart
from modules.market.kline_chart import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.market.kline_store
from modules.market.kline_store import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.market.kline_stream
from modules.market.kline_stream import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.core.locale_fix
from modules.core.locale_fix import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.market.market
from modules.market.market import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.market.market_sessions
from modules.market.market_sessions import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.settings.nav_settings
from modules.settings.nav_settings import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.trading.order_pending
from modules.trading.order_pending import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.trading.pending_order_worker
from modules.trading.pending_order_worker import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.trading.position_sizing
from modules.trading.position_sizing import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.trading.position_stream
from modules.trading.position_stream import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.trading.product_recommend
from modules.trading.product_recommend import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.trading.recommend_store
from modules.trading.recommend_store import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.trading.recommend_stream
from modules.trading.recommend_stream import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.trading.recommend_trend
from modules.trading.recommend_trend import * # noqa: F401,F403
+5
View File
@@ -0,0 +1,5 @@
# Copyright (c) 2025-2026 马建军. All rights reserved.
# 专有软件 — 未经授权禁止复制、传播、转售。
# 严禁用于:带单/代客理财、向他人推荐期货品种或买卖建议、融资配资等业务。
# 详见 LICENSE.zh-CN.txt 与 docs/软件购买与使用协议.md
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.risk.account_risk_lib
from modules.risk.account_risk_lib import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.trading.sl_tp_guard
from modules.trading.sl_tp_guard import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.stats.stats_engine
from modules.stats.stats_engine import * # noqa: F401,F403
+5
View File
@@ -0,0 +1,5 @@
# Copyright (c) 2025-2026 马建军. All rights reserved.
# 专有软件 — 未经授权禁止复制、传播、转售。
# 严禁用于:带单/代客理财、向他人推荐期货品种或买卖建议、融资配资等业务。
# 详见 LICENSE.zh-CN.txt 与 docs/软件购买与使用协议.md
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.strategy.fib_lib
from modules.strategy.fib_lib import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.strategy.strategy_db
from modules.strategy.strategy_db import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.strategy.strategy_roll_lib
from modules.strategy.strategy_roll_lib import * # noqa: F401,F403
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.strategy.strategy_roll_monitor_lib
from modules.strategy.strategy_roll_monitor_lib import * # noqa: F401,F403
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.strategy.strategy_snapshot_lib
from modules.strategy.strategy_snapshot_lib import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.strategy.strategy_trend_lib
from modules.strategy.strategy_trend_lib import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.core.symbols
from modules.core.symbols import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.trading.trade_log_lib
from modules.trading.trade_log_lib import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.trading.trade_notify
from modules.trading.trade_notify import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.core.trading_context
from modules.core.trading_context import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.ctp.vnpy_bridge
from modules.ctp.vnpy_bridge import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.notify.wechat_notify
from modules.notify.wechat_notify import * # noqa: F401,F403
+483 -504
View File
File diff suppressed because it is too large Load Diff
+57
View File
@@ -0,0 +1,57 @@
# 服务配置
HOST=0.0.0.0
PORT=6600
DEBUG=false
SECRET_KEY=change-this-to-a-random-secret-key
ADMIN_USERNAME=admin
ADMIN_PASSWORD=change-me-on-first-login
ADMIN_SYNC_FROM_ENV=false
WECHAT_WEBHOOK=
QUOTE_SOURCE=sina
THS_REFRESH_TOKEN=
# 交易模式:simulation=SimNowlive=期货公司(系统设置页可改)
TRADING_MODE=simulation
POSITION_SIZING_MODE=risk
RISK_PERCENT=1
# CTP 断线后后台自动重连(true/false)
CTP_AUTO_RECONNECT=true
# —— SimNow 模拟盘(也可在「系统设置 → CTP 连接」配置,优先于本文件)——
SIMNOW_USER=
SIMNOW_PASSWORD=
SIMNOW_BROKER_ID=9999
# 7×24 / 日盘前置(deploy.sh 会自动 nc 探测并写入可用线路)
SIMNOW_TD_ADDRESS=tcp://180.168.146.187:10201
SIMNOW_MD_ADDRESS=tcp://180.168.146.187:10211
SIMNOW_APP_ID=simnow_client_test
SIMNOW_AUTH_CODE=0000000000000000
# SimNow 看穿式前置固定用「实盘」;仅穿透式测评才用「测试」
SIMNOW_ENV=实盘
# —— 期货公司实盘(后期接入)——
CTP_LIVE_USER=
CTP_LIVE_PASSWORD=
CTP_LIVE_BROKER_ID=
CTP_LIVE_TD_ADDRESS=
CTP_LIVE_MD_ADDRESS=
CTP_LIVE_APP_ID=
CTP_LIVE_AUTH_CODE=
CTP_LIVE_PRODUCT_INFO=
# 账户冷静期
RISK_CONTROL_ENABLED=true
RISK_COOLING_HOURS_MANUAL=4
RISK_COOLING_HOURS_MANUAL_JOURNAL=1
RISK_MANUAL_CLOSE_DAILY_LIMIT=2
MAX_ACTIVE_POSITIONS=1
RISK_DAILY_POSITION_LIMIT=5
RISK_DAILY_TRADING_RISK_PCT=2
TRADING_DAY_RESET_HOUR=8
# —— 数据库(SQLite futures.db,路径见 modules/core/paths.py)——
+36
View File
@@ -0,0 +1,36 @@
{
"ag": {"exchange": "SHFE", "mult": 15, "open_fixed": 2.0, "open_ratio": 0, "close_yesterday_fixed": 2.0, "close_yesterday_ratio": 0, "close_today_fixed": 4.0, "close_today_ratio": 0},
"au": {"exchange": "SHFE", "mult": 1000, "open_fixed": 4.0, "open_ratio": 0, "close_yesterday_fixed": 4.0, "close_yesterday_ratio": 0, "close_today_fixed": 0, "close_today_ratio": 0},
"cu": {"exchange": "SHFE", "mult": 5, "open_fixed": 0.1, "open_ratio": 0, "close_yesterday_fixed": 0.1, "close_yesterday_ratio": 0, "close_today_fixed": 0.2, "close_today_ratio": 0},
"al": {"exchange": "SHFE", "mult": 5, "open_fixed": 6.0, "open_ratio": 0, "close_yesterday_fixed": 6.0, "close_yesterday_ratio": 0, "close_today_fixed": 6.0, "close_today_ratio": 0},
"zn": {"exchange": "SHFE", "mult": 5, "open_fixed": 6.0, "open_ratio": 0, "close_yesterday_fixed": 6.0, "close_yesterday_ratio": 0, "close_today_fixed": 0, "close_today_ratio": 0},
"pb": {"exchange": "SHFE", "mult": 5, "open_fixed": 0.08, "open_ratio": 0, "close_yesterday_fixed": 0.08, "close_yesterday_ratio": 0, "close_today_fixed": 0, "close_today_ratio": 0},
"ni": {"exchange": "SHFE", "mult": 1, "open_fixed": 6.0, "open_ratio": 0, "close_yesterday_fixed": 6.0, "close_yesterday_ratio": 0, "close_today_fixed": 6.0, "close_today_ratio": 0},
"sn": {"exchange": "SHFE", "mult": 1, "open_fixed": 6.0, "open_ratio": 0, "close_yesterday_fixed": 6.0, "close_yesterday_ratio": 0, "close_today_fixed": 6.0, "close_today_ratio": 0},
"rb": {"exchange": "SHFE", "mult": 10, "open_fixed": 2.0, "open_ratio": 0, "close_yesterday_fixed": 2.0, "close_yesterday_ratio": 0, "close_today_fixed": 4.0, "close_today_ratio": 0},
"hc": {"exchange": "SHFE", "mult": 10, "open_fixed": 2.0, "open_ratio": 0, "close_yesterday_fixed": 2.0, "close_yesterday_ratio": 0, "close_today_fixed": 4.0, "close_today_ratio": 0},
"ru": {"exchange": "SHFE", "mult": 10, "open_fixed": 6.0, "open_ratio": 0, "close_yesterday_fixed": 6.0, "close_yesterday_ratio": 0, "close_today_fixed": 12.0, "close_today_ratio": 0},
"fu": {"exchange": "SHFE", "mult": 10, "open_fixed": 0.1, "open_ratio": 0, "close_yesterday_fixed": 0.1, "close_yesterday_ratio": 0, "close_today_fixed": 0, "close_today_ratio": 0},
"bu": {"exchange": "SHFE", "mult": 10, "open_fixed": 0.1, "open_ratio": 0, "close_yesterday_fixed": 0.1, "close_yesterday_ratio": 0, "close_today_fixed": 0.2, "close_today_ratio": 0},
"sp": {"exchange": "SHFE", "mult": 10, "open_fixed": 0.1, "open_ratio": 0, "close_yesterday_fixed": 0.1, "close_yesterday_ratio": 0, "close_today_fixed": 0.1, "close_today_ratio": 0},
"sc": {"exchange": "INE", "mult": 1000, "open_fixed": 40.0, "open_ratio": 0, "close_yesterday_fixed": 40.0, "close_yesterday_ratio": 0, "close_today_fixed": 0, "close_today_ratio": 0},
"i": {"exchange": "DCE", "mult": 100, "open_fixed": 2.0, "open_ratio": 0, "close_yesterday_fixed": 2.0, "close_yesterday_ratio": 0, "close_today_fixed": 4.0, "close_today_ratio": 0},
"j": {"exchange": "DCE", "mult": 100, "open_fixed": 2.4, "open_ratio": 0, "close_yesterday_fixed": 2.4, "close_yesterday_ratio": 0, "close_today_fixed": 2.4, "close_today_ratio": 0},
"jm": {"exchange": "DCE", "mult": 60, "open_fixed": 2.4, "open_ratio": 0, "close_yesterday_fixed": 2.4, "close_yesterday_ratio": 0, "close_today_fixed": 2.4, "close_today_ratio": 0},
"m": {"exchange": "DCE", "mult": 10, "open_fixed": 2.4, "open_ratio": 0, "close_yesterday_fixed": 2.4, "close_yesterday_ratio": 0, "close_today_fixed": 2.4, "close_today_ratio": 0},
"y": {"exchange": "DCE", "mult": 10, "open_fixed": 5.0, "open_ratio": 0, "close_yesterday_fixed": 5.0, "close_yesterday_ratio": 0, "close_today_fixed": 5.0, "close_today_ratio": 0},
"p": {"exchange": "DCE", "mult": 10, "open_fixed": 5.0, "open_ratio": 0, "close_yesterday_fixed": 5.0, "close_yesterday_ratio": 0, "close_today_fixed": 5.0, "close_today_ratio": 0},
"c": {"exchange": "DCE", "mult": 10, "open_fixed": 2.4, "open_ratio": 0, "close_yesterday_fixed": 2.4, "close_yesterday_ratio": 0, "close_today_fixed": 2.4, "close_today_ratio": 0},
"l": {"exchange": "DCE", "mult": 5, "open_fixed": 2.0, "open_ratio": 0, "close_yesterday_fixed": 2.0, "close_yesterday_ratio": 0, "close_today_fixed": 2.0, "close_today_ratio": 0},
"pp": {"exchange": "DCE", "mult": 5, "open_fixed": 2.0, "open_ratio": 0, "close_yesterday_fixed": 2.0, "close_yesterday_ratio": 0, "close_today_fixed": 2.0, "close_today_ratio": 0},
"if": {"exchange": "CFFEX", "mult": 300, "open_fixed": 0, "open_ratio": 0.000092, "close_yesterday_fixed": 0, "close_yesterday_ratio": 0.000092, "close_today_fixed": 0, "close_today_ratio": 0.000276},
"ih": {"exchange": "CFFEX", "mult": 300, "open_fixed": 0, "open_ratio": 0.000092, "close_yesterday_fixed": 0, "close_yesterday_ratio": 0.000092, "close_today_fixed": 0, "close_today_ratio": 0.000276},
"ic": {"exchange": "CFFEX", "mult": 200, "open_fixed": 0, "open_ratio": 0.000092, "close_yesterday_fixed": 0, "close_yesterday_ratio": 0.000092, "close_today_fixed": 0, "close_today_ratio": 0.000276},
"im": {"exchange": "CFFEX", "mult": 200, "open_fixed": 0, "open_ratio": 0.000092, "close_yesterday_fixed": 0, "close_yesterday_ratio": 0.000092, "close_today_fixed": 0, "close_today_ratio": 0.000276},
"ma": {"exchange": "CZCE", "mult": 10, "open_fixed": 4.0, "open_ratio": 0, "close_yesterday_fixed": 4.0, "close_yesterday_ratio": 0, "close_today_fixed": 4.0, "close_today_ratio": 0},
"ta": {"exchange": "CZCE", "mult": 5, "open_fixed": 6.0, "open_ratio": 0, "close_yesterday_fixed": 6.0, "close_yesterday_ratio": 0, "close_today_fixed": 0, "close_today_ratio": 0},
"sr": {"exchange": "CZCE", "mult": 10, "open_fixed": 6.0, "open_ratio": 0, "close_yesterday_fixed": 6.0, "close_yesterday_ratio": 0, "close_today_fixed": 0, "close_today_ratio": 0},
"cf": {"exchange": "CZCE", "mult": 5, "open_fixed": 8.6, "open_ratio": 0, "close_yesterday_fixed": 8.6, "close_yesterday_ratio": 0, "close_today_fixed": 0, "close_today_ratio": 0},
"fg": {"exchange": "CZCE", "mult": 20, "open_fixed": 12.0, "open_ratio": 0, "close_yesterday_fixed": 12.0, "close_yesterday_ratio": 0, "close_today_fixed": 12.0, "close_today_ratio": 0},
"sa": {"exchange": "CZCE", "mult": 20, "open_fixed": 7.2, "open_ratio": 0, "close_yesterday_fixed": 7.2, "close_yesterday_ratio": 0, "close_today_fixed": 7.2, "close_today_ratio": 0}
}
+145 -12
View File
@@ -1,6 +1,14 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# 国内期货监控系统 - Ubuntu 一键部署 # 国内期货 · 交易复盘系统 - Ubuntu 一键部署 / 更新
# root 用户 | 目录 /opt/qihuo | 端口 6600 | PM2 # root 用户 | 目录 /opt/qihuo | 端口 6600 | PM2
#
# 已内置修复(避免重复踩坑):
# - vnpy_ctp 编译:build-essential python3-dev pkg-config
# - CTP 登录崩溃:zh_CN.GB18030 + zh_CN.UTF-8 locale
# - 时区:Asia/Shanghai(与 SimNow 交易时段一致)
# - SimNow 前置:自动探测可用线路并写入 .env
# - PM2 环境变量:LANG/LC_ALL(见 ecosystem.config.cjs
# - .env 缺项补全:SIMNOW_ENV、CTP_AUTO_RECONNECT
set -euo pipefail set -euo pipefail
@@ -8,12 +16,81 @@ APP_DIR="/opt/qihuo"
REPO_URL="https://git.bz121.com/dekun/qihuo.git" REPO_URL="https://git.bz121.com/dekun/qihuo.git"
SERVICE_NAME="qihuo" SERVICE_NAME="qihuo"
# SimNow 前置候选(按优先级;部署时自动 nc 探测)
SIMNOW_FRONTS=(
"180.168.146.187:10201:10211"
"180.168.146.187:10202:10212"
"180.168.146.187:10130:10131"
"218.202.237.33:10203:10213"
)
if [ "$(id -u)" -ne 0 ]; then if [ "$(id -u)" -ne 0 ]; then
echo "请使用 root 用户运行: sudo bash deploy.sh" echo "请使用 root 用户运行: sudo bash deploy.sh"
exit 1 exit 1
fi fi
probe_tcp() {
local host="$1" port="$2"
if command -v nc &>/dev/null; then
nc -z -w 3 "$host" "$port" &>/dev/null
return $?
fi
timeout 3 bash -c "echo >/dev/tcp/${host}/${port}" 2>/dev/null
}
ensure_locale_gen() {
local pat="$1"
if [ -f /etc/locale.gen ] && grep -q "^# ${pat}" /etc/locale.gen; then
sed -i "s/^# ${pat}/${pat}/" /etc/locale.gen
fi
}
ensure_env_key() {
local file="$1" key="$2" val="$3"
if [ ! -f "$file" ]; then
return
fi
if grep -q "^${key}=" "$file"; then
return
fi
echo "${key}=${val}" >>"$file"
echo " .env 补全: ${key}=${val}"
}
pick_simnow_front() {
local host td_port md_port
for entry in "${SIMNOW_FRONTS[@]}"; do
IFS=: read -r host td_port md_port <<<"$entry"
if probe_tcp "$host" "$td_port" && probe_tcp "$host" "$md_port"; then
echo "tcp://${host}:${td_port}|tcp://${host}:${md_port}"
return 0
fi
echo " SimNow 前置不可达: ${host} ${td_port}/${md_port}" >&2
done
return 1
}
update_simnow_front_in_env() {
local env_file="$1"
local picked td md
picked="$(pick_simnow_front)" || {
echo "警告: 未能探测到可用 SimNow 前置,请手动编辑 ${env_file}(见 docs/SIMNOW.md"
return 1
}
td="${picked%%|*}"
md="${picked##*|}"
echo "==> SimNow 可用前置: TD=${td} MD=${md}"
if grep -q "^SIMNOW_TD_ADDRESS=" "$env_file"; then
sed -i "s|^SIMNOW_TD_ADDRESS=.*|SIMNOW_TD_ADDRESS=${td}|" "$env_file"
sed -i "s|^SIMNOW_MD_ADDRESS=.*|SIMNOW_MD_ADDRESS=${md}|" "$env_file"
else
echo "SIMNOW_TD_ADDRESS=${td}" >>"$env_file"
echo "SIMNOW_MD_ADDRESS=${md}" >>"$env_file"
fi
}
echo "==> 检查系统依赖..." echo "==> 检查系统依赖..."
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq apt-get update -qq
need_install() { need_install() {
@@ -26,6 +103,29 @@ need_install python3 python3
need_install python3-venv python3-venv need_install python3-venv python3-venv
need_install git git need_install git git
echo "==> 安装 vnpy_ctp 编译依赖..."
apt-get install -y build-essential python3-dev pkg-config locales netcat-openbsd
echo "==> 配置时区 Asia/Shanghai..."
if command -v timedatectl &>/dev/null; then
timedatectl set-timezone Asia/Shanghai || true
fi
echo "==> 配置 CTP 所需 localezh_CN.GB18030 等)..."
ensure_locale_gen "zh_CN.GB18030 GB18030"
ensure_locale_gen "zh_CN.UTF-8 UTF-8"
ensure_locale_gen "en_US.UTF-8 UTF-8"
locale-gen zh_CN.GB18030 zh_CN.UTF-8 en_US.UTF-8 2>/dev/null || locale-gen
update-locale LANG=zh_CN.UTF-8 LC_ALL=zh_CN.UTF-8 2>/dev/null || true
export LANG=zh_CN.UTF-8
export LC_ALL=zh_CN.UTF-8
if ! locale -a 2>/dev/null | grep -qi gb18030; then
echo "错误: zh_CN.GB18030 未生成,CTP 连接后会崩溃"
exit 1
fi
echo " locale OK: $(locale -a 2>/dev/null | grep -i gb18030 | head -1)"
if ! command -v pm2 &>/dev/null; then if ! command -v pm2 &>/dev/null; then
echo "==> 安装 PM2..." echo "==> 安装 PM2..."
if ! command -v npm &>/dev/null; then if ! command -v npm &>/dev/null; then
@@ -51,34 +151,67 @@ else
cd "$APP_DIR" cd "$APP_DIR"
fi fi
echo "==> 创建 Python 虚拟环境..." echo "==> Python 虚拟环境与依赖..."
python3 -m venv "$APP_DIR/venv" if [ ! -d "$APP_DIR/venv" ]; then
python3 -m venv "$APP_DIR/venv"
fi
# shellcheck disable=SC1091
source "$APP_DIR/venv/bin/activate" source "$APP_DIR/venv/bin/activate"
pip install --upgrade pip -q pip install --upgrade pip -q
pip install -r "$APP_DIR/requirements.txt" -q pip install -r "$APP_DIR/requirements.txt"
python -c "from vnpy_ctp import CtpGateway; print('vnpy_ctp OK')"
if [ ! -f "$APP_DIR/.env" ]; then echo "==> 配置 config/.env..."
echo "==> 生成 .env(请编辑 ADMIN_PASSWORD 后重启)..." ENV_FILE="$APP_DIR/config/.env"
cp "$APP_DIR/.env.example" "$APP_DIR/.env" mkdir -p "$APP_DIR/config"
if [ ! -f "$ENV_FILE" ]; then
cp "$APP_DIR/config/.env.example" "$ENV_FILE"
RAND_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))") RAND_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
sed -i "s/change-this-to-a-random-secret-key/${RAND_KEY}/" "$APP_DIR/.env" sed -i "s/change-this-to-a-random-secret-key/${RAND_KEY}/" "$ENV_FILE"
echo " 已生成 config/.env,请编辑 SIMNOW_USER / ADMIN_PASSWORD"
fi fi
ensure_env_key "$ENV_FILE" "SIMNOW_ENV" "实盘"
ensure_env_key "$ENV_FILE" "CTP_AUTO_RECONNECT" "true"
ensure_env_key "$ENV_FILE" "SIMNOW_BROKER_ID" "9999"
ensure_env_key "$ENV_FILE" "SIMNOW_APP_ID" "simnow_client_test"
ensure_env_key "$ENV_FILE" "SIMNOW_AUTH_CODE" "0000000000000000"
update_simnow_front_in_env "$ENV_FILE" || true
mkdir -p "$APP_DIR/logs" mkdir -p "$APP_DIR/logs"
echo "==> 验证 CTP 环境..."
if grep -q "^SIMNOW_USER=.\+" "$ENV_FILE" 2>/dev/null && \
grep -q "^SIMNOW_PASSWORD=.\+" "$ENV_FILE" 2>/dev/null; then
set +e
python "$APP_DIR/scripts/test_simnow.py"
CTP_TEST=$?
set -e
if [ "$CTP_TEST" -ne 0 ]; then
echo "警告: SimNow 连接测试未通过,请检查 .env 账号与网络(见 docs/SIMNOW.md"
else
echo " SimNow CTP 连接测试通过"
fi
else
echo " 跳过 CTP 测试(未配置 SIMNOW_USER / SIMNOW_PASSWORD"
fi
echo "==> PM2 启动/重启服务..." echo "==> PM2 启动/重启服务..."
cd "$APP_DIR" cd "$APP_DIR"
pm2 delete "$SERVICE_NAME" 2>/dev/null || true if pm2 describe "$SERVICE_NAME" &>/dev/null; then
pm2 start ecosystem.config.cjs pm2 restart ecosystem.config.cjs --update-env
else
pm2 start ecosystem.config.cjs
fi
pm2 save pm2 save
echo "" echo ""
echo "==========================================" echo "=========================================="
echo " 部署完成" echo " 部署完成"
echo " 目录: ${APP_DIR}" echo " 目录: ${APP_DIR}"
echo " 用户: root" echo " 时区: $(date +%Z) $(date '+%Y-%m-%d %H:%M:%S')"
echo " 端口: 6600" echo " 端口: 6600"
echo " 访问: http://<服务器IP>:6600" echo " 访问: http://$(hostname -I 2>/dev/null | awk '{print $1}'):6600"
echo " 日志: pm2 logs ${SERVICE_NAME}" echo " 日志: pm2 logs ${SERVICE_NAME}"
echo " SimNow 注册: docs/SIMNOW.md"
echo " 开机自启: pm2 startup && pm2 save" echo " 开机自启: pm2 startup && pm2 save"
echo "==========================================" echo "=========================================="
+152
View File
@@ -0,0 +1,152 @@
# AI 分析
**页面路径**`/ai`(须在系统设置 → 导航显示 中开启「AI 分析」)
**相关文件**`ai_client.py``ai_worker.py``ai_messages.py``templates/ai_messages.html`
---
## 功能概述
| 能力 | 说明 |
|------|------|
| 开仓分析 | 手动/关键位开仓成交后后台分析 |
| 平仓分析 | 写入 `trade_logs` 平仓记录后分析 |
| 日终报告 | 每日定时汇总当日交易与持仓 |
| 消息存档 | 全部写入 `ai_messages` 表,页面可浏览 |
| 微信推送 | 可选:分析成功后推送到企业微信 |
---
## 配置项(系统设置 → AI 分析)
| 设置键 | 说明 | 默认 |
|--------|------|------|
| `ai_enabled` | 总开关 | 关 |
| `ai_provider` | `ollama` / `openai` | ollama |
| `ai_ollama_base_url` | Ollama 地址 | `http://127.0.0.1:11434` |
| `ai_ollama_model` | Ollama 模型 | `qwen2.5:7b` |
| `ai_openai_base_url` | OpenAI 兼容 API | `https://api.openai.com/v1` |
| `ai_openai_api_key` | API Key | 空 |
| `ai_openai_model` | 模型名 | `gpt-4o-mini` |
| `ai_daily_report_enabled` | 日终报告开关 | 开 |
| `ai_daily_report_hour` | 报告时刻(时) | 15 |
| `ai_daily_report_minute` | 报告时刻(分) | 5 |
---
## 触发时机
### 1. 开仓分析(`kind=open`
- **触发**:下单监控手动开仓 **成交** 且填写止损 → `notify_manual_open_filled()` 调度。
- **标题**`{品种名} 开仓`
- **Payload**symbol、direction、entry、stop_loss、take_profit、lots、capital
### 2. 关键位开仓(`kind=key_open`
- **触发**:箱体/收敛突破自动单成交 → `notify_key_breakout_open()` 调度。
- **标题**`{品种名} 关键位开仓`
- **Payload**monitor_type、trade_mode、break_side、entry、stop_loss、take_profit、lots
### 3. 平仓分析(`kind=close`
- **触发**`trade_logs` 新增平仓 → `notify_trade_log_close()` 调度。
- **标题**`{品种名} 平仓`
- **Payload**source、result、pnl_net、entry、close_price、lots
### 4. 日终报告(`kind=daily_report`
- **触发**:后台 `ai_worker` 每分钟检查;到达设定时刻且当日未生成过。
- **标题**`{YYYY-MM-DD} 日终持仓与交易报告`
- **Payload**:当日成交汇总、胜负次数、净盈亏、active 持仓列表
> 开仓/平仓的 AI 分析 **默认不推微信**(仅写入页面);日终报告与可在设置中开启的推送见下文。
---
## 分析逻辑
**System Prompt**(固定):
```
你是国内期货交易复盘助手。根据提供的结构化交易数据,
用简洁中文给出 3~6 条要点:风险、纪律、改进建议。
不要编造未提供的数据;金额单位为元。
```
**User Prompt**
```
事件类型:{event_kind}
数据:
{JSON payload}
```
**接口**
- Ollama`POST {base}/api/chat`
- OpenAI 兼容:`POST {base}/chat/completions`temperature=0.4
失败时内容前加 `⚠`,仍写入 `ai_messages`
---
## 微信推送
| 类型 | 条件 | 模板 |
|------|------|------|
| 事件分析 | AI 调用成功 **且** 调度时传入 `send_wechat_fn` | 见 [WECHAT.md §12](./WECHAT.md#12-ai-分析) |
| 日终报告 | `ai_daily_report_enabled=1` 且 AI 成功 | `🤖 {date} 日终持仓与交易报告\n\n{content[:1800]}` |
**说明**
- 当前实现中,开仓/平仓触发的 `schedule_ai_event_analysis()` **未绑定** `send_wechat_fn`,故 **仅存档到 /ai 页面**
- 日终报告在 `maybe_run_daily_ai_report()` 中会推微信(成功时)。
- 正文截断为 **1800 字符**,避免超长。
---
## 下单逻辑
AI **不参与下单**,仅事后分析。无 `assert_can_open` 或 CTP 交互。
---
## 风控规则
AI 模块无独立风控。其输入数据来自已发生的交易事件,不改变账户冻结、保证金等状态。
---
## 后台任务
- 线程名:`ai-worker`
- 启动延迟 30 秒后,每 **60 秒** 检查是否该跑日终报告
-`background_task`(计划/关键位)独立运行
---
## 数据存储
`ai_messages`
| 字段 | 说明 |
|------|------|
| kind | open / key_open / close / daily_report |
| title | 标题 |
| content | AI 回复正文 |
| meta | JSON payload |
| created_at | 创建时间 |
页面 `/ai` 按时间倒序展示;上方含使用说明卡片。
---
## 相关文档
- [WECHAT.md §12](./WECHAT.md#12-ai-分析) — 推送模板
- [ORDER_MONITOR.md](./ORDER_MONITOR.md) — 手动开仓触发 AI
- [KEY_MONITORS.md](./KEY_MONITORS.md) — 关键位开仓触发 AI
- [SETTINGS.md](./SETTINGS.md) — 配置入口
+50
View File
@@ -0,0 +1,50 @@
# 主目录结构
```
qihuo/ # 主文件夹(仓库根)
├── app.py # 主程序入口(Flask 启动)
├── requirements.txt
├── deploy.sh # 一键部署脚本
├── ecosystem.config.cjs # PM2 启动配置
├── config/
│ ├── .env.example # 环境变量模板
│ └── .env # 运行时配置(git 忽略)
├── modules/ # 业务模块(每个模块 register(deps)
│ ├── core/ # DB、路径、公共工具
│ ├── web/ # 页面路由 + static/ + templates/
│ ├── trading/ # 下单监控、持仓、推荐
│ ├── ctp/ # vn.py / CTP 连接与报单
│ ├── risk/ # 账户风控
│ ├── strategy/ # 趋势、滚仓策略
│ ├── keys/ # 关键位
│ ├── plans/ # 开单计划
│ ├── market/ # 行情、K 线
│ ├── records/ # 交易记录、复盘
│ ├── stats/ # 统计、看板
│ ├── settings/ # 系统设置
│ ├── notify/ # 微信、AI 消息
│ ├── fees/ # 手续费
│ └── backup/ # 备份
├── _legacy/ # 旧 import 兼容 shimPM2 PYTHONPATH
├── data/ # 静态数据(如 fee_rates.json
├── docs/ # 文档
├── scripts/ # 运维/诊断脚本(非运行时)
├── futures.db # SQLite(未配 PG 时)
├── uploads/
└── logs/
```
根目录 `_legacy/` 为旧 `import db_conn` 等路径的兼容层;新代码请 `from modules.xxx import ...`
## 进程模型
- **单进程**PM2 仅 `qihuo``app.py` + CTP 同进程)
- 详见 [DEPLOY.md](./DEPLOY.md)
## 模块契约
每个 `modules/<name>/` 提供 `register(deps: AppDeps)`;主程序 `app.py` 只做串联,不写业务。
## 发布
见 [DEPLOY.md](./DEPLOY.md)**本地修改 → git push → 服务器 git pull**,禁止 SCP。
+145
View File
@@ -0,0 +1,145 @@
# 数据备份与恢复
qihuo 支持自动备份数据库与复盘附件,生成可在其他 Linux 服务器恢复的压缩包。
存储后端由 `.env` 决定:
| 后端 | 备份包内主文件 | 说明 |
|------|----------------|------|
| SQLite(默认) | `futures.db` | 本地单文件库 |
| PostgreSQL | `postgres_dump.sql` | `pg_dump` 逻辑备份 |
PostgreSQL 部署与迁移见 **[POSTGRES.md](./POSTGRES.md)**。
---
## 备份内容
| 内容 | 说明 |
|------|------|
| `futures.db` | SQLite 主库(仅 SQLite 模式) |
| `postgres_dump.sql` | PostgreSQL 逻辑备份(仅 PostgreSQL 模式) |
| `uploads/` | 复盘截图、自动 K 线图(若存在) |
| `manifest.json` | 备份时间、**backend** 字段、文件清单 |
| `RESTORE.md` | 包内恢复说明 |
| `restore.sh` | 一键恢复脚本 |
**不包含** `.env`(含 CTP 密码、`DATABASE_URL` 等),请单独安全保管或在新服务器重新配置。
---
## 备份目录
默认:**`/root/qihuo_backup`**
可通过环境变量覆盖:
```bash
# /opt/qihuo/.env 或 systemd/PM2 环境
QIHUO_BACKUP_DIR=/data/qihuo_backup
```
---
## 系统设置页
路径:**系统设置 → 数据备份与恢复**
- **立即备份**:后台生成 `qihuo_backup_YYYYMMDD_HHMMSS.tar.gz`
- **每日自动备份**:默认每天 **03:00**Asia/Shanghai)执行
- **保留份数**:默认保留最近 **30** 份,超出自动删除最旧文件
- **下载**:列表中点击「下载」获取压缩包
PostgreSQL 模式下需服务器已安装 `pg_dump``apt install postgresql-client` 或完整 `postgresql` 包)。
---
## 在新服务器恢复
### 方式一:使用包内脚本(推荐)
```bash
# 1. 上传压缩包到目标机
scp qihuo_backup_20260626_030015.tar.gz root@新服务器:/root/
# 2. 解压并恢复
cd /root
tar -xzf qihuo_backup_20260626_030015.tar.gz
cd qihuo_backup_20260626_030015
chmod +x restore.sh
# SQLite:直接恢复 futures.db
RESTORE_DIR=/opt/qihuo ./restore.sh
# PostgreSQL:先配置 /opt/qihuo/.env 的 DATABASE_URL,再执行
export RESTORE_DIR=/opt/qihuo
# 若 .env 在 RESTORE_DIR 下且含 DATABASE_URLrestore.sh 会自动 source
./restore.sh
```
默认恢复到 **`/root/qihuo`**。若生产目录为 `/opt/qihuo`
```bash
RESTORE_DIR=/opt/qihuo ./restore.sh
```
### 方式二:手工复制(SQLite)
```bash
tar -xzf qihuo_backup_20260626_030015.tar.gz
cd qihuo_backup_20260626_030015
pm2 stop qihuo
cp futures.db /opt/qihuo/futures.db
cp -a uploads/. /opt/qihuo/uploads/
pm2 restart qihuo
```
### 方式三:手工导入(PostgreSQL
```bash
pm2 stop qihuo
export DATABASE_URL=postgresql://qihuo:密码@127.0.0.1:5432/qihuo
psql "$DATABASE_URL" -f postgres_dump.sql
cp -a uploads/. /opt/qihuo/uploads/
pm2 restart qihuo
```
### 恢复后检查清单
1. 已部署 qihuo 代码与 Python 虚拟环境(见 [DEPLOY.md](./DEPLOY.md)
2. 已配置 `.env``DATABASE_URL` 或 SQLite、`SECRET_KEY`、CTP 账号等)
3. PostgreSQL:库已创建且 `DATABASE_URL` 可连接
4. 访问 Web 登录,检查交易记录、统计页是否正常
5. CTP 模式需在新环境重新连接柜台
---
## 注意事项
- **恢复前务必停止 qihuo**,避免进程占用数据库导致覆盖不完整
- SQLite 备份使用 SQLite `backup` API,并在 WAL 模式下尝试 checkpoint
- PostgreSQL 备份使用 `pg_dump`,恢复使用 `psql -f`
- 自动备份在应用后台线程执行,与 Web 服务同进程
- 大体积 `uploads/` 会使压缩包变大,可按需定期清理无用截图
- 不要将含 `.env`、数据库的压缩包上传到公开网盘
---
## 故障排查
| 现象 | 处理 |
|------|------|
| 设置页无备份列表 | 检查 `/root/qihuo_backup` 目录权限,进程需可写 |
| 立即备份无反应 | 查看 PM2 日志;可能上一任务仍在进行 |
| PostgreSQL 备份失败 | 安装 `postgresql-client`;检查 `DATABASE_URL` |
| 下载 404 | 文件名须为系统生成的 `qihuo_backup_*.tar.gz` |
| 恢复后无法登录 | 确认数据已导入实际使用的库(SQLite 文件或 PG) |
| 恢复后 CTP 连不上 | 在新服务器配置正确的 `.env` CTP 参数 |
---
## 相关文档
- [POSTGRES.md](./POSTGRES.md) — PostgreSQL 一键部署、迁移、备份恢复
- [DEPLOY.md](./DEPLOY.md) — 部署与目录结构
- [FEATURES.md](./FEATURES.md) — 功能与路由一览
+238
View File
@@ -0,0 +1,238 @@
# 期货公司实盘 CTP
本文说明如何接入 **期货公司实盘 CTP**,并对比 **SimNow 模拟盘****实盘** 的开平仓逻辑是否一致。
相关代码:`vnpy_bridge.py``ctp_worker.py``ctp_ipc_client.py``ctp_settings.py``trading_context.py``install_trading.py`
---
## 进程架构(Web / CTP Worker
自 2026-03 起,CTP 与 vn.py **不在 Flask 进程内**运行:
| 组件 | PM2 名 | 说明 |
|------|--------|------|
| Web | `qihuo` | 页面与 API`QIHUO_CTP_ROLE=client`,经 `127.0.0.1:6601` IPC 访问 Worker |
| CTP Worker | `qihuo-ctp` | 唯一加载 vnpy_ctp;连接、报单、持仓回调、止盈止损、滚仓 pending 监控 |
SimNow 与期货公司实盘的 **报单代码路径不变**,仍走 `execute_order()``vnpy_bridge.send_order()`Web 侧为 IPC 代理,Worker 侧为原生调用。
部署与重启见 [DEPLOY.md](./DEPLOY.md)。
---
## 一、SimNow 与实盘的关系
| 项目 | 模拟盘(SimNow) | 实盘(期货公司 CTP |
|------|------------------|----------------------|
| 系统设置 | **模拟盘 · SimNow** | **期货公司实盘** |
| 配置项 | `SIMNOW_*` / `simnow_*` | `CTP_LIVE_*` / `ctp_live_*` |
| 连接对象 | SimNow 仿真前置 | 开户期货公司提供的 TD/MD 前置 |
| 资金与持仓 | SimNow 柜台 | 真实资金与持仓 |
| **报单代码路径** | `execute_order()``vnpy_bridge.send_order()` | **完全相同** |
> 本系统 **没有** 本地假撮合。模拟盘与实盘均通过 **vnpy_ctp** 向 CTP 柜台发真实委托;区别仅在于 **连哪组前置、用哪套账号**,以及柜台返回的保证金/费率/拒单规则。
SimNow 接入步骤见 [SIMNOW.md](./SIMNOW.md)。
---
## 二、实盘接入前准备
### 1. 向期货公司申请
通常需要:
1. 期货账户已开户并完成适当性评估
2. 申请 **CTP 程序化交易****API 接入**(各公司流程不同)
3. 获取 **看穿式监管** 所需的:
- 经纪商代码(BrokerID
- 投资者代码 / 资金账号
- 交易密码
- **交易前置(TD)**、**行情前置(MD)** 地址(`tcp://IP:端口`
- **AppID(产品名称)**、**AuthCode(授权编码)** — 实盘必须由期货公司分配,**不能** 使用 SimNow 的 `simnow_client_test`
4. 在期货公司 **仿真/实盘环境** 中确认 AppID 已报备、账号已绑定
具体以开户营业部或官方 API 文档为准。
### 2. 配置 `.env`
```env
TRADING_MODE=live
CTP_LIVE_USER=你的投资者代码
CTP_LIVE_PASSWORD=你的交易密码
CTP_LIVE_BROKER_ID=期货公司BrokerID
CTP_LIVE_TD_ADDRESS=tcp://xxx.xxx.xxx.xxx:端口
CTP_LIVE_MD_ADDRESS=tcp://xxx.xxx.xxx.xxx:端口
CTP_LIVE_APP_ID=期货公司分配的AppID
CTP_LIVE_AUTH_CODE=期货公司分配的AuthCode
CTP_LIVE_ENV=实盘
```
也可在 **系统设置 → CTP 连接 → 期货公司实盘** 中填写;**页面保存优先于 `.env`**。
| 变量 | 说明 |
|------|------|
| `CTP_LIVE_USER` | 投资者代码(InvestorID),非手机号 |
| `CTP_LIVE_PASSWORD` | 交易密码 |
| `CTP_LIVE_BROKER_ID` | 期货公司 BrokerID(每家不同,**不是** SimNow 的 9999 |
| `CTP_LIVE_TD_ADDRESS` | 交易服务器 |
| `CTP_LIVE_MD_ADDRESS` | 行情服务器 |
| `CTP_LIVE_APP_ID` | 看穿式 AppID |
| `CTP_LIVE_AUTH_CODE` | 看穿式授权码 |
| `CTP_LIVE_ENV` | 一般为 **实盘**;测评环境按期货公司要求 |
### 3. 切换与连接
1. **系统设置 → 交易模式****期货公司实盘**
2. 保存 CTP 实盘账号与前置
3. **下单监控****连接 CTP**
4. 顶栏显示 **CTP 已连接**、**期货公司实盘**,权益为柜台资金
修改模式或前置后建议 **重连 CTP**`pm2 restart ecosystem.config.cjs --update-env`(同时重启 Web 与 Worker)。
---
## 三、开平仓逻辑(代码层)
模拟盘与实盘共用 **`vnpy_bridge.send_order()`**,映射如下。
### 开仓
| 本系统参数 | CTP / vnpy |
|------------|------------|
| `offset=open` + `direction=long` | Direction.**LONG** + Offset.**OPEN**(买开) |
| `offset=open` + `direction=short` | Direction.**SHORT** + Offset.**OPEN**(卖开) |
### 平仓
| 本系统参数 | CTP 方向 | 开平标志 |
|------------|----------|----------|
| `offset=close_long` + 持多 | Direction.**SHORT**(卖平) | 见下表 |
| `offset=close_short` + 持空 | Direction.**LONG**(买平) | 见下表 |
**开平标志** 由 VeighNa **`OffsetConverter`**`vnpy/trader/converter.py`)自动转换:策略层发 `Offset.CLOSE`,框架按今/昨仓拆单为 `CLOSETODAY` / `CLOSEYESTERDAY`(与 CTA 引擎 `sell()`/`cover()` 一致)。持仓今昨数据来自 CTP `OnRspQryInvestorPosition`**PositionDate** 字段,修正 vnpy 网关合并误差后喂给 OffsetConverter。
| 交易所 | 规则 |
|--------|------|
| **大商所(DCE** 等 | 使用通用 **CLOSE** |
| **上期所(SHFE)、能源(INE** | OffsetConverter 按今/昨可平量自动拆单 |
| 持仓来源 | CTP **PositionDate**1=今仓、2=昨仓)缓存,修正 `PositionData.yd_volume` |
这与国内期货公司 CTP 客户端、快期等 **标准投机平仓逻辑** 一致。
### 委托类型
| 页面选项 | vnpy 类型 | 说明 |
|----------|-----------|------|
| 限价 | `OrderType.LIMIT` | 价格按最小变动价位取整 |
| 市价 | `OrderType.FAK` + **对手价(买一/卖一)** + 滑点 | 非「无价格市价单」;止损约 12 跳、强平约 20 跳(强平另受权益滑点预留上限约束) |
止盈止损触发、手动平仓走 `urgency=stop_loss`;日亏损强平走 `urgency=risk_flatten`
---
## 四、SimNow 开平仓是否符合期货公司实盘逻辑?
### 结论(简要)
| 层级 | 是否一致 | 说明 |
|------|----------|------|
| **CTP 报单语义**(开/平、买卖方向、平今平昨) | **是** | 同一套 `send_order`,符合国内 CTP 规范 |
| **交易所平今平昨规则** | **是** | 按 SHFE/INE/CZCE/CFFEX 与 DCE 分别处理 |
| **连接与鉴权** | **否(环境不同)** | BrokerID、前置、AppID/AuthCode 必须换期货公司参数 |
| **柜台业务规则** | **部分一致** | SimNow 仿真在保证金、合约、拒单细节上可能与实盘有差异 |
| **本程序附加层** | **两模式相同** | 本地 SL/TP、移动保本、风控、微信推送等与柜台无关,SimNow/实盘行为一致 |
### 一致的部分(可放心联调)
1. **开仓**:买开 / 卖开 → `Offset.OPEN`
2. **平仓**:平多 = 卖 + 平今/平昨/平;平空 = 买 + 平今/平昨/平
3. **持仓同步**:来自 CTP 柜台回报,写入监控与 `trade_logs` 同步逻辑相同
4. **撤单、挂单超时**:对 CTP 委托号操作,两模式相同
5. **策略 / 关键位自动单**:最终都调用 `execute_order()`,无「模拟专用分支」
在 SimNow 上验证通过的开平仓流程,**报单层面** 可直接用于实盘;实盘主要风险在 **账号权限、资金、AppID 报备、网络到期货公司前置**,而非本系统换一套开平逻辑。
### 可能差异(接入实盘时需自行核对)
| 项目 | SimNow | 实盘 |
|------|--------|------|
| BrokerID | 固定 9999 | 各期货公司不同 |
| AppID / AuthCode | 默认测试值 | 必须向期货公司申请 |
| 保证金 / 手续费 | 仿真柜台 | 真实费率,以 CTP 查询为准 |
| 合约限制 | 部分合约或规则简化 | 以期货公司 + 交易所为准 |
| 拒单原因 | 仿真环境 | 资金不足、非交易时间、未报备 AppID、风控拒单等 |
| 看穿式 | `SIMNOW_ENV=实盘` | `CTP_LIVE_ENV=实盘`,且 AppID 必须在期货公司报备 |
### 本系统「非 CTP 标准」但两模式相同的部分
以下 **不是** 期货公司客户端自带功能,而是 **本程序本地逻辑**SimNow 与实盘 **行为一致**,但都与「只用手动在快期下单」不同:
| 功能 | 说明 |
|------|------|
| 本地止盈 / 止损 | 程序监控价格,触发后 **再向 CTP 发平仓单** |
| 移动保本 | 动态抬止损,触发后市价 FAK 平仓 |
| 挂单超时撤单 | 本系统定时检查 pending 监控并撤单 |
| 账户冷静期 / 仓位上限 | 本系统风控,在发单前拦截 |
| 开单计划 / 关键支阻区 | 仅提醒,不自动报单(关键位自动单除外) |
实盘使用时需知:CTP 柜台 **不会** 自动执行上述本地 SL/TP;只有程序运行且 CTP 已连接时才会生效。
---
## 五、实盘常见问题
| 现象 | 处理 |
|------|------|
| 连接失败 / 4097 | 核对 AppID、AuthCode、BrokerID;升级 `vnpy_ctp``CTP_LIVE_ENV=实盘` |
| 不合法的登录 | 投资者代码或密码错误;是否在期货公司柜台改过密码 |
| 登录成功但拒单 | 资金、交易时段、合约代码、价格 tick、AppID 未报备 |
| 平今平昨报错 | 检查是否持有对应今/昨仓;上期所等必须正确平今/平昨 |
| 服务器连不上前置 | 期货公司是否要求 **白名单 IP**;云服务器出网是否放行 |
| 与 SimNow 行为不一致 | 优先查 **保证金、费率、合约**;报单方向/开平标志可在日志中看 `CTP 报单 … offset=` |
诊断 SimNow 可用 `python scripts/test_simnow.py`;实盘可将 `.env` 临时改为 live 配置后在同脚本逻辑下连 TD(需自行改脚本或看 PM2/应用日志)。
---
## 六、日志中如何确认报单
连接成功后,下单会在日志中输出类似:
```text
CTP 报单 rb2510 SHFE Direction.SHORT 2手 @3205 offset=Offset.CLOSETODAY type=OrderType.FAK
```
- **offset=Offset.OPEN** → 开仓
- **offset=Offset.CLOSETODAY / CLOSEYESTERDAY / CLOSE** → 平仓类型
- **Direction** 与持仓方向相反 → 平仓方向正确
SimNow 与实盘日志格式相同,仅 `mode` 与前置地址不同。
---
## 七、相关文档
| 文档 | 内容 |
|------|------|
| [SIMNOW.md](./SIMNOW.md) | SimNow 注册与仿真接入 |
| [TRADING.md](./TRADING.md) | 下单监控、两种通道概览 |
| [ORDER_MONITOR.md](./ORDER_MONITOR.md) | 手动开平仓、SL/TP、移动保本 |
| [DEPLOY.md](./DEPLOY.md) | 部署与环境变量总表 |
| [RISK.md](./RISK.md) | 账户风控(实盘同样生效) |
---
## 八、实盘接入检查清单
- [ ] 已向期货公司申请 CTP / 程序化权限
- [ ] 已获取 BrokerID、TD/MD 地址、AppID、AuthCode
- [ ] `.env` 或系统设置中 `CTP_LIVE_*` 已填写
- [ ] 系统设置交易模式为 **期货公司实盘**
- [ ] 下单监控 **连接 CTP** 成功,权益为真实资金
- [ ] 用小仓位测试:开仓 → 平仓(含上期所品种测平今)
- [ ] 确认本地 SL/TP 触发后能在 CTP 看到平仓委托
- [ ] 生产环境限制访问(HTTPS、防火墙、强密码)
+590
View File
@@ -0,0 +1,590 @@
# 部署文档
国内期货 · 交易复盘系统 — Ubuntu 服务器部署、更新与运维说明。
---
## 代码发布铁律(强制,不容置疑)
**所有代码变更必须且只能按以下三步执行,不得跳过、不得变通:**
| 步骤 | 在哪里 | 做什么 |
|------|--------|--------|
| **1. 本地修改** | 开发机 / 本仓库工作区 | 改代码、自测 |
| **2. 提交仓库** | `git.bz121.com` | `git add``git commit``git push origin main`(或约定分支) |
| **3. 更新服务器** | `/opt/qihuo` | **仅** `git fetch` + `git reset --hard origin/main`(或 `git pull`)→ 依赖/迁移 → `pm2 restart` |
### 严禁事项
- **禁止** 用 `scp``rsync`、SFTP、手工复制等方式把 `.py` / `.js` / `.html` / 模板 / 静态资源 **直接覆盖** 到服务器。
- **禁止** 在服务器上 `vim` 改业务代码后长期不提交仓库(`.env`、日志、上传文件除外)。
- **禁止** 「服务器上先改一版、本地以后再补提交」——服务器代码必须与远端 Git **完全一致**
违反上述规则会导致:`git pull` 冲突、Web 与 Worker 版本不一致、问题无法复现、回滚困难。**一律视为部署事故。**
### 服务器唯一合法更新命令
代码已推送到远端后,在服务器执行:
```bash
cd /opt/qihuo
git fetch origin
git reset --hard origin/main
source venv/bin/activate
pip install -r requirements.txt
python scripts/run_schema_migrate.py
pm2 restart ecosystem.config.cjs --update-env
pm2 save
```
或使用 `bash deploy.sh`(内部同样通过 Git 拉取,见下文)。
### 数据与配置(不受 Git 管理)
以下文件 **不**`git pull` 更新,卸载/重装时须 **单独备份与恢复**
- `/opt/qihuo/config/.env`(兼容旧版 `/opt/qihuo/.env`
- `/opt/qihuo/futures.db`SQLite)或 PostgreSQL 数据
- `/opt/qihuo/uploads/`
- `/opt/qihuo/backups/`(若有)
---
## 部署概要
| 项目 | 默认值 |
|------|--------|
| 部署目录 | `/opt/qihuo` |
| 运行用户 | `root`(与 `deploy.sh` / PM2 配置一致) |
| Web 端口 | `6600`(对外) |
| CTP Worker 端口 | `6601`(仅 `127.0.0.1`,Web 进程 IPC 调用,勿对外开放) |
| 进程管理 | PM2**仅** `qihuo`Flask + CTP 单进程) |
| 数据库 | **生产推荐 PostgreSQL**(见 [POSTGRES.md](./POSTGRES.md));未配置 `DATABASE_URL` 时使用 SQLite `futures.db` |
| 仓库 | https://git.bz121.com/dekun/qihuo.git |
### 进程架构(2026-07 起:单进程)
| PM2 应用 | 说明 |
|----------|------|
| `qihuo` | Flask Web + **vn.py / CTP 同进程**`vnpy_bridge.CtpBridge` |
详见 [ARCHITECTURE.md](./ARCHITECTURE.md)。旧版 `qihuo-ctp` 独立 Worker **已废弃**`ecosystem.config.cjs` 不再启动该进程。
---
## 环境要求
- **系统**Ubuntu 20.04+(推荐)
- **Python**3.10+vnpy_ctp 要求 ≥3.10
- **Node.js + PM2**:进程守护与开机自启
- **编译工具**(安装 vnpy_ctp 时需要):`build-essential``python3-dev``pkg-config`
- **网络**
- `hq.sinajs.cn`(新浪行情)
- 企业微信 API(若启用推送)
- `git.bz121.com`(拉取代码)
- `pypi.org`pip 安装依赖)
- SimNow / 期货公司 **CTP 前置地址**(下单与持仓,见下文)
---
## 一键部署(推荐)
**root** 登录服务器后执行:
```bash
cd /opt/qihuo
# 若目录不存在,先克隆:
# git clone https://git.bz121.com/dekun/qihuo.git /opt/qihuo
bash deploy.sh
```
`deploy.sh` 会自动完成:
1. 安装系统依赖:`python3``git``build-essential``python3-dev``pkg-config``locales``netcat-openbsd``pm2`
2. **时区**设为 `Asia/Shanghai`(与 SimNow 交易时段一致)
3. **locale**:生成 `zh_CN.GB18030``zh_CN.UTF-8`CTP 登录必需,缺则进程崩溃)
4. `git pull``git clone``/opt/qihuo`
5. 创建/保留虚拟环境 `venv``pip install -r requirements.txt`,验证 `vnpy_ctp`
6. 首次生成 `.env`,并补全 `SIMNOW_ENV=实盘``CTP_AUTO_RECONNECT=true` 等缺项
7. **自动探测 SimNow 前置**`nc` 测端口),写入可用的 `SIMNOW_TD/MD_ADDRESS`(优先 `182.254.243.31`,其次 `180.168.146.187`
8. 若已配置 SimNow 账号,运行 `scripts/test_simnow.py` 验证连接
9. `pm2 restart ecosystem.config.cjs --update-env` 或首次 `pm2 start ecosystem.config.cjs`,并 `pm2 save`(仅 **`qihuo`** 一个进程)
部署完成后访问:`http://<服务器IP>:6600`
### PostgreSQL 生产库(推荐)
消除 SQLite 并发 `database is locked`,一键安装 PostgreSQL 并迁移:
```bash
cd /opt/qihuo
git pull
# 新装 PostgreSQL + 空库
sudo bash scripts/deploy_postgres.sh
# 从现有 futures.db 迁移
MIGRATE_SQLITE=1 sudo bash scripts/deploy_postgres.sh
```
完整说明、手动步骤、备份恢复见 **[POSTGRES.md](./POSTGRES.md)**。
> 再次部署只需 `cd /opt/qihuo && bash deploy.sh`,无需手工装 locale 或改前置地址。
---
## 服务器卸载与全新部署(Git 唯一来源)
当服务器代码被 SCP 弄乱、版本不可信、或需要与仓库 **完全对齐** 时,按本节 **卸载后重装**。全程 **只** 通过 Git 获取代码,**不得** SCP 复制业务文件。
### 1. 备份(必做)
```bash
# 在服务器上
cp /opt/qihuo/config/.env /root/qihuo.env.bak 2>/dev/null || cp /opt/qihuo/.env /root/qihuo.env.bak 2>/dev/null || true
# SQLite
cp /opt/qihuo/futures.db /root/futures.db.bak 2>/dev/null || true
# PostgreSQL 见 POSTGRES.md 备份命令
tar czf /root/qihuo_uploads.bak.tar.gz -C /opt/qihuo uploads 2>/dev/null || true
```
### 2. 卸载 PM2 与代码目录
```bash
pm2 stop qihuo 2>/dev/null || true
pm2 delete qihuo 2>/dev/null || true
pm2 save
rm -rf /opt/qihuo
```
> **不删除** `/root/qihuo.env.bak`、`/root/futures.db.bak` 等备份。
### 3. 从 Git 全新克隆并部署
```bash
git clone https://git.bz121.com/dekun/qihuo.git /opt/qihuo
cd /opt/qihuo
cp /root/qihuo.env.bak .env
# SQLite 恢复(若使用)
cp /root/futures.db.bak futures.db 2>/dev/null || true
bash deploy.sh
```
### 4. 验收
```bash
cd /opt/qihuo && git log -1 --oneline # 须与远端 main 最新提交一致
pm2 status # qihuo 为 online
```
浏览器访问 `http://<服务器IP>:6600` 登录验证。
此后所有更新 **只** 走上文「代码发布铁律」三步,**禁止** 再使用 SCP 更新代码。
---
## 手动部署
### 1. 安装系统依赖
```bash
apt update
apt install -y python3 python3-venv python3-pip python3-dev pkg-config git nodejs npm build-essential locales netcat-openbsd
timedatectl set-timezone Asia/Shanghai
sed -i '/^# zh_CN.GB18030/s/^# //' /etc/locale.gen
sed -i '/^# zh_CN.UTF-8/s/^# //' /etc/locale.gen
locale-gen zh_CN.GB18030 zh_CN.UTF-8
update-locale LANG=zh_CN.UTF-8 LC_ALL=zh_CN.UTF-8
npm install -g pm2
```
`build-essential``python3-dev``pkg-config` 用于编译安装 **vnpy_ctp**CTP 网关)。Meson 通过 pkg-config 查找 Python 头文件;缺 `pkg-config` 时会报 `Python dependency not found`
### 2. 克隆代码
```bash
git clone https://git.bz121.com/dekun/qihuo.git /opt/qihuo
cd /opt/qihuo
```
### 3. Python 虚拟环境与依赖
```bash
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
```
依赖已包含 **vnpy**、**vnpy_ctp**CTP 报单)、**akshare**(手续费同步)。安装完成后可验证:
```bash
python -c "from vnpy_ctp import CtpGateway; print('vnpy_ctp OK')"
```
若提示找不到模块,查看本文「CTP / vnpy 故障排查」一节。
```bash
cp config/.env.example config/.env
nano config/.env
```
| 变量 | 说明 |
|------|------|
| `HOST` | 监听地址,默认 `0.0.0.0` |
| `PORT` | 端口,默认 `6600` |
| `SECRET_KEY` | Flask Session 密钥,务必随机 |
| `ADMIN_USERNAME` | 初始管理员用户名 |
| `ADMIN_PASSWORD` | 初始管理员密码(仅首次建库生效) |
| `ADMIN_SYNC_FROM_ENV` | `true` 时重启可从 `.env` 同步账号密码 |
| `WECHAT_WEBHOOK` | 企业微信机器人地址(可选) |
| `QUOTE_SOURCE` | `sina`(默认)/ `ths` / `auto` |
| `THS_REFRESH_TOKEN` | 同花顺 iFinD token(机构用户) |
| `SIMNOW_USER` | SimNow 仿真账号(模拟盘必填) |
| `SIMNOW_PASSWORD` | SimNow 密码 |
| `SIMNOW_TD_ADDRESS` | SimNow 交易前置(以官网最新为准) |
| `SIMNOW_MD_ADDRESS` | SimNow 行情前置 |
| `CTP_LIVE_*` | 期货公司实盘 CTP(后期接入,见 `.env.example` |
| `TRADING_MODE` | `simulation`SimNow/ `live`(实盘) |
| `QIHUO_CTP_WORKER_TOKEN` | Web ↔ Worker IPC 鉴权(默认见 `ecosystem.config.cjs`,生产建议改随机串并保持两进程一致) |
| `QIHUO_CTP_WORKER_URL` | Web 侧 Worker 地址,默认 `http://127.0.0.1:6601` |
| `DATABASE_URL` | PostgreSQL 连接串(可选,见 [POSTGRES.md](./POSTGRES.md) |
示例:
```env
HOST=0.0.0.0
PORT=6600
SECRET_KEY=请替换为随机长字符串
ADMIN_USERNAME=admin
ADMIN_PASSWORD=你的强密码
ADMIN_SYNC_FROM_ENV=false
WECHAT_WEBHOOK=
QUOTE_SOURCE=sina
# —— SimNow 模拟盘(注册步骤见 docs/SIMNOW.md)——
SIMNOW_USER=你的SimNow账号
SIMNOW_PASSWORD=你的密码
SIMNOW_BROKER_ID=9999
SIMNOW_TD_ADDRESS=tcp://180.168.146.187:10201
SIMNOW_MD_ADDRESS=tcp://180.168.146.187:10211
SIMNOW_APP_ID=simnow_client_test
SIMNOW_AUTH_CODE=0000000000000000
SIMNOW_ENV=实盘
TRADING_MODE=simulation
```
SimNow 前置地址会随官网更新,部署前请到 [SimNow 官网](https://www.simnow.com.cn/) 核对 **7×24** 或交易时段地址。
### 6. PM2 启动
```bash
cd /opt/qihuo
pm2 start ecosystem.config.cjs # 启动 qihuo + qihuo-ctp
pm2 save
pm2 startup # 按提示执行命令,实现开机自启
```
确认两个进程均为 `online`
```bash
pm2 status
# 应看到 qihuo 与 qihuo-ctp
```
### 7. 创建日志目录(若不存在)
```bash
mkdir -p /opt/qihuo/logs /opt/qihuo/uploads
```
---
## 更新部署
> **强制流程**:本地修改 → `git push` → 服务器 `git fetch && git reset --hard origin/main` → 迁移 → `pm2 restart`。
> **禁止 SCP 复制代码。** 详见上文 [代码发布铁律](#代码发布铁律强制不容置疑)。
代码已推送后,在服务器执行:
```bash
cd /opt/qihuo
git fetch origin
git reset --hard origin/main
source venv/bin/activate
pip install -r requirements.txt
python scripts/run_schema_migrate.py
pm2 restart ecosystem.config.cjs --update-env
pm2 save
```
> 更新后执行 `pm2 restart ecosystem.config.cjs --update-env` 即可(仅 `qihuo`)。
若服务器曾用 SCP 覆盖文件导致 `git pull` 冲突,用 `git reset --hard origin/main` 与远端对齐。
`vnpy_ctp` 安装失败(常见于缺少编译环境):
```bash
apt install -y build-essential python3-dev pkg-config
source venv/bin/activate
pip install --no-cache-dir vnpy vnpy_ctp
pm2 restart ecosystem.config.cjs --update-env
```
应用启动时会自动执行 SQLite 表结构迁移(`ALTER TABLE` 容错),一般无需手工改库。
### 首次启用 CTP 下单
1. 浏览器登录 → **系统设置** 确认 **模拟盘 · SimNow**
2. 确认 `pm2 status`**`qihuo-ctp` 为 online**
3. 打开 **下单监控** 页 → 点击 **连接 CTP**(或由后台自动重连)
4. 连接成功后:权益来自柜台、显示 CTP 持仓、可报单与可开仓品种筛选
CTP 连接与重连在 **`qihuo-ctp` Worker** 内执行;页面仅轮询状态,**切换页面不会重复发起连接**。
详见 [TRADING.md](./TRADING.md)。
---
## PM2 常用命令
```bash
pm2 status # 查看 qihuo / qihuo-ctp 状态
pm2 logs qihuo # Web 日志
pm2 logs qihuo-ctp # CTP Worker 日志
pm2 logs qihuo --lines 100
pm2 restart ecosystem.config.cjs --update-env # 同时重启两个进程(推荐)
pm2 restart qihuo # 仅重启 Web
pm2 restart qihuo-ctp # 仅重启 CTP WorkerWeb 应仍可访问)
pm2 stop qihuo # 停止 Web
pm2 delete qihuo # 删除 Web 进程
pm2 save # 保存进程列表
```
日志文件:
- `/opt/qihuo/logs/pm2-out.log``pm2-error.log` — Web`qihuo`
- `/opt/qihuo/logs/pm2-ctp-out.log``pm2-ctp-error.log` — CTP Worker`qihuo-ctp`
---
## 本地开发
```bash
git clone https://git.bz121.com/dekun/qihuo.git
cd qihuo
python3 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env
python app.py
```
浏览器访问:`http://127.0.0.1:6600`
---
## 账号与密码
| 场景 | 操作 |
|------|------|
| 首次部署 | `.env` 中设置 `ADMIN_USERNAME` / `ADMIN_PASSWORD` 后启动 |
| 已部署后改 `.env` 密码 | 设 `ADMIN_SYNC_FROM_ENV=true``pm2 restart ecosystem.config.cjs --update-env` |
| 网页改密码 | 登录 → 系统设置 |
| 忘记密码 | `cd /opt/qihuo && source venv/bin/activate && python reset_admin.py` |
账号数据在 `futures.db``settings` 表,不会仅因改 `.env` 自动更新(除非开启 `ADMIN_SYNC_FROM_ENV`)。
---
## 数据库与数据文件
| 路径 | 说明 |
|------|------|
| `/opt/qihuo/futures.db` | 主数据库 |
| `/opt/qihuo/uploads/` | 复盘截图、自动 K 线图 |
| `/opt/qihuo/data/fee_rates.json` | 默认手续费表(可重载) |
| `/root/qihuo_backup/` | 系统自动备份目录(`.tar.gz` |
### 自动备份(推荐)
系统设置 → **数据备份与恢复**
- 默认每天 03:00 自动备份到 `/root/qihuo_backup`
-`futures.db``uploads/`,可在其他服务器恢复
- 设置页可立即备份、下载历史压缩包
完整说明见 **[BACKUP.md](./BACKUP.md)**。
### 手工备份(备选)
```bash
cp /opt/qihuo/futures.db /opt/qihuo/futures.db.bak.$(date +%Y%m%d)
```
### 手工补列(极少需要)
若极老版本库缺少字段,可对照报错执行(新版本启动会自动迁移):
```bash
sqlite3 /opt/qihuo/futures.db "ALTER TABLE key_monitors ADD COLUMN sina_code TEXT;"
```
---
## Nginx 反向代理(可选)
将 6600 反代到 80/443,并配置 HTTPS
```nginx
server {
listen 80;
server_name your.domain.com;
location / {
proxy_pass http://127.0.0.1:6600;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
---
## 防火墙
若使用 `ufw`,开放端口:
```bash
ufw allow 6600/tcp
# 或使用 Nginx 时只开放 80/443
```
---
## 故障排查
| 现象 | 可能原因 | 处理 |
|------|----------|------|
| 无法访问 6600 | 服务未启动 / 防火墙 | `pm2 status``pm2 logs qihuo` |
| **`qihuo-ctp` 不在线 / 反复重启** | vnpy 崩溃、SimNow 前置不可达、locale 缺失 | `pm2 logs qihuo-ctp --lines 200`;核对 SimNow 前置与 `zh_CN.GB18030` |
| **页面显示 CTP 未连接但 Worker 正常** | Web 与 Worker Token 不一致 | 检查 `ecosystem.config.cjs` 两进程 `QIHUO_CTP_WORKER_TOKEN` 相同后重启 |
| **API 报 `CTP worker unavailable`** | Worker 未启动或 6601 不可达 | `curl -s http://127.0.0.1:6601/health``pm2 restart qihuo-ctp` |
| 登录失败 | 密码未同步 | 网页改密或 `reset_admin.py` |
| 现价一直 `--` | 新浪网络不可达 | 检查服务器能否访问 `hq.sinajs.cn` |
| 关键位 500 | 缺 `sina_code` 列 | `git pull` 重启;或手工 `ALTER TABLE` |
| K 线生成失败 | matplotlib 未装 | `pip install matplotlib==3.9.2` |
| 手续费同步失败 | akshare 异常 | 使用「重载 JSON」或检查 akshare |
| **未安装 vnpy / vnpy_ctp** | 依赖未装或编译失败 | 见下方「CTP / vnpy 故障排查」 |
| **CTP 连接超时** | SimNow 地址/账号/非交易时段 | 核对 `.env` 与 SimNow 官网前置 |
| **下单监控无持仓** | 未连接 CTP 或确实无仓 | 先点「连接 CTP」 |
| **`Could not resolve host`** | 服务器 DNS 故障 | 配置 systemd-resolved 公共 DNS,见下方 |
| `database is locked` | SQLite 并发 | **推荐改 PostgreSQL**`MIGRATE_SQLITE=1 bash scripts/deploy_postgres.sh`,见 [POSTGRES.md](./POSTGRES.md) |
| `git pull` 冲突 | 曾用 SCP 覆盖文件(**禁止**) | 按 [服务器卸载与全新部署](#服务器卸载与全新部署git-唯一来源) 或 `git reset --hard origin/main` 与远端对齐 |
查看应用是否在监听:
```bash
ss -tlnp | grep 6600
```
### DNS 无法解析(git / curl 均失败)
`curl cip.cc``git pull``Could not resolve host`
```bash
mkdir -p /etc/systemd/resolved.conf.d
cat > /etc/systemd/resolved.conf.d/dns.conf <<'EOF'
[Resolve]
DNS=223.5.5.5 8.8.8.8
FallbackDNS=1.1.1.1
EOF
systemctl restart systemd-resolved
resolvectl flush-caches
```
验证:`resolvectl query git.bz121.com``curl cip.cc`
---
页面提示 **「未安装 vnpy / vnpy_ctp」** 表示 Python 环境未成功安装 CTP 网关,下单与柜台持仓不可用(看盘、策略、复盘仍可用)。
**1. 安装依赖**
```bash
cd /opt/qihuo
source venv/bin/activate
apt install -y build-essential python3-dev pkg-config # 首次需要
pip install -r requirements.txt
python -c "from vnpy_ctp import CtpGateway; print('OK')"
pm2 restart ecosystem.config.cjs --update-env
```
**2. 配置 SimNow`.env`**
注册与查投资者代码见 [SIMNOW.md](./SIMNOW.md)。填写 `SIMNOW_USER`(投资者代码)、`SIMNOW_PASSWORD`,前置地址以 SimNow 官网为准。
**3. 连接**
登录系统 → **下单监控****连接 CTP**。成功则顶栏显示「CTP 已连接」,权益变为 SimNow 账户资金。
**4. 常见错误**
| 日志/现象 | 处理 |
|-----------|------|
| `pip install vnpy_ctp` 编译失败 / `Python dependency not found` | 安装 `build-essential python3-dev pkg-config` 后重试 |
| CTP 连接超时 | 检查前置 IP、端口、SimNow 是否维护、是否在允许连接时段 |
| 连接后立即崩溃 `locale::facet::_S_create_c_locale` | CTP 需 **zh_CN.GB18030**`sed -i '/^# zh_CN.GB18030/s/^# //' /etc/locale.gen && locale-gen zh_CN.GB18030`,再 `pm2 restart ecosystem.config.cjs --update-env` |
| 服务器 `180.168.146.187` 超时 | 换 SimNow 备用前置 `182.254.243.31:30001/30011`(见 [SIMNOW.md](./SIMNOW.md) |
| 已连接但下单拒单 | 检查合约代码、价格精度、是否有足够保证金 |
---
## 安全建议
1. 部署后立即修改默认密码
2. 勿将 `.env``futures.db` 提交到公开仓库
3. 生产环境使用 HTTPS + 限制访问 IP
4. 定期备份:系统设置页自动备份至 `/root/qihuo_backup`,或见 [BACKUP.md](docs/BACKUP.md)
---
## 目录结构(部署后)
```
/opt/qihuo/
├── app.py
├── vnpy_bridge.py # CTP 桥接(Web=IPC 代理,Worker=原生 vn.py
├── ctp_ipc_client.py # Web → Worker HTTP 客户端
├── ctp_worker.py # 独立 CTP Worker 入口(PM2: qihuo-ctp
├── recommend_store.py # 可开仓品种缓存
├── recommend_stream.py # 可开仓品种 SSE 推送
├── venv/
├── futures.db
├── .env
├── logs/
│ ├── pm2-out.log
│ ├── pm2-error.log
│ ├── pm2-ctp-out.log
│ └── pm2-ctp-error.log
├── uploads/
├── data/fee_rates.json
├── ecosystem.config.cjs # PM2qihuo + qihuo-ctp
├── deploy.sh
├── requirements.txt # 含 vnpy、vnpy_ctp
└── docs/
├── FEATURES.md
├── DEPLOY.md
└── TRADING.md
```
---
## 相关文档
- [功能说明文档](./FEATURES.md)
- [SimNow 注册与接入说明](./SIMNOW.md)
- [手续费与导航设置](./FEES.md)
- [交易与 SimNow 配置](./TRADING.md)
- [README](../README.md)
+291
View File
@@ -0,0 +1,291 @@
# 功能说明文档
国内期货 · 交易复盘系统(Flask + SQLite + vnpy_ctp + PM2)。
> **分板块详细说明(含下单逻辑、风控、微信模板)** → [INDEX.md](./INDEX.md)
> 重点:[WECHAT.md](./WECHAT.md) · [AI.md](./AI.md) · [RISK.md](./RISK.md)
---
## 系统概览
| 项目 | 说明 |
|------|------|
| 访问端口 | 默认 `6600` |
| 默认首页 | 登录后 `/`**下单监控** `/positions` |
| 数据存储 | SQLite `futures.db` |
| 行情 | 默认新浪;可选同花顺 iFinD |
| 合约代码 | 同花顺格式(`ag2606``SR609``IF2606` |
| 主题 | 页头深色 / 浅色切换 |
### 导航结构
| 菜单 | 路径 | 可关闭 |
|------|------|--------|
| **下单监控** | `/positions` | 否(默认首页) |
| 策略交易 | `/strategy` | 是 |
| 开单计划 | `/plans` | 是 |
| 关键位监控 | `/keys` | 否 |
| 行情 K 线 | `/market` | 是 |
| 交易记录与复盘 | `/records` | 否 |
| 统计分析 | `/stats` | 否 |
| AI 分析 | `/ai` | 是 |
| 手续费配置 | `/fees` | 是 |
| 系统设置 | `/settings` | 否 |
关闭项在 **系统设置 → 导航显示** 配置;直接访问 URL 会提示并跳回下单监控。
---
## 下单监控
**路径**`/positions` · 详见 [ORDER_MONITOR.md](./ORDER_MONITOR.md)
### 顶栏
- 模拟盘 / 实盘模式、CTP 连接状态、风险状态
- 权益、可用资金(连接 CTP 后来自柜台)
- **连接 CTP** / 重连;断线自动重连;开盘前 30 分钟自动连接
### 期货下单
- 品种联想(可开仓品种表与下拉一致;小账户或 CTP 未连接时仅四品种,见 [TRADING.md](./TRADING.md)
- 方向、手数(固定手数 / 固定金额计仓)
- 限价 / 市价(FAK)、止盈、止损
- **移动保本**(默认关闭):开启后隐藏止盈与盈亏比,仅填止损;由移动止损监控平仓,不设固定止盈
- 非交易时段禁止报单
### 当前持仓
- 开仓委托先显示 **挂单中**,成交后显示为 active 持仓
- 挂单超时自动撤单;交易时段内可 **手动撤单**
- 持仓卡片:浮盈亏、保证金、止盈止损、平仓等
- 数据经 SSE 推送,无需整页刷新
### 可开仓品种
- 按当前权益与保证金上限筛选可开品种,养成开仓纪律、限制仓位
- **权益 ≤20 万** 或 **CTP 未连接** 时,仅展示并可交易:玉米、豆粕、甲醇、螺纹钢(SimNow/实盘一致);未连接时最大手数按 **10 万权益** 估算
- **夜盘时段** 仅显示有夜盘品种,并标注「夜盘」
- **行业分类**、走势(多头/空头/震荡/转多/转空)、跳空、昨日成交量(手)、成交额
- 支持行业筛选与多字段排序
- 每日后台刷新缓存
详见 [TRADING.md](./TRADING.md)。
---
## 策略交易
**路径**`/strategy``/strategy/records` · 详见 [STRATEGY.md](./STRATEGY.md)
- 趋势回调(首仓/补仓/止盈须 CTP + 交易时段)
- 顺势加仓:**市价**须交易时段;**突破**可休盘提交监控,开盘触价成交
- 策略记录单独归档
---
## 开单计划
**路径**`/plans` · 详见 [PLANS.md](./PLANS.md)
- 录入当日计划:主力合约、方向、决策区间、止损、止盈
- 状态:`planned``active``closed` / `expired`
- 现价进入区间 → 企业微信推送并激活
- 激活后监控止盈/止损,触发写入 `trade_records` 并关闭计划
- 列表约 1 秒轮询 `/api/plan_prices`
---
## 关键位监控
**路径**`/keys` · 详细规则见 [KEY_MONITORS.md](./KEY_MONITORS.md)
- **箱体突破 / 收敛突破**:5m 收盘突破 → 顺势/反转自动市价单;止损=突破 K±2 跳;盈亏比默认 2(可改);可选移动保本(默认 3R 止盈)
- **关键支阻区**:上沿阻力 + 下沿支撑;5m 收盘突破 → 微信提醒最多 3 次(间隔约 5 分钟),不自动开仓
- 删除后归档至监控历史;列表约 1 秒轮询 `/api/key_prices`
---
## 行情 K 线
**路径**`/market` · 详见 [MARKET.md](./MARKET.md)
- 多周期 K 线(TradingView Lightweight Charts
- 支持 CTP 连接后部分数据增强
- 需在导航中开启
---
## 交易记录与复盘
**路径**`/records``/trades` 重定向至此) · 详见 [RECORDS.md](./RECORDS.md)
### 资金曲线
- 页顶 Lightweight Charts 资金曲线
- 随深色/浅色主题自动切换颜色
### 交易记录
- **CTP 已连接** 时打开页面自动同步柜台成交(来源「柜台」)
- 程序写入的记录来源为「本地」,可核对、删除
- 表头固定,表体约 10 行高度内滚动
- **修改/核对开关**:开启后可编辑并「核对修改」
- **填入复盘**:预填复盘表单
主要字段:品种、类型、方向、成交价、止损/止盈、手数、保证金、盈亏、手续费、净盈亏、最新资金、结果。
### 复盘上传 / 复盘历史
- 手动复盘表单、截图、自动 K 线图(matplotlib
- 按本日/本周/本月/自定义日期筛选历史
---
## 统计分析
**路径**`/stats` · 详见 [STATS.md](./STATS.md)
### 汇总指标(单行卡片)
总交易次数、胜率、平均盈利/亏损、盈亏比、连续亏损、最大回撤、最大盈亏金额及占比、累计手续费、情绪单数量/占比。
进入页面自动加载(`/api/stats`),无手动「重新计算」按钮。
### 分项统计
下拉选择维度:按时间、周、月、品种、手续费、方向、交易类型、情绪单等,表格展示分组指标。
数据来源:`trade_logs`(主)+ `review_records`(情绪单等)。
---
## AI 分析
**路径**`/ai` · 详见 [AI.md](./AI.md)
- 开仓/平仓/关键位成交后后台 AI 复盘(Ollama 或 OpenAI 兼容 API
- 日终持仓与交易报告(可推微信)
- 消息存档于 `/ai` 页面;配置见系统设置 → AI 分析
---
## 手续费配置
**路径**`/fees`
- **默认**:连接 CTP 后同步柜台费率(`source=ctp`
- 备选:本地 `data/fee_rates.json`、AKShare 参考表 × 倍率
- 详见 [FEES.md](./FEES.md)
---
## 系统设置
**路径**`/settings` · 详见 [SETTINGS.md](./SETTINGS.md)
| 功能 | 说明 |
|------|------|
| 导航显示 | 开关可选菜单项 |
| 交易模式 | SimNow / 实盘 CTP |
| 计仓模式 | 固定手数、固定金额 |
| 保证金上限、移动保本缓冲、挂单超时 | 保证金上限默认 30%;移动保本缓冲为达 1R 后止损相对开仓价的跳数(默认 2 跳) |
| CTP 连接 | SimNow / 实盘前置与账号(可覆盖 `.env` |
| 参考资金 | CTP 未连接时用于可开仓筛选与估算 |
| 企业微信 Webhook | 计划/关键位/交易/AI 推送 · 见 [WECHAT.md](./WECHAT.md) |
| 登录账号 | 用户名/密码,同步写入 `.env` |
| 数据备份与恢复 | 自动/手动备份、下载压缩包、恢复说明 |
| 深色/浅色主题 | 页头切换 |
备份详情见 [BACKUP.md](./BACKUP.md)。
忘记密码:`python reset_admin.py`
---
## 品种与行情
### 合约代码格式
| 交易所 | 示例 |
|--------|------|
| 上期所 / 大商所 / 能源 | `ag2606``rb2605`(小写+4位年月) |
| 郑商所 | `SR609``MA606`(大写+3位年月) |
| 中金所 | `IF2606` |
### 行情源
| 配置 | 说明 |
|------|------|
| `QUOTE_SOURCE=sina` | 默认新浪 |
| `QUOTE_SOURCE=ths` | iFinD token |
| `QUOTE_SOURCE=auto` | 有 token 优先同花顺 |
---
## 数据库表(简要)
| 表名 | 用途 |
|------|------|
| `settings` | 密码、微信、资金、导航、交易参数 |
| `order_plans` | 开单计划 |
| `key_monitors` | 关键位监控 |
| `trade_logs` | 平仓交易记录(含 `source``ctp_trade_key` |
| `review_records` | 复盘 |
| `trade_records` | 计划自动止盈止损记录 |
| `fee_rates` | 手续费缓存 |
| `product_recommend_cache` | 可开仓品种缓存 |
| `stats_cache` | 统计缓存 |
数据库文件:项目根目录 `futures.db`
---
## 后台任务
| 任务 | 说明 |
|------|------|
| 计划/关键位轮询 | 约 3 秒,触发判断与微信推送 |
| 可开仓品种刷新 | 每日 + 按需 |
| 持仓 SSE | 前端订阅 `/api/trading/stream` |
| CTP 开盘前连接 | 默认开盘前 30 分钟(`qihuo-ctp` Worker |
| 挂单超时撤单 | 可配置分钟数 |
| 止盈止损守护 | `qihuo-ctp` Worker 内 tick 监控 |
| 滚仓 pending 监控 | `qihuo-ctp` Worker,交易时段扫描突破触价 |
| 数据库自动备份 | 每日定时(默认 03:00)写入 `/root/qihuo_backup` |
---
## 核心文件
```
qihuo/
├── app.py # 主路由、计划/关键位/记录/统计
├── install_trading.py # 下单、可开仓品种、策略路由
├── vnpy_bridge.py # CTP 桥接(Web=IPCWorker=vn.py
├── ctp_worker.py # 独立 CTP WorkerPM2: qihuo-ctp
├── ctp_ipc_client.py # Web → Worker HTTP 客户端
├── ctp_trade_sync.py # 柜台成交同步到 trade_logs
├── product_recommend.py # 可开仓品种计算
├── stats_engine.py # 统计分析
├── db_backup.py # 数据库备份与恢复包
├── fee_specs.py / ctp_fee_sync.py
├── market.py / kline_chart.py
├── templates/ static/
└── docs/
```
---
## 安全提示
- 部署后立即修改默认密码
- 勿将 `.env` 提交仓库
- 生产建议 Nginx + HTTPS,限制 6600 访问范围
---
## 仓库
https://git.bz121.com/dekun/qihuo.git
+53
View File
@@ -0,0 +1,53 @@
# 手续费与导航设置
## 手续费数据源
| 模式 | 说明 |
|------|------|
| **CTP 柜台**(推荐) | 连接 SimNow/实盘 CTP 后,查询柜台费率并缓存到 `fee_rates``source=ctp` |
| **本地 / AKShare** | `data/fee_rates.json` 或 AKShare 参考表 × 倍率,离线估算 |
### 计算公式
```
单边手续费 = 固定(元/手) × 手数 + 比例 × 成交价 × 合约乘数 × 手数
往返手续费 = 开仓费 + 平仓费(同日持仓用平今,否则平昨)
```
### 同步时机
1. 连接 CTP 成功后 — 后台自动同步主力合约费率
2. **手续费配置页** — 「从 CTP 同步费率」
3. 计算某品种时 — 缓存缺失则单品种查询
---
## 导航显示开关
**系统设置 → 导航显示** 可开关:
| key | 菜单 |
|-----|------|
| `fees` | 手续费配置 |
| `plans` | 开单计划 |
| `market` | 行情 K 线 |
| `strategy` | 策略交易 |
关闭后顶栏隐藏;直接访问 URL 会提示并跳转到 **下单监控**
始终显示:**下单监控**、关键位监控、交易记录与复盘、统计分析、系统设置。
设置保存在 `settings.nav_items`JSON)。
---
## 相关文件
| 文件 | 说明 |
|------|------|
| `fee_specs.py` | 费率计算 |
| `ctp_fee_sync.py` | CTP 同步 |
| `nav_settings.py` | 导航开关 |
| `vnpy_bridge.py` | CTP 连接 |
详见 [DEPLOY.md](./DEPLOY.md)、[TRADING.md](./TRADING.md)。
+64
View File
@@ -0,0 +1,64 @@
# 文档索引
国内期货 · 交易复盘系统各板块说明。每篇文档包含 **下单逻辑**、**风控规则**,并在需要时引用 [微信推送模板](./WECHAT.md)。
---
## 总览
| 文档 | 说明 |
|------|------|
| [FEATURES.md](./FEATURES.md) | 功能总览与导航结构 |
| [RISK.md](./RISK.md) | **全局账户风控**(冷静期、仓位上限、保证金、品种范围) |
| [风控说明.md](./风控说明.md) | **数据看板风控卡片**(各指标含义与颜色规则) |
| [WECHAT.md](./WECHAT.md) | **企业微信推送**(全部消息类型与完整模板) |
| [AI.md](./AI.md) | **AI 分析**(配置、触发、输出、推送) |
---
## 各板块说明
| 板块 | 路径 | 文档 |
|------|------|------|
| 数据看板 | `/dashboard` | [风控说明.md](./风控说明.md)(看板内嵌摘要) |
| 风控说明 | `/risk-guide` | [风控说明.md](./风控说明.md)(完整页面) |
| 下单监控 | `/positions` | [ORDER_MONITOR.md](./ORDER_MONITOR.md) |
| 策略交易 | `/strategy` | [STRATEGY.md](./STRATEGY.md) |
| 开单计划 | `/plans` | [PLANS.md](./PLANS.md) |
| 关键位监控 | `/keys` | [KEY_MONITORS.md](./KEY_MONITORS.md) |
| 行情 K 线 | `/market` | [MARKET.md](./MARKET.md) |
| 交易记录与复盘 | `/records` | [RECORDS.md](./RECORDS.md) |
| 统计分析 | `/stats` | [STATS.md](./STATS.md) |
| 手续费配置 | `/fees` | [FEES.md](./FEES.md) |
| 系统设置 | `/settings` | [SETTINGS.md](./SETTINGS.md) |
---
## 部署与运维
| 文档 | 说明 |
|------|------|
| [TRADING.md](./TRADING.md) | 可开仓品种、计仓、SimNow/实盘 |
| [SIMNOW.md](./SIMNOW.md) | SimNow 仿真注册与接入 |
| [CTP_LIVE.md](./CTP_LIVE.md) | **期货公司实盘 CTP** 与开平仓对比 |
| [DEPLOY.md](./DEPLOY.md) | 部署说明(含 **代码发布铁律**:仅 Git 三步,禁止 SCP) |
| [POSTGRES.md](./POSTGRES.md) | **PostgreSQL 生产库**(一键部署、迁移、备份恢复) |
| [BACKUP.md](./BACKUP.md) | 数据备份与恢复 |
---
## 快速查找
| 想了解… | 看这里 |
|---------|--------|
| 手动下单怎么校验 | [ORDER_MONITOR.md](./ORDER_MONITOR.md) |
| 关键位自动单规则 | [KEY_MONITORS.md](./KEY_MONITORS.md) |
| 开单计划何时推送 | [PLANS.md](./PLANS.md) + [WECHAT.md#开单计划](./WECHAT.md) |
| 开仓/平仓微信长文格式 | [WECHAT.md#结构化推送](./WECHAT.md) |
| AI 何时分析、推什么 | [AI.md](./AI.md) |
| 手动平仓后为何冻结 | [RISK.md#冷静期与日冻结](./RISK.md) |
| 看板风控各指标什么意思 | [风控说明.md](./风控说明.md) |
| 移动保本怎么抬止损 | [ORDER_MONITOR.md#移动保本](./ORDER_MONITOR.md) |
| 实盘 CTP 怎么接、和 SimNow 开平是否一样 | [CTP_LIVE.md](./CTP_LIVE.md) |
| CTP 双进程、Worker 日志、6601 端口 | [DEPLOY.md](./DEPLOY.md) |
| 休盘能否提交突破滚仓 | [STRATEGY.md](./STRATEGY.md) |
+93
View File
@@ -0,0 +1,93 @@
# 关键位监控
**页面路径**`/keys`
关键位监控用于在指定价格区间上设置 **5 分钟收盘** 触发规则,分为 **自动单**(箱体/收敛突破)与 **仅微信提醒**(关键支阻区)两类。
**文档索引**[INDEX.md](./INDEX.md) · 微信模板:[WECHAT.md §6–§8](./WECHAT.md) · 风控:[RISK.md](./RISK.md)
---
## 监控类型
### 箱体突破 / 收敛突破(自动单)
| 项目 | 规则 |
|------|------|
| 触发 | 5 分钟 K 线 **收盘价** 高于上沿或低于下沿 |
| 顺势 / 反转 | 顺势:上破做多、下破做空;反转:上破做空、下破做多 |
| 下单 | CTP 已连接且在交易时段内,自动 **市价开仓** |
| 手数 | 按系统设置的风险比例与保证金上限计算 |
| 止损 | 突破 K 线最低价(多)/ 最高价(空)± **2 个最小变动价位** |
| 盈亏比 | 默认 **2**,可在新增监控时修改(0.5~10) |
| 移动保本 | 可选;开启后盈亏比默认 **3**,达 3R 止盈价自动平仓;同时启用移动保本止损逻辑(达 1R 后抬止损) |
| 成交后 | 进入 **下单监控** 持仓列表,`monitor_type` 显示为「箱体突破」或「收敛突破」 |
| 结案 | 触发并尝试下单后,本条监控移入历史(无论成败,同一根 5m K 线不重复触发) |
**前提**:CTP 已连接、处于交易时段、账户风控允许开仓。
### 关键支阻区(仅提醒)
| 项目 | 规则 |
|------|------|
| 区间 | **上沿 = 阻力**,**下沿 = 支撑**,合并为一个关键支阻区 |
| 触发 | 5m 收盘突破上沿或跌破下沿 |
| 推送 | 企业微信,见 [WECHAT §8](./WECHAT.md#8-关键支阻区提醒) |
| 次数 | 最多 **3 次**,间隔约 **5 分钟**(人工盯盘提醒) |
| 自动开仓 | **否** |
| 结案 | 第 3 次推送后自动归档 |
历史数据中的「关键阻力位」「关键支撑位」按 **关键支阻区** 同样规则处理。
---
## 与旧版差异
- 旧版:tick 现价触碰即推送,箱体/收敛仅微信提醒
- 新版:**统一 5m 收盘** 触发;箱体/收敛改为 **自动市价单**;阻力/支撑合并为 **关键支阻区** 三轮微信提醒
---
## 相关配置
- **企业微信 Webhook**:系统设置 → 企业微信推送
- **风险比例 / 保证金上限**:系统设置 → 交易相关(影响自动单手数)
- **移动保本跳数缓冲**:系统设置 → `trailing_be_tick_buffer`(自动单开启移动保本时生效)
---
## 技术说明
- 后台任务 `background_task` 约每 3 秒扫描一次 `key_monitors`
- 5m K 线优先 CTP,否则新浪/本地缓存
- 自动单逻辑:`key_monitor_lib.py` + `install_trading._execute_key_breakout`
- 止盈止损监控:`sl_tp_guard.py`(移动保本 + 显式止盈价可同时生效)
## 下单逻辑(自动单)
1. 后台 `background_task` 约每 3 秒调用 `run_key_monitor_check()`
2. 拉取 5m K 线(优先 CTP),判断收盘价是否突破上/下沿。
3. **箱体突破 / 收敛突破**
- 顺势/反转决定方向(上破做多或做空等)。
- 校验:交易时段、CTP、`assert_can_open()`、[RISK.md](./RISK.md) 全部规则。
- 手数:按 `risk_percent` + `max_margin_pct` 计算。
- **止损**:突破 K 线最低(多)/ 最高(空)± **2 跳**
- **止盈**:默认盈亏比 **2**(可改 0.5~10);开启移动保本时默认 **3R** TP。
- **报单**:CTP 市价开仓 → 写入 `trade_order_monitors``sl_tp_guard` 守护。
4. 同一根 5m K 线只触发一次;触发后移入历史(成败皆然)。
5. 成交:微信 [§7](./WECHAT.md#7-关键位开仓成功);失败/摘要:[§6](./WECHAT.md#6-关键位自动单)。
## 风控规则
| 规则 | 自动单 | 支阻区 |
|------|--------|--------|
| assert_can_open | ✓ | — |
| 保证金 / 品种 | ✓ | — |
| 交易时段 | ✓ | — |
| 自动下单 | ✓ | ✗ |
全局规则见 [RISK.md](./RISK.md)。
---
详见 [FEATURES.md](./FEATURES.md) · [INDEX.md](./INDEX.md)
+51
View File
@@ -0,0 +1,51 @@
# 行情 K 线
**页面路径**`/market`
**相关文件**`market.py``kline_chart.py``templates/market.html`
---
## 功能概述
- 多周期 K 线图表(TradingView Lightweight Charts
- 品种选择与周期切换
- CTP 连接后部分品种可获取更及时数据
须在 **系统设置 → 导航显示** 开启「行情 K 线」;关闭后直接访问 URL 会提示并跳回下单监控。
---
## 下单逻辑
**本板块无下单功能**,纯行情展示。
---
## 风控规则
无独立风控。不涉及报单、推送或账户状态变更。
---
## 微信推送
**无**。行情页不发送任何企业微信消息。
---
## 数据来源
| 优先级 | 来源 |
|--------|------|
| 1 | CTP tick/K 线(已连接时) |
| 2 | 新浪 / 同花顺 iFinD(见 `QUOTE_SOURCE` 配置) |
合约代码格式见 [FEATURES.md](./FEATURES.md#合约代码格式)。
---
## 相关文档
- [FEATURES.md](./FEATURES.md)
- [SETTINGS.md](./SETTINGS.md)
+151
View File
@@ -0,0 +1,151 @@
# 下单监控
**页面路径**`/positions`(默认首页)
**相关文件**`install_trading.py``sl_tp_guard.py``pending_order_worker.py``product_recommend.py`
---
## 功能结构
| 区域 | 说明 |
|------|------|
| 顶栏 | 模拟/实盘、CTP 状态、风险状态、权益、连接按钮 |
| 期货下单 | 手动开仓/平仓表单 |
| 当前持仓 | pending 挂单 + active 持仓卡片 |
| 可开仓品种 | 按权益与保证金筛选的推荐表 |
---
## 下单逻辑
### 手动开仓
1. 选择品种、方向、限价/市价(FAK)、手数或计仓模式。
2. **前置校验**(全部通过才报单):
- CTP 已连接且不在连接中
- 处于 **交易时段**
- `assert_can_open()` — 见 [RISK.md](./RISK.md)
- `assert_product_allowed_for_capital()` — 小账户四品种限制
- 固定金额模式须填 **止损价**
- 开启 **移动保本** 须填止损(不可只填止盈)
- 保证金占用 ≤ `max_margin_pct`
- 单笔手数 ≤ 50
3. **计仓**
- 固定手数 → 使用 `fixed_lots`
- 固定金额 → `calc_lots_by_amount()` 按止损距离算手数
4. **报单**`execute_order()` → CTP。
5. **成交后**
- 写入 `trade_order_monitors`status=active 或 pending
- 注册本地 SL/TP 守护(`sl_tp_guard`
- 微信:结构化开仓推送(有止损)或简版
- AI:后台开仓分析(有止损时)
### 挂单(限价未立即成交)
- status=`pending`,显示「挂单中」。
- **超时撤单**`pending_order_worker` 每 10 秒 reconcile(有 pending 时);默认超时见系统设置 `pending_order_timeout_sec`
- 微信:`委托已提交 · … 挂单中(N 分钟未成交自动撤单)`
### 手动平仓
- 市价平仓当前品种方向持仓。
- 关闭对应 monitor,写入 `trade_logs`
- 若当日手动平仓次数超限 → 触发冷静期([RISK.md](./RISK.md))。
- 微信:结构化平仓推送(成交同步后)。
### 止盈止损守护
后台 `sl_tp_guard` 线程 + tick 事件:
| 条件 | 动作 |
|------|------|
| 价格触及 **止盈价** | 市价平仓,结果=止盈 |
| 价格触及 **止损价** | 市价平仓,结果=止损 |
| 开启 **移动保本** | 见下节 |
触发时先推简版「平仓委托已提交」,成交写入 trade_logs 后推 **结构化平仓**
---
## 移动保本
**开启条件**:下单表单勾选「移动保本」,**必须填写止损**,不设固定止盈。
**逻辑**`sl_tp_guard._update_trailing_stop_loss`):
| 浮盈 R 倍数 | 止损移动至 |
|-------------|------------|
| ≥ 1R | 开仓价 ± `trailing_be_tick_buffer` 跳(保本) |
| ≥ 2R | 开仓价 ± 1R |
| ≥ nR | 开仓价 ± (n1)R |
**平仓结果标签**
- 1R 锁定后止损出场 → **保本止盈**
- 2R 及以上锁定后止损出场 → **移动止盈**
关键位自动单若开启移动保本且设目标盈亏比(默认 3R),达 TP 仍按 **止盈** 平仓;移动止损与显式 TP **可同时生效**
---
## 风控规则
本板块执行 [RISK.md](./RISK.md) 全部规则,另加:
| 规则 | 说明 |
|------|------|
| 非交易时段 | 禁止开仓 |
| 移动保本 | 必须填止损 |
| 固定金额 | 必须填止损才能算手数 |
| 保证金 | 新开仓前校验总占用 |
| 单笔 50 手 | 超限拒绝 |
顶栏 **风险状态** 实时反映:正常 / 冷静期 / 日冻结 / 仓位上限。
---
## 可开仓品种表
- 按当前权益、`max_margin_pct`、合约保证金计算可开手数。
- 权益 ≤20 万或 CTP 未连接:仅四品种;未连接按 10 万权益估算。
- 夜盘时段过滤无夜盘品种。
- 每日后台刷新 `product_recommend_cache`
详见 [TRADING.md](./TRADING.md)。
---
## 微信推送
| 事件 | 模板 |
|------|------|
| 开仓成交(有止损) | [WECHAT §1](./WECHAT.md#1-手动开仓成功) |
| 开仓成交(无止损) | [WECHAT §2](./WECHAT.md#2-简版开仓) |
| 挂单提交 | [WECHAT §3](./WECHAT.md#3-挂单提交) |
| 平仓完成 | [WECHAT §4](./WECHAT.md#4-平仓完成) |
| SL/TP 触发 | [WECHAT §5](./WECHAT.md#5-本地止盈止损触发) |
| AI 分析 | [AI.md](./AI.md)(仅页面,默认不推微信) |
---
## 数据流
```
用户下单 → API /api/trade/order
→ CTP 报单
→ trade_order_monitors
→ sl_tp_guard 监控
→ 平仓 → trade_logs → 微信 + AI
```
SSE`/api/trading/stream` 推送持仓快照,无需整页刷新。
---
## 相关文档
- [RISK.md](./RISK.md)
- [WECHAT.md](./WECHAT.md)
- [TRADING.md](./TRADING.md)
- [SETTINGS.md](./SETTINGS.md)
+108
View File
@@ -0,0 +1,108 @@
# 开单计划
**页面路径**`/plans`
**相关文件**`app.py``check_order_plans``background_task`
---
## 功能概述
录入 **当日交易计划**:品种、方向、决策价格区间、止损、止盈、决策理由。系统轮询现价,进入区间后 **微信提醒** 并激活监控;触及 TP/SL 后记录结果并结案。
> **不自动向 CTP 下单**,仅提醒 + 记录。
---
## 计划状态
```
planned → active → closed
↘ expired(过期,非当日)
```
| 状态 | 含义 |
|------|------|
| `planned` | 已录入,等待价格进入决策区间 |
| `active` | 已进入区间,监控 TP/SL |
| `closed` | 触及止盈或止损,已结案 |
| `expired` | 非当日计划自动过期 |
---
## 下单逻辑
本模块 **无 CTP 报单**
### 1. 录入(用户操作)
- 主力合约、方向(多/空)
- **决策区间**`zone_lower` ~ `zone_upper`
- 止损价、止盈价
- 决策理由(可选,推送时展示)
### 2. 触发激活(后台 ~3 秒)
条件:`status=planned``zone_lower ≤ 现价 ≤ zone_upper`
动作:
1. 发送微信 [WECHAT §9](./WECHAT.md#9-开单计划)
2. `status → active`,记录 `triggered_at`
### 3. 止盈止损监控(active
| 方向 | 止盈 | 止损 |
|------|------|------|
| 多 | 现价 ≥ take_profit | 现价 ≤ stop_loss |
| 空 | 现价 ≤ take_profit | 现价 ≥ stop_loss |
触发后:
1. 微信 [WECHAT §10](./WECHAT.md#10-开单计划结果)
2. 写入 `trade_records`monitor_type=开单计划)
3. `status → closed`
### 4. 前端轮询
列表约 1 秒请求 `/api/plan_prices` 刷新现价。
---
## 风控规则
| 项 | 说明 |
|----|------|
| 账户冷静期 | **不校验** — 计划不自动开仓 |
| 保证金 / 品种 | **不校验** |
| 交易时段 | 轮询全天运行,非交易时段仍可推送 |
| 同日计划 | `plan_date` 为当日;跨日 `expire_old_plans()` |
用户收到提醒后若手动下单,须自行遵守 [RISK.md](./RISK.md)。
---
## 微信推送
| 事件 | 文档 |
|------|------|
| 进入决策区间 | [WECHAT §9](./WECHAT.md#9-开单计划) |
| 触及 TP/SL | [WECHAT §10](./WECHAT.md#10-开单计划结果) |
**配置**:系统设置 → 企业微信 Webhook。
---
## 数据库
`order_plans`symbol、direction、zone_upper/lower、stop_loss、take_profit、status、plan_date 等。
触发结果写入 `trade_records`(非 `trade_logs`,不计入 CTP 同步统计主表)。
---
## 相关文档
- [WECHAT.md](./WECHAT.md)
- [ORDER_MONITOR.md](./ORDER_MONITOR.md) — 收到提醒后手动下单
- [RECORDS.md](./RECORDS.md)
+271
View File
@@ -0,0 +1,271 @@
# PostgreSQL 生产数据库
qihuo 支持两种存储后端:
| 模式 | 配置 | 适用场景 |
|------|------|----------|
| **SQLite**(默认) | 不设置 `DATABASE_URL` | 本地开发、单机轻量试用 |
| **PostgreSQL**(推荐生产) | `.env``DATABASE_URL=postgresql://...` | 7×24 运行、多线程并发、消除 `database is locked` |
配置 `DATABASE_URL` 后,应用自动使用 **连接池**(默认 2–20 连接),无需改业务代码。
---
## 为什么用 PostgreSQL
SQLite 在同一文件上同一时刻只允许一个写者。qihuo 单进程内有多路后台线程(持仓刷新、止盈守护、挂单同步、统计缓存等)和 HTTP 请求同时写库,容易出现:
```
position worker failed: database is locked
bootstrap position snapshot: database is locked
```
PostgreSQL 面向并发读写设计,多连接、行级锁、连接池,与专业交易软件「服务端数据库 + 内存快照」的思路一致。
---
## 一键部署(新服务器 / 已有 qihuo)
在已执行过 `deploy.sh` 的服务器上,以 **root** 运行:
```bash
cd /opt/qihuo
git pull # 获取最新代码
sudo bash scripts/deploy_postgres.sh
```
脚本会自动:
1. 安装 `postgresql` / `postgresql-contrib`
2. 创建数据库 `qihuo`、用户 `qihuo`(随机密码,终端会打印)
3. 写入 `/opt/qihuo/.env``DATABASE_URL``PG_POOL_MIN``PG_POOL_MAX`
4. `pip install psycopg psycopg-pool`
5. 执行 `init_db()` 建表
6. `pm2 restart qihuo --update-env`
### 从现有 SQLite 迁移
`/opt/qihuo/futures.db` 已有数据:
```bash
cd /opt/qihuo
MIGRATE_SQLITE=1 sudo bash scripts/deploy_postgres.sh
```
会:
- 初始化 PostgreSQL 表结构
- 运行 `scripts/migrate_sqlite_to_postgres.py` 导入全部表
- 将旧库备份为 `futures.db.pre_pg.YYYYMMDD_HHMMSS`(可用 `BACKUP_SQLITE=0` 跳过)
迁移前建议先做一次 Web 设置页 **立即备份** 或:
```bash
cp /opt/qihuo/futures.db /root/futures.db.bak.$(date +%Y%m%d)
pm2 stop qihuo
MIGRATE_SQLITE=1 sudo bash scripts/deploy_postgres.sh
```
### 环境变量(可选)
| 变量 | 默认 | 说明 |
|------|------|------|
| `APP_DIR` | `/opt/qihuo` | 应用目录 |
| `PG_DB` | `qihuo` | 数据库名 |
| `PG_USER` | `qihuo` | 数据库用户 |
| `PG_PASSWORD` | 随机 | 不设则脚本生成 |
| `PG_HOST` | `127.0.0.1` | 主机 |
| `PG_PORT` | `5432` | 端口 |
| `MIGRATE_SQLITE` | `0` | `1` 时从 `futures.db` 迁移 |
| `BACKUP_SQLITE` | `1` | 迁移后是否备份旧 SQLite 文件 |
---
## 手动部署
### 1. 安装 PostgreSQLUbuntu
```bash
apt update
apt install -y postgresql postgresql-contrib
systemctl enable postgresql
systemctl start postgresql
```
### 2. 创建库与用户
```bash
sudo -u postgres psql <<'SQL'
CREATE USER qihuo WITH PASSWORD '请改为强密码';
CREATE DATABASE qihuo OWNER qihuo;
GRANT ALL PRIVILEGES ON DATABASE qihuo TO qihuo;
SQL
```
### 3. 配置 `.env`
```bash
cd /opt/qihuo
cat >> .env <<'EOF'
DATABASE_URL=postgresql://qihuo:请改为强密码@127.0.0.1:5432/qihuo
PG_POOL_MIN=2
PG_POOL_MAX=20
EOF
```
### 4. 安装 Python 驱动并初始化
```bash
source venv/bin/activate
pip install -r requirements.txt
export $(grep -v '^#' .env | xargs) # 或手动 export DATABASE_URL
python3 -c "from app import init_db; init_db()"
```
### 5. 迁移 SQLite(可选)
```bash
python3 scripts/migrate_sqlite_to_postgres.py --sqlite /opt/qihuo/futures.db
# 仅预览行数:
python3 scripts/migrate_sqlite_to_postgres.py --dry-run
```
### 6. 重启应用
```bash
pm2 restart qihuo --update-env
pm2 logs qihuo --lines 30
```
启动后日志中不应再频繁出现 `database is locked`SQLite 特有)。
---
## 连接池
| 变量 | 默认 | 说明 |
|------|------|------|
| `PG_POOL_MIN` | `2` | 池内最少连接 |
| `PG_POOL_MAX` | `20` | 池内最多连接 |
每个 HTTP 请求 / 后台 worker 从池中借连接,用毕归还。PM2 请保持 **`instances: 1`**(见 `ecosystem.config.cjs`);若要多实例,共用同一 `DATABASE_URL` 即可,PostgreSQL 可承受。
---
## 备份
### 方式一:系统设置页(推荐)
**系统设置 → 数据备份与恢复 → 立即备份**
PostgreSQL 模式下包内为 `postgres_dump.sql``pg_dump` 逻辑备份),而非 `futures.db`
### 方式二:命令行
```bash
# 需与 .env 中 DATABASE_URL 一致
source /opt/qihuo/venv/bin/activate
set -a && source /opt/qihuo/.env && set +a
pg_dump --no-owner --no-acl -f /root/qihuo_backup/manual_$(date +%Y%m%d_%H%M%S).sql "$DATABASE_URL"
```
### 方式三:每日自动备份
设置页开启 **每日自动备份**(默认 03:00),保留份数默认 30。备份目录默认 `/root/qihuo_backup`
详见 [BACKUP.md](./BACKUP.md)。
---
## 恢复
### 从 qihuo 备份包恢复(含 restore.sh
```bash
pm2 stop qihuo
cd /root
tar -xzf qihuo_backup_YYYYMMDD_HHMMSS.tar.gz
cd qihuo_backup_YYYYMMDD_HHMMSS
# 确保 /opt/qihuo/.env 已配置 DATABASE_URL
export RESTORE_DIR=/opt/qihuo
chmod +x restore.sh
./restore.sh
pm2 restart qihuo
```
`manifest.json``"backend": "postgres"` 表示包内为 `postgres_dump.sql`
### 手工 psql 恢复
```bash
pm2 stop qihuo
export DATABASE_URL=postgresql://qihuo:密码@127.0.0.1:5432/qihuo
# 空库或需覆盖的库
psql "$DATABASE_URL" -f /path/to/postgres_dump.sql
cp -a uploads_backup/. /opt/qihuo/uploads/ # 若有附件
pm2 restart qihuo
```
### 恢复后检查
1. Web 登录正常
2. **交易记录**、**统计** 页数据完整
3. **系统设置** 中 CTP、资金等配置仍在
4. 连接 CTP,持仓页刷新正常
5. `pm2 logs qihuo` 无持续数据库报错
---
## 回退到 SQLite
1. `pm2 stop qihuo`
2. 注释或删除 `.env``DATABASE_URL`
3. 确保 `/opt/qihuo/futures.db` 存在(可用迁移前备份 `futures.db.pre_pg.*`
4. `pm2 restart qihuo`
---
## 故障排查
| 现象 | 可能原因 | 处理 |
|------|----------|------|
| `未安装 psycopg` | 未 pip install | `pip install -r requirements.txt` |
| `pg_dump 失败` | 未装客户端 / URL 错误 | `apt install postgresql-client`;检查 `DATABASE_URL` |
| 迁移后缺表 | 未 init_db | `python3 -c "from app import init_db; init_db()"` 后重跑迁移 |
| 登录失败 | 只恢复了 SQL 未恢复 settings | 检查 `settings` 表是否有 `admin_password_hash` |
| 连接拒绝 | PostgreSQL 未启动 | `systemctl status postgresql` |
| 仍见 locked | 未切到 PG,仍用 SQLite | `grep DATABASE_URL /opt/qihuo/.env``pm2 restart --update-env` |
### 验证当前后端
```bash
cd /opt/qihuo && source venv/bin/activate
set -a && source .env && set +a
python3 -c "from db_conn import database_label, db_backend; print(db_backend(), database_label())"
```
应输出 `postgres PostgreSQL (...)`
### 查看 PostgreSQL 连接
```bash
sudo -u postgres psql -d qihuo -c "SELECT count(*) FROM pg_stat_activity WHERE datname='qihuo';"
```
---
## 安全建议
- `DATABASE_URL` 含密码,勿提交到 git`.env` 权限建议 `chmod 600`
- 备份包、`postgres_dump.sql` 含交易与账号数据,勿上传公开网盘
- 生产库仅监听 `127.0.0.1`,不暴露 5432 到公网
- 定期测试 **备份 → 解压 → restore.sh → 登录** 全流程
---
## 相关文档
- [DEPLOY.md](./DEPLOY.md) — 应用一键部署
- [BACKUP.md](./BACKUP.md) — 备份策略与设置页说明
- [FEATURES.md](./FEATURES.md) — 功能与数据表概览
+85
View File
@@ -0,0 +1,85 @@
# 交易记录与复盘
**页面路径**`/records``/trades` 重定向)
**相关文件**`app.py``ctp_trade_sync.py``sl_tp_guard.py`write_trade_log
---
## 功能结构
| 区域 | 说明 |
|------|------|
| 资金曲线 | Lightweight Charts,随主题变色 |
| 交易记录 | `trade_logs` 主表 |
| 复盘表单 | 手动复盘 + 截图 + 自动 K 线图 |
| 复盘历史 | 按日/周/月/自定义筛选 |
---
## 下单逻辑
本板块 **不提供报单**。记录来源:
| 来源 | source / monitor_type | 写入方式 |
|------|----------------------|----------|
| 程序平仓 | 本地 | sl_tp_guard、手动平仓 API |
| 柜台同步 | 柜台 | CTP 成交同步 `ctp_trade_sync` |
| 开单计划 | 开单计划 | 仅 `trade_records`,非 trade_logs 主统计 |
### CTP 同步
打开页面且 CTP 已连接时,自动拉取柜台成交写入 `trade_logs`(去重键 `ctp_trade_key`)。
### 手动编辑
「修改/核对开关」开启后可编辑字段并「核对修改」。
---
## 风控规则
### 复盘与账户冻结
提交复盘表单时若勾选 **情绪问题选项**(怕踏空、报复开仓等),可能触发:
- **日冻结** — 当日禁止新开仓([RISK.md](./RISK.md)
- 手动平仓超限后的冷静期可因填写日记缩短为 **1 小时**
### 记录本身
交易记录页不改变持仓;删除/编辑仅影响本地数据库。
---
## 平仓后的联动
`trade_logs` 新增平仓记录时:
1. **微信**:结构化平仓 [WECHAT §4](./WECHAT.md#4-平仓完成)
2. **AI**:后台平仓分析 → `/ai` 页面([AI.md](./AI.md)
---
## 主要字段
品种、类型(monitor_type)、方向、开仓/平仓价、止损/止盈、手数、保证金、盈亏、手续费、净盈亏、最新资金、结果、持仓时长。
---
## 微信推送
| 事件 | 模板 |
|------|------|
| 程序/同步平仓 | [WECHAT §4](./WECHAT.md#4-平仓完成) |
开单计划结果见 [PLANS.md](./PLANS.md)(§10 简版模板)。
---
## 相关文档
- [STATS.md](./STATS.md) — 统计来源 trade_logs
- [RISK.md](./RISK.md) — 复盘触发冻结
- [WECHAT.md](./WECHAT.md)
- [AI.md](./AI.md)
+127
View File
@@ -0,0 +1,127 @@
# 全局账户风控
本文汇总 **所有板块共用** 的风控规则。各板块下单前均会调用 `assert_can_open()` 或等价校验。
相关代码:`risk/account_risk_lib.py``position_sizing.py``product_recommend.py`
---
## 日亏损风控(强平线)
| 项 | 默认值 | 说明 |
|----|--------|------|
| `daily_loss_force_close_pct` | 2 | 系统设置:当日亏损(已实现+浮亏)占 **权益** 比例;**≥ 即强制平掉全部持仓** 并当日禁止开仓 |
| `daily_loss_slippage_buffer_pct` | 1 | 强平执行允许的额外滑点占权益比例;与强平线合计默认 **3%** 上限 |
| 环境变量兜底 | `RISK_DAILY_TRADING_RISK_PCT` | 未配置系统设置时强平线可回退到此 env |
- 亏损口径:**当日已平仓亏损 + 当前持仓浮亏**(含隔夜跳空),除以当前 CTP 权益。
- 达限后:后台 `daily_loss_guard` 撤平仓挂单 → 对手价 FAK 强平 → `daily_frozen` → 看板/下单页显示 **风控**,开仓按钮灰色。
- 与单笔止损关系:止损为常规退出;日亏损线为账户级熔断。
---
| 项 | 默认值 | 说明 |
|----|--------|------|
| `MAX_ACTIVE_POSITIONS` | 1 | 同时 **active** 持仓监控数量上限 |
| 环境变量 | `.env` | `MAX_ACTIVE_POSITIONS=1` |
- 达到上限后:**禁止新开仓**,但允许 **滚仓/顺势加仓**`can_roll=True`)。
- 状态标签:**仓位上限冻结**。
---
## 冷静期与日冻结
| 项 | 默认值 | 说明 |
|----|--------|------|
| `RISK_CONTROL_ENABLED` | true | 关闭后跳过冷静期逻辑 |
| `RISK_MANUAL_CLOSE_DAILY_LIMIT` | 2 | 当日 **手动平仓** 次数上限 |
| `RISK_COOLING_HOURS_MANUAL` | 4 | 超限后冷静期(小时) |
| `RISK_COOLING_HOURS_MANUAL_JOURNAL` | 1 | 填写复盘情绪日记后缩短为 1 小时 |
| `TRADING_DAY_RESET_HOUR` | 8 | 交易日重置时刻(8 点前算上一交易日) |
### 触发流程
1. 用户在下单监控 **手动平仓**,且当日手动平仓次数 ≥ 上限。
2. 进入 **4h 冻结**(或填写复盘表单中的情绪问题选项后 → **1h 冻结**)。
3. 冻结期间 `can_trade=False`,禁止一切新开仓。
### 日冻结
- 复盘表单勾选情绪问题(怕踏空、报复开仓、盈利飘了、拿不住单、扛单、重仓违规等)并提交后,可触发 **当日日冻结**
- 日冻结期间禁止新开仓,次日(按 `TRADING_DAY_RESET_HOUR`)重置。
---
## 保证金占用上限
| 项 | 配置位置 | 默认值 | 用途 |
|----|----------|--------|------|
| 单仓保证金上限 | 系统设置 `max_margin_pct` | 30% | **新开仓**:拟开 + 已有占用,占权益不得超过此值 |
| 综合保证金上限 | 系统设置 `roll_max_margin_pct` | 50% | **单仓模式**:滚仓/加仓合计上限;**多仓模式**:所有持仓合计上限 |
- 看板 **综合保证金占比** 的分母为 **50%(综合上限)**,不是 30%。详见 [风控说明.md](./风控说明.md#保证金占比核心规则)。
- 新开仓前仍按 30% 收紧手数;滚仓/多仓合计按 50% 校验(见 [STRATEGY.md](./STRATEGY.md))。
---
## 计仓与单笔手数
| 模式 | 说明 |
|------|------|
| 固定手数 | 使用系统设置 `fixed_lots` |
| 固定金额(以损定仓) | 须填止损价;手数 = 固定风险金额 ÷ 每手风险;同时受保证金上限约束 |
| 单笔上限 | `DEFAULT_MAX_ORDER_LOTS = 50` |
固定金额模式公式(简化):
```
每手风险 = |入场价 − 止损价| × 合约乘数
手数 = floor(固定风险金额 / 每手风险)
再按 max_margin_pct 收紧
```
---
## 可交易品种范围
| 条件 | 规则 |
|------|------|
| 权益 ≤ 20 万 **或** CTP 未连接 | 仅可交易:**玉米、豆粕、甲醇、螺纹钢** |
| CTP 未连接时估算 | 可开仓表按 **10 万权益** 估算最大手数 |
| 夜盘时段 | 可开仓表仅显示有夜盘品种 |
详见 [TRADING.md](./TRADING.md)、[ORDER_MONITOR.md](./ORDER_MONITOR.md)。
---
## 交易时段
- 非交易时段:**禁止报单**(开仓/平仓 API 返回 403)。
- CTP 未连接:禁止下单;连接中提示稍候。
---
## 各板块风控对照
| 板块 | 开仓前校验 | 特殊规则 |
|------|------------|----------|
| 下单监控 | 全部全局规则 + 保证金 + 品种 | 移动保本须填止损 |
| 关键位自动单 | 同上 + 交易时段 + CTP | 手数按 risk_percent 计算 |
| 策略首仓 | 同上 | 趋势回调计划单独表 |
| 滚仓/加仓 | 仓位上限冻结时仍可滚仓 | 滚仓保证金单独上限 |
| 开单计划 | **不自动下单**,仅提醒 | 无 assert_can_open |
| 关键支阻区 | 不自动下单 | 仅微信提醒 |
---
## 风险状态展示
下单监控顶栏、**数据看板 → 风控说明** 均展示当前状态。看板各指标释义与颜色见 [风控说明.md](./风控说明.md)。
| 状态 | 含义 |
|------|------|
| 正常 | 可新开仓 |
| 1h / 4h 冻结 | 冷静期中 |
| 日冻结 | 当日禁止新开仓 |
| 仓位上限冻结 | 已达 active 上限,可滚仓不可开新仓 |
+81
View File
@@ -0,0 +1,81 @@
# 系统设置
**页面路径**`/settings`
**相关文件**`app.py``install_trading.py``nav_settings.py`
---
## 配置分区
| 分区 | 主要项 | 影响板块 |
|------|--------|----------|
| 导航显示 | 策略、计划、行情、手续费、AI 等开关 | 全部可关闭菜单 |
| 交易模式 | SimNow / 实盘 CTP | 下单、策略、同步 |
| 计仓与风险 | 固定手数/固定金额、risk_percent、max_margin_pct、roll_max_margin_pct、日亏损强平线 | [ORDER_MONITOR](./ORDER_MONITOR.md)、[STRATEGY](./STRATEGY.md) |
| 移动保本 | trailing_be_tick_buffer | 下单、关键位自动单 |
| 挂单超时 | pending_order_timeout_sec | 下单监控 pending |
| CTP 连接 | 前置、账号(可覆盖 .env) | 全部交易 |
| 参考资金 | CTP 未连接时权益估算 | 可开仓品种表 |
| 企业微信 | wechat_webhook | [WECHAT.md](./WECHAT.md) 全部推送 |
| AI 分析 | 见 [AI.md](./AI.md) | /ai 页面 |
| 登录账号 | 用户名/密码 → .env | 登录 |
| 备份恢复 | 见 [BACKUP.md](./BACKUP.md) | 数据库 |
---
## 下单逻辑
设置页 **不直接下单**。修改项在下次 API 调用时生效;部分 CTP 配置需重连。
---
## 风控相关默认值
| 设置键 | 默认 | 说明 |
|--------|------|------|
| risk_percent | 1 | 单笔风险占权益 % |
| max_margin_pct | 30 | 新开仓保证金上限 |
| roll_max_margin_pct | 单独 | 滚仓保证金上限 |
| daily_loss_force_close_pct | 2 | 日亏损强平线(%权益) |
| daily_loss_slippage_buffer_pct | 1 | 强平滑点预留(%权益),与强平线合计默认 3% |
| fixed_lots / fixed_amount | — | 计仓模式 |
| trailing_be_tick_buffer | 2 | 移动保本 1R 缓冲跳数 |
环境变量风控(冷静期、仓位上限)见 [RISK.md](./RISK.md),通常在 `.env` 配置。
---
## 微信推送
配置 **企业微信 Webhook** 后,以下模块推送生效:
- 开单计划、关键位、下单、策略、AI 日终(见 [WECHAT.md](./WECHAT.md) 索引)
未配置 Webhook:所有 `send_wechat_msg()` 静默跳过。
---
## 导航开关
`nav_settings.py``NAV_TOGGLES` 控制菜单可见性;`@require_nav` 保护路由。
| 键 | 菜单 |
|----|------|
| strategy | 策略交易 |
| plans | 开单计划 |
| market | 行情 K 线 |
| fees | 手续费配置 |
| ai | AI 分析 |
下单监控、关键位、记录、统计、设置 **不可关闭**
---
## 相关文档
- [INDEX.md](./INDEX.md)
- [RISK.md](./RISK.md)
- [WECHAT.md](./WECHAT.md)
- [AI.md](./AI.md)
- [BACKUP.md](./BACKUP.md)
+232
View File
@@ -0,0 +1,232 @@
# SimNow 仿真账号注册与接入
SimNow 是上海期货交易所全资子公司 **上海期货信息技术有限公司(上期技术)** 提供的期货、期权 **CTP 仿真交易平台**。本系统模拟盘通过 **vnpy_ctp** 连接 SimNow 前置,下单、持仓、权益均来自 SimNow 柜台(非本地假资金)。
- **官网**https://www.simnow.com.cn/
- **客服电话**400-920-6816
- **客服邮箱**helpdesk_a@sfit.shfe.com.cn
---
## 一、SimNow 是什么
| 对比项 | SimNow | 期货公司自有模拟 |
|--------|--------|------------------|
| 接口 | 标准 **CTP**(与实盘一致) | 各公司自建,接口不统一 |
| 维护方 | 上期技术(官方) | 各期货公司 |
| 适用场景 | 程序下单、策略联调、熟悉规则 | 人工在该公司客户端练习 |
本项目的 **模拟盘 · SimNow** 模式,即把 `.env` 中的账号连到 SimNow 的 CTP 前置,与 **期货公司实盘 CTP** 使用同一套报单代码路径(见 [CTP_LIVE.md](./CTP_LIVE.md))。
---
## 二、注册前须知
1. **访问时段**:官网多数时间在 **交易日白天** 可正常访问(约 9:0011:30、13:00–15:00,及夜盘相关时段)。非交易时段可能打不开或功能受限,属平台策略,请换交易时段再试。
2. **手机号**:一个手机号只能注册 **一个** 仿真账户。
3. **登录账号不是手机号**:CTP / 快期 / 本系统里填的是 **投资者代码(InvestorID**,注册成功后需在官网查询,不要填手机号。
4. **密码**:注册后首次用客户端(如快期)登录时,可能要求 **修改交易密码**;修改后的密码才用于 CTP 连接。
5. **长期不用会被冻结**:长期未登录或未改密码的账号可能被冻结(持仓与资金清零)。需在官网 **业务 → 激活账号**,再通过 **重置资金** 恢复(通常次日生效)。
---
## 三、注册步骤
### 1. 打开官网
浏览器访问:https://www.simnow.com.cn/
若打不开,请在工作日 **日盘或夜盘交易时段** 重试。
### 2. 点击「注册账号」
首页右上角 **注册账号**(或类似入口)。
### 3. 验证手机号
- 输入 **手机号**
- 输入 **图片验证码**
- 点击 **立即注册**
### 4. 填写账户信息
- 设置 **登录密码**(即后续 CTP 交易密码,请牢记)
- 选择接口类型时选 **标准 CTP**(若页面有该选项)
- 输入 **短信验证码**
- 提交完成注册
### 5. 查询投资者代码(重要)
注册完成后:
1. 在官网 **投资者登录**
2. 进入 **业务导航****查询投资者代码**(页面文案可能略有变化)
3. 记下 **投资者代码**(一般为数字,例如 `123456`
> **本系统 `.env` 里的 `SIMNOW_USER` 填这个投资者代码,不要填手机号。**
### 6. (建议)重置模拟资金
登录官网后:
- **业务导航 → 重置资金** 或 **入金**
SimNow 默认会给一定模拟资金;若账号被冻结后激活,资金可能为 0,需在此重置。
### 7. (建议)用快期验证账号
1. 官网 **终端下载** → 下载 **快期 V2 / V3** 等客户端
2. 安装后用 **投资者代码 + 交易密码** 登录
3. 若提示 **首次登录须修改密码**,按提示改密后再登录
4. 能看到资金与行情,说明账号可用
验证通过后,再将同一 **投资者代码****密码** 写入本系统 `.env`
---
## 四、在本系统中配置
### 1. 编辑 `.env`
在服务器或本地项目目录:
```bash
cp .env.example .env
nano .env # 或用其他编辑器
```
填写 SimNow 相关项(示例):
```env
TRADING_MODE=simulation
SIMNOW_USER=123456 # 投资者代码,不是手机号
SIMNOW_PASSWORD=你的交易密码
SIMNOW_BROKER_ID=9999
SIMNOW_TD_ADDRESS=tcp://180.168.146.187:10201
SIMNOW_MD_ADDRESS=tcp://180.168.146.187:10211
SIMNOW_APP_ID=simnow_client_test
SIMNOW_AUTH_CODE=0000000000000000
SIMNOW_ENV=实盘
```
| 变量 | 说明 |
|------|------|
| `SIMNOW_USER` | 投资者代码(InvestorID |
| `SIMNOW_PASSWORD` | 交易密码(快期能登录的同一密码) |
| `SIMNOW_BROKER_ID` | 固定 **9999**SimNow 默认) |
| `SIMNOW_TD_ADDRESS` | 交易前置,以官网最新为准 |
| `SIMNOW_MD_ADDRESS` | 行情前置,以官网最新为准 |
| `SIMNOW_APP_ID` / `SIMNOW_AUTH_CODE` | SimNow 仿真默认测试值,一般无需改 |
| `SIMNOW_ENV` | **实盘**(SimNow 看穿式前置必须);仅穿透式测评才用「测试」 |
### 2. 前置地址(7×24 与交易时段)
SimNow 提供多种仿真环境,**IP 与端口会随官网公告调整**,部署前务必登录官网核对:
- **7×24 环境**:适合非交易时段联调程序(本仓库 `.env.example` 默认示例多为该环境)
- **交易时段环境**:与实盘时段、规则更接近
在官网 **产品与服务****API 下载 / 接入说明** 中查看当前 **交易前置(TD**、**行情前置(MD)** 地址,格式为:
```text
tcp://IP:端口
```
修改 `.env` 后需重启应用;也可在 **系统设置 → CTP 连接** 中维护(优先于 `.env`)。
```bash
pm2 restart qihuo
```
**云服务器网络说明**`182.254.243.31` 段前置已停用(Connection refused),请勿再使用。官方前置为 `180.168.146.187:10201/10211`。若服务器 `nc -zv 180.168.146.187 10201` 超时,属于**出网/防火墙**问题,需联系云厂商放行或换能访问 SimNow 的网络,无法仅靠改代码解决。
旧文档备用地址(已失效,仅作排查参考):
```env
# 勿用 — 已 dead
# SIMNOW_TD_ADDRESS=tcp://182.254.243.31:30001
```
### 3. 网页端连接 CTP
1. 登录本系统
2. **系统设置** → 确认 **模拟盘 · SimNow**
3. 打开 **下单监控**`/positions`
4. 点击 **连接 CTP**
5. 顶栏显示 **CTP 已连接**,权益变为 SimNow 账户资金即成功
连接成功后:下单、持仓、浮盈均来自 SimNow 柜台;**系统设置里的「参考资金」不再用于交易**,仅 CTP 未连接时用于可开仓品种筛选与以损定仓估算。
---
## 五、常见问题
### 官网打不开
-**交易日 9:0015:00** 或夜盘时段访问
- 平台维护期间会不可用,留意官网 **通知公告**
### 连接 CTP 超时 / 失败
在服务器运行诊断脚本(会测端口并尝试登录,输出具体 CTP 报错):
```bash
cd /opt/qihuo
source venv/bin/activate
python scripts/test_simnow.py
```
| 现象 | 处理 |
|------|------|
| 端口探测失败 | 服务器出网或防火墙问题,`nc -zv 180.168.146.187 10201` |
| 报错 **4097** / 握手失败 | `pip install -U vnpy vnpy_ctp``.env``SIMNOW_ENV=实盘` |
| **不合法的登录** | 投资者代码/密码错,或未在快期改过一次密码 |
| **连续登录失败次数超限(75** | 短时间失败太多次被临时封禁;等待 30~60 分钟,快期验证密码后再连,勿反复点连接 |
| 快期能登、脚本不能 | 多为网络或前置地址,换 SimNow 官网其他组前置试 |
| 连上后进程崩溃 `locale::facet::_S_create_c_locale` | **必须**安装 `zh_CN.GB18030``sed -i '/^# zh_CN.GB18030/s/^# //' /etc/locale.gen && locale-gen zh_CN.GB18030` |
### 提示「未安装 vnpy / vnpy_ctp」
Python 环境未成功安装 CTP 网关,与 SimNow 账号无关。在服务器执行:
```bash
cd /opt/qihuo
apt install -y build-essential python3-dev pkg-config
source venv/bin/activate
pip install -r requirements.txt
python -c "from vnpy_ctp import CtpGateway; print('OK')"
pm2 restart qihuo
```
### 连接成功但下单拒单
- 检查合约代码、价格精度、涨跌停
- 确认 SimNow 账户 **有足够保证金**(可在官网重置资金)
- 部分合约在仿真环境可能受限,换主力合约试
### 忘记密码
在 SimNow 官网使用 **重置密码**(需登录或按官网流程操作)。
---
## 六、与本项目其他文档的关系
| 文档 | 内容 |
|------|------|
| [TRADING.md](./TRADING.md) | 模拟盘 / 实盘通道、API、页面说明 |
| [CTP_LIVE.md](./CTP_LIVE.md) | 期货公司实盘 CTP 与 SimNow 开平仓对比 |
| [DEPLOY.md](./DEPLOY.md) | 服务器部署、vnpy 编译、PM2、环境变量总表 |
---
## 七、快速检查清单
- [ ] 已在 https://www.simnow.com.cn/ 注册并完成短信验证
- [ ] 已查询并保存 **投资者代码**(非手机号)
- [ ] 已用快期客户端成功登录(必要时已修改交易密码)
- [ ] `.env``SIMNOW_USER``SIMNOW_PASSWORD` 已填写
- [ ] 前置地址与官网 **7×24 或交易时段** 说明一致
- [ ] `pip install -r requirements.txt``vnpy_ctp` 导入成功
- [ ] 系统 **下单监控****连接 CTP** 成功
+56
View File
@@ -0,0 +1,56 @@
# 统计分析
**页面路径**`/stats`
**相关文件**`stats_engine.py``app.py``/api/stats`
---
## 功能概述
### 汇总指标(页顶卡片)
总交易次数、胜率、平均盈利/亏损、盈亏比、连续亏损、最大回撤、最大盈亏金额及占比、累计手续费、情绪单数量/占比。
进入页面自动加载 `/api/stats`,无手动「重新计算」按钮。
### 分项统计
下拉维度:按时间、周、月、品种、手续费、方向、交易类型、情绪单等,表格展示分组指标。
---
## 下单逻辑
**无**。只读展示,不触发报单。
---
## 风控规则
**无**。统计结果不影响账户状态。
---
## 数据来源
| 表 | 用途 |
|----|------|
| `trade_logs` | 主:成交、盈亏、手续费 |
| `review_records` | 情绪单标记等 |
| `stats_cache` | 缓存加速 |
开单计划的 `trade_records` **不计入** 此处主统计(除非另有合并逻辑)。
---
## 微信推送
**无**
---
## 相关文档
- [RECORDS.md](./RECORDS.md)
- [FEES.md](./FEES.md) — 手续费计算
+131
View File
@@ -0,0 +1,131 @@
# 策略交易
**页面路径**`/strategy``/strategy/records`
**相关文件**`install_trading.py``strategy/strategy_roll_lib.py``strategy/strategy_roll_monitor_lib.py`
---
## 功能概述
| 策略 | 说明 |
|------|------|
| 趋势回调 | 首仓 + 网格补仓 + 统一止盈 |
| 顺势加仓(滚仓) | 对已有 active 持仓加仓,单独保证金上限 |
趋势首仓与 **市价滚仓****交易时段内****CTP 已连接**
**突破加仓** 可在 **休盘**(小节休息、午间、日盘收盘后)提交监控,开盘触价后由 Worker 自动市价加仓。
---
## 趋势回调 — 下单逻辑
### 1. 创建计划(预览 → 确认首仓)
用户填写:品种、方向、止损、补仓上沿、止盈、风险比例等 → 系统预览手数/网格 → 确认后 **市价开首仓**
**校验**(与下单监控相同):
- 交易时段、CTP 连接
- `assert_can_open()`、[RISK.md](./RISK.md)
- 品种范围、保证金上限
**成交后**
- 写入 `trend_pullback_plans`status=active
- 创建/关联 `trade_order_monitors`monitor_type=trend
- 微信:`趋势回调首仓 {sym} {first_lots}手`
### 2. 运行中监控
后台扫描 active 计划:
| 条件 | 动作 |
|------|------|
| 价格触及 **take_profit** | 全部平仓,计划结案 |
| 价格回落至 **网格档位** | 按档位补仓(不超过 remainder_lots |
**补仓**:市价加仓,更新均价与监控。
**止盈**:市价全平。
微信:
- 止盈 → `趋势回调止盈 {sym}`
- 补仓 → `趋势回调补仓 {sym} +{add_lots}手 @档位{N}`
### 3. 策略记录
`/strategy/records` 单独归档已结束计划。
---
## 顺势加仓(滚仓)— 下单逻辑
针对 **已有 active 持仓监控** 的加仓预览与执行。须 **固定金额(以损定仓)** 模式;**移动保本** 持仓不可滚仓。
### 加仓方式
| 方式 | 提交时机 | 执行 |
|------|----------|------|
| **市价加仓** | 仅 **交易时段** | 预览 → 10 秒倒计时 → 立即 CTP 市价成交 |
| **突破加仓** | **任意时间**(含休盘) | 预览 → **提交监控** → 标记价穿越突破价后 Worker 自动市价加仓 |
休盘提交突破加仓时,几何校验放宽为「止损 vs 突破价」关系,不强制要求实时现价;开盘后有行情后按触价逻辑成交。
### 特殊风控
| 项 | 说明 |
|----|------|
| 仓位上限冻结 | **仍允许滚仓**`can_roll=True` |
| `roll_max_margin_pct` | 滚仓后总保证金占用单独上限 |
| 手数收紧 | `cap_lots_for_margin_budget()` 按滚仓上限裁剪 |
流程:
- **市价**:预览 → 确认 → CTP 市价加仓 → 更新 monitor
- **突破**:预览 → 提交 pending 腿 → `check_roll_monitors`(在 `qihuo-ctp` Worker 内)触价成交
---
## 风控规则
| 规则 | 趋势首仓 | 市价滚仓 | 突破滚仓(pending) |
|------|----------|----------|---------------------|
| assert_can_open | ✓ | 仓位冻结时仍可 | 仓位冻结时仍可 |
| max_margin_pct | ✓ 首仓 | — | — |
| roll_max_margin_pct | — | ✓ | ✓(预览时按突破价估算) |
| 交易时段 | ✓ | ✓ | 提交 **不要求**;成交须交易时段 |
| CTP 连接 | ✓ | ✓ | 提交 **不要求**;触价成交须 CTP |
| 品种范围 | ✓ | ✓ | ✓ |
| 单笔 50 手 | ✓ | ✓ | ✓ |
全局规则见 [RISK.md](./RISK.md)。
---
## 止盈止损
趋势持仓纳入 `sl_tp_guard` 本地监控(与下单监控相同机制)。
- monitor_type = `trend` / `roll`
- 平仓写入 `trade_logs`,来源标签「趋势回调」「顺势加仓」
---
## 微信推送
| 事件 | 模板 |
|------|------|
| 首仓 | [WECHAT §11](./WECHAT.md#11-策略趋势回调) |
| 止盈 | 同上 |
| 补仓 | 同上 |
| 结构化平仓 | [WECHAT §4](./WECHAT.md#4-平仓完成) |
---
## 相关文档
- [ORDER_MONITOR.md](./ORDER_MONITOR.md)
- [RISK.md](./RISK.md)
- [WECHAT.md](./WECHAT.md)
+140
View File
@@ -0,0 +1,140 @@
# 下单监控与策略交易
## 默认首页
登录或访问 `/` 后进入 **下单监控**`/positions`)。页面包含:
| 区域 | 说明 |
|------|------|
| 顶栏 | 交易模式、CTP 状态、权益/可用、连接 CTP |
| 期货下单 | 限价/市价报单、止盈/止损、移动保本、以损定仓/固定手数 |
| 当前持仓 | CTP 持仓卡片、挂单中、撤单、平仓 |
| 可开仓品种 | 按权益与保证金上限筛选、行业分类、走势/跳空/成交量排序 |
`/trade``/recommend` 均重定向到 `/positions`(可开仓品种锚点 `#recommend`)。
## 两种交易通道
| 设置 | 实际连接 | 资金 |
|------|----------|------|
| **模拟盘** | SimNowvnpy → CTP 仿真前置) | SimNow 账户权益 |
| **实盘** | 期货公司 CTP`.env``CTP_LIVE_*` | 柜台权益 |
模拟盘与实盘均走 **vnpy_ctp**,无本地假撮合。
**实盘接入与开平仓对比** → [CTP_LIVE.md](./CTP_LIVE.md) · SimNow → [SIMNOW.md](./SIMNOW.md)
### CTP 双进程与连接行为
| 项目 | 说明 |
|------|------|
| Web | PM2 `qihuo`,端口 6600 |
| CTP Worker | PM2 `qihuo-ctp`,端口 6601(本机) |
| 连接/重连 | 在 Worker 内执行;**小节休息、午间休盘** 仍保持连接(便于读缓存权益/持仓) |
| 页面行为 | 各页 **仅轮询** CTP 状态,**不会在打开页面时自动发起连接** |
| 权益缓存 | CTP 短暂断开时,看板/持仓可回退读最近快照 |
手动 **连接 CTP** 仍通过 **下单监控** 页按钮或 API;成功后顶栏显示「CTP 已连接」。
## 下单与持仓
- **限价开仓**:先显示「挂单中」,柜台成交后进入持仓监控;超时未成交自动撤单(时长见系统设置)
- **撤单**:交易时段内可手动撤单;非交易时段按钮不可用
- **平仓**:程序平仓写入 `trade_logs`(来源「本地」)
- **持仓数据**SSE `/api/trading/stream` 推送,约 1 秒刷新
## 可开仓品种
- 用于开仓纪律与仓位限制:按保证金上限计算最大手数,仅展示当前权益下可开的品种
- 每日后台刷新列表(`/api/recommend/stream`
- 最大手数 = floor(权益 × 保证金上限 ÷ 1 手保证金)
- **1 手保证金**:**CTP 已连接** 时优先读取柜台合约的 `long_margin_ratio` / `short_margin_ratio` 与乘数计算(表格标注「柜台」);未连接或合约信息暂不可用时,才用本地参考保证金率估算
- 开仓前校验、固定金额计仓、保证金占用比例检查均与上述规则一致,避免交易所上调保证金后仍按旧比例显示可开手数
- 展示近一周日线走势、跳空、昨日成交量(手)、成交额
- 可按 **行业** 筛选,支持多字段排序
- **夜盘时段**:品种下拉与可开仓表仅显示有夜盘交易的品种,并带「夜盘」标记
### 小账户品种范围(≤20 万)
权益 **不超过 20 万元** 时,系统限制可浏览、可搜索、可报单的品种为以下 **4 个**
| 品种 | 代码 |
|------|------|
| 玉米 | `c` |
| 豆粕 | `m` |
| 甲醇 | `MA` |
| 螺纹钢 | `rb` |
适用范围:
- 可开仓品种表
- 期货下单品种联想 / 下拉
- 开仓报单校验(含趋势策略首仓)
**SimNow 与实盘规则一致**:**CTP 未连接** 时,可开仓表 **当前权益固定按 10 万** 估算最大手数,并 **仅展示四品种**(玉米、豆粕、甲醇、螺纹钢);与系统设置中的参考资金无关。连接 CTP 后改用柜台权益;若柜台权益 ≤20 万,同样仅上述四品种。
页面会提示:「未连接 CTP,按 10 万权益估算最大手数,仅显示并可交易 …」。
## 期货下单 · 止盈止损与移动保本
本地止盈止损由程序监控持仓,触发后市价平仓(与 CTP 柜台委托/持仓数据独立)。
### 默认模式(移动保本关闭)
- 可同时填写 **止盈**、**止损**
- 填写入场价与止损/止盈后,表单下方显示 **盈亏比**、止损金额、止盈金额(如有)
- 本地监控:价格触及止盈或止损即平仓
### 移动保本(可选,默认关闭)
勾选 **移动保本** 后:
| 表单项 | 行为 |
|--------|------|
| 止盈 | **隐藏**,不提交止盈价 |
| 盈亏比 / 止盈金额 | **不显示** |
| 止损 | **必填**,仅保留止损输入 |
平仓逻辑:
- **不再** 按固定止盈价监控
- 程序按 **移动止损** 管理出场:浮盈达 **1R** 后止损移至开仓价 ± N 跳(保本);达 **2R** 移至 1R,依次类推(N 见系统设置「移动保本缓冲」)
- 开启移动保本 **必须填写止损价**,否则无法开仓
持仓卡片在开启移动保本时同样 **不展示盈亏比、盈利金额、止盈状态**,仅保留止损与移动保本进度(如已锁 N R)。
## 策略交易
| 页面 | 路径 |
|------|------|
| 策略交易 | `/strategy` |
| 策略记录 | `/strategy/records` |
趋势回调、顺势加仓等策略需先在下单监控完成开仓,再在策略页配置。
## 参考资金
系统设置中的「参考资金」在 **CTP 未连接** 时用于以损定仓估算;连接后自动改用柜台权益。
可开仓品种与品种白名单:**未连接 CTP 时** 可开仓表按 **10 万权益** 估算最大手数,且仅四品种;连接后若柜台权益 ≤20 万,同样仅上述四品种。
## 首次使用 SimNow
完整步骤见 **[SimNow 注册与接入说明](./SIMNOW.md)**。
简要:注册 SimNow → 填写 `.env` → 安装 vnpy_ctp → 登录系统 → **下单监控****连接 CTP**
## 主要 API
| 接口 | 说明 |
|------|------|
| `POST /api/ctp/connect` | 连接 SimNow 或实盘 CTP |
| `GET /api/ctp/status` | 连接状态 |
| `POST /api/trade/order` | 报单(需已连接 CTP |
| `POST /api/trading/order/cancel` | 撤单(交易时段) |
| `POST /api/trading/close` | 平仓 |
| `GET /api/trading/stream` | 持仓 SSE |
| `GET /api/recommend/list` | 可开仓品种 JSON |
| `GET /api/recommend/stream` | 可开仓品种 SSE |
| `POST /api/strategy/trend/execute` | 执行趋势策略 |
详见 [DEPLOY.md](./DEPLOY.md) 中 CTP 故障排查。
+305
View File
@@ -0,0 +1,305 @@
# 企业微信推送
**配置**:系统设置 → 企业微信 Webhook(`wechat_webhook`)。
**发送函数**`app.send_wechat_msg()` — 所有推送正文前自动加前缀:
```
【国内期货】
{正文}
```
未配置 Webhook 时静默跳过,不报错。
---
## 推送类型索引
| # | 类型 | 触发场景 | 模板章节 |
|---|------|----------|----------|
| 1 | 手动开仓成功(结构化) | 下单监控成交且填写止损 | [§1](#1-手动开仓成功) |
| 2 | 手动开仓(简版) | 成交但未填止损 | [§2](#2-简版开仓) |
| 3 | 挂单提交 | 限价单未立即成交 | [§3](#3-挂单提交) |
| 4 | 平仓完成(结构化) | trade_logs 写入平仓记录 | [§4](#4-平仓完成) |
| 5 | SL/TP 触发(简版) | 本地止盈止损市价平仓委托已提交 | [§5](#5-本地止盈止损触发) |
| 6 | 关键位自动单结果 | 箱体/收敛突破尝试下单 | [§6](#6-关键位自动单) |
| 7 | 关键位开仓成功(结构化) | 自动单成交 | [§7](#7-关键位开仓成功) |
| 8 | 关键支阻区提醒 | 5m 收盘突破区间 | [§8](#8-关键支阻区提醒) |
| 9 | 开单计划触发 | 现价进入决策区间 | [§9](#9-开单计划) |
| 10 | 开单计划止盈/止损 | 激活后价格触及 TP/SL | [§10](#10-开单计划结果) |
| 11 | 策略趋势回调 | 首仓/止盈/补仓 | [§11](#11-策略趋势回调) |
| 12 | AI 分析 | 开仓/平仓/日终报告 | [§12](#12-ai-分析) |
实现文件:`wechat_notify.py``trade_notify.py``key_monitor_lib.py``app.py``install_trading.py``ai_worker.py`
---
## 结构化推送
以下模板由 `wechat_notify.py` 生成,字段随实际成交数据填充。
### 1. 手动开仓成功
**函数**`format_open_success()` · **来源**`trade_notify.notify_manual_open_filled()`
**触发**:下单监控开仓 **成交** 且填写了 **止损价**
**模板**
```
📈 {品种名} 开仓成功
💼 账户:{模拟盘/实盘}
🧾 订单基础信息
📌 来源:期货下单
🔖 委托号:{order_id}
📈 方向:多头(long)/ 空头(short
⚠ 单笔风控:{risk_percent}%≈{risk_amount}元
📊 仓位配置
账户权益:{capital} 元
开仓手数:{lots} 手
占用保证金:{margin} 元
仓位占比:{margin_pct}%
🎯 价位 & 盈亏比
开仓价:{entry}
止损价:{stop_loss}
止盈价:{take_profit}
计划盈亏比:RR {rr} : 1
移动保本:1.0R → {be_px}(缓冲 {be_tick_buffer} 跳) ← 仅开启移动保本时
📌 状态
✅ 已进入下单监控,本地 SL/TP 守护
```
**示例**
```
📈 螺纹钢 开仓成功
💼 账户:SimNow 模拟
🧾 订单基础信息
📌 来源:期货下单
📈 方向:多头(long
⚠ 单笔风控:1%≈1000.00元
📊 仓位配置
账户权益:100000.00 元
开仓手数:2 手
占用保证金:8500.00 元
仓位占比:8.50%
🎯 价位 & 盈亏比
开仓价:3200
止损价:3180
止盈价:3240
计划盈亏比:RR 2 : 1
📌 状态
✅ 已进入下单监控,本地 SL/TP 守护
```
---
### 4. 平仓完成
**函数**`format_close_done()` · **来源**`trade_notify.notify_trade_log_close()`
**触发**`trade_logs` 新增平仓记录(SL/TP 守护、CTP 同步、手动平仓等)。
**模板**
```
📈/📉 {品种名} 平仓完成
💼 账户:{mode_label}
🧾 平仓概要
📌 方向:多头(long)/ 空头(short
📌 平仓结果:{止盈|止损|保本止盈|移动止盈|手动平仓}
💰 本单净盈亏:+/-{pnl_net} 元
⏱ 持仓时长:{N分钟|N小时N分钟|N天…}
💵 账户权益:{equity_after} 元
🎯 价位(计划)
开仓价:{entry}
平仓价:{close_price}
止盈价:{take_profit}
止损价:{stop_loss}
📎 备注
成交价不在计划止盈/止损带内(可能为手动或其他类型平仓) ← 可选
```
---
### 7. 关键位开仓成功
**函数**`format_key_open_success()` — 在 §1 基础上追加:
```
📎 关键位触发
类型:{箱体突破|收敛突破}
模式:{顺势|反转} · {向上突破|向下突破}
5m 收盘:{bar_time}
```
**来源字段**`{monitor_type}·{trade_mode}`(如「箱体突破·顺势」)。
---
### 8. 关键支阻区提醒
**函数**`format_zone_alert()` · **最多 3 次**,间隔约 **5 分钟**
**模板**
```
📌 {品种名} 关键位突破提醒({1|2|3}/3
🧾 突破概要
📌 类型:关键支阻区
⏱ 触发时间:{bar_time}
📊 上沿:{upper}|下沿:{lower}
💹 触发收盘:{close_price}
🎯 {向上突破/向下突破}({多头/空头})
📍 突破价位:{boundary}
📎 说明
· 人工盯盘,共推送 3 次(间隔约 5 分钟)
· 推送完毕后本条监控自动结案
· 不参与自动开仓
```
---
## 简版与业务推送
### 2. 简版开仓
**触发**:成交但未填止损。
```
{模拟盘/实盘} 开仓 {symbol} {direction} {lots}手 @{price}
```
---
### 3. 挂单提交
**触发**:限价开仓委托已提交、尚未成交。
```
委托已提交 · {symbol} {direction} {lots}手挂单中({N} 分钟未成交自动撤单)
```
---
### 5. 本地止盈止损触发
**触发**`sl_tp_guard` 本地检测到 TP/SL,已提交市价平仓委托(结构化平仓推送在成交写入 trade_logs 后另行发送)。
```
{止盈|止损} {symbol} {direction} {lots}手 @{mark},平仓委托已提交
```
---
### 6. 关键位自动单
**函数**`format_auto_breakout_msg()` · **成败均推送**
**模板**
```
✅/❌ {品种名} {箱体突破|收敛突破}自动单
⏱ 5m 收盘:{bar_time}
🎯 {向上突破|向下突破} · {顺势|反转} · {做多|做空}
💹 入场:{entry} 止损:{sl} 止盈:{tp}(盈亏比 {rr}
📦 手数:{lots}
🛡 已开启移动保本(达目标盈亏比自动止盈) ← 可选
{detail} ← 失败原因等
```
---
### 9. 开单计划
**触发**`planned` → 现价进入 `[zone_lower, zone_upper]`
```
【开单计划触发】{name} ({symbol})
方向:做多/做空
决策区间:{zone_lower} ~ {zone_upper}
决策理由:{reason}
当前价:{p}
止损:{stop_loss} 止盈:{take_profit}
```
---
### 10. 开单计划结果
**触发**`active` 状态下触及止盈或止损。
```
[做多/做空] {name} 已{止盈|止损}
决策区间:{zone_lower} ~ {zone_upper}
止损:{stop_loss} 止盈:{take_profit}
当前价:{p}
```
> 开单计划 **不自动下单**,仅微信提醒并写入 `trade_records`。
---
### 11. 策略趋势回调
| 事件 | 模板 |
|------|------|
| 首仓成交 | `趋势回调首仓 {sym} {first_lots}手` |
| 止盈平仓 | `趋势回调止盈 {sym}` |
| 补仓成交 | `趋势回调补仓 {sym} +{add_lots}手 @档位{done+1}` |
---
### 12. AI 分析
详见 [AI.md#微信推送](./AI.md#微信推送)。
**事件分析**(开仓/平仓后,AI 成功时):
```
🤖 AI 分析 · {title}
{content 前 1800 字}
```
**日终报告**
```
🤖 {YYYY-MM-DD} 日终持仓与交易报告
{content 前 1800 字}
```
---
## 推送与板块对照
| 板块 | 推送类型 |
|------|----------|
| 下单监控 | §1 §2 §3 §4 §5 §12 |
| 关键位监控 | §6 §7 §8 |
| 开单计划 | §9 §10 |
| 策略交易 | §11 §4 |
| AI 分析 | §12 |
---
## 注意事项
1. 所有消息经 `【国内期货】` 前缀发送,企业微信群机器人 `msgtype=text`
2. 结构化开仓/平仓需 **填写止损** 才会发送完整模板;否则为简版。
3. AI 推送仅在 AI 调用 **成功** 时发送;失败内容只写入 `/ai` 页面。
4. 关键支阻区第 3 次推送后监控自动归档,不再推送。
+185
View File
@@ -0,0 +1,185 @@
# 软件购买与使用协议(个人版)
> **说明**:本协议为个人购买者使用模板。正式交付时可打印或转为 PDF,由双方签字/确认。
> 本模板不构成法律意见;金额较大或机构/共享交易室合作,建议由执业律师审阅后使用。
---
**协议编号**_______________
**签订日期**_______________
---
## 甲方(著作权人 / 许可方)
- **姓名**:马建军
- **联系电话**18364911125
- **微信**dekun03
## 乙方(被许可方 / 购买方)
- **姓名**_______________
- **联系电话**_______________
- **微信/邮箱**_______________
---
## 第一条 软件与交付内容
1.1 甲方向乙方提供的软件名称为 **「国内期货 · 交易复盘系统」**(以下简称「本软件」),包括甲方交付时约定版本的源代码、部署说明及必要配置指导。
1.2 **交付方式**(勾选适用项):
- [ ] 部署服务:甲方协助乙方在乙方指定服务器完成安装与基础配置
- [ ] 源代码:甲方提供约定版本源代码(Git 归档 / 压缩包 / 私有仓库只读权限,择一填写:_______________
- [ ] 其他:_______________
1.3 **交付版本标识**(建议填写 Git 提交号或日期):_______________
---
## 第二条 授权范围
2.1 甲方授予乙方 **非独占、不可转让、不可再许可** 的个人使用许可。
2.2 乙方仅可将本软件部署在 **乙方本人名下单一实例**(一台 VPS 或一台个人电脑服务器,二选一或填写:_______________),供 **乙方本人** 用于个人期货交易的纪律管理、记录与复盘。
2.3 本授权 **不包括** 以下权利(须另行书面协议并支付费用):
- 共享交易室、培训室、跟单室等多人共用或对外经营
- 白标、OEM、二次分发、转售源码
- 将本软件作为带单、荐品种、配资等业务的工具或平台
---
## 第三条 严禁用途(乙方承诺)
乙方承诺 **不得** 利用本软件从事以下行为:
1. **带单、代客理财、代客下单、信号群喊单、跟单服务** 等可能违反期货监管及咨询资质要求的行为;
2. **向他人推荐、介绍特定期货品种、合约或具体买卖方向**,并以此向他人收费或获利;
3. **融资、配资、分仓、对赌、非法吸收资金** 等资金融通或变相配资行为;
4. **复制、传播、转售、出租、出借** 源代码或部署包给任何第三方;
5. **删除、篡改** 软件内或文档中的版权声明与许可说明;
6. 其他违反中国法律法规及期货监管规定的行为。
乙方违反本条,甲方有权 **立即终止许可**;乙方已付费用 **不予退还**(法律另有强制性规定的除外)。因乙方违规导致甲方损失的,乙方应依法赔偿。
---
## 第四条 费用与支付
4.1 乙方应向甲方支付:
| 项目 | 金额(元) | 备注 |
|------|------------|------|
| 部署服务费 | | |
| 源代码许可费 | | |
| 其他 | | |
| **合计** | | |
4.2 支付方式:_______________
4.3 甲方收到约定款项后 ___ 个工作日内完成交付(或双方另行约定)。
---
## 第五条 更新、维护与支持
5.1 **版本更新**(勾选):
- [ ] 本次交付为固定版本,后续大版本更新需 **另行付费**
- [ ] 含 ___ 个月内的缺陷修复与小版本更新(不含新功能模块)
- [ ] 其他:_______________
5.2 支持方式与范围:_______________(如:微信答疑、远程协助次数等)。
5.3 超出约定范围的支持,双方可 **另行协商费用**
---
## 第六条 知识产权
6.1 本软件之著作权及其他知识产权 **均归甲方所有**。乙方仅获得本协议第二条约定之 **有限使用权**,不取得著作权转让或共有。
6.2 乙方可在本协议授权范围内备份源代码供 **自用**,不得用于再分发。
---
## 第七条 免责声明与风险提示
7.1 本软件为 **交易纪律与记录辅助工具**,不提供投资咨询,不构成任何 **投资建议、收益承诺或交易信号**
7.2 **期货交易风险极大**,乙方须具备相应风险承受能力,独立作出交易决策,盈亏由乙方 **自行承担**
7.3 因 CTP/SimNow/网络/服务器/第三方接口故障、断线、延迟等导致的数据偏差、下单失败或损失,甲方在已尽合理交付与说明义务的前提下, **不承担** 由此产生的交易损失(法律强制性规定除外)。
7.4 甲方不保证软件持续符合某一交易所或期货公司的全部最新规则;监管或接口变化时,乙方应配合升级或调整配置。
---
## 第八条 责任限制
8.1 除因甲方 **故意或重大过失** 直接导致乙方人身或财产损害的情形外,甲方对乙方因使用或无法使用本软件产生的 **间接损失、交易亏损、数据丢失、业务中断** 等不承担责任。
8.2 在任何情况下,甲方对乙方的 **累计赔偿责任** 不超过乙方就本协议 **实际已支付给甲方的费用总额**(法律强制性规定除外)。
---
## 第九条 保密
9.1 乙方对交付的 **未公开源代码、部署文档、配置信息** 负有保密义务,不得向无关第三方披露,法律法规或监管要求除外。
9.2 保密期限:许可终止后 **三(3)年** 内仍有效(源代码本身仍不得非法传播)。
---
## 第十条 协议期限与终止
10.1 本协议自双方签字/确认之日起生效。个人使用许可为 **长期有效**,直至依本条终止。
10.2 有下列情形之一的,甲方有权终止许可,乙方应停止使用并销毁多余副本(保留一份备份法律允许的范围内自用备份除外):
- 乙方违反第三条严禁用途或第二条授权范围;
- 乙方非法转售、传播源码;
- 乙方从事违法经营活动并使用本软件。
10.3 终止后,乙方 **不得** 继续使用本软件开展新业务;已产生的法律责任不因终止而免除。
---
## 第十一条 争议解决
11.1 本协议之订立、效力、解释、履行及争议解决均适用 **中华人民共和国法律**
11.2 双方因本协议发生争议,应先友好协商;协商不成的,任一方可向 **甲方住所地有管辖权的人民法院** 提起诉讼。
---
## 第十二条 其他
12.1 本协议与仓库根目录 `LICENSE.zh-CN.txt` 内容不一致的, **以本协议为准**(仅针对甲乙双方之间)。
12.2 本协议一式两份,甲乙双方各执一份,具有同等效力(电子确认、微信确认截图与纸质同等有效,双方认可时)。
12.3 未尽事宜,双方可签订 **补充协议**;补充协议与本协议具有同等效力。
---
## 签署栏
**甲方(许可方)**
签名:_______________
日期:_______________
**乙方(被许可方)**
签名:_______________
日期:_______________
---
## 附件(可选)
- [ ] 交付清单(版本号、文件列表、服务器信息)
- [ ] 部署完成确认单
- [ ] 乙方身份证复印件(线下签约时)
+122
View File
@@ -0,0 +1,122 @@
# 风控说明
**页面**`/risk-guide`(顶栏「风控说明」)· 数据看板内嵌卡片同步展示摘要指标
本文说明账户 **保证金占比**、各风控指标含义、颜色规则及配置来源。全局风控逻辑详见 [RISK.md](./RISK.md)。
---
## 保证金占比(核心规则)
系统设置中有两个保证金上限,默认 **单仓 30%**、**综合 50%**`max_margin_pct` / `roll_max_margin_pct`)。
| 模式 | 判定 | 30%(单仓上限) | 50%(综合上限) |
|------|------|-----------------|-----------------|
| **单仓模式** | `MAX_ACTIVE_POSITIONS = 1` | **单仓保证金上限**:新开仓时,拟开仓位 + 已有持仓占用保证金,占权益不得超过 30% | **滚仓保证金上限**:滚仓/顺势加仓时,总占用保证金占权益不得超过 50% |
| **多仓模式** | `MAX_ACTIVE_POSITIONS > 1` | **单仓保证金上限**:每一新开仓仍按 30% 约束该笔/该品种保证金 | **多仓保证金上限**:所有持仓合计占用保证金占权益不得超过 50% |
### 看板如何展示
| 看板指标 | 含义 | 对比上限 |
|----------|------|----------|
| **综合保证金占比** | 当前 **全部持仓** 占用保证金 ÷ 账户权益 | 斜杠后为 **50%**(单仓模式=滚仓上限,多仓模式=多仓上限) |
| **单仓保证金上限** | 新开仓单笔/单品种保证金天花板 | 固定显示 **30%**(系统设置) |
| **滚仓保证金上限** / **多仓保证金上限** | 单仓模式下为滚仓专用;多仓模式下为合计上限 | 固定显示 **50%**(系统设置) |
> **示例**:权益 10 万、占用保证金 2.55 万 → 综合保证金占比 **25.55% / 50%**(不是 30%)。新开仓仍受 30% 单仓上限约束;滚仓或多仓合计最高可到 50%。
---
## 状态行(看板卡片顶部)
顶栏一行文字为 **当前风控结论**,例如:
| 显示 | 含义 |
|------|------|
| 正常 · 可新开仓 | 未触发冻结,可新开仓 |
| 仓位上限冻结 · 已达仓位上限 1/1 | 同时 active 持仓数已达上限,禁止新开仓,**滚仓/加仓仍允许** |
| 日冻结 | 复盘勾选情绪问题、当日手动平仓超限或日限额触发,禁止新开仓 |
- **绿色**:当前可交易(`can_trade=true`
- **红色**:当前禁止新开仓(`can_trade=false`
---
## 指标一览
| 指标 | 说明 | 配置来源 |
|------|------|----------|
| **风控开关** | 是否启用账户风控(持仓/日限额等) | `.env``RISK_CONTROL_ENABLED` |
| **持仓限制** | 当前 active 持仓数 / 同时持仓上限 | `.env``MAX_ACTIVE_POSITIONS` |
| **日持仓限制** | 当日已开仓次数(含已平)/ 日开仓上限 | `.env``RISK_DAILY_POSITION_LIMIT`(默认 5 |
| **日亏损风控** | 当日亏损(已实现+浮亏)占权益 / 强平线 | 系统设置 `daily_loss_force_close_pct`(默认 2%+ `daily_loss_slippage_buffer_pct`(默认 1% |
| **手动平仓次数** | 当日手动平仓次数 / 上限(超限日冻结) | `.env``RISK_MANUAL_CLOSE_DAILY_LIMIT` |
| **综合保证金占比** | 占用保证金占权益 / **综合上限(50%** | 实时计算 + 系统设置 `roll_max_margin_pct` |
| **单仓保证金上限** | 新开仓保证金占权益上限 | 系统设置 `max_margin_pct`(默认 30% |
| **滚仓/多仓保证金上限** | 单仓=滚仓上限;多仓=合计上限 | 系统设置 `roll_max_margin_pct`(默认 50% |
| **计仓模式** | 固定金额(以损定仓)或固定手数 | 系统设置 |
| **交易日切** | 统计日重置时刻 | `.env``TRADING_DAY_RESET_HOUR`(默认 8:00 |
---
## 颜色规则(看板 UI
### 风控开关
| 状态 | 颜色 |
|------|------|
| 开启 | **绿色** |
| 关闭 | **红色** |
### 综合保证金占比
显示格式:`已用% / 综合上限%`(综合上限默认 **50%**
| 已用占综合上限比例 | 已用部分颜色 |
|--------------------|--------------|
| &lt; 85% | **绿色**(安全) |
| 85% ~ 100% | **琥珀色**(接近上限) |
| ≥ 100% | **红色**(已达或超过综合上限) |
斜杠后的 **50%****琥珀色**,与「滚仓/多仓保证金上限」一致。
### 单仓 / 综合保证金上限
| 指标 | 数值颜色 |
|------|----------|
| 单仓保证金上限(30% | **蓝色** |
| 滚仓/多仓保证金上限(50%) | **琥珀色** |
### 持仓方向(持仓信息、平仓记录)
| 方向 | 颜色 |
|------|------|
| 做多 | **绿色** |
| 做空 | **红色** |
---
## 导航与设置
- 顶栏 **风控说明** 即本页(`/risk-guide`),内容由 `docs/风控说明.md` 同步渲染。
- 可在 **系统设置 → 导航显示** 中关闭「风控说明」入口;关闭后顶栏隐藏,直接访问 URL 将跳回下单监控。
---
## 与全局风控的关系
- 看板 **实时展示** 账户风控状态;下单前各板块仍调用 `assert_can_open()` 做相同校验。
- **日亏损风控**、**日持仓限制** 与「同时持仓上限」并列生效;达日亏损强平线将 **强制清仓** 并禁止新开仓。
- **期货不使用本系统「手动平仓冷静期」**(交易所自有规则);手动平仓仅计入当日次数,超限触发日冻结。
- **综合保证金占比** 使用 CTP 柜台权益与占用保证金实时计算;断线时可能短暂显示 `—`
---
## 相关文档
| 文档 | 内容 |
|------|------|
| [RISK.md](./RISK.md) | 全局账户风控规则与 env 变量 |
| [SETTINGS.md](./SETTINGS.md) | 保证金上限、计仓模式、导航开关 |
| [ORDER_MONITOR.md](./ORDER_MONITOR.md) | 下单监控顶栏风控状态 |
| [INDEX.md](./INDEX.md) | 文档总索引 |
+21 -5
View File
@@ -1,19 +1,35 @@
const path = require("path");
const fs = require("fs");
const ROOT = __dirname;
const venvCandidates = [
path.join(ROOT, "venv", "bin", "python"),
path.join(ROOT, "venv", "Scripts", "python.exe"),
];
const interpreter = venvCandidates.find((p) => fs.existsSync(p)) || "python3";
module.exports = { module.exports = {
apps: [ apps: [
{ {
name: "qihuo", name: "qihuo",
script: "app.py", script: "app.py",
cwd: "/opt/qihuo", cwd: ROOT,
interpreter: "/opt/qihuo/venv/bin/python", interpreter,
instances: 1, instances: 1,
autorestart: true, autorestart: true,
watch: false, watch: false,
max_memory_restart: "300M", max_memory_restart: "8192M",
env: { env: {
NODE_ENV: "production", NODE_ENV: "production",
PYTHONPATH: path.join(ROOT, "_legacy"),
LANG: "zh_CN.UTF-8",
LC_ALL: "zh_CN.UTF-8",
LC_CTYPE: "zh_CN.UTF-8",
QIHUO_STARTUP_WORKERS: "8",
QIHUO_MEMORY_MB: "8192",
}, },
error_file: "/opt/qihuo/logs/pm2-error.log", error_file: path.join(ROOT, "logs", "pm2-error.log"),
out_file: "/opt/qihuo/logs/pm2-out.log", out_file: path.join(ROOT, "logs", "pm2-out.log"),
time: true, time: true,
}, },
], ],
+6
View File
@@ -0,0 +1,6 @@
# Copyright (c) 2025-2026 马建军. All rights reserved.
"""Backward-compatible shim — implementation in modules.trading.install."""
from modules.trading.install import install_trading
__all__ = ["install_trading"]
-257
View File
@@ -1,257 +0,0 @@
"""复盘 K 线:新浪拉取 + matplotlib 生成截图。"""
import json
import logging
import os
import re
from datetime import datetime
from typing import Optional
from zoneinfo import ZoneInfo
import requests
from symbols import ths_to_codes
logger = logging.getLogger(__name__)
TZ = ZoneInfo("Asia/Shanghai")
PERIOD_MINUTES = {
"1m": "1",
"3m": "3",
"5m": "5",
"15m": "15",
"30m": "30",
"1h": "60",
"4h": "240",
}
def ths_to_sina_chart_symbol(symbol: str) -> Optional[str]:
"""ag2608 -> AG2608(新浪 K 线接口合约代码)。"""
code = (symbol or "").strip()
if not code:
return None
codes = ths_to_codes(code)
if codes:
sina = codes.get("sina_code", "")
if sina.startswith("nf_"):
return sina[3:]
if sina.startswith("CFF_RE_"):
return sina[7:]
ths = codes.get("ths_code", "")
return ths.upper() if ths else None
m = re.match(r"^([A-Za-z]+)(\d+)$", code)
if m:
return m.group(1).upper() + m.group(2)
return None
def _parse_jsonp(text: str) -> Optional[list]:
m = re.search(r"\((.*)\)\s*;?\s*$", text.strip(), re.DOTALL)
if not m:
return None
try:
data = json.loads(m.group(1))
return data if isinstance(data, list) else None
except json.JSONDecodeError:
return None
def fetch_sina_klines(symbol: str, period: str) -> list:
"""拉取新浪期货分钟 K 线。"""
chart_sym = ths_to_sina_chart_symbol(symbol)
if not chart_sym:
return []
if period == "1d":
return _fetch_sina_daily(chart_sym)
typ = PERIOD_MINUTES.get(period)
if not typ:
return []
ts = datetime.now(TZ).strftime("%Y%m%d%H%M%S")
url = (
"https://stock2.finance.sina.com.cn/futures/api/jsonp.php/"
f"var_{chart_sym}_{typ}_{ts}=/InnerFuturesNewService.getFewMinLine"
f"?symbol={chart_sym}&type={typ}"
)
try:
resp = requests.get(
url,
timeout=20,
headers={"Referer": "https://finance.sina.com.cn"},
)
bars = _parse_jsonp(resp.text)
return bars or []
except Exception as exc:
logger.warning("fetch kline failed %s %s: %s", chart_sym, period, exc)
return []
def _fetch_sina_daily(chart_sym: str) -> list:
url = (
"https://stock2.finance.sina.com.cn/futures/api/json.php/"
f"IndexService.getInnerFuturesDailyKLine?symbol={chart_sym}"
)
try:
resp = requests.get(url, timeout=20, headers={"Referer": "https://finance.sina.com.cn"})
raw = resp.json()
if not raw:
return []
out = []
for row in raw:
if isinstance(row, list) and len(row) >= 5:
out.append({
"d": row[0],
"o": row[1],
"h": row[2],
"l": row[3],
"c": row[4],
})
return out
except Exception as exc:
logger.warning("fetch daily kline failed %s: %s", chart_sym, exc)
return []
def _parse_dt(value: str) -> Optional[datetime]:
if not value:
return None
v = value.strip().replace("T", " ")
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M"):
try:
return datetime.strptime(v, fmt).replace(tzinfo=TZ)
except ValueError:
continue
try:
return datetime.fromisoformat(value.strip()).replace(tzinfo=TZ)
except ValueError:
return None
def _bar_datetime(bar: dict) -> Optional[datetime]:
d = bar.get("d")
if not d:
return None
try:
return datetime.strptime(d, "%Y-%m-%d %H:%M:%S").replace(tzinfo=TZ)
except ValueError:
return None
def _select_bars(
bars: list,
cutoff: datetime,
count: int,
) -> list:
filtered = []
for bar in bars:
dt = _bar_datetime(bar)
if dt and dt <= cutoff:
filtered.append(bar)
if not filtered:
filtered = bars
if count > 0 and len(filtered) > count:
filtered = filtered[-count:]
return filtered
def generate_review_kline_chart(
symbol: str,
periods: list[str],
count: int,
cutoff_label: str,
open_time: str,
close_time: str,
entry_price: Optional[float],
stop_loss: Optional[float],
take_profit: Optional[float],
close_price: Optional[float],
upload_dir: str,
) -> Optional[str]:
"""生成双周期 K 线复盘图,返回 uploads 目录下的文件名。"""
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
now = datetime.now(TZ)
if cutoff_label == "开仓时间":
cutoff = _parse_dt(open_time) or now
elif cutoff_label == "当前时间":
cutoff = now
else:
cutoff = _parse_dt(close_time) or now
open_dt = _parse_dt(open_time)
close_dt = _parse_dt(close_time)
valid_periods = [p for p in periods if p]
if not valid_periods:
valid_periods = ["15m", "1h"]
fig, axes = plt.subplots(
len(valid_periods), 1,
figsize=(14, 4.5 * len(valid_periods)),
facecolor="#0a0a10",
squeeze=False,
)
plotted = False
for idx, period in enumerate(valid_periods):
ax = axes[idx, 0]
bars = fetch_sina_klines(symbol, period)
bars = _select_bars(bars, cutoff, count)
if not bars:
ax.set_facecolor("#12121a")
ax.text(0.5, 0.5, f"No {period} data", ha="center", va="center", color="#888")
ax.set_xticks([])
ax.set_yticks([])
continue
times = [_bar_datetime(b) for b in bars]
closes = [float(b["c"]) for b in bars]
highs = [float(b["h"]) for b in bars]
lows = [float(b["l"]) for b in bars]
ax.set_facecolor("#12121a")
ax.plot(times, closes, color="#4cc2ff", linewidth=1.2)
ax.fill_between(
times, lows, highs,
color="#4cc2ff", alpha=0.12,
)
levels = [
(entry_price, "#eac147", "Entry"),
(stop_loss, "#ff6666", "SL"),
(take_profit, "#4cd97f", "TP"),
(close_price, "#c4c4ff", "Close"),
]
for price, color, label in levels:
if price is not None:
ax.axhline(price, color=color, linewidth=0.9, linestyle="--", alpha=0.85)
ax.text(times[-1], price, label, color=color, fontsize=8, va="bottom")
if open_dt:
ax.axvline(open_dt, color="#888", linewidth=0.8, linestyle=":", alpha=0.7)
if close_dt:
ax.axvline(close_dt, color="#aaa", linewidth=0.8, linestyle=":", alpha=0.7)
chart_sym = ths_to_sina_chart_symbol(symbol) or symbol
ax.set_title(f"{chart_sym} {period}", color="#eaeaea", fontsize=11, pad=8)
ax.tick_params(colors="#888", labelsize=8)
for spine in ax.spines.values():
spine.set_color("#2e2e45")
ax.xaxis.set_major_formatter(mdates.DateFormatter("%m-%d %H:%M"))
ax.grid(True, color="#1e1e30", linewidth=0.5)
plotted = True
if not plotted:
plt.close(fig)
return None
fig.tight_layout()
ts = datetime.now(TZ).strftime("%Y%m%d%H%M%S")
chart_sym = ths_to_sina_chart_symbol(symbol) or "chart"
filename = f"{ts}_kline_{chart_sym}.png"
path = os.path.join(upload_dir, filename)
fig.savefig(path, dpi=120, facecolor=fig.get_facecolor())
plt.close(fig)
return filename
+3
View File
@@ -0,0 +1,3 @@
# Copyright (c) 2025-2026 马建军. All rights reserved.
"""Qihuo feature modules package."""
+5
View File
@@ -0,0 +1,5 @@
# Copyright (c) 2025-2026 马建军. All rights reserved.
from modules.backup.routes import register
__all__ = ["register"]
+767
View File
@@ -0,0 +1,767 @@
# Copyright (c) 2025-2026 马建军. All rights reserved.
# 专有软件 — 未经授权禁止复制、传播、转售。
# 严禁用于:带单/代客理财、向他人推荐期货品种或买卖建议、融资配资等业务。
# 详见 LICENSE.zh-CN.txt 与 docs/软件购买与使用协议.md
"""数据库备份:SQLite futures.db,含 uploads 与一键恢复脚本。"""
from __future__ import annotations
import json
import logging
import os
import re
import shutil
import sqlite3
import subprocess
import sys
import tarfile
import tempfile
import threading
import time
from datetime import datetime
from pathlib import Path
from typing import Any, Callable, IO, Optional
from zoneinfo import ZoneInfo
from modules.core.db_conn import DB_PATH
logger = logging.getLogger(__name__)
TZ = ZoneInfo("Asia/Shanghai")
BACKUP_FILENAME_RE = re.compile(r"^qihuo_backup_\d{8}_\d{6}\.tar\.gz$")
BACKUP_LAST_KEY = "backup_last_at"
BACKUP_KEEP_KEY = "backup_keep_count"
BACKUP_AUTO_KEY = "backup_auto_enabled"
BACKUP_HOUR_KEY = "backup_auto_hour"
DEFAULT_KEEP_COUNT = 30
DEFAULT_AUTO_HOUR = 3
CHECK_INTERVAL_SEC = 3600
_backup_lock = threading.Lock()
RESTORE_STATUS_FILE = "restore_status.json"
RESTORE_CONFIRM_TOKEN = "RESTORE"
RESTORE_MD = """# qihuo 备份恢复说明
本压缩包由 qihuo 系统自动生成可在另一台 Linux 服务器上恢复交易数据
## 包内文件
| 文件/目录 | 说明 |
|-----------|------|
| `futures.db` | SQLite 主库 |
| `uploads/` | 复盘截图与 K 线图若备份时存在 |
| `.env` | 环境配置`config/.env` 或根目录 `.env` |
| `manifest.json` | 备份元数据 |
| `restore.sh` | 一键恢复脚本 |
## 快速恢复(推荐)
1. 将本压缩包上传到目标服务器例如 `/root/`
2. 解压并执行恢复脚本
```bash
cd /root
tar -xzf qihuo_backup_YYYYMMDD_HHMMSS.tar.gz
cd qihuo_backup_YYYYMMDD_HHMMSS
chmod +x restore.sh
./restore.sh
```
默认恢复到 **`/root/qihuo`**指定应用目录
```bash
RESTORE_DIR=/opt/qihuo ./restore.sh
```
3. 在新服务器部署 qihuo 代码与 Python 环境 `docs/DEPLOY.md`
4. 若包内含 `.env``restore.sh` 会自动恢复到应用目录无需手工复制
5. 重启服务`pm2 restart qihuo`
## 手工恢复
```bash
mkdir -p /opt/qihuo/uploads
cp futures.db /opt/qihuo/futures.db
cp -a uploads/. /opt/qihuo/uploads/
```
## 注意
- 恢复前请停止 qihuo 进程
- `.env` 含敏感信息请妥善保管备份包
- 详见 `docs/BACKUP.md`
"""
def _app_root() -> Path:
from modules.core.paths import ROOT
return ROOT
def default_backup_dir() -> str:
env = (os.getenv("QIHUO_BACKUP_DIR") or "").strip()
if env:
return env
if os.name == "nt":
return str(_app_root() / "qihuo_backup")
return "/root/qihuo_backup"
def default_restore_dir() -> str:
env = (os.getenv("QIHUO_RESTORE_DIR") or "").strip()
if env:
return env
if os.name == "nt":
return str(_app_root())
return "/root/qihuo"
def restore_target_dir() -> Path:
"""Web/API 恢复目标目录,默认当前应用根目录。"""
env = (os.getenv("QIHUO_RESTORE_DIR") or "").strip()
if env:
return Path(env)
return _app_root()
def backup_dir() -> Path:
path = Path(default_backup_dir())
path.mkdir(parents=True, exist_ok=True)
return path
def backup_in_progress() -> bool:
return _backup_lock.locked()
def _restore_status_path() -> Path:
from modules.core.paths import DATA_DIR
DATA_DIR.mkdir(parents=True, exist_ok=True)
return DATA_DIR / RESTORE_STATUS_FILE
def _write_restore_status(state: str, message: str = "", **extra: Any) -> None:
payload = {
"state": state,
"message": message,
"updated_at": datetime.now(TZ).isoformat(timespec="seconds"),
}
payload.update(extra)
_restore_status_path().write_text(
json.dumps(payload, ensure_ascii=False, indent=2),
encoding="utf-8",
)
def get_restore_status() -> dict:
path = _restore_status_path()
if not path.is_file():
return {"state": "idle", "message": "", "updated_at": ""}
try:
data = json.loads(path.read_text(encoding="utf-8"))
if isinstance(data, dict):
data.setdefault("state", "idle")
data.setdefault("message", "")
return data
except Exception:
pass
return {"state": "idle", "message": "", "updated_at": ""}
def restore_in_progress() -> bool:
return get_restore_status().get("state") in ("pending", "running")
def _manifest_root_prefix(tar: tarfile.TarFile) -> str:
for member in tar.getmembers():
name = member.name.rstrip("/")
if name.endswith("/manifest.json") or name == "manifest.json":
if name == "manifest.json":
return ""
return name[: -len("/manifest.json")]
raise ValueError("备份包缺少 manifest.json")
def _read_manifest_from_tar(tar: tarfile.TarFile) -> dict:
root = _manifest_root_prefix(tar)
manifest_name = f"{root}/manifest.json" if root else "manifest.json"
try:
member = tar.getmember(manifest_name)
except KeyError as exc:
raise ValueError("备份包缺少 manifest.json") from exc
extracted = tar.extractfile(member)
if not extracted:
raise ValueError("无法读取 manifest.json")
data = json.loads(extracted.read().decode("utf-8"))
if not isinstance(data, dict):
raise ValueError("manifest.json 格式无效")
return data
def _validate_manifest(manifest: dict) -> None:
if manifest.get("app") != "qihuo":
raise ValueError("不是有效的 qihuo 备份包")
backend = (manifest.get("backend") or "sqlite").strip()
if backend == "postgres":
raise ValueError("不再支持 PostgreSQL 备份,请使用 SQLite 备份包")
if backend != "sqlite":
raise ValueError("manifest 缺少或无效的数据库类型")
def _member_exists(tar: tarfile.TarFile, root: str, name: str) -> bool:
candidates = [name]
if root:
candidates.append(f"{root}/{name}")
return any(_tar_has_member(tar, path) for path in candidates)
def _tar_has_member(tar: tarfile.TarFile, path: str) -> bool:
try:
tar.getmember(path)
return True
except KeyError:
return False
def _validate_archive_contents(tar: tarfile.TarFile, root: str) -> None:
if not _member_exists(tar, root, "futures.db"):
raise ValueError("备份缺少 futures.db")
def _manifest_preview(manifest: dict, path: Path) -> dict:
stat = path.stat()
created_at = (manifest.get("created_at") or "").strip()
return {
"name": path.name,
"created_at": created_at,
"includes_uploads": bool(manifest.get("includes_uploads")),
"includes_env": bool(manifest.get("includes_env")),
"env_restore_path": (manifest.get("env_restore_path") or "").strip(),
"size": stat.st_size,
"size_mb": round(stat.st_size / (1024 * 1024), 2),
"mtime": datetime.fromtimestamp(stat.st_mtime, TZ).isoformat(timespec="seconds"),
}
def peek_manifest(path: Path) -> dict:
with tarfile.open(path, "r:gz") as tar:
return _read_manifest_from_tar(tar)
def inspect_backup_archive(path: Path) -> dict:
with tarfile.open(path, "r:gz") as tar:
manifest = _read_manifest_from_tar(tar)
_validate_manifest(manifest)
root = _manifest_root_prefix(tar)
_validate_archive_contents(tar, root)
return _manifest_preview(manifest, path)
def _allocate_backup_filename(manifest: dict, preferred: str = "") -> str:
preferred = (preferred or "").strip()
if preferred and BACKUP_FILENAME_RE.match(preferred):
candidate = backup_dir() / preferred
if not candidate.exists():
return preferred
created = (manifest.get("created_at") or "").strip()
stamp = ""
if created:
try:
stamp = datetime.fromisoformat(created).strftime("%Y%m%d_%H%M%S")
except ValueError:
stamp = ""
if not stamp:
stamp = datetime.now(TZ).strftime("%Y%m%d_%H%M%S")
name = f"qihuo_backup_{stamp}.tar.gz"
if not (backup_dir() / name).exists():
return name
stamp = datetime.now(TZ).strftime("%Y%m%d_%H%M%S")
return f"qihuo_backup_{stamp}.tar.gz"
def save_uploaded_backup(stream: IO[bytes], original_filename: str = "") -> tuple[str, dict]:
with tempfile.NamedTemporaryFile(delete=False, suffix=".tar.gz") as tmp:
shutil.copyfileobj(stream, tmp)
tmp_path = Path(tmp.name)
try:
info = inspect_backup_archive(tmp_path)
manifest = peek_manifest(tmp_path)
filename = _allocate_backup_filename(manifest, original_filename)
dest = backup_dir() / filename
shutil.move(str(tmp_path), str(dest))
info["name"] = filename
return filename, info
except Exception:
tmp_path.unlink(missing_ok=True)
raise
def _pm2_available() -> bool:
return shutil.which("pm2") is not None
def _pm2_stop() -> None:
if not _pm2_available():
logger.warning("pm2 not found, skip stop")
return
proc = subprocess.run(
["pm2", "stop", "qihuo"],
capture_output=True,
text=True,
check=False,
)
if proc.returncode != 0:
logger.warning("pm2 stop qihuo: %s", proc.stderr.strip() or proc.stdout.strip())
def _pm2_restart() -> None:
if not _pm2_available():
logger.warning("pm2 not found, skip restart")
return
proc = subprocess.run(
["pm2", "restart", "qihuo"],
capture_output=True,
text=True,
check=False,
)
if proc.returncode != 0:
proc = subprocess.run(
["pm2", "start", "qihuo"],
capture_output=True,
text=True,
check=False,
)
if proc.returncode != 0:
raise RuntimeError(proc.stderr.strip() or proc.stdout.strip() or "pm2 restart 失败")
def _extract_member_to_path(tar: tarfile.TarFile, member_name: str, dest: Path) -> None:
try:
member = tar.getmember(member_name)
except KeyError:
return
extracted = tar.extractfile(member)
if not extracted:
return
dest.parent.mkdir(parents=True, exist_ok=True)
with open(dest, "wb") as out:
shutil.copyfileobj(extracted, out)
def _restore_uploads_dir(tar: tarfile.TarFile, root: str, restore_dir: Path) -> None:
prefix = f"{root}/uploads" if root else "uploads"
uploads_dest = restore_dir / "uploads"
uploads_dest.mkdir(parents=True, exist_ok=True)
found = False
for member in tar.getmembers():
if member.name == prefix or member.name.startswith(prefix + "/"):
found = True
rel = member.name[len(prefix) :].lstrip("/")
if not rel:
continue
target = uploads_dest / rel
if member.isdir():
target.mkdir(parents=True, exist_ok=True)
else:
target.parent.mkdir(parents=True, exist_ok=True)
extracted = tar.extractfile(member)
if extracted:
with open(target, "wb") as out:
shutil.copyfileobj(extracted, out)
if not found:
logger.info("backup has no uploads/")
def _reload_env_file(env_path: Path) -> None:
if not env_path.is_file():
return
try:
from dotenv import load_dotenv
load_dotenv(str(env_path), override=True)
except Exception as exc:
logger.warning("reload .env failed: %s", exc)
def _perform_restore(archive_path: Path, restore_dir: Path) -> dict:
restore_dir.mkdir(parents=True, exist_ok=True)
with tarfile.open(archive_path, "r:gz") as tar:
manifest = _read_manifest_from_tar(tar)
_validate_manifest(manifest)
root = _manifest_root_prefix(tar)
_validate_archive_contents(tar, root)
env_member = f"{root}/.env" if root else ".env"
env_restore_path = (manifest.get("env_restore_path") or "config/.env").strip()
if manifest.get("includes_env") and _tar_has_member(tar, env_member):
env_dest = restore_dir / env_restore_path
_extract_member_to_path(tar, env_member, env_dest)
_reload_env_file(env_dest)
db_member = f"{root}/futures.db" if root else "futures.db"
db_dest = Path(DB_PATH)
if not db_dest.is_absolute():
db_dest = restore_dir / db_dest.name
_extract_member_to_path(tar, db_member, db_dest)
_restore_uploads_dir(tar, root, restore_dir)
return {
"restore_dir": str(restore_dir),
"includes_env": bool(manifest.get("includes_env")),
"includes_uploads": bool(manifest.get("includes_uploads")),
}
def run_restore_job(archive_path: Path) -> None:
filename = archive_path.name
restore_dir = restore_target_dir()
try:
_write_restore_status(
"running",
"正在停止服务…",
filename=filename,
step="stop",
restore_dir=str(restore_dir),
)
_pm2_stop()
_write_restore_status(
"running",
"正在恢复数据…",
filename=filename,
step="restore",
restore_dir=str(restore_dir),
)
summary = _perform_restore(archive_path.resolve(), restore_dir)
_write_restore_status(
"running",
"正在重启服务…",
filename=filename,
step="restart",
restore_dir=str(restore_dir),
)
_pm2_restart()
_write_restore_status(
"done",
"恢复完成,服务已重启",
filename=filename,
restore_dir=str(restore_dir),
summary=summary,
)
except Exception as exc:
logger.exception("restore failed: %s", exc)
_write_restore_status(
"error",
str(exc),
filename=filename,
restore_dir=str(restore_dir),
)
try:
_pm2_restart()
except Exception as restart_exc:
logger.warning("restart after restore error: %s", restart_exc)
def schedule_restore(filename: str) -> tuple[bool, str]:
if _backup_lock.locked():
return False, "备份进行中,请稍后再试"
if restore_in_progress():
return False, "恢复进行中,请稍后再试"
try:
path = resolve_backup_file(filename)
inspect_backup_archive(path)
except (ValueError, FileNotFoundError) as exc:
return False, str(exc)
_write_restore_status(
"pending",
"恢复任务已提交…",
filename=filename,
restore_dir=str(restore_target_dir()),
)
script = Path(__file__).resolve().parent / "restore_job.py"
subprocess.Popen(
[sys.executable, str(script), str(path.resolve())],
start_new_session=True,
cwd=str(_app_root()),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
return True, "恢复已开始,服务将短暂中断后自动重启"
def get_backup_last_at(get_setting: Callable[[str, str], str]) -> str:
return (get_setting(BACKUP_LAST_KEY, "") or "").strip()
def _backup_sqlite(src_path: str, dst_path: str) -> None:
src = sqlite3.connect(src_path, timeout=30)
try:
try:
src.execute("PRAGMA wal_checkpoint(TRUNCATE)")
except sqlite3.OperationalError:
pass
dst = sqlite3.connect(dst_path)
try:
src.backup(dst)
dst.commit()
finally:
dst.close()
finally:
src.close()
def _env_backup_info() -> tuple[Optional[Path], str]:
"""返回 (源 .env 路径, 恢复到应用目录的相对路径)。"""
from modules.core.paths import CONFIG_DIR, ENV_FILE, LEGACY_ENV_FILE
if ENV_FILE.is_file():
return ENV_FILE, "config/.env"
if LEGACY_ENV_FILE.is_file():
return LEGACY_ENV_FILE, ".env"
return None, "config/.env"
def _copy_env_into_backup(work: Path) -> tuple[bool, str]:
env_src, env_restore_path = _env_backup_info()
if env_src and env_src.is_file():
shutil.copy2(env_src, work / ".env")
return True, env_restore_path
return False, env_restore_path
def _write_restore_script(dest: Path, *, env_restore_path: str = "") -> None:
env_block = ""
if env_restore_path:
env_block = f"""
if [ -f "$SCRIPT_DIR/.env" ]; then
ENV_DEST="$RESTORE_DIR/{env_restore_path}"
mkdir -p "$(dirname "$ENV_DEST")"
cp -f "$SCRIPT_DIR/.env" "$ENV_DEST"
echo "已复制 .env -> $ENV_DEST"
fi
"""
script = f"""#!/bin/bash
set -euo pipefail
RESTORE_DIR="${{RESTORE_DIR:-{default_restore_dir()}}}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
mkdir -p "$RESTORE_DIR/uploads"
{env_block}
if [ -f "$SCRIPT_DIR/futures.db" ]; then
cp -f "$SCRIPT_DIR/futures.db" "$RESTORE_DIR/futures.db"
echo "已复制 futures.db -> $RESTORE_DIR/futures.db"
fi
if [ -d "$SCRIPT_DIR/uploads" ]; then
cp -a "$SCRIPT_DIR/uploads/." "$RESTORE_DIR/uploads/"
echo "已复制 uploads -> $RESTORE_DIR/uploads/"
fi
echo ""
echo "恢复完成。目标目录: $RESTORE_DIR"
echo "下一步: pm2 restart qihuo"
echo "详见 RESTORE.md 与 docs/BACKUP.md"
"""
dest.write_text(script, encoding="utf-8")
def create_backup(*, include_uploads: bool = True, include_env: bool = True) -> tuple[str, str]:
"""创建 tar.gz 备份,返回 (文件名, 说明)。"""
if not os.path.isfile(DB_PATH):
raise FileNotFoundError(f"数据库不存在: {DB_PATH}")
with _backup_lock:
stamp = datetime.now(TZ).strftime("%Y%m%d_%H%M%S")
folder_name = f"qihuo_backup_{stamp}"
filename = f"{folder_name}.tar.gz"
out_path = backup_dir() / filename
app_root = _app_root()
upload_src = app_root / "uploads"
with tempfile.TemporaryDirectory(prefix="qihuo_bak_") as tmp:
work = Path(tmp) / folder_name
work.mkdir()
_backup_sqlite(DB_PATH, str(work / "futures.db"))
if include_uploads and upload_src.is_dir():
shutil.copytree(upload_src, work / "uploads", dirs_exist_ok=True)
includes_env = False
env_restore_path = "config/.env"
if include_env:
includes_env, env_restore_path = _copy_env_into_backup(work)
if not includes_env:
logger.warning("backup: .env not found (checked config/.env and root .env)")
manifest = {
"app": "qihuo",
"backend": "sqlite",
"created_at": datetime.now(TZ).isoformat(timespec="seconds"),
"db_path": DB_PATH,
"includes_uploads": include_uploads and upload_src.is_dir(),
"includes_env": includes_env,
"env_restore_path": env_restore_path if includes_env else "",
"default_restore_dir": default_restore_dir(),
"files": sorted(p.name for p in work.iterdir()),
}
(work / "manifest.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2),
encoding="utf-8",
)
(work / "RESTORE.md").write_text(RESTORE_MD, encoding="utf-8")
_write_restore_script(
work / "restore.sh",
env_restore_path=env_restore_path if includes_env else "",
)
with tarfile.open(out_path, "w:gz") as tar:
tar.add(work, arcname=folder_name)
size_mb = out_path.stat().st_size / (1024 * 1024)
env_note = "含 .env" if includes_env else "未含 .env"
return filename, f"备份已生成 {filename}{size_mb:.2f} MB{env_note}"
def list_backups(*, with_manifest: bool = True) -> list[dict]:
items: list[dict] = []
for path in sorted(backup_dir().glob("qihuo_backup_*.tar.gz"), reverse=True):
if not BACKUP_FILENAME_RE.match(path.name):
continue
stat = path.stat()
item = {
"name": path.name,
"size": stat.st_size,
"size_mb": round(stat.st_size / (1024 * 1024), 2),
"mtime": datetime.fromtimestamp(stat.st_mtime, TZ).isoformat(timespec="seconds"),
"created_at": "",
"includes_env": False,
"includes_uploads": False,
}
if with_manifest:
try:
manifest = peek_manifest(path)
item["created_at"] = (manifest.get("created_at") or "").strip()
item["includes_env"] = bool(manifest.get("includes_env"))
item["includes_uploads"] = bool(manifest.get("includes_uploads"))
except Exception as exc:
logger.debug("read manifest %s: %s", path.name, exc)
items.append(item)
return items
def resolve_backup_file(filename: str) -> Path:
name = (filename or "").strip()
if not BACKUP_FILENAME_RE.match(name):
raise ValueError("无效的备份文件名")
path = (backup_dir() / name).resolve()
root = backup_dir().resolve()
if not str(path).startswith(str(root) + os.sep) and path != root:
raise ValueError("无效的备份路径")
if not path.is_file():
raise FileNotFoundError("备份文件不存在")
return path
def prune_old_backups(keep: int) -> int:
keep_n = max(1, int(keep or DEFAULT_KEEP_COUNT))
files = list_backups()
removed = 0
for item in files[keep_n:]:
try:
resolve_backup_file(item["name"]).unlink()
removed += 1
except Exception as exc:
logger.warning("prune backup %s: %s", item["name"], exc)
return removed
def run_backup_job(
*,
get_setting: Callable[[str, str], str],
set_setting: Callable[[str, str], None],
include_uploads: bool = True,
include_env: bool = True,
) -> tuple[str, str]:
keep = DEFAULT_KEEP_COUNT
try:
keep = max(5, min(200, int(get_setting(BACKUP_KEEP_KEY, str(DEFAULT_KEEP_COUNT)) or DEFAULT_KEEP_COUNT)))
except ValueError:
pass
filename, msg = create_backup(include_uploads=include_uploads, include_env=include_env)
set_setting(BACKUP_LAST_KEY, datetime.now(TZ).isoformat(timespec="seconds"))
removed = prune_old_backups(keep)
if removed:
msg = f"{msg},已清理 {removed} 个旧备份"
return filename, msg
def schedule_backup(
*,
get_setting: Callable[[str, str], str],
set_setting: Callable[[str, str], None],
include_uploads: bool = True,
include_env: bool = True,
) -> tuple[bool, str]:
if _backup_lock.locked():
return False, "备份进行中,请稍后再试"
if restore_in_progress():
return False, "恢复进行中,请稍后再试"
def _run() -> None:
try:
run_backup_job(
get_setting=get_setting,
set_setting=set_setting,
include_uploads=include_uploads,
include_env=include_env,
)
except Exception as exc:
logger.exception("backup failed: %s", exc)
threading.Thread(target=_run, daemon=True, name="qihuo-backup-run").start()
return True, "已在后台开始备份,请稍后刷新本页查看"
def _should_auto_backup(get_setting: Callable[[str, str], str]) -> bool:
if (get_setting(BACKUP_AUTO_KEY, "1") or "1").strip() not in ("1", "true", "yes"):
return False
try:
hour = int(get_setting(BACKUP_HOUR_KEY, str(DEFAULT_AUTO_HOUR)) or DEFAULT_AUTO_HOUR)
except ValueError:
hour = DEFAULT_AUTO_HOUR
hour = max(0, min(23, hour))
now = datetime.now(TZ)
if now.hour != hour:
return False
last = get_backup_last_at(get_setting)
if last and last[:10] == now.date().isoformat():
return False
return True
def start_backup_worker(
*,
get_setting_fn: Callable[[str, str], str],
set_setting_fn: Callable[[str, str], None],
interval: int = CHECK_INTERVAL_SEC,
) -> None:
"""后台线程:按设定小时每日自动备份。"""
def _loop() -> None:
time.sleep(30)
while True:
try:
if _should_auto_backup(get_setting_fn):
filename, msg = run_backup_job(
get_setting=get_setting_fn,
set_setting=set_setting_fn,
include_uploads=True,
include_env=True,
)
logger.info("auto backup: %s%s", filename, msg)
except Exception as exc:
logger.warning("backup worker: %s", exc)
time.sleep(max(300, interval))
threading.Thread(target=_loop, daemon=True, name="qihuo-backup-worker").start()
+26
View File
@@ -0,0 +1,26 @@
# Copyright (c) 2025-2026 马建军. All rights reserved.
"""Detached restore worker — survives pm2 stop of the parent web process."""
from __future__ import annotations
import sys
from pathlib import Path
def main(argv: list[str] | None = None) -> int:
args = argv if argv is not None else sys.argv[1:]
if len(args) != 1:
print("usage: python -m modules.backup.restore_job <backup.tar.gz>", file=sys.stderr)
return 2
archive = Path(args[0]).resolve()
if not archive.is_file():
print(f"backup not found: {archive}", file=sys.stderr)
return 1
from modules.backup.db_backup import run_restore_job
run_restore_job(archive)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+102
View File
@@ -0,0 +1,102 @@
# Copyright (c) 2025-2026 马建军. All rights reserved.
"""HTTP routes for backup module."""
from __future__ import annotations
import logging
from flask import jsonify, request, send_file
logger = logging.getLogger(__name__)
def register(deps) -> None:
app = deps.app
login_required = deps.login_required
get_setting = deps.get_setting
from modules.backup.db_backup import (
RESTORE_CONFIRM_TOKEN,
backup_dir,
backup_in_progress,
get_backup_last_at,
get_restore_status,
inspect_backup_archive,
list_backups,
resolve_backup_file,
restore_in_progress,
save_uploaded_backup,
schedule_restore,
)
@app.route("/api/backup/list")
@login_required
def api_backup_list():
return jsonify(
{
"dir": str(backup_dir()),
"last_at": get_backup_last_at(get_setting),
"running": backup_in_progress(),
"restore": get_restore_status(),
"items": list_backups(),
}
)
@app.route("/api/backup/download/<filename>")
@login_required
def api_backup_download(filename):
try:
path = resolve_backup_file(filename)
except (ValueError, FileNotFoundError) as exc:
return jsonify({"error": str(exc)}), 404
return send_file(path, as_attachment=True, download_name=path.name)
@app.route("/api/backup/upload", methods=["POST"])
@login_required
def api_backup_upload():
if backup_in_progress():
return jsonify({"error": "备份进行中,请稍后再试"}), 409
if restore_in_progress():
return jsonify({"error": "恢复进行中,请稍后再试"}), 409
upload = request.files.get("file")
if not upload or not upload.filename:
return jsonify({"error": "请选择备份文件"}), 400
if not upload.filename.lower().endswith(".tar.gz"):
return jsonify({"error": "仅支持 .tar.gz 备份包"}), 400
try:
name, info = save_uploaded_backup(upload.stream, upload.filename)
return jsonify({"ok": True, "name": name, "info": info})
except ValueError as exc:
return jsonify({"error": str(exc)}), 400
except Exception:
logger.exception("backup upload failed")
return jsonify({"error": "上传失败,请检查备份包是否完整"}), 500
@app.route("/api/backup/info/<filename>")
@login_required
def api_backup_info(filename):
try:
path = resolve_backup_file(filename)
return jsonify(inspect_backup_archive(path))
except (ValueError, FileNotFoundError) as exc:
return jsonify({"error": str(exc)}), 404
@app.route("/api/backup/restore", methods=["POST"])
@login_required
def api_backup_restore():
data = request.get_json(silent=True) or {}
filename = (data.get("filename") or request.form.get("filename") or "").strip()
confirm = (data.get("confirm") or request.form.get("confirm") or "").strip()
if confirm != RESTORE_CONFIRM_TOKEN:
return jsonify({"error": "请确认恢复操作"}), 400
if not filename:
return jsonify({"error": "缺少备份文件名"}), 400
ok, msg = schedule_restore(filename)
if ok:
return jsonify({"ok": True, "message": msg}), 202
return jsonify({"error": msg}), 409
@app.route("/api/backup/restore/status")
@login_required
def api_backup_restore_status():
return jsonify(get_restore_status())
+8
View File
@@ -0,0 +1,8 @@
# Copyright (c) 2025-2026 马建军. All rights reserved.
"""Core bootstrap and shared types."""
from modules.core.bootstrap import register_all_modules, start_module_workers
from modules.core.deps import AppDeps
__all__ = ["AppDeps", "register_all_modules", "start_module_workers"]
+55
View File
@@ -0,0 +1,55 @@
# Copyright (c) 2025-2026 马建军. All rights reserved.
"""Application module registry and startup wiring."""
from __future__ import annotations
import importlib
import logging
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from modules.core.deps import AppDeps
logger = logging.getLogger(__name__)
# Registration order: core services first, trading last among features.
_MODULE_NAMES = (
"modules.web",
"modules.market",
"modules.keys",
"modules.plans",
"modules.notify",
"modules.records",
"modules.stats",
"modules.fees",
"modules.backup",
"modules.settings",
"modules.risk",
"modules.strategy",
"modules.ctp",
"modules.trading",
)
def register_all_modules(deps: "AppDeps") -> None:
for name in _MODULE_NAMES:
mod = importlib.import_module(name)
register = getattr(mod, "register", None)
if not callable(register):
logger.warning("module %s has no register()", name)
continue
register(deps)
logger.debug("registered %s", name)
def start_module_workers(deps: "AppDeps") -> None:
"""Background threads owned by feature modules."""
from modules.ctp.vnpy_bridge import try_init_vnpy
try_init_vnpy({})
for name in ("modules.market",):
mod = importlib.import_module(name)
start = getattr(mod, "start_workers", None)
if callable(start):
start(deps)
+280
View File
@@ -0,0 +1,280 @@
# Copyright (c) 2025-2026 马建军. All rights reserved.
# 专有软件 — 未经授权禁止复制、传播、转售。
# 严禁用于:带单/代客理财、向他人推荐期货品种或买卖建议、融资配资等业务。
# 详见 LICENSE.zh-CN.txt 与 docs/软件购买与使用协议.md
"""期货合约简介:东方财富 / 新浪 / AKShare。"""
import logging
import re
from typing import Any, Optional
import requests
from modules.core.contract_specs import get_contract_spec
from modules.core.symbols import ths_to_codes, search_symbols
logger = logging.getLogger(__name__)
EM_LABEL_MAP = {
"vname": "交易品种",
"vcode": "交易代码",
"jydw": "交易单位",
"bjdw": "报价单位",
"market": "交易所",
"zxbddw": "最小变动价位",
"zdtbfd": "涨跌停幅度",
"hyjgyf": "合约月份",
"jysj": "交易时间",
"zhjyr": "最后交易日",
"zhjgr": "交割日期",
"jgpj": "交割品级",
"zcjybzj": "最低交易保证金",
"jgfs": "交割方式",
"jgdd": "交割地点",
"ssrq": "上市日期",
}
DISPLAY_ORDER = [
"交易品种",
"交易代码",
"交易单位",
"报价单位",
"最小变动价位",
"最低交易保证金",
"涨跌停幅度",
"合约月份",
"交易时间",
"最后交易日",
"交割日期",
"交割方式",
"交割地点",
"交割品级",
"上市日期",
"交易所",
]
SKIP_ITEMS = {"", "-", "None", "nan", "null"}
def _normalize_ths_code(raw: str) -> Optional[str]:
code = (raw or "").strip()
if not code:
return None
# 已是完整合约
if re.match(r"^[A-Za-z]+\d{3,4}$", code):
return code
# 仅品种字母时尝试匹配主力
results = search_symbols(code)
if results:
return results[0].get("ths_code") or code
codes = ths_to_codes(code)
if codes:
return codes["ths_code"]
return code
def _to_sina_quote_symbol(ths_code: str) -> str:
m = re.match(r"^([A-Za-z]+)(\d+)$", ths_code.strip())
if not m:
return ths_code.upper()
return m.group(1).upper() + m.group(2)
def _to_em_page_symbol(ths_code: str) -> str:
return ths_code.strip().lower() + "F"
def _clean_value(val: Any) -> str:
if val is None:
return ""
s = str(val).strip()
if s in SKIP_ITEMS:
return ""
return s
def _rows_from_dict(data: dict[str, str]) -> list[dict]:
rows: list[dict] = []
seen: set[str] = set()
for label in DISPLAY_ORDER:
val = _clean_value(data.get(label))
if not val:
continue
hint = _clean_value(data.get(f"{label}_hint"))
rows.append({"label": label, "value": val, "hint": hint})
seen.add(label)
for label, val in data.items():
if label.endswith("_hint") or label in seen:
continue
val = _clean_value(val)
if val:
rows.append({"label": label, "value": val, "hint": ""})
return rows
def _add_computed_hints(ths_code: str, data: dict[str, str]) -> None:
spec = get_contract_spec(ths_code)
mult = spec.get("mult") or 0
tick_raw = data.get("最小变动价位", "")
m = re.search(r"([\d.]+)", tick_raw)
if m and mult:
tick = float(m.group(1))
data["最小变动价位_hint"] = f"一手合约最小波动{round(tick * mult, 2)}"
def _fetch_em_direct(em_symbol: str) -> dict[str, str]:
page_url = f"https://quote.eastmoney.com/qihuo/{em_symbol}.html"
r = requests.get(page_url, timeout=12)
r.encoding = r.apparent_encoding or "utf-8"
inner = None
for pat in [
r"futures_([A-Za-z0-9_]+)",
r"#(futures_[A-Za-z0-9_]+)",
r"/(futures_[A-Za-z0-9_]+)",
]:
m = re.search(pat, r.text)
if m:
inner = m.group(1).replace("futures_", "")
break
if not inner:
raise ValueError("无法解析东方财富合约标识")
info_url = f"https://futsse-static.eastmoney.com/redis?msgid={inner}_info"
r2 = requests.get(info_url, timeout=12)
payload = r2.json()
if not isinstance(payload, dict):
raise ValueError("东方财富返回数据无效")
out: dict[str, str] = {}
for key, label in EM_LABEL_MAP.items():
val = _clean_value(payload.get(key))
if val:
out[label] = val
if not out:
raise ValueError("东方财富合约字段为空")
return out
def _fetch_em_akshare(em_symbol: str) -> dict[str, str]:
import akshare as ak
df = ak.futures_contract_detail_em(symbol=em_symbol)
out: dict[str, str] = {}
for _, row in df.iterrows():
label = _clean_value(row.get("item"))
val = _clean_value(row.get("value"))
if label and val:
if label == "跌涨停板幅度":
label = "涨跌停幅度"
if label == "最后交割日":
label = "交割日期"
if label == "上市交易所":
label = "交易所"
if label == "合约交割月份":
label = "合约月份"
if label == "最初交易保证金":
label = "最低交易保证金"
if label == "最小变动价格":
label = "最小变动价位"
out[label] = val
return out
def _fetch_sina_direct(sina_symbol: str) -> dict[str, str]:
from io import StringIO
import pandas as pd
url = f"https://finance.sina.com.cn/futures/quotes/{sina_symbol}.shtml"
r = requests.get(url, timeout=12, headers={"Referer": "https://finance.sina.com.cn/"})
r.encoding = "gb2312"
tables = pd.read_html(StringIO(r.text))
if len(tables) < 7:
raise ValueError("新浪页面结构变化")
temp_df = tables[6]
parts = []
for ncol in [slice(0, 2), slice(2, 4), slice(4, None)]:
part = temp_df.iloc[:, ncol]
part.columns = ["item", "value"]
parts.append(part)
merged = pd.concat(parts, axis=0, ignore_index=True)
out: dict[str, str] = {}
for _, row in merged.iterrows():
label = _clean_value(row["item"])
val = _clean_value(row["value"])
if not label or not val or len(label) > 80 or "发帖" in val:
continue
out[label] = val
return out
def _fetch_sina_akshare(sina_symbol: str) -> dict[str, str]:
import akshare as ak
df = ak.futures_contract_detail(symbol=sina_symbol)
out: dict[str, str] = {}
for _, row in df.iterrows():
label = _clean_value(row.get("item"))
val = _clean_value(row.get("value"))
if label and val and "发帖" not in val:
out[label] = val
return out
def _merge_profile(primary: dict[str, str], secondary: dict[str, str]) -> dict[str, str]:
merged = dict(secondary)
merged.update(primary)
return merged
def get_contract_profile(raw_symbol: str) -> Optional[dict]:
ths_code = _normalize_ths_code(raw_symbol)
if not ths_code:
return None
em_symbol = _to_em_page_symbol(ths_code)
sina_symbol = _to_sina_quote_symbol(ths_code)
data: dict[str, str] = {}
source_parts: list[str] = []
# 东方财富(字段与看盘软件简介接近)
try:
try:
data = _fetch_em_akshare(em_symbol)
source_parts.append("东方财富")
except ImportError:
data = _fetch_em_direct(em_symbol)
source_parts.append("东方财富")
except Exception as exc:
logger.warning("eastmoney profile failed %s: %s", em_symbol, exc)
# 新浪补充交割地点、上市日期等
sina_data: dict[str, str] = {}
try:
try:
sina_data = _fetch_sina_akshare(sina_symbol)
except ImportError:
sina_data = _fetch_sina_direct(sina_symbol)
if sina_data:
source_parts.append("新浪")
except Exception as exc:
logger.warning("sina profile failed %s: %s", sina_symbol, exc)
if sina_data:
data = _merge_profile(data, sina_data)
if not data:
return None
_add_computed_hints(ths_code, data)
rows = _rows_from_dict(data)
if not rows:
return None
return {
"ths_code": ths_code,
"symbol_name": data.get("交易品种", ""),
"exchange": data.get("交易所", ""),
"rows": rows,
"source": " + ".join(source_parts) if source_parts else "未知",
}

Some files were not shown because too many files have changed in this diff Show More