ARTICLE · 1270568
大道至简提供源码



X_1:=(3*CLOSE+LOW+OPEN+HIGH)/6;
BS:=(20*X_1+19*REF(X_1,1)+18*REF(X_1,2)+17*REF(X_1,3)+16*REF(X_1,4)+15*REF(X_1,5)+14*REF(X_1,6)+13*REF(X_1,7)+12*REF(X_1,8)+11*REF(X_1,9)+10*REF(X_1,10)+9*REF(X_1,11)+8*REF(X_1,12)+7*REF(X_1,13)+6*REF(X_1,14)+5*REF(X_1,15)+4*REF(X_1,16)+3*REF(X_1,17)+2*REF(X_1,18)+REF(X_1,20))/210,LINETHICK3;
DS:=MA(BS,5),LINETHICK3;
DRAWBAND(BS,RGB(200,0,0),DS,RGB(0,200,0));
{主图均线}
MA10:=MA(C,10);
MA100:=MA(C,100);
MA148:=MA(C,20);
MA10线:=MA10,COLORYELLOW,LINETHICK2;
MA148线:=MA148,COLORRED,LINETHICK2;
{============ 幅度过滤,防横盘来回买卖 ============}
MINRATE:=0.012; {最小波动1.2%,可自行调整 0.01~0.02}
金叉:=CROSS(MA10,MA148);
多头:=MA10>MA148;
站上10:=C>MA10 AND C/REF(C,1)-1>MINRATE;
跌破10:=C<MA10 AND REF(C,1)/C-1>MINRATE;
转空:=CROSS(MA148,MA10);
{建仓、买回信号}
建仓:=金叉;
买回:=多头 AND 站上10 AND REF(跌破10,1) AND NOT(DS>BS);
BUYSIGNAL:=建仓 OR 买回;
DRAWICON(BUYSIGNAL,L*0.998,1);
DRAWTEXT(BUYSIGNAL,L*0.995,'建/买回'),COLORYELLOW;
{减仓 清仓}
减仓:=多头 AND 跌破10 AND NOT(H<DS);
DRAWICON(减仓,H*1.0002,2);
DRAWTEXT(减仓,H*1.0005,'减留底'),COLORGREEN;
DRAWICON(转空,H*1.002,2);
DRAWTEXT(转空,H*1.005,'清仓'),COLORRED;
{==================== 支撑线 + B/S突破跌破标记【手机兼容版】 ====================}
BB:=BARSLAST(BUYSIGNAL);
BUYLOWVAL:=REF(L,BB);
{画支撑线}
支撑线:IF(BB>=0,BUYLOWVAL,DRAWNULL),COLORCYAN,LINETHICK1;
{跌破支撑 → S 卖出(收盘价下穿支撑)}
破支撑:=CROSS(BUYLOWVAL,C);
DRAWTEXT(破支撑,H*1.002,'S'),COLORMAGENTA;
{重新向上突破支撑 → B 买回(收盘价上穿支撑)}
突破支撑:=CROSS(C,BUYLOWVAL);
DRAWTEXT(突破支撑,L*0.998,'B'),COLORMAGENTA;
{==================== 原顶底K信号(修复成交量BUG)=====================}
基准K:= L - MA(C,8) > ABS(C-O) AND V>MA(V,120)*1.5;
N:=BARSLAST(基准K);
触发顶:= N>0 AND N<=3 AND L < REF(L,N);
DRAWTEXT(触发顶 AND MA(C,3)>MA(C,13), REF(H,N)*1.001,'顶'),COLORYELLOW;
基准K底:= MA(C,8) - H > ABS(C-O) AND V>MA(V,120)*1.5;
M:=BARSLAST(基准K底);
触发底:= M>0 AND M<=3 AND H > REF(H,M);
DRAWTEXT(触发底 AND MA(C,3)<MA(C,13), REF(L,M)*0.999,'底'),COLORRED;
在通达信新建指标,复制上面代码粘贴到手机里面保存就可以了,指标只是辅助工具,市场变化无常,指标信号不代表100%准确,盈亏自负!
接着改成策略在软件里面自动交易,策略源码如下:
-- coding: utf-8 --
"""BS红绿带自动交易策略。
指标来源(与主图「BS红绿带」完全同一套算法,保证"图上看到的=策略执行的"):
信号 → 动作映射(由用户定义):┌──────────────────────┬────────────┬──────────────────────────────────────┐│ 图上标记 │ 输出列 │ 动作 │├──────────────────────┼────────────┼──────────────────────────────────────┤│ 建/买回 ↑ │ SIG_BUY │ 买入:程序设置的数量 ││ B(重新站上支撑) │ SIG_REGAIN_SUPPORT │ 买入:程序设置的数量 ││ 减留底 ↓ │ SIG_REDUCE │ 卖出:可用 - 最小保留量 ││ │ │ (可转债留 10 张 / 股票留 100 股) ││ 清仓 ↓ │ SIG_CLEAR │ 卖出:全部可用数量 ││ S(跌破支撑) │ SIG_BREAK_SUPPORT │ 卖出:全部可用数量 ││ 顶 │ SIG_TOP │ 卖出:可用的一半(向下取整到最小单位) ││ 底 │ SIG_BOTTOM │ 买入:设置数量的一半(向下取整到最小单位) │└──────────────────────┴────────────┴──────────────────────────────────────┘
执行约束(实盘安全相关,全部可测):
按 K 线去重:同一标的、同一根 K 线、同一个动作只执行一次。 反复轮询时不会重复下单;跨 K 线(新的一根)才允许再次触发。 单根 K 线只做一个动作:同一根 K 线同时命中多个信号时,按风险优先 顺序取最高优先级执行一次,避免"先买后卖"自相矛盾。 清仓 / S > 减留底 > 顶卖一半 > 建·买回 / B > 底买一半 卖出不超过可用数量:一律以券商可用余额为上限,绝不超卖。 最小单位取整:可转债 10 张、股票 100 股。一半数量向下取整到最小单位, 不足一个最小单位则不动作(日志说明原因),不会出现 5 张、50 股这种废单。 失败不重试轰炸:下单返回 False 时记录错误并计入该 K 线已处理集合, 不在一根 K 线内反复重试;下一根 K 线才重新评估。
与主程序的关系: · 买卖统一走 App.buy() / App.sell()(风控、优先级队列、模拟盘分流、通知全复用) · 「底买一半」调用 buy(..., skip_trade_mode_calib=True),避免"金额模式"把 一半数量重新按固定资金算成满仓
使用方式(main_qt5.py):
from core.bs_band_strategy import BSBandStrategy self.bs_band_strategy = BSBandStrategy(self) self.bs_band_strategy.load_config(self.config)
监控守护循环内: self.bs_band_strategy.monitor_and_trade()"""
fromfutureimport annotations
import loggingimport timefrom typing import Dict, List, Optional, Tuple
import numpy as npimport pandas as pd
logger = logging.getLogger('BSBandStrategy')
══════════════════════════════════════════════════════════════
常量
══════════════════════════════════════════════════════════════
: 动作标识
ACT_SELL_ALL = 'sell_all'
清仓 / S:卖出全部可用ACT_SELL_REDUCE = 'sell_reduce' # 减留底:卖到只剩最小保留量ACT_SELL_HALF = 'sell_half' # 顶:卖出可用的一半ACT_BUY_FULL = 'buy_full' # 建/买回、B:买入设置数量ACT_BUY_HALF = 'buy_half' # 底:买入设置数量的一半
: 动作中文名(日志/通知)
ACTION_LABEL = { ACT_SELL_ALL: '清仓', ACT_SELL_REDUCE: '减留底', ACT_SELL_HALF: '顶卖一半', ACT_BUY_FULL: '建/买回', ACT_BUY_HALF: '底买一半',}
: 同一根 K 线内多信号并存的优先级(越小越优先)
ACTION_PRIORITY = { ACT_SELL_ALL: 0, ACT_SELL_REDUCE: 1, ACT_SELL_HALF: 2, ACT_BUY_FULL: 3, ACT_BUY_HALF: 4,}
: K 线列名兼容
_COL_ALIASES = { 'open': ('open', '开盘', 'Open', 'O'), 'high': ('high', '最高', 'High', 'H'), 'low': ('low', '最低', 'Low', 'L'), 'close': ('close', '收盘', 'Close', 'C'), 'volume': ('volume', '成交量', 'Volume', 'vol', 'V'),}
_TIME_COLS = ('day', 'datetime', 'date', 'time', 'timestamp', 'DateTime')
def _is_bond(code: str) -> bool: """可转债代码判定(与 core.sim_trading.is_bond 保持一致)。""" c = str(code or '').strip()[:3] return c in ('110', '111', '112', '113', '118', '123', '127', '128')
def _lot(code: str) -> int: """最小交易单位:可转债 10 张,其余 100 股。""" return 10 if _is_bond(code) else 100
defflag(row, column: str) -> bool:"""安全读取一行里的布尔信号列。"""try:val = row[column]except Exception:return Falsetry:if isinstance(val, (bool, np.bool)): return bool(val) if val is None: return False if isinstance(val, float) and not np.isfinite(val): return False return bool(val) except Exception: return False
class BSBandStrategy:"""BS红绿带策略:指标信号 → 自动下单。"""# ── 默认参数(可被 alpha_config.json 覆盖)──DEFAULT_KLINE_PERIOD = 'day'DEFAULT_CONFIRM_MODE = 'live'# live=用当前K线;close=只用已收盘的上一根DEFAULT_KLINE_COUNT = 320MIN_KLINE_FOR_INDICATOR = 40# MA(V,120) + BARSLAST 需要足够历史ERROR_LOG_INTERVAL_SECONDS = 300# 同一标的取数失败日志节流def __init__(self, app=None):”””Args:app: QuantAppQt5 实例,用于取 K 线、持仓、执行买卖”””self.app = app# 开关与参数self._enabled = Falseself.kline_period = self.DEFAULT_KLINE_PERIODself.confirm_mode = self.DEFAULT_CONFIRM_MODEself.kline_count = self.DEFAULT_KLINE_COUNTself.keep_min_override = 0# 0 = 自动(转债10 / 股票100)self.top_bottom_half = True# 顶卖一半 / 底买一半self.monitor_list: List[str] = []# 监控列表(留空=仅持仓)self.cooldown_seconds = 0# 额外冷却(秒),0=只靠 K 线去重# 运行态self._fired: Dict[str, Tuple[str, set]] = {}# code -> (bar_ts, {action,...})self._last_action: Dict[str, dict] = {}self._err_log_at: Dict[str, float] = {}self._last_scan_at = 0.0# ══════════════════════════════════════════════════════════# 配置# ══════════════════════════════════════════════════════════def load_config(self, config):”””从 alpha_config.json 配置加载参数(键名与 qt_config_dialog 一致)。”””if config is None:returnget_value = config.get if hasattr(config, 'get') else (lambda k, d=None: d)raw_enabled = get_value('是否启用BS红绿带策略', '否')self._enabled = str(raw_enabled).strip() in ('是', 'true', 'True', '1', 'yes', 'on')period = get_value('BS红绿带K线周期', None)if period is not None and str(period).strip():self.kline_period = str(period).strip()mode = get_value('BS红绿带信号确认', None)if mode is not None:mode = str(mode).strip()self.confirm_mode = 'close' if mode in ('收盘确认', '收盘', 'close') else 'live'try:v = get_value('BS红绿带最小K线数量', None)if v is not None:self.kline_count = max(self.MIN_KLINE_FOR_INDICATOR, int(float(v)))except (TypeError, ValueError):passtry:v = get_value('BS红绿带留底数量', None)if v is not None:self.keep_min_override = max(0, int(float(v)))except (TypeError, ValueError):passhalf_raw = get_value('BS红绿带顶底半仓', '是')self.top_bottom_half = str(half_raw).strip() in ('是', 'true', 'True', '1', 'yes', 'on')try:v = get_value('BS红绿带信号冷却秒', None)if v is not None:self.cooldown_seconds = max(0, int(float(v)))except (TypeError, ValueError):pass# 监控列表:支持 list 或逗号串watch = get_value('BS红绿带监控列表', None)codes: List[str] = []if isinstance(watch, (list, tuple)):for item in watch:c = str(item).strip()[:6]if c.isdigit() and len(c) == 6:codes.append(c)elif watch is not None:text = str(watch).replace(',', ',').replace(' ', ',')for item in text.split(','):c = item.strip()[:6]if c.isdigit() and len(c) == 6:codes.append(c)self.monitor_list = sorted(set(codes))logger.info('[BS红绿带] 配置加载: enabled=%s period=%s confirm=%s keep_min=%s ''top_bottom_half=%s cooldown=%ds watch=%d只',self._enabled, self.kline_period, self.confirm_mode,self.keep_min_override or '自动', self.top_bottom_half,self.cooldown_seconds, len(self.monitor_list),)def set_enabled(self, enabled: bool):self._enabled = bool(enabled)logger.info('[BS红绿带] 策略已%s', '启用' if self._enabled else '停用')def is_enabled(self) -> bool:return self._enabled# ══════════════════════════════════════════════════════════# 对外主入口# ══════════════════════════════════════════════════════════def monitor_and_trade(self, positions=None) -> list:”””扫描标的、判断信号并执行买卖。Args:positions: 持仓列表;None 时从 app.position_data 读取Returns:list: 本次执行的交易 [{code,name,action,price,qty,reason}]”””if not self._enabled:return []# ── 扫描节流:日线不必 10 秒一扫(K 线不会变),分钟线才需要盯 ──now = time.time()if self._last_scan_at and (now - self._last_scan_at) < self._min_scan_interval():return []self._last_scan_at = nowtrades: list = []try:pos_map = self._position_map(positions)codes = sorted(set(pos_map.keys()) | set(self.monitor_list))if not codes:return []for code in codes:try:trade = self._process_code(code, pos_map.get(code) or {})if trade:trades.append(trade)except Exception as e:self._log_throttled(code, '单标的处理异常: %s' % e, exc_info=True)except Exception as e:logger.error('[BS红绿带] 监控异常: %s', e, exc_info=True)return tradesdef check_signals_only(self, positions=None) -> list:”””只算信号不下单(预览/调试用)。”””signals: list = []try:pos_map = self._position_map(positions)codes = sorted(set(pos_map.keys()) | set(self.monitor_list))for code in codes:try:info = self._evaluate(code, pos_map.get(code) or {}, dry_run=True)if info:signals.append(info)except Exception as e:logger.debug('[BS红绿带] 预览异常 %s: %s', code, e)except Exception as e:logger.warning('[BS红绿带] 预览异常: %s', e)return signals# ══════════════════════════════════════════════════════════# 单标的处理# ══════════════════════════════════════════════════════════def _process_code(self, code: str, pos: dict) -> Optional[dict]:decision = self._evaluate(code, pos, dry_run=False)if not decision:return Noneaction = decision['action']qty = int(decision['qty'])price = float(decision['price'])name = decision['name']reason = decision['reason']bar_ts = decision['bar_ts']if qty <= 0:return None# ── K 线级去重:同一根 K 线的同一动作只做一次 ──if self._already_fired(code, bar_ts, action):return None# ── 单根 K 线只做一个动作:已有更高优先级动作执行过就跳过 ──prev = self._fired.get(code)if prev and prev[0] == bar_ts and prev[1]:prev_best = min(prev[1], key=lambda a: ACTION_PRIORITY.get(a, 9))if ACTION_PRIORITY.get(prev_best, 9) <= ACTION_PRIORITY.get(action, 9):return Noneok = self._submit(code, name, action, price, qty, reason, decision)# 无论成功失败都标记,避免一根 K 线内反复重试轰炸券商self._mark_fired(code, bar_ts, action)if not ok:return Nonerecord = {'code': code, 'name': name, 'action': action,'action_label': ACTION_LABEL.get(action, action),'price': price, 'qty': qty, 'reason': reason, 'bar_ts': bar_ts,'at': time.time(),}self._last_action[code] = recordreturn recorddef _evaluate(self, code: str, pos: dict, dry_run: bool) -> Optional[dict]:”””计算某标的当前应执行的动作(不落单)。”””df = self._get_kline(code)if df is None or len(df) < self.MIN_KLINE_FOR_INDICATOR:return Nonefrom domain.algorithms.bs_band_main_indicator import calc_bs_band_main_indicatorindicator = calc_bs_band_main_indicator(df)if indicator is None or len(indicator) == 0:return Noneidx = self._bar_index(len(indicator))if idx is None:return Nonerow = indicator.iloc[idx]bar_ts = self._bar_timestamp(df, idx)if bar_ts is None:return Noneprice = float(pd.to_numeric(df['close'], errors='coerce').iloc[-1] or 0)if not np.isfinite(price) or price <= 0:return Noneavailable = int(pos.get('available') or 0)name = str(pos.get('name') or code)# 买入基础数量 = 程序设置的数量(交易模式 金额/数量 已在 App 侧解析)base_qty = self._default_buy_qty(code, price)action, qty, reason = self._decide(row, available, base_qty, code)if action is None:return Nonereturn {'code': code, 'name': name, 'action': action, 'qty': int(qty),'price': price, 'reason': reason, 'bar_ts': bar_ts,'available': available, 'base_qty': base_qty,}def _decide(self, row, available: int, base_qty: int,code: str) -> Tuple[Optional[str], int, str]:”””信号 → (动作, 数量, 原因)。按风险优先顺序判定,命中即返回。”””lot = _lot(code)keep = self._keep_min(code)# ① 清仓 / S(跌破支撑):卖出全部可用if _flag(row, 'SIG_CLEAR'):qty = self._sellable_all(available, lot)if qty > 0:return ACT_SELL_ALL, qty, '清仓(MA10下穿MA20)'return None, 0, '清仓信号但无可用数量'if _flag(row, 'SIG_BREAK_SUPPORT'):qty = self._sellable_all(available, lot)if qty > 0:return ACT_SELL_ALL, qty, 'S:跌破支撑线'return None, 0, 'S信号但无可用数量'# ② 减留底:卖到只剩最小保留量if _flag(row, 'SIG_REDUCE'):qty = int((max(0, available - keep) // lot) * lot)if qty > 0:return ACT_SELL_REDUCE, qty, '减留底(保留%d,卖出%d)' % (keep, qty)return None, 0, '减留底信号但可用数量不足(可用%d 保留%d)' % (available, keep)# ③ 顶:卖出可用的一半(向下取整到最小单位)if self.top_bottom_half and _flag(row, 'SIG_TOP'):qty = int((available // lot // 2) * lot)if qty > 0:return ACT_SELL_HALF, qty, '顶:卖出一半(可用%d→%d)' % (available, qty)return None, 0, '顶信号但不足半个最小单位(可用%d 单位%d)' % (available, lot)# ④ 建仓 / 买回 / B:买入程序设置的数量if _flag(row, 'SIG_BUY'):if base_qty > 0:return ACT_BUY_FULL, base_qty, '建仓/买回'return None, 0, '建/买回信号但设置数量为0'if _flag(row, 'SIG_REGAIN_SUPPORT'):if base_qty > 0:return ACT_BUY_FULL, base_qty, 'B:重新站上支撑线'return None, 0, 'B信号但设置数量为0'# ⑤ 底:买入设置数量的一半(向下取整到最小单位)if self.top_bottom_half and _flag(row, 'SIG_BOTTOM'):qty = int((base_qty // lot // 2) * lot)if qty > 0:return ACT_BUY_HALF, qty, '底:买入一半(设置%d→%d)' % (base_qty, qty)return None, 0, '底信号但设置数量的一半不足一个最小单位(设置%d 单位%d)' % (base_qty, lot)return None, 0, ''# ══════════════════════════════════════════════════════════# 下单# ══════════════════════════════════════════════════════════def _submit(self, code: str, name: str, action: str, price: float,qty: int, reason: str, decision: dict) -> bool:”””统一走 App.buy / App.sell。”””if self.app is None:return Falselabel = ACTION_LABEL.get(action, action)try:if action in (ACT_BUY_FULL, ACT_BUY_HALF):buy_kwargs = dict(code=code, price=price, qty=int(qty),strategy_name='BS红绿带', signal=reason,name=name, notify=True,)if action == ACT_BUY_HALF:# 「一半」必须是精确的一半:关闭金额模式重算 + 关闭情绪放大buy_kwargs['skip_trade_mode_calib'] = Truebuy_kwargs['apply_bond_sentiment'] = Falselogger.warning('[BS红绿带] 买入 %s(%s) %s 价格%.3f 数量%d 原因:%s',name, code, label, price, qty, reason)ok = self.app.buy(**buy_kwargs)else:logger.warning('[BS红绿带] 卖出 %s(%s) %s 价格%.3f 数量%d 原因:%s',name, code, label, price, qty, reason)ok = self.app.sell(code=code, price=price, qty=int(qty),strategy_name='BS红绿带', signal=reason,name=name, notify=True)return bool(ok)except Exception as e:logger.error('[BS红绿带] 下单异常 %s %s: %s', code, label, e, exc_info=True)return False# ══════════════════════════════════════════════════════════# 数量/参数辅助# ══════════════════════════════════════════════════════════def _keep_min(self, code: str) -> int:”””最小保留量:配置优先,否则转债 10 张 / 股票 100 股。”””if self.keep_min_override > 0:return int(self.keep_min_override)return _lot(code)def _min_scan_interval(self) -> float:”””两次扫描的最小间隔(秒)。日线 60s,分钟线 30s。”””period = str(self.kline_period or '').strip().lower()if period in ('day', 'daily', '1d', 'd'):return 60.0return 30.0@staticmethoddef _sellable_all(available: int, lot: int) -> int:”””全部可卖数量:按最小单位取整;不足一个最小单位时(零碎持仓)全卖。”””available = int(available or 0)if available <= 0:return 0if available < lot:return availablereturn int((available // lot) * lot)def _default_buy_qty(self, code: str, price: float) -> int:”””程序设置的单笔买入数量(复用主程序的交易模式解析)。”””try:if self.app is not None and hasattr(self.app, '_default_buy_qty'):qty = int(self.app._default_buy_qty(code, price) or 0)return max(0, qty)except Exception as e:logger.debug('[BS红绿带] 读取程序买入数量失败 %s: %s', code, e)lot = _lot(code)return lot# ══════════════════════════════════════════════════════════# 数据获取# ══════════════════════════════════════════════════════════def _get_kline(self, code: str) -> Optional[pd.DataFrame]:”””取配置周期的 K 线(先 ws_client,再 data_manager 兜底)。”””if self.app is None:return Nonedf = Nonetry:ws = getattr(self.app, 'ws_client', None)if ws is not None and hasattr(ws, 'get_kline'):df = ws.get_kline(code, period=self.kline_period, count=self.kline_count)except Exception as e:self._log_throttled(code, 'WS取K线失败: %s' % e)if df is None or len(df) == 0:try:dm = getattr(self.app, 'data_manager', None)if dm is not None and hasattr(dm, 'fetch_kline_data'):df = dm.fetch_kline_data(code, period=self.kline_period,count=self.kline_count)except Exception as e:self._log_throttled(code, 'data_manager取K线失败: %s' % e)if df is None or len(df) == 0:return Nonereturn self._normalize_df(df)@staticmethoddef _normalize_df(df: pd.DataFrame) -> pd.DataFrame:”””列名归一化到 open/high/low/close/volume(指标函数依赖英文列名)。”””if not isinstance(df, pd.DataFrame):return Nonerename = {}for target, aliases in _COL_ALIASES.items():if target in df.columns:continuefor alias in aliases:if alias in df.columns:rename[alias] = targetbreakout = df.rename(columns=rename) if rename else dffor col in ('open', 'high', 'low', 'close'):if col not in out.columns:return Noneif 'volume' not in out.columns:out = out.copy()out['volume'] = 0.0return out.reset_index(drop=True)def _bar_index(self, length: int) -> Optional[int]:”””决定用哪一根 K 线做判断。· live :当前(最新)K 线 —— 盘中实时响应· close :倒数第二根 —— 只用已收盘的 K 线,最保守”””if length <= 0:return Noneif self.confirm_mode == 'close':return length - 2 if length >= 2 else Nonereturn length - 1@staticmethoddef _bar_timestamp(df: pd.DataFrame, idx: int) -> Optional[str]:”””取 K 线唯一标识(用于去重)。同一根 K 线时间戳稳定不变。”””for col in _TIME_COLS:if col in df.columns:try:val = df[col].iloc[idx]text = str(val).strip()if text and text.lower() not in ('nan', 'nat', 'none'):return textexcept Exception:continue# 没有时间列:用行号兜底(同长度下稳定)return 'idx:%d' % int(idx)# ══════════════════════════════════════════════════════════# 持仓解析# ══════════════════════════════════════════════════════════def _position_map(self, positions=None) -> Dict[str, dict]:”””持仓列表 → {code: {'name':.., 'available':..}}。”””if positions is None and self.app is not None:positions = getattr(self.app, 'position_data', None) or []result: Dict[str, dict] = {}for pos in (positions or []):code, name, available = self._parse_position(pos)if not code:continueresult[code] = {'name': name, 'available': int(available)}return result@staticmethoddef _parse_position(pos) -> Tuple[str, str, int]:”””解析一条持仓,返回 (code, name, available)。”””try:if isinstance(pos, dict):code = str(pos.get('证券代码') or pos.get('code') or '').strip()[:6]name = str(pos.get('证券名称') or pos.get('name') or code).strip()available = int(float(pos.get('可用余额') or pos.get('available') orpos.get('可卖数量') or pos.get('可用数量') or 0))else:code = str(getattr(pos, 'code', '') or '').strip()[:6]name = str(getattr(pos, 'name', '') or code).strip()available = int(getattr(pos, 'available', 0) or 0)if not code.isdigit() or len(code) != 6:return '', '', 0return code, name or code, max(0, available)except Exception:return '', '', 0# ══════════════════════════════════════════════════════════# 去重 / 日志# ══════════════════════════════════════════════════════════def _already_fired(self, code: str, bar_ts: str, action: str) -> bool:”””同一根 K 线的同一动作已执行过 → True。额外冷却期内也返回 True。”””rec = self._fired.get(code)if rec and rec[0] == bar_ts and action in rec[1]:return True# 额外冷却(同一标的最近一次下单后 N 秒内不再动作)if self.cooldown_seconds > 0:last = self._last_action.get(code) or {}try:if (time.time() - float(last.get('at') or 0)) < self.cooldown_seconds:return Trueexcept (TypeError, ValueError):passreturn Falsedef _mark_fired(self, code: str, bar_ts: str, action: str):rec = self._fired.get(code)if rec and rec[0] == bar_ts:rec[1].add(action)self._fired[code] = (bar_ts, rec[1])else:self._fired[code] = (bar_ts, {action})if code in self._last_action:self._last_action[code]['at'] = time.time()else:self._last_action[code] = {'at': time.time()}def _log_throttled(self, code: str, message: str, exc_info: bool = False):”””同一标的的重复错误日志按间隔节流,避免刷爆日志文件。”””now = time.time()last = self._err_log_at.get(code, 0.0)if now - last < self.ERROR_LOG_INTERVAL_SECONDS:returnself._err_log_at[code] = nowlogger.warning('[BS红绿带] %s %s', code, message, exc_info=exc_info)# ══════════════════════════════════════════════════════════# 状态查询(供 UI 展示)# ══════════════════════════════════════════════════════════def get_status(self) -> dict:return {'enabled': self._enabled,'period': self.kline_period,'confirm_mode': self.confirm_mode,'watch_count': len(self.monitor_list),'keep_min': self.keep_min_override or 'auto','top_bottom_half': self.top_bottom_half,'cooldown': self.cooldown_seconds,'last_scan_at': self._last_scan_at,'last_actions': dict(self._last_action),}def reset_fired(self, code: str = None):”””清空去重记录(换周期/手动重置时用)。”””if code is None:self._fired.clear()else:self._fired.pop(str(code).strip()[:6], None)
粘贴时由于格式可能会出现错误,如遇到问题可以留言!
文中全部内容仅为作者本人的个人观点,不构成任何投资建议。 一切盈亏风险由读者自行承担。















































































