Skip to content

Sbayki #131

@mamdoohalbadri-ops

Description

@mamdoohalbadri-ops

-- coding: utf-8 --

برنامج ممدوح — TradeSbayki (نسخة محدثة كاملة في ملف واحد)

import time
from datetime import datetime
import numpy as np
import pandas as pd
import streamlit as st
import plotly.graph_objects as go

تحليلات

import ccxt
from ta.trend import EMAIndicator, MACD
from ta.momentum import RSIIndicator
from ta.volatility import BollingerBands, AverageTrueRange

أخبار

import feedparser

(اختياري) تحديث تلقائي

try:
from streamlit_autorefresh import st_autorefresh
HAS_AR = True
except Exception:
HAS_AR = False

================= إعداد الصفحة العامة =================

st.set_page_config(page_title="برنامج ممدوح — TradeSbayki (كامل محدث)", layout="wide")
st.title("👋 برنامج ممدوح — TradeSbayki (كامل محدث)")
st.caption("كل الأدوات داخل خانات (Tabs) — التحليل المتقدم، شبكة الأوامر، المحفظة، أفضل الساعات (متقدمة)، الأخبار")

st.markdown("""
سياسة الحفظ:

  • يتم حفظ مستويات القرار فقط (المقاومة/الدعوم) داخل الجلسة.
  • لا يتم حفظ أي مدخلات تداول (رأس المال، عدد الأوامر، الرسوم…).
    """)

تهيئة حالة الجلسة

if "levels_saved" not in st.session_state:
st.session_state["levels_saved"] = None
if "last_price" not in st.session_state:
st.session_state["last_price"] = None

================= توابع وذاكرة مؤقتة =================

@st.cache_resource(show_spinner=False)
def get_okx():
ex = ccxt.okx({"enableRateLimit": True})
ex.load_markets()
return ex

@st.cache_data(ttl=30, show_spinner=False, max_entries=64)
def fetch_ohlcv_okx(sym: str, tf: str, lim: int) -> pd.DataFrame:
ex = get_okx()
ohlcv = ex.fetch_ohlcv(sym, timeframe=tf, limit=lim)
df = pd.DataFrame(ohlcv, columns=["ts", "Open", "High", "Low", "Close", "Volume"])
df["ts"] = pd.to_datetime(df["ts"], unit="ms")
for c in ["Open","High","Low","Close","Volume"]:
df[c] = pd.to_numeric(df[c], downcast="float")
return df

def add_indicators(df: pd.DataFrame) -> pd.DataFrame:
dfa = df.copy()

# EMAs
dfa["EMA5"]  = EMAIndicator(dfa["Close"], 5).ema_indicator()
dfa["EMA10"] = EMAIndicator(dfa["Close"], 10).ema_indicator()
dfa["EMA20"] = EMAIndicator(dfa["Close"], 20).ema_indicator()

# Bollinger
bb = BollingerBands(dfa["Close"], 20, 2)
dfa["BOLL_mid"] = bb.bollinger_mavg()
dfa["BOLL_ub"]  = bb.bollinger_hband()
dfa["BOLL_lb"]  = bb.bollinger_lband()
dfa["BOLL_bw%"] = (dfa["BOLL_ub"] - dfa["BOLL_lb"]) / dfa["BOLL_mid"] * 100.0
dfa["BOLL_%B"]  = (dfa["Close"] - dfa["BOLL_lb"]) / (dfa["BOLL_ub"] - dfa["BOLL_lb"])

# MACD
macd = MACD(dfa["Close"], 26, 12, 9)
dfa["MACD_diff"]   = macd.macd()
dfa["MACD_signal"] = macd.macd_signal()
dfa["MACD_hist"]   = macd.macd_diff()

# RSI + ATR
dfa["RSI6"]  = RSIIndicator(dfa["Close"], 6).rsi()
dfa["RSI14"] = RSIIndicator(dfa["Close"], 14).rsi()
atr = AverageTrueRange(dfa["High"], dfa["Low"], dfa["Close"], 14)
dfa["ATR14"] = atr.average_true_range()

return dfa

def last_swings(dfa: pd.DataFrame, lookback: int = 48):
sub = dfa.tail(lookback)
return float(sub["High"].max()), float(sub["Low"].min())

