parent
246ea8cf75
commit
1008c9352e
@ -1,554 +0,0 @@
|
|||||||
from typing import Dict, List
|
|
||||||
from datetime import datetime
|
|
||||||
from loguru import logger
|
|
||||||
from swarms.structs.tree_swarm import TreeAgent, Tree, ForestSwarm
|
|
||||||
import asyncio
|
|
||||||
import json
|
|
||||||
import aiohttp
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
|
|
||||||
# Configure logging
|
|
||||||
logger.add("forex_forest.log", rotation="500 MB", level="INFO")
|
|
||||||
|
|
||||||
|
|
||||||
class ForexDataFeed:
|
|
||||||
"""Real-time forex data collector using free open sources"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.pairs = [
|
|
||||||
"EUR/USD",
|
|
||||||
"GBP/USD",
|
|
||||||
"USD/JPY",
|
|
||||||
"AUD/USD",
|
|
||||||
"USD/CAD",
|
|
||||||
]
|
|
||||||
|
|
||||||
async def fetch_ecb_rates(self) -> Dict:
|
|
||||||
"""Fetch exchange rates from European Central Bank (no key required)"""
|
|
||||||
try:
|
|
||||||
url = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml"
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.get(url) as response:
|
|
||||||
xml_data = await response.text()
|
|
||||||
|
|
||||||
root = ET.fromstring(xml_data)
|
|
||||||
rates = {}
|
|
||||||
for cube in root.findall(".//*[@currency]"):
|
|
||||||
currency = cube.get("currency")
|
|
||||||
rate = float(cube.get("rate"))
|
|
||||||
rates[currency] = rate
|
|
||||||
|
|
||||||
# Calculate cross rates
|
|
||||||
rates["EUR"] = 1.0 # Base currency
|
|
||||||
cross_rates = {}
|
|
||||||
for pair in self.pairs:
|
|
||||||
base, quote = pair.split("/")
|
|
||||||
if base in rates and quote in rates:
|
|
||||||
cross_rates[pair] = rates[base] / rates[quote]
|
|
||||||
|
|
||||||
return cross_rates
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error fetching ECB rates: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
async def fetch_forex_factory_data(self) -> Dict:
|
|
||||||
"""Scrape trading data from Forex Factory"""
|
|
||||||
try:
|
|
||||||
url = "https://www.forexfactory.com"
|
|
||||||
headers = {
|
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
||||||
}
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.get(
|
|
||||||
url, headers=headers
|
|
||||||
) as response:
|
|
||||||
text = await response.text()
|
|
||||||
|
|
||||||
soup = BeautifulSoup(text, "html.parser")
|
|
||||||
|
|
||||||
# Get calendar events
|
|
||||||
calendar = []
|
|
||||||
calendar_table = soup.find(
|
|
||||||
"table", class_="calendar__table"
|
|
||||||
)
|
|
||||||
if calendar_table:
|
|
||||||
for row in calendar_table.find_all(
|
|
||||||
"tr", class_="calendar__row"
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
event = {
|
|
||||||
"currency": row.find(
|
|
||||||
"td", class_="calendar__currency"
|
|
||||||
).text.strip(),
|
|
||||||
"event": row.find(
|
|
||||||
"td", class_="calendar__event"
|
|
||||||
).text.strip(),
|
|
||||||
"impact": row.find(
|
|
||||||
"td", class_="calendar__impact"
|
|
||||||
).text.strip(),
|
|
||||||
"time": row.find(
|
|
||||||
"td", class_="calendar__time"
|
|
||||||
).text.strip(),
|
|
||||||
}
|
|
||||||
calendar.append(event)
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
|
|
||||||
return {"calendar": calendar}
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error fetching Forex Factory data: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
async def fetch_tradingeconomics_data(self) -> Dict:
|
|
||||||
"""Scrape economic data from Trading Economics"""
|
|
||||||
try:
|
|
||||||
url = "https://tradingeconomics.com/calendar"
|
|
||||||
headers = {
|
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
||||||
}
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.get(
|
|
||||||
url, headers=headers
|
|
||||||
) as response:
|
|
||||||
text = await response.text()
|
|
||||||
|
|
||||||
soup = BeautifulSoup(text, "html.parser")
|
|
||||||
|
|
||||||
# Get economic indicators
|
|
||||||
indicators = []
|
|
||||||
calendar_table = soup.find("table", class_="table")
|
|
||||||
if calendar_table:
|
|
||||||
for row in calendar_table.find_all("tr")[
|
|
||||||
1:
|
|
||||||
]: # Skip header
|
|
||||||
try:
|
|
||||||
cols = row.find_all("td")
|
|
||||||
indicator = {
|
|
||||||
"country": cols[0].text.strip(),
|
|
||||||
"indicator": cols[1].text.strip(),
|
|
||||||
"actual": cols[2].text.strip(),
|
|
||||||
"previous": cols[3].text.strip(),
|
|
||||||
"consensus": cols[4].text.strip(),
|
|
||||||
}
|
|
||||||
indicators.append(indicator)
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
|
|
||||||
return {"indicators": indicators}
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(
|
|
||||||
f"Error fetching Trading Economics data: {e}"
|
|
||||||
)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
async def fetch_dailyfx_data(self) -> Dict:
|
|
||||||
"""Scrape market analysis from DailyFX"""
|
|
||||||
try:
|
|
||||||
url = "https://www.dailyfx.com/market-news"
|
|
||||||
headers = {
|
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
||||||
}
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.get(
|
|
||||||
url, headers=headers
|
|
||||||
) as response:
|
|
||||||
text = await response.text()
|
|
||||||
|
|
||||||
soup = BeautifulSoup(text, "html.parser")
|
|
||||||
|
|
||||||
# Get market news and analysis
|
|
||||||
news = []
|
|
||||||
articles = soup.find_all("article", class_="dfx-article")
|
|
||||||
for article in articles[:10]: # Get latest 10 articles
|
|
||||||
try:
|
|
||||||
news_item = {
|
|
||||||
"title": article.find("h3").text.strip(),
|
|
||||||
"summary": article.find("p").text.strip(),
|
|
||||||
"currency": article.get(
|
|
||||||
"data-currency", "General"
|
|
||||||
),
|
|
||||||
"timestamp": article.find("time").get(
|
|
||||||
"datetime"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
news.append(news_item)
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
|
|
||||||
return {"news": news}
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error fetching DailyFX data: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
async def fetch_all_data(self) -> Dict:
|
|
||||||
"""Fetch and combine all forex data sources"""
|
|
||||||
try:
|
|
||||||
# Fetch data from all sources concurrently
|
|
||||||
rates, ff_data, te_data, dx_data = await asyncio.gather(
|
|
||||||
self.fetch_ecb_rates(),
|
|
||||||
self.fetch_forex_factory_data(),
|
|
||||||
self.fetch_tradingeconomics_data(),
|
|
||||||
self.fetch_dailyfx_data(),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Combine all data
|
|
||||||
market_data = {
|
|
||||||
"exchange_rates": rates,
|
|
||||||
"calendar": ff_data.get("calendar", []),
|
|
||||||
"economic_indicators": te_data.get("indicators", []),
|
|
||||||
"market_news": dx_data.get("news", []),
|
|
||||||
"timestamp": datetime.now().isoformat(),
|
|
||||||
}
|
|
||||||
|
|
||||||
return market_data
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error fetching all data: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
# Rest of the ForexForestSystem class remains the same...
|
|
||||||
|
|
||||||
# (Previous ForexDataFeed class code remains the same...)
|
|
||||||
|
|
||||||
# Specialized Agent Prompts
|
|
||||||
TECHNICAL_ANALYST_PROMPT = """You are an expert forex technical analyst agent.
|
|
||||||
Your responsibilities:
|
|
||||||
1. Analyze real-time exchange rate data for patterns and trends
|
|
||||||
2. Calculate cross-rates and currency correlations
|
|
||||||
3. Generate trading signals based on price action
|
|
||||||
4. Monitor market volatility and momentum
|
|
||||||
5. Identify key support and resistance levels
|
|
||||||
|
|
||||||
Data Format:
|
|
||||||
- You will receive exchange rates from ECB and calculated cross-rates
|
|
||||||
- Focus on major currency pairs and their relationships
|
|
||||||
- Consider market volatility and trading volumes
|
|
||||||
|
|
||||||
Output Format:
|
|
||||||
{
|
|
||||||
"analysis_type": "technical",
|
|
||||||
"timestamp": "ISO timestamp",
|
|
||||||
"signals": [
|
|
||||||
{
|
|
||||||
"pair": "Currency pair",
|
|
||||||
"trend": "bullish/bearish/neutral",
|
|
||||||
"strength": 1-10,
|
|
||||||
"key_levels": {"support": [], "resistance": []},
|
|
||||||
"recommendation": "buy/sell/hold"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}"""
|
|
||||||
|
|
||||||
FUNDAMENTAL_ANALYST_PROMPT = """You are an expert forex fundamental analyst agent.
|
|
||||||
Your responsibilities:
|
|
||||||
1. Analyze economic calendar events and their impact
|
|
||||||
2. Evaluate economic indicators from Trading Economics
|
|
||||||
3. Assess market news and sentiment from DailyFX
|
|
||||||
4. Monitor central bank actions and policies
|
|
||||||
5. Track geopolitical events affecting currencies
|
|
||||||
|
|
||||||
Data Format:
|
|
||||||
- Economic calendar events with impact levels
|
|
||||||
- Latest economic indicators and previous values
|
|
||||||
- Market news and analysis from reliable sources
|
|
||||||
- Central bank statements and policy changes
|
|
||||||
|
|
||||||
Output Format:
|
|
||||||
{
|
|
||||||
"analysis_type": "fundamental",
|
|
||||||
"timestamp": "ISO timestamp",
|
|
||||||
"assessments": [
|
|
||||||
{
|
|
||||||
"currency": "Currency code",
|
|
||||||
"economic_outlook": "positive/negative/neutral",
|
|
||||||
"key_events": [],
|
|
||||||
"impact_score": 1-10,
|
|
||||||
"bias": "bullish/bearish/neutral"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}"""
|
|
||||||
|
|
||||||
MARKET_SENTIMENT_PROMPT = """You are an expert market sentiment analysis agent.
|
|
||||||
Your responsibilities:
|
|
||||||
1. Analyze news sentiment from DailyFX articles
|
|
||||||
2. Track market positioning and bias
|
|
||||||
3. Monitor risk sentiment and market fear/greed
|
|
||||||
4. Identify potential market drivers
|
|
||||||
5. Detect sentiment shifts and extremes
|
|
||||||
|
|
||||||
Data Format:
|
|
||||||
- Market news and analysis articles
|
|
||||||
- Trading sentiment indicators
|
|
||||||
- Risk event calendar
|
|
||||||
- Market commentary and analysis
|
|
||||||
|
|
||||||
Output Format:
|
|
||||||
{
|
|
||||||
"analysis_type": "sentiment",
|
|
||||||
"timestamp": "ISO timestamp",
|
|
||||||
"sentiment_data": [
|
|
||||||
{
|
|
||||||
"pair": "Currency pair",
|
|
||||||
"sentiment": "risk-on/risk-off",
|
|
||||||
"strength": 1-10,
|
|
||||||
"key_drivers": [],
|
|
||||||
"outlook": "positive/negative/neutral"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}"""
|
|
||||||
|
|
||||||
STRATEGY_COORDINATOR_PROMPT = """You are the lead forex strategy coordination agent.
|
|
||||||
Your responsibilities:
|
|
||||||
1. Synthesize technical, fundamental, and sentiment analysis
|
|
||||||
2. Generate final trading recommendations
|
|
||||||
3. Manage risk exposure and position sizing
|
|
||||||
4. Coordinate entry and exit points
|
|
||||||
5. Monitor open positions and adjust strategies
|
|
||||||
|
|
||||||
Data Format:
|
|
||||||
- Analysis from technical, fundamental, and sentiment agents
|
|
||||||
- Current market rates and conditions
|
|
||||||
- Economic calendar and news events
|
|
||||||
- Risk parameters and exposure limits
|
|
||||||
|
|
||||||
Output Format:
|
|
||||||
{
|
|
||||||
"analysis_type": "strategy",
|
|
||||||
"timestamp": "ISO timestamp",
|
|
||||||
"recommendations": [
|
|
||||||
{
|
|
||||||
"pair": "Currency pair",
|
|
||||||
"action": "buy/sell/hold",
|
|
||||||
"confidence": 1-10,
|
|
||||||
"entry_points": [],
|
|
||||||
"stop_loss": float,
|
|
||||||
"take_profit": float,
|
|
||||||
"rationale": "string"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}"""
|
|
||||||
|
|
||||||
|
|
||||||
class ForexForestSystem:
|
|
||||||
"""Main system coordinating the forest swarm and data feeds"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initialize the forex forest system"""
|
|
||||||
self.data_feed = ForexDataFeed()
|
|
||||||
|
|
||||||
# Create Technical Analysis Tree
|
|
||||||
technical_agents = [
|
|
||||||
TreeAgent(
|
|
||||||
system_prompt=TECHNICAL_ANALYST_PROMPT,
|
|
||||||
agent_name="Price Action Analyst",
|
|
||||||
model_name="gpt-4o",
|
|
||||||
),
|
|
||||||
TreeAgent(
|
|
||||||
system_prompt=TECHNICAL_ANALYST_PROMPT,
|
|
||||||
agent_name="Cross Rate Analyst",
|
|
||||||
model_name="gpt-4o",
|
|
||||||
),
|
|
||||||
TreeAgent(
|
|
||||||
system_prompt=TECHNICAL_ANALYST_PROMPT,
|
|
||||||
agent_name="Volatility Analyst",
|
|
||||||
model_name="gpt-4o",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Create Fundamental Analysis Tree
|
|
||||||
fundamental_agents = [
|
|
||||||
TreeAgent(
|
|
||||||
system_prompt=FUNDAMENTAL_ANALYST_PROMPT,
|
|
||||||
agent_name="Economic Data Analyst",
|
|
||||||
model_name="gpt-4o",
|
|
||||||
),
|
|
||||||
TreeAgent(
|
|
||||||
system_prompt=FUNDAMENTAL_ANALYST_PROMPT,
|
|
||||||
agent_name="News Impact Analyst",
|
|
||||||
model_name="gpt-4o",
|
|
||||||
),
|
|
||||||
TreeAgent(
|
|
||||||
system_prompt=FUNDAMENTAL_ANALYST_PROMPT,
|
|
||||||
agent_name="Central Bank Analyst",
|
|
||||||
model_name="gpt-4o",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Create Sentiment Analysis Tree
|
|
||||||
sentiment_agents = [
|
|
||||||
TreeAgent(
|
|
||||||
system_prompt=MARKET_SENTIMENT_PROMPT,
|
|
||||||
agent_name="News Sentiment Analyst",
|
|
||||||
model_name="gpt-4o",
|
|
||||||
),
|
|
||||||
TreeAgent(
|
|
||||||
system_prompt=MARKET_SENTIMENT_PROMPT,
|
|
||||||
agent_name="Risk Sentiment Analyst",
|
|
||||||
model_name="gpt-4o",
|
|
||||||
),
|
|
||||||
TreeAgent(
|
|
||||||
system_prompt=MARKET_SENTIMENT_PROMPT,
|
|
||||||
agent_name="Market Positioning Analyst",
|
|
||||||
model_name="gpt-4o",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Create Strategy Coordination Tree
|
|
||||||
strategy_agents = [
|
|
||||||
TreeAgent(
|
|
||||||
system_prompt=STRATEGY_COORDINATOR_PROMPT,
|
|
||||||
agent_name="Lead Strategy Coordinator",
|
|
||||||
model_name="gpt-4",
|
|
||||||
temperature=0.5,
|
|
||||||
),
|
|
||||||
TreeAgent(
|
|
||||||
system_prompt=STRATEGY_COORDINATOR_PROMPT,
|
|
||||||
agent_name="Risk Manager",
|
|
||||||
model_name="gpt-4",
|
|
||||||
temperature=0.5,
|
|
||||||
),
|
|
||||||
TreeAgent(
|
|
||||||
system_prompt=STRATEGY_COORDINATOR_PROMPT,
|
|
||||||
agent_name="Position Manager",
|
|
||||||
model_name="gpt-4",
|
|
||||||
temperature=0.5,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Create trees
|
|
||||||
self.technical_tree = Tree(
|
|
||||||
tree_name="Technical Analysis", agents=technical_agents
|
|
||||||
)
|
|
||||||
self.fundamental_tree = Tree(
|
|
||||||
tree_name="Fundamental Analysis",
|
|
||||||
agents=fundamental_agents,
|
|
||||||
)
|
|
||||||
self.sentiment_tree = Tree(
|
|
||||||
tree_name="Sentiment Analysis", agents=sentiment_agents
|
|
||||||
)
|
|
||||||
self.strategy_tree = Tree(
|
|
||||||
tree_name="Strategy Coordination", agents=strategy_agents
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create forest swarm
|
|
||||||
self.forest = ForestSwarm(
|
|
||||||
trees=[
|
|
||||||
self.technical_tree,
|
|
||||||
self.fundamental_tree,
|
|
||||||
self.sentiment_tree,
|
|
||||||
self.strategy_tree,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info("Forex Forest System initialized successfully")
|
|
||||||
|
|
||||||
async def prepare_analysis_task(self) -> str:
|
|
||||||
"""Prepare the analysis task with real-time data"""
|
|
||||||
try:
|
|
||||||
market_data = await self.data_feed.fetch_all_data()
|
|
||||||
|
|
||||||
task = {
|
|
||||||
"action": "analyze_forex_markets",
|
|
||||||
"market_data": market_data,
|
|
||||||
"timestamp": datetime.now().isoformat(),
|
|
||||||
"analysis_required": [
|
|
||||||
"technical",
|
|
||||||
"fundamental",
|
|
||||||
"sentiment",
|
|
||||||
"strategy",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.dumps(task, indent=2)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error preparing analysis task: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
async def run_analysis_cycle(self) -> Dict:
|
|
||||||
"""Run a complete analysis cycle with the forest swarm"""
|
|
||||||
try:
|
|
||||||
# Prepare task with real-time data
|
|
||||||
task = await self.prepare_analysis_task()
|
|
||||||
|
|
||||||
# Run forest swarm analysis
|
|
||||||
result = self.forest.run(task)
|
|
||||||
|
|
||||||
# Parse and validate results
|
|
||||||
analysis = (
|
|
||||||
json.loads(result)
|
|
||||||
if isinstance(result, str)
|
|
||||||
else result
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info("Analysis cycle completed successfully")
|
|
||||||
return analysis
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in analysis cycle: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
async def monitor_markets(self, interval_seconds: int = 300):
|
|
||||||
"""Continuously monitor markets and run analysis"""
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
# Run analysis cycle
|
|
||||||
analysis = await self.run_analysis_cycle()
|
|
||||||
|
|
||||||
# Log results
|
|
||||||
logger.info("Market analysis completed")
|
|
||||||
logger.debug(
|
|
||||||
f"Analysis results: {json.dumps(analysis, indent=2)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Process any trading signals
|
|
||||||
if "recommendations" in analysis:
|
|
||||||
await self.process_trading_signals(
|
|
||||||
analysis["recommendations"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Wait for next interval
|
|
||||||
await asyncio.sleep(interval_seconds)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in market monitoring: {e}")
|
|
||||||
await asyncio.sleep(60)
|
|
||||||
|
|
||||||
async def process_trading_signals(
|
|
||||||
self, recommendations: List[Dict]
|
|
||||||
):
|
|
||||||
"""Process and log trading signals from analysis"""
|
|
||||||
try:
|
|
||||||
for rec in recommendations:
|
|
||||||
logger.info(
|
|
||||||
f"Trading Signal: {rec['pair']} - {rec['action']}"
|
|
||||||
)
|
|
||||||
logger.info(f"Confidence: {rec['confidence']}/10")
|
|
||||||
logger.info(f"Entry Points: {rec['entry_points']}")
|
|
||||||
logger.info(f"Stop Loss: {rec['stop_loss']}")
|
|
||||||
logger.info(f"Take Profit: {rec['take_profit']}")
|
|
||||||
logger.info(f"Rationale: {rec['rationale']}")
|
|
||||||
logger.info("-" * 50)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error processing trading signals: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
# Example usage
|
|
||||||
async def main():
|
|
||||||
"""Main function to run the Forex Forest System"""
|
|
||||||
try:
|
|
||||||
system = ForexForestSystem()
|
|
||||||
await system.monitor_markets()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in main: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Set up asyncio event loop and run the system
|
|
||||||
asyncio.run(main())
|
|
Loading…
Reference in new issue