diff --git a/frontend/components/dashboard/scan-terminal/MarketOverviewBanner.module.css b/frontend/components/dashboard/scan-terminal/MarketOverviewBanner.module.css
index 3de641b0..e9502ba0 100644
--- a/frontend/components/dashboard/scan-terminal/MarketOverviewBanner.module.css
+++ b/frontend/components/dashboard/scan-terminal/MarketOverviewBanner.module.css
@@ -43,17 +43,6 @@
background: rgba(77, 163, 255, 0.04);
}
-.badge {
- display: inline-flex;
- align-items: center;
- gap: 5px;
- color: var(--color-accent-primary, #4DA3FF);
- font-weight: 700;
- font-size: 12px;
- letter-spacing: 0.03em;
- flex-shrink: 0;
-}
-
.preview {
flex: 1;
min-width: 0;
@@ -142,9 +131,6 @@
padding: 4px 8px;
}
- .badge {
- font-size: 11px;
- }
}
/* Mobile < 640px */
diff --git a/frontend/components/dashboard/scan-terminal/MarketOverviewBanner.tsx b/frontend/components/dashboard/scan-terminal/MarketOverviewBanner.tsx
index 6bb29e6c..a2e3da75 100644
--- a/frontend/components/dashboard/scan-terminal/MarketOverviewBanner.tsx
+++ b/frontend/components/dashboard/scan-terminal/MarketOverviewBanner.tsx
@@ -95,10 +95,6 @@ export function MarketOverviewBanner({
onClick={() => setCollapsed((c) => !c)}
aria-label={isEn ? "Toggle market overview" : "切换市场概览"}
>
-
-
- {isEn ? "AI Overview" : "AI 概览"}
-
{collapsed && overviewText ? overviewText.slice(0, isEn ? 120 : 60) + "…" : ""}
diff --git a/frontend/components/dashboard/scan-terminal/__tests__/aiCityReadSoftTimeout.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/aiCityReadSoftTimeout.test.ts
new file mode 100644
index 00000000..716a08fb
--- /dev/null
+++ b/frontend/components/dashboard/scan-terminal/__tests__/aiCityReadSoftTimeout.test.ts
@@ -0,0 +1,33 @@
+import fs from "node:fs";
+import path from "node:path";
+
+function assert(condition: unknown, message: string) {
+ if (!condition) throw new Error(message);
+}
+
+export function runTests() {
+ const projectRoot = process.cwd();
+ const hookPath = path.join(
+ projectRoot,
+ "components",
+ "dashboard",
+ "scan-terminal",
+ "use-ai-city-forecast.ts",
+ );
+ const source = fs.readFileSync(hookPath, "utf8");
+
+ assert(
+ source.includes("AI_CITY_READ_SOFT_TIMEOUT_MS") &&
+ source.includes("softTimeoutId"),
+ "AI city read hook must use a soft timeout so airport-read loading does not linger",
+ );
+ assert(
+ source.includes("ai_soft_timeout_fallback") &&
+ source.includes("buildAiCityErrorForecastState"),
+ "AI city read soft timeout must switch to the fast fallback state while allowing later merge",
+ );
+ assert(
+ !source.includes("controller.abort()"),
+ "AI city read soft timeout should not abort the stream; late AI results should still merge",
+ );
+}
diff --git a/frontend/components/dashboard/scan-terminal/__tests__/cityDetailMergeChartCoverage.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/cityDetailMergeChartCoverage.test.ts
new file mode 100644
index 00000000..e4160313
--- /dev/null
+++ b/frontend/components/dashboard/scan-terminal/__tests__/cityDetailMergeChartCoverage.test.ts
@@ -0,0 +1,24 @@
+import fs from "node:fs";
+import path from "node:path";
+
+function assert(condition: unknown, message: string) {
+ if (!condition) throw new Error(message);
+}
+
+export function runTests() {
+ const source = fs.readFileSync(
+ path.join(process.cwd(), "hooks", "useDashboardStore.tsx"),
+ "utf8",
+ );
+
+ assert(
+ source.includes("function pickRicherHourly"),
+ "city detail merge should have an explicit hourly-series preservation helper",
+ );
+ assert(
+ /hourly:\s*pickRicherHourly\(\s*current\.hourly,\s*incoming\.hourly\s*\)/.test(
+ source,
+ ),
+ "deep-analysis refresh must not let sparse incoming hourly data overwrite chart-capable cached hourly data",
+ );
+}
diff --git a/frontend/components/dashboard/scan-terminal/__tests__/marketOverviewBadge.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/marketOverviewBadge.test.ts
new file mode 100644
index 00000000..0888618a
--- /dev/null
+++ b/frontend/components/dashboard/scan-terminal/__tests__/marketOverviewBadge.test.ts
@@ -0,0 +1,25 @@
+import fs from "node:fs";
+import path from "node:path";
+
+function assert(condition: unknown, message: string) {
+ if (!condition) throw new Error(message);
+}
+
+export function runTests() {
+ const projectRoot = process.cwd();
+ const bannerPath = path.join(
+ projectRoot,
+ "components",
+ "dashboard",
+ "scan-terminal",
+ "MarketOverviewBanner.tsx",
+ );
+ const source = fs.readFileSync(bannerPath, "utf8");
+
+ assert(
+ !source.includes("AI Overview") &&
+ !source.includes("AI 概览") &&
+ !source.includes("styles.badge"),
+ "market overview banner should not render the AI overview badge",
+ );
+}
diff --git a/frontend/components/dashboard/scan-terminal/use-ai-city-forecast.ts b/frontend/components/dashboard/scan-terminal/use-ai-city-forecast.ts
index e6970734..f06473db 100644
--- a/frontend/components/dashboard/scan-terminal/use-ai-city-forecast.ts
+++ b/frontend/components/dashboard/scan-terminal/use-ai-city-forecast.ts
@@ -15,6 +15,8 @@ import {
readReadyCachedAiForecastState,
} from "./ai-city-forecast-stream-state";
+const AI_CITY_READ_SOFT_TIMEOUT_MS = 4_500;
+
export function useAiCityForecast({
detail,
detailCityName,
@@ -50,6 +52,7 @@ export function useAiCityForecast({
return;
}
let cancelled = false;
+ let resolved = false;
const cacheKey = buildAiCityForecastCacheKey(aiForecastKey);
const requestKey = buildAiCityForecastRequestKey(cacheKey, aiRefreshToken);
@@ -69,6 +72,17 @@ export function useAiCityForecast({
report,
});
setAiForecast(loadingState);
+ const softTimeoutId = window.setTimeout(() => {
+ if (cancelled || resolved) return;
+ const fallbackState = buildAiCityErrorForecastState({
+ cacheKey,
+ detail,
+ error: "ai_soft_timeout_fallback",
+ isEn,
+ report,
+ });
+ setAiForecast(fallbackState);
+ }, AI_CITY_READ_SOFT_TIMEOUT_MS);
void scanTerminalClient.streamAiCityRead({
city: detailCityName,
forceRefresh: aiRefreshToken > 0,
@@ -88,6 +102,8 @@ export function useAiCityForecast({
})
.then((payload) => {
if (!payload) return;
+ resolved = true;
+ window.clearTimeout(softTimeoutId);
const readyState = buildAiCityReadyForecastState({
cacheKey,
detail,
@@ -100,6 +116,8 @@ export function useAiCityForecast({
}
})
.catch((error) => {
+ resolved = true;
+ window.clearTimeout(softTimeoutId);
const errorState = buildAiCityErrorForecastState({
cacheKey,
detail,
@@ -113,6 +131,7 @@ export function useAiCityForecast({
});
return () => {
cancelled = true;
+ window.clearTimeout(softTimeoutId);
};
}, [
aiForecastKey,
diff --git a/frontend/hooks/useDashboardStore.tsx b/frontend/hooks/useDashboardStore.tsx
index e19aa538..5a78bf4f 100644
--- a/frontend/hooks/useDashboardStore.tsx
+++ b/frontend/hooks/useDashboardStore.tsx
@@ -492,6 +492,23 @@ function pickRicherForecast(
};
}
+function countHourlyPoints(value: CityDetail["hourly"] | undefined) {
+ const times = Array.isArray(value?.times) ? value?.times || [] : [];
+ const temps = Array.isArray(value?.temps) ? value?.temps || [] : [];
+ return Math.min(times.length, temps.length);
+}
+
+function pickRicherHourly(
+ currentValue: CityDetail["hourly"] | undefined,
+ incomingValue: CityDetail["hourly"] | undefined,
+) {
+ const incomingCount = countHourlyPoints(incomingValue);
+ const currentCount = countHourlyPoints(currentValue);
+ if (incomingCount <= 0) return currentValue;
+ if (currentCount <= 0) return incomingValue;
+ return incomingCount >= currentCount ? incomingValue : currentValue;
+}
+
function pickPreferredNearbyStations(
currentValue: CityDetail["official_nearby"] | CityDetail["mgm_nearby"],
incomingValue: CityDetail["official_nearby"] | CityDetail["mgm_nearby"],
@@ -536,6 +553,7 @@ function mergeCityDetail(
incoming.multi_model_daily,
),
forecast: pickRicherForecast(current.forecast, incoming.forecast),
+ hourly: pickRicherHourly(current.hourly, incoming.hourly),
official_nearby: pickPreferredNearbyStations(
current.official_nearby,
incoming.official_nearby,
diff --git a/src/bot/runtime_coordinator.py b/src/bot/runtime_coordinator.py
index d0457d7f..604fa914 100644
--- a/src/bot/runtime_coordinator.py
+++ b/src/bot/runtime_coordinator.py
@@ -84,6 +84,7 @@ class StartupCoordinator:
def start_all(self) -> RuntimeStatus:
loops = [
self._start_airport_high_freq_loop(),
+ self._start_market_monitor_push_loop(),
self._start_dashboard_prewarm_loop(),
self._start_polygon_wallet_loop(),
self._start_polymarket_wallet_activity_loop(),
@@ -175,6 +176,33 @@ class StartupCoordinator:
),
)
+ def _start_market_monitor_push_loop(self) -> LoopStatus:
+ enabled = _env_bool("TELEGRAM_MARKET_MONITOR_PUSH_ENABLED", True)
+ chat_ids = import_module(
+ "src.utils.telegram_chat_ids"
+ ).get_market_monitor_chat_ids_from_env()
+ telegram_push = import_module("src.utils.telegram_push")
+ interval = int(getattr(telegram_push, "MARKET_MONITOR_INTERVAL_SEC", 60))
+ cities_count = len(getattr(telegram_push, "MARKET_MONITOR_CITIES", []))
+ details = {
+ "mode": "market-monitor-periodic",
+ "interval_sec": interval,
+ "cities_count": cities_count,
+ "chat_targets": len(chat_ids),
+ "window": "every 60s, available Polymarket scans only",
+ }
+ validation_error = None if chat_ids else "missing_TELEGRAM_MARKET_MONITOR_CHAT_IDS"
+ return self._start_with_validation(
+ key="market_monitor_push",
+ label="市场监控频道推送",
+ configured_enabled=enabled,
+ details=details,
+ validation_error=validation_error,
+ starter=lambda: telegram_push.start_market_monitor_push_loop(
+ self.bot,
+ ),
+ )
+
def _start_dashboard_prewarm_loop(self) -> LoopStatus:
enabled = _env_bool("POLYWEATHER_DASHBOARD_PREWARM_ENABLED", False)
interval = max(30, _env_int("POLYWEATHER_PREWARM_INTERVAL_SEC", 300))
diff --git a/src/utils/telegram_chat_ids.py b/src/utils/telegram_chat_ids.py
index 02cb2264..7f9a0d9a 100644
--- a/src/utils/telegram_chat_ids.py
+++ b/src/utils/telegram_chat_ids.py
@@ -60,6 +60,26 @@ def get_polymarket_wallet_activity_chat_ids_from_env() -> List[str]:
)
+def get_market_monitor_chat_ids_from_env() -> List[str]:
+ """
+ Preferred envs for market monitor push:
+ - TELEGRAM_MARKET_MONITOR_CHAT_IDS (comma-separated)
+ - TELEGRAM_MARKET_MONITOR_CHAT_ID (single)
+
+ Falls back to global TELEGRAM_CHAT_IDS / TELEGRAM_CHAT_ID for compatibility.
+ """
+ dedicated = parse_telegram_chat_ids(
+ os.getenv("TELEGRAM_MARKET_MONITOR_CHAT_IDS"),
+ os.getenv("TELEGRAM_MARKET_MONITOR_CHAT_ID"),
+ )
+ if dedicated:
+ return dedicated
+ return parse_telegram_chat_ids(
+ os.getenv("TELEGRAM_CHAT_IDS"),
+ os.getenv("TELEGRAM_CHAT_ID"),
+ )
+
+
def get_primary_telegram_chat_id_from_env() -> str:
chat_ids = get_telegram_chat_ids_from_env()
return chat_ids[0] if chat_ids else ""
diff --git a/src/utils/telegram_push.py b/src/utils/telegram_push.py
index 5261ca31..15afd19c 100644
--- a/src/utils/telegram_push.py
+++ b/src/utils/telegram_push.py
@@ -15,7 +15,10 @@ from src.database.runtime_state import (
get_state_storage_mode,
)
from src.data_collection.city_registry import CITY_REGISTRY
-from src.utils.telegram_chat_ids import get_telegram_chat_ids_from_env
+from src.utils.telegram_chat_ids import (
+ get_market_monitor_chat_ids_from_env,
+ get_telegram_chat_ids_from_env,
+)
SEVERITY_RANK = {
@@ -486,7 +489,117 @@ def _alert_signature(alert_payload: Dict[str, Any]) -> str:
# ── high-freq airport push loop ──
HIGH_FREQ_AIRPORT_CITIES = {"seoul", "busan", "tokyo", "ankara", "helsinki", "amsterdam", "istanbul", "paris", "hong kong", "lau fau shan", "taipei"}
-HIGH_FREQ_AIRPORT_ICAO = {"seoul": "RKSI", "busan": "RKPK", "tokyo": "44166", "ankara": "17128", "helsinki": "EFHK", "amsterdam": "EHAM", "istanbul": "17058", "paris": "LFPB", "hong kong": "HKO", "lau fau shan": "LFS", "taipei": "466920"}
+HIGH_FREQ_AIRPORT_ICAO = {"seoul": "RKSI", "busan": "RKPK", "tokyo": "44166", "ankara": "17128", "helsinki": "EFHK", "amsterdam": "EHAM", "istanbul": "17058", "paris": "LFPB", "hong kong": "HKO", "lau fau shan": "LFS", "taipei": "466920", "beijing": "ZBAA", "shanghai": "ZSPD", "guangzhou": "ZGGG", "shenzhen": "ZGSZ", "qingdao": "ZSQD", "chengdu": "ZUUU", "chongqing": "ZUCK", "wuhan": "ZHHH"}
+MARKET_MONITOR_INTERVAL_SEC = 60
+MARKET_MONITOR_CITIES = [
+ "seoul", "busan", "tokyo", "ankara", "helsinki", "amsterdam",
+ "istanbul", "paris", "hong kong", "lau fau shan", "taipei",
+ "new york", "los angeles", "chicago", "denver", "atlanta",
+ "miami", "san francisco", "houston", "dallas", "austin", "seattle",
+ "beijing", "shanghai", "guangzhou", "shenzhen", "qingdao",
+ "chengdu", "chongqing", "wuhan",
+]
+
+_FUNCTION_HASHTAGS = {
+ "runway": "#跑道观测",
+ "market": "#市场监控",
+ "trade": "#交易机会",
+}
+
+
+def _city_hashtag(city: Optional[str]) -> Optional[str]:
+ text = str(city or "").strip()
+ if not text:
+ return None
+ parts = [part for part in re.split(r"[^A-Za-z0-9]+", text.title()) if part]
+ if not parts:
+ return None
+ return "#" + "".join(parts)
+
+
+def _station_hashtag(station: Optional[str]) -> Optional[str]:
+ text = re.sub(r"[^A-Za-z0-9]+", "", str(station or "").upper())
+ return f"#{text}" if text else None
+
+
+def _build_telegram_hashtag_line(
+ kind: str,
+ *,
+ city: Optional[str] = None,
+ station: Optional[str] = None,
+ extra: Optional[List[str]] = None,
+) -> str:
+ tags: List[str] = []
+ primary = _FUNCTION_HASHTAGS.get(kind)
+ if primary:
+ tags.append(primary)
+ for item in extra or []:
+ tag = _FUNCTION_HASHTAGS.get(item, item)
+ if tag and tag not in tags:
+ tags.append(tag)
+ city_tag = _city_hashtag(city)
+ if city_tag and city_tag not in tags:
+ tags.append(city_tag)
+ station_tag = _station_hashtag(station)
+ if station_tag and station_tag not in tags:
+ tags.append(station_tag)
+ return " ".join(tags)
+
+
+def _format_percent(value: Any) -> str:
+ try:
+ numeric = float(value)
+ except Exception:
+ return "--"
+ sign = "+" if numeric > 0 else ""
+ return f"{sign}{numeric:.1f}%"
+
+
+def _format_prob(value: Any) -> str:
+ try:
+ numeric = float(value)
+ except Exception:
+ return "--"
+ if numeric <= 1:
+ numeric *= 100
+ return f"{numeric:.1f}%"
+
+
+def _build_market_monitor_message(city: str, city_weather: Dict[str, Any]) -> str:
+ market = city_weather.get("market_scan") or {}
+ current = city_weather.get("current") or {}
+ deb = city_weather.get("deb") or {}
+ local_time = str(city_weather.get("local_time") or "").strip() or "--"
+ city_label = str(city or "").strip().title()
+ signal = str(market.get("signal_label") or market.get("signal_status") or "MONITOR").strip()
+ confidence = str(market.get("confidence") or "low").strip()
+ bucket = str(
+ market.get("selected_bucket")
+ or (market.get("forecast_bucket") or {}).get("label")
+ or "--"
+ ).strip()
+ yes_buy = (
+ market.get("yes_buy")
+ if market.get("yes_buy") is not None
+ else (market.get("forecast_bucket") or {}).get("yes_buy")
+ )
+ edge = market.get("edge_percent")
+ current_temp = current.get("temp")
+ deb_pred = deb.get("prediction")
+
+ lines = [
+ _build_telegram_hashtag_line("market", city=city),
+ f"{city_label} 1分钟市场监控 {local_time}",
+ f"信号:{signal} · 置信度:{confidence}",
+ ]
+ if bucket and bucket != "--":
+ lines.append(f"桶位:{bucket}")
+ lines.append(f"Yes 买价:{_format_prob(yes_buy)} · Edge:{_format_percent(edge)}")
+ if current_temp is not None or deb_pred is not None:
+ current_text = f"{float(current_temp):.1f}°C" if current_temp is not None else "--"
+ deb_text = f"{float(deb_pred):.1f}°C" if deb_pred is not None else "--"
+ lines.append(f"当前:{current_text} · DEB:{deb_text}")
+ return "\n".join(lines)
_AIRPORT_PUSH_STATE_PATH = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
@@ -562,7 +675,9 @@ def _build_airport_status_message(
"ankara": "Esenboğa", "helsinki": "Vantaa", "amsterdam": "Schiphol",
"istanbul": "Airport", "paris": "Le Bourget",
"hong kong": "Observatory", "lau fau shan": "Lau Fau Shan",
- "taipei": "Songshan"}
+ "taipei": "Songshan", "beijing": "ZBAA", "shanghai": "ZSPD",
+ "guangzhou": "ZGGG", "shenzhen": "ZGSZ", "qingdao": "ZSQD",
+ "chengdu": "ZUUU", "chongqing": "ZUCK", "wuhan": "ZHHH"}
en_name = city.title()
ap_name = _AIRPORT_EN.get(city, "")
time_suffix = f" {local_time}" if local_time else ""
@@ -599,7 +714,12 @@ def _build_airport_status_message(
and latest_temp - max_so_far >= 0.3)
flag = " \U0001f536新" if new_high else ""
- lines = [header + flag, ""]
+ hashtag_line = _build_telegram_hashtag_line(
+ "runway",
+ city=city,
+ station=airport_icao,
+ )
+ lines = [hashtag_line, header + flag, ""]
runway_shown = False
if runway_pairs and runway_temps and len(runway_pairs) == len(runway_temps):
for (r1, r2), (t, _d) in zip(runway_pairs, runway_temps):
@@ -894,3 +1014,63 @@ def start_high_freq_airport_push_loop(bot: Any, config: Dict[str, Any]) -> Optio
thread.start()
logger.info("airport high-freq push loop thread started")
return thread
+
+
+def _run_market_monitor_cycle(bot: Any, chat_ids: List[str]) -> bool:
+ sent_any = False
+ try:
+ from web.app import _analyze
+ except Exception as exc:
+ logger.warning("market monitor push skipped: analyze import failed: {}", exc)
+ return False
+
+ for city in MARKET_MONITOR_CITIES:
+ try:
+ city_weather = _analyze(city)
+ market = city_weather.get("market_scan") or {}
+ if not market.get("available"):
+ continue
+ message = _build_market_monitor_message(city, city_weather)
+ for chat_id in chat_ids:
+ try:
+ bot.send_message(chat_id, message)
+ sent_any = True
+ except Exception as exc:
+ logger.warning("market monitor push failed city={} chat_id={}: {}", city, chat_id, exc)
+ except Exception:
+ logger.exception("market monitor cycle failed for city={}", city)
+ return sent_any
+
+
+def start_market_monitor_push_loop(bot: Any) -> Optional[threading.Thread]:
+ enabled = _env_bool("TELEGRAM_MARKET_MONITOR_PUSH_ENABLED", True)
+ chat_ids = get_market_monitor_chat_ids_from_env()
+ if not enabled:
+ logger.info("market monitor push loop disabled")
+ return None
+ if not chat_ids:
+ logger.warning("market monitor push loop skipped: TELEGRAM_MARKET_MONITOR_CHAT_IDS/TELEGRAM_CHAT_IDS is not set")
+ return None
+
+ interval_sec = MARKET_MONITOR_INTERVAL_SEC
+
+ def _runner() -> None:
+ logger.info(
+ "market monitor push loop started cities={} interval={}s chat_targets={}",
+ len(MARKET_MONITOR_CITIES), interval_sec, len(chat_ids),
+ )
+ while True:
+ cycle_started = time.time()
+ _run_market_monitor_cycle(bot=bot, chat_ids=chat_ids)
+ elapsed = time.time() - cycle_started
+ sleep_sec = max(5, interval_sec - int(elapsed))
+ time.sleep(sleep_sec)
+
+ thread = threading.Thread(
+ target=_runner,
+ name="market-monitor-pusher",
+ daemon=True,
+ )
+ thread.start()
+ logger.info("market monitor push loop thread started")
+ return thread
diff --git a/tests/test_telegram_hashtags.py b/tests/test_telegram_hashtags.py
new file mode 100644
index 00000000..e1c76e6b
--- /dev/null
+++ b/tests/test_telegram_hashtags.py
@@ -0,0 +1,55 @@
+from src.utils.telegram_push import (
+ MARKET_MONITOR_INTERVAL_SEC,
+ _build_airport_status_message,
+ _build_market_monitor_message,
+)
+
+
+def test_airport_status_message_starts_with_runway_city_and_station_hashtags():
+ text = _build_airport_status_message(
+ "qingdao",
+ {
+ "current": {"temp": 22.8},
+ "deb": {"prediction": 24.0},
+ "airport_current": {"max_so_far": 23.1, "max_temp_time": "13:00"},
+ "amos": {
+ "observation_time": "2026-05-15T05:00:00Z",
+ "runway_obs": {
+ "temperatures": [(23.0, "TDZ"), (23.2, "MID"), (23.1, "END")],
+ },
+ },
+ },
+ 24.0,
+ "13:00",
+ )
+
+ first_line = text.splitlines()[0]
+ assert first_line == "#跑道观测 #Qingdao #ZSQD"
+ assert "Qingdao / ZSQD 13:00" in text
+
+
+def test_market_monitor_message_starts_with_market_hashtag_and_city():
+ text = _build_market_monitor_message(
+ "shanghai",
+ {
+ "local_time": "14:01",
+ "current": {"temp": 29.4},
+ "deb": {"prediction": 31.2},
+ "market_scan": {
+ "available": True,
+ "signal_label": "MONITOR",
+ "confidence": "medium",
+ "selected_bucket": "31° or higher",
+ "yes_buy": 0.42,
+ "edge_percent": 8.4,
+ },
+ },
+ )
+
+ assert text.splitlines()[0] == "#市场监控 #Shanghai"
+ assert "1分钟市场监控" in text
+ assert "Edge:+8.4%" in text
+
+
+def test_market_monitor_default_interval_is_one_minute():
+ assert MARKET_MONITOR_INTERVAL_SEC == 60