def compute_live_levels(dfa: pd.DataFrame):
row = dfa.iloc[-1]
swingH, swingL = last_swings(dfa, 48)
atr = float(row["ATR14"]) if not np.isnan(row["ATR14"]) else 0.0
resistance = swingH
support1 = swingL
support2 = max(support1 - 0.5 * atr, 0.0)
return float(resistance), float(support1), float(support2)

def build_analysis_table(dfa: pd.DataFrame, resistance: float, support1: float, support2: float) -> pd.DataFrame:
row = dfa.iloc[-1]
price = float(row["Close"])
prev = float(dfa["Close"].iloc[-2])
change_pct = (price - prev) / prev * 100.0

swingH, swingL = last_swings(dfa, 48)
ema_state = "صاعد" if row["EMA5"] > row["EMA10"] > row["EMA20"] else \
            ("تعافٍ" if row["EMA5"] > row["EMA10"] and price > row["EMA20"] else "هابط")
macd_state = "إيجابي" if row["MACD_hist"] > 0 else "سلبي"
rsi_state  = "قوي" if row["RSI14"] >= 60 else ("متوازن" if row["RSI14"] >= 45 else "ضعيف")
vol_state  = "↑" if (row["Volume"] >= (row["Volume"].rolling(5).mean().iloc[-1])) else "↓"

data = {
    "البند": [
        "السعر الحالي","التغير %","EMA5","EMA10","EMA20","حالة المتوسطات",
        "BOLL وسط","BOLL علوي","BOLL سفلي","عرض الباند %","%B",
        "ATR14","MACD (diff)","Signal","Histogram","حالة MACD",
        "RSI6","RSI14","حالة RSI",
        "حجم",
        "قمة (48)","قاع (48)",
        "📌 مقاومة القرار (محفوظة/حيّة)","📌 دعم 1 (محفوظ)","📌 دعم 2 (محفوظ)",
        "قرار التداول"
    ],
    "القيمة": [
        f"{price:,.2f}", f"{change_pct:+.2f}%",
        f"{row['EMA5']:.2f}", f"{row['EMA10']:.2f}", f"{row['EMA20']:.2f}", ema_state,
        f"{row['BOLL_mid']:.2f}", f"{row['BOLL_ub']:.2f}", f"{row['BOLL_lb']:.2f}", f"{row['BOLL_bw%']:.2f}%", f"{row['BOLL_%B']:.2f}",
        f"{row['ATR14']:.2f}", f"{row['MACD_diff']:.2f}", f"{row['MACD_signal']:.2f}", f"{row['MACD_hist']:.2f}", macd_state,
        f"{row['RSI6']:.2f}", f"{row['RSI14']:.2f}", rsi_state,
        f"{row['Volume']:.2f}",
        f"{swingH:,.2f}", f"{swingL:,.2f}",
        f"{resistance:,.2f}", f"{support1:,.2f}", f"{support2:,.2f}",
        ""  # يملأ أدناه
    ]
}
df_tbl = pd.DataFrame(data)

# قرار التداول
if price > resistance and row["RSI14"] > 50 and row["MACD_hist"] > 0:
    decision = "✅ اختراق مقاومة → شراء بسيط (تعزيز)"
elif price <= support1:
    decision = "✅ عند دعم 1 → تعزيز كمي"
elif price <= support2:
    decision = "✅ عند دعم 2 → شراء إضافي (بحذر)"
elif row["EMA5"] < row["EMA10"] and row["MACD_hist"] < 0 and row["RSI14"] < 45:
    decision = "❌ لا تشتري الآن — زخم هابط"
else:
    decision = "⏳ ترقّب إغلاق شمعة/تأكيد MACD"
df_tbl.loc[df_tbl["البند"]=="قرار التداول","القيمة"] = decision
return df_tbl

def plot_candles(dfa: pd.DataFrame, title: str = ""):
"""رسم شموع + EMA + Bollinger"""
fig = go.Figure()
fig.add_trace(go.Candlestick(
x=dfa["ts"], open=dfa["Open"], high=dfa["High"], low=dfa["Low"], close=dfa["Close"],
name="OHLC"
))
for col, nm in [("EMA5","EMA5"),("EMA10","EMA10"),("EMA20","EMA20")]:
fig.add_trace(go.Scatter(x=dfa["ts"], y=dfa[col], mode="lines", name=nm))
fig.add_trace(go.Scatter(x=dfa["ts"], y=dfa["BOLL_ub"], mode="lines", name="BOLL Upper"))
fig.add_trace(go.Scatter(x=dfa["ts"], y=dfa["BOLL_mid"], mode="lines", name="BOLL Mid"))
fig.add_trace(go.Scatter(x=dfa["ts"], y=dfa["BOLL_lb"], mode="lines", name="BOLL Lower"))
fig.update_layout(height=520, title=title, xaxis_rangeslider_visible=False, margin=dict(l=10,r=10,t=40,b=10))
return fig

================= الخانات (Tabs) =================

tab1, tab2, tab3, tab4, tab5 = st.tabs([
"📈 لايف + التحليل (كامل)",
"🧩 شبكة الأوامر",
"💼 المحفظة",
"⏰ أفضل الساعات (متقدمة)",
"📰 أخبار اليوم",
])

-------------------------------------------------

📈 لايف + التحليل (كامل) — يحفظ المستويات فقط

-------------------------------------------------

with tab1:
st.subheader("📈 لايف + التحليل — نسخة محدثة كاملة")

c1, c2, c3, c4 = st.columns(4)
with c1:
    symbol = st.text_input("الرمز", value="ETH/USDT", key="live_symbol")
with c2:
    timeframe = st.selectbox("الفريم", ["1m","3m","5m","15m","30m","1h","2h","4h","6h","12h","1d"], index=6, key="live_tf")
with c3:
    limit = st.slider("عدد الشموع", 150, 2000, 800, step=50, key="live_lim")
with c4:
    refresh_sec = st.number_input("تحديث كل (ثوانٍ)", min_value=0, value=30, step=5, key="live_ref",
                                  help="ضع 0 لإيقاف التحديث التلقائي")

if refresh_sec > 0 and HAS_AR:
    st_autorefresh(interval=refresh_sec*1000, limit=None, key="auto_refresh_live_full")

try:
    df = fetch_ohlcv_okx(symbol, timeframe, limit)
    if df.empty:
        st.warning("لا توجد بيانات متاحة لهذا الرمز/الفريم.")
    else:
        dfa = add_indicators(df)
        price_now = float(dfa["Close"].iloc[-1])
        st.session_state["last_price"] = price_now  # إرساله لشبكة الأوامر

        # حساب/حفظ المستويات
        recalc = st.button("🔄 إعادة حساب المستويات", key="btn_recalc_levels_full")
        if (st.session_state.get("levels_saved") is None) or recalc:
            R, S1, S2 = compute_live_levels(dfa)
            st.session_state["levels_saved"] = {"R": R, "S1": S1, "S2": S2}

        levels = st.session_state["levels_saved"]
        resistance, support1, support2 = levels["R"], levels["S1"], levels["S2"]

        # Metrics
        m1, m2, m3, m4 = st.columns(4)
        with m1: st.metric("💎 السعر الحالي", f"{price_now:,.2f}")
        with m2: st.metric("📌 مقاومة القرار", f"{resistance:,.2f}")
        with m3: st.metric("📌 دعم 1", f"{support1:,.2f}")
        with m4: st.metric("📌 دعم 2", f"{support2:,.2f}")
        st.caption("📌 يتم حفظ المقاومة/الدعوم فقط داخل الجلسة.")

        # رسم الشموع كامل
        st.plotly_chart(plot_candles(dfa, f"{symbol} — {timeframe}"), use_container_width=True)

        # جدول التحليل
        st.markdown("#### 🔎 جدول التحليل الكامل")
        table = build_analysis_table(dfa, resistance, support1, support2)
        st.dataframe(table, use_container_width=True)

        # ملاحظات وتنبيهات فنية
        row = dfa.iloc[-1]
        notes = []
        if row["EMA5"] > row["EMA10"] > row["EMA20"]:
            notes.append("ترتيب EMAs صاعد")
        if row["MACD_hist"] > 0 and row["MACD_diff"] > row["MACD_signal"]:
            notes.append("MACD صاعد")
        if row["RSI14"] >= 70:
            notes.append("تنبيه: RSI قرب تشبع شرائي")
        if row["RSI14"] <= 30:
            notes.append("تنبيه: RSI قرب تشبع بيعي")
        if row["BOLL_%B"] <= 0.05:
            notes.append("قرب الحد السفلي للبولنجر")
        if row["BOLL_%B"] >= 0.95:
            notes.append("قرب الحد العلوي للبولنجر")
        st.markdown("**ملاحظات سريعة:** " + ("، ".join(notes) if notes else "—"))

        # قرار نهائي مختصر
        if price_now > resistance and row["RSI14"] > 50 and row["MACD_hist"] > 0:
            st.success("✅ اختراق مقاومة → شراء بسيط (تعزيز)")
        elif price_now <= support1:
            st.warning("✅ عند دعم 1 → تعزيز كمي")
        elif price_now <= support2:
            st.error("✅ عند دعم 2 → شراء بحذر")
        else:
            st.info("⏳ ترقّب إغلاق شمعة/تأكيد MACD")
except Exception as e:
    st.error(f"خطأ: {e}")

-------------------------------------------------

🧩 شبكة الأوامر (كاملة)

-------------------------------------------------

with tab2:
st.subheader("🧩 شبكة الأوامر — كاملة")

# السعر من التبويب الأول إن وُجد
initial_price = st.session_state.get("last_price") or 3500.0
price_now = st.number_input("السعر الحالي (يمكن تعبئته تلقائيًا من خانة اللايف)", min_value=0.0, value=float(initial_price), step=0.1, key="grid_price")

g1, g2, g3, g4 = st.columns(4)
with g1:
    capital = st.number_input("💰 رأس المال (USDT)", min_value=10.0, value=600.0, step=10.0, key="cap")
with g2:
    leverage = st.number_input("⚖️ الرافعة ×", min_value=1.0, max_value=50.0, value=5.0, step=0.5, key="lev")
with g3:
    n_orders = st.slider("عدد الأوامر", 1, 30, 10, key="n_orders")
with g4:
    step_pct = st.number_input("📐 خطوة التدرّج %", min_value=0.1, value=1.25, step=0.05, key="step_pct")

h1, h2, h3, h4 = st.columns(4)
with h1:
    tp_pct = st.number_input("🎯 هدف الربح % لكل أمر", min_value=0.1, value=0.80, step=0.05, key="tp_pct")
with h2:
    fee_pct = st.number_input("💸 الرسوم لكل جهة %", min_value=0.01, max_value=0.50, value=0.10, step=0.01, key="fee_pct")
with h3:
    alloc_mode = st.selectbox("توزيع رأس المال", ["متساوٍ", "يزداد كلما نزل"], index=1, key="alloc_mode")
with h4:
    qty_round = st.number_input("تقريب الكمية لـ", min_value=1e-8, value=1e-4, step=1e-4, format="%.8f", key="qty_round")

def build_grid_table(price: float) -> pd.DataFrame:
    rows = []
    effective_cap = capital * leverage
    weights = (np.ones(n_orders) if alloc_mode == "متساوٍ" else np.linspace(1, 2.0, n_orders))
    weights = weights / weights.sum()

    for i in range(1, n_orders + 1):
        buy_price = price * (1 - step_pct/100.0 * i)
        alloc_usdt = effective_cap * weights[i-1]
        qty = max(alloc_usdt / buy_price, 0.0)
        qty = np.floor(qty / qty_round) * qty_round

        tp_price = buy_price * (1 + tp_pct/100.0)
        gross_pnl = (tp_price - buy_price) * qty
        fees = (fee_pct/100.0) * buy_price * qty + (fee_pct/100.0) * tp_price * qty
        net_pnl = gross_pnl - fees

        rows.append({
            "مستوى": i,
            "سعر الشراء": buy_price,
            "الكمية": qty,
            "قيمة الأمر (USDT)": buy_price * qty,
            "هدف جني الربح": tp_price,
            "بعد عن السعر الحالي %": (buy_price/price - 1)*100.0,
            "رسوم تقديرية (USDT)": fees,
            "ربح صافٍ (USDT)": net_pnl,
            "ربح % على مبلغ الأمر": (net_pnl / (buy_price*qty) * 100.0) if buy_price*qty>0 else 0.0,
        })

    df_grid = pd.DataFrame(rows)
    totals = {
        "مستوى": "الإجمالي",
        "سعر الشراء": "",
        "الكمية": df_grid["الكمية"].sum(),
        "قيمة الأمر (USDT)": df_grid["قيمة الأمر (USDT)"].sum(),
        "هدف جني الربح": "",
        "بعد عن السعر الحالي %": "",
        "رسوم تقديرية (USDT)": df_grid["رسوم تقديرية (USDT)"].sum(),
        "ربح صافٍ (USDT)": df_grid["ربح صافٍ (USDT)"].sum(),
        "ربح % على مبلغ الأمر": ""
    }
    df_grid = pd.concat([df_grid, pd.DataFrame([totals])], ignore_index=True)
    return df_grid

grid_df = build_grid_table(price_now)
# تنسيق الأرقام لعرض أجمل
fmt_cols = {
    "سعر الشراء": "{:,.4f}",
    "الكمية": "{:,.6f}",
    "قيمة الأمر (USDT)": "{:,.2f}",
    "هدف جني الربح": "{:,.4f}",
    "بعد عن السعر الحالي %": "{:+.2f}%",
    "رسوم تقديرية (USDT)": "{:,.4f}",
    "ربح صافٍ (USDT)": "{:,.4f}",
    "ربح % على مبلغ الأمر": "{:+.2f}%"
}
for c, f in fmt_cols.items():
    if c in grid_df.columns and pd.api.types.is_numeric_dtype(grid_df[c]):
        grid_df[c] = grid_df[c].apply(lambda v: f.format(v))

st.markdown("#### 📋 جدول أوامر الشراء")
st.dataframe(grid_df, use_container_width=True)

-------------------------------------------------

💼 المحفظة (بدون حفظ)

-------------------------------------------------

with tab3:
st.subheader("💼 محفظة ممدوح — إدخال يدوي (بدون حفظ)")

st.caption("أدخل مراكزك يدويًا — لن تُحفظ بعد انتهاء الجلسة.")
colA, colB, colC = st.columns(3)
with colA:
    symbols = st.text_area("العملات (سطر لكل رمز)", "ETH/USDT\nBTC/USDT\nLINK/USDT", key="pf_syms")
with colB:
    qtys    = st.text_area("الكميات", "0.25\n0.01\n10", key="pf_qtys")
with colC:
    avgp    = st.text_area("متوسطات الشراء", "3500\n60000\n12", key="pf_avgp")

try:
    syms = [s.strip() for s in symbols.splitlines() if s.strip()]
    qs   = [float(x) for x in qtys.splitlines()]
    aps  = [float(x) for x in avgp.splitlines()]
    assert len(syms) == len(qs) == len(aps), "عدد الأسطر غير متطابق."
    dfp = pd.DataFrame({"الرمز": syms, "الكمية": qs, "متوسط الشراء": aps})
    dfp["قيمة التكلفة (USDT)"] = dfp["الكمية"] * dfp["متوسط الشراء"]
    st.dataframe(dfp, use_container_width=True)
except Exception as e:
    st.error(f"تأكد من صحة الإدخال: {e}")

-------------------------------------------------

⏰ أفضل الساعات (متقدمة)

-------------------------------------------------

with tab4:
st.subheader("⏰ أفضل الساعات — نسخة متقدمة")

c1, c2, c3 = st.columns(3)
with c1:
    bh_symbol = st.text_input("الرمز", "ETH/USDT", key="bh_sym")
with c2:
    bh_timeframe = st.selectbox("الفريم", ["5m","15m","30m","1h","4h","1d"], index=3, key="bh_tf")
with c3:
    bh_limit = st.slider("عدد الشموع", 120, 3000, 1200, step=60, key="bh_lim")

@st.cache_data(ttl=90, show_spinner=False)
def fetch_bh(sym, tf, lim):
    ex = get_okx()
    ohlcv = ex.fetch_ohlcv(sym, timeframe=tf, limit=lim)
    df = pd.DataFrame(ohlcv, columns=["ts","Open","High","Low","Close","Volume"])
    df["ts"] = pd.to_datetime(df["ts"], unit="ms")
    return df

try:
    df_bh = fetch_bh(bh_symbol, bh_timeframe, bh_limit)
    if df_bh.empty:
        st.warning("لا توجد بيانات.")
    else:
        # تغيّر النسبي لكل شمعة
        df_bh["chg%"] = df_bh["Close"].pct_change() * 100.0
        df_bh["hour"] = df_bh["ts"].dt.hour
        df_bh["day"] = df_bh["ts"].dt.day_name()

        # إحصاءات حسب الساعة
        agg = df_bh.groupby("hour").agg(
            متوسط_التغير_pct=("chg%","mean"),
            تذبذب_std=("chg%","std"),
            أعلى_إغلاق=("Close","max"),
            أدنى_إغلاق=("Close","min"),
            عدد_الشموع=("Close","count")
        ).reset_index().sort_values("متوسط_التغير_pct", ascending=False)

        # تصنيف الحالة لكل ساعة
        def state(row):
            if row["متوسط_التغير_pct"] >= 0.15 and row["تذبذب_std"] <= 0.6:
                return "⬆️ صعود مستقر"
            if row["متوسط_التغير_pct"] >= 0.00 and row["تذبذب_std"] > 0.6:
                return "↗️ صعود متذبذب"
            if row["متوسط_التغير_pct"] < 0 and row["تذبذب_std"] > 0.6:
                return "↘️ هبوط متذبذب"
            if row["متوسط_التغير_pct"] < 0 and row["تذبذب_std"] <= 0.6:
                return "⬇️ هبوط مستقر"
            return "➖ حيادي"
        agg["الحالة"] = agg.apply(state, axis=1)

        # تنبيهات مبكرة (تقاطع تغيّر من سالب لموجب في آخر 24 ساعة)
        last_day = df_bh.tail(24 if bh_timeframe=="1h" else 96)  # تقريبي للفريمات الأصغر
        flip = (last_day["chg%"].iloc[-2] < 0) and (last_day["chg%"].iloc[-1] > 0)
        if flip:
            st.success("🔔 تنبيه: تحوّل الزخم مؤخراً من سلبي إلى إيجابي (آخر نافذة).")

        st.markdown("#### 🕒 جدول «أفضل الساعات» (متقدم)")
        # تلوين بسيط عبر تنسيق أرقام كنص
        show = agg.copy()
        for c, fmt in [("متوسط_التغير_pct", "{:+.2f}%"), ("تذبذب_std", "{:.2f}"),
                       ("أعلى_إغلاق", "{:,.2f}"), ("أدنى_إغلاق", "{:,.2f}")]:
            show[c] = show[c].apply(lambda v: fmt.format(v))
        st.dataframe(show, use_container_width=True)

        # ملخّص نصي
        st.markdown("#### 📝 ملخّص")
        counts = agg["الحالة"].value_counts().to_dict()
        total_h = len(agg)
        def pct(x): 
            return f"{(x/total_h*100):.0f}%" if total_h else "0%"
        lines = [f"- عدد الساعات المقيمة: {total_h}"]
        for key in ["⬆️ صعود مستقر","↗️ صعود متذبذب","⬇️ هبوط مستقر","↘️ هبوط متذبذب","➖ حيادي"]:
            if key in counts: lines.append(f"- {key}: {counts[key]} ({pct(counts[key])})")
        st.write("\n".join(lines))

        # أفضل 3 ساعات صعود/هبوط
        top_up = agg.sort_values("متوسط_التغير_pct", ascending=False).head(3)["hour"].tolist()
        top_dn = agg.sort_values("متوسط_التغير_pct", ascending=True ).head(3)["hour"].tolist()
        st.info(f"🟢 أقوى ساعات صعود: {top_up} — 🔴 أقوى ساعات هبوط: {top_dn}")

except Exception as e:
    st.error(f"خطأ: {e}")

-------------------------------------------------

📰 أخبار اليوم (RSS)

-------------------------------------------------

with tab5:
st.subheader("📰 أخبار اليوم — نصيًا (RSS)")

feeds = {
    "CoinDesk": "https://www.coindesk.com/arc/outboundfeeds/rss/",
    "CoinTelegraph": "https://cointelegraph.com/rss",
    "Investing Crypto": "https://www.investing.com/rss/news_301.rss",
}

cols = st.columns(len(feeds))
for i, (name, url) in enumerate(feeds.items()):
    with cols[i]:
        st.markdown(f"### {name}")
        try:
            d = feedparser.parse(url)
            if not d.entries:
                st.caption("لا توجد عناصر.")
                continue
            for e in d.entries[:8]:
                title = e.get("title", "بدون عنوان")
                link  = e.get("link", "")
                date  = e.get("published", "")
                try:
                    if hasattr(e, "published_parsed") and e.published_parsed:
                        date = datetime(*e.published_parsed[:6]).strftime("%Y-%m-%d %H:%M")
                except Exception:
                    pass
                st.write(f"- [{title}]({link}) — {date}")
        except Exception as ex:
            st.error(f"تعذّر جلب {name}: {ex}")

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